fedilab-Android-App/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageView.java

2297 lines
77 KiB
Java

// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"
package com.theartofdev.edmodo.cropper;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import androidx.exifinterface.media.ExifInterface;
import java.lang.ref.WeakReference;
import java.util.UUID;
/**
* Custom view that provides cropping capabilities to an image.
*/
public class CropImageView extends FrameLayout {
// region: Fields and Consts
/**
* Image view widget used to show the image for cropping.
*/
private final ImageView mImageView;
/**
* Overlay over the image view to show cropping UI.
*/
private final CropOverlayView mCropOverlayView;
/**
* The matrix used to transform the cropping image in the image view
*/
private final Matrix mImageMatrix = new Matrix();
/**
* Reusing matrix instance for reverse matrix calculations.
*/
private final Matrix mImageInverseMatrix = new Matrix();
/**
* Progress bar widget to show progress bar on async image loading and cropping.
*/
private final ProgressBar mProgressBar;
/**
* Rectangle used in image matrix transformation calculation (reusing rect instance)
*/
private final float[] mImagePoints = new float[8];
/**
* Rectangle used in image matrix transformation for scale calculation (reusing rect instance)
*/
private final float[] mScaleImagePoints = new float[8];
/**
* Animation class to smooth animate zoom-in/out
*/
private CropImageAnimation mAnimation;
private Bitmap mBitmap;
/**
* The image rotation value used during loading of the image so we can reset to it
*/
private int mInitialDegreesRotated;
/**
* How much the image is rotated from original clockwise
*/
private int mDegreesRotated;
/**
* if the image flipped horizontally
*/
private boolean mFlipHorizontally;
/**
* if the image flipped vertically
*/
private boolean mFlipVertically;
private int mLayoutWidth;
private int mLayoutHeight;
private int mImageResource;
/**
* The initial scale type of the image in the crop image view
*/
private ScaleType mScaleType;
/**
* if to save bitmap on save instance state.<br>
* It is best to avoid it by using URI in setting image for cropping.<br>
* If false the bitmap is not saved and if restore is required to view will be empty, storing the
* bitmap requires saving it to file which can be expensive. default: false.
*/
private boolean mSaveBitmapToInstanceState = false;
/**
* if to show crop overlay UI what contains the crop window UI surrounded by background over the
* cropping image.<br>
* default: true, may disable for animation or frame transition.
*/
private boolean mShowCropOverlay = true;
/**
* if to show progress bar when image async loading/cropping is in progress.<br>
* default: true, disable to provide custom progress bar UI.
*/
private boolean mShowProgressBar = true;
/**
* if auto-zoom functionality is enabled.<br>
* default: true.
*/
private boolean mAutoZoomEnabled = true;
/**
* The max zoom allowed during cropping
*/
private int mMaxZoom;
/**
* callback to be invoked when crop overlay is released.
*/
private OnSetCropOverlayReleasedListener mOnCropOverlayReleasedListener;
/**
* callback to be invoked when crop overlay is moved.
*/
private OnSetCropOverlayMovedListener mOnSetCropOverlayMovedListener;
/**
* callback to be invoked when crop window is changed.
*/
private OnSetCropWindowChangeListener mOnSetCropWindowChangeListener;
/**
* callback to be invoked when image async loading is complete.
*/
private OnSetImageUriCompleteListener mOnSetImageUriCompleteListener;
/**
* callback to be invoked when image async cropping is complete.
*/
private OnCropImageCompleteListener mOnCropImageCompleteListener;
/**
* The URI that the image was loaded from (if loaded from URI)
*/
private Uri mLoadedImageUri;
/**
* The sample size the image was loaded by if was loaded by URI
*/
private int mLoadedSampleSize = 1;
/**
* The current zoom level to to scale the cropping image
*/
private float mZoom = 1;
/**
* The X offset that the cropping image was translated after zooming
*/
private float mZoomOffsetX;
/**
* The Y offset that the cropping image was translated after zooming
*/
private float mZoomOffsetY;
/**
* Used to restore the cropping windows rectangle after state restore
*/
private RectF mRestoreCropWindowRect;
/**
* Used to restore image rotation after state restore
*/
private int mRestoreDegreesRotated;
/**
* Used to detect size change to handle auto-zoom using {@link #handleCropWindowChanged(boolean,
* boolean)} in {@link #layout(int, int, int, int)}.
*/
private boolean mSizeChanged;
/**
* Temp URI used to save bitmap image to disk to preserve for instance state in case cropped was
* set with bitmap
*/
private Uri mSaveInstanceStateBitmapUri;
/**
* Task used to load bitmap async from UI thread
*/
private WeakReference<BitmapLoadingWorkerTask> mBitmapLoadingWorkerTask;
/**
* Task used to crop bitmap async from UI thread
*/
private WeakReference<BitmapCroppingWorkerTask> mBitmapCroppingWorkerTask;
// endregion
public CropImageView(Context context) {
this(context, null);
}
public CropImageView(Context context, AttributeSet attrs) {
super(context, attrs);
CropImageOptions options = null;
Intent intent = context instanceof Activity ? ((Activity) context).getIntent() : null;
if (intent != null) {
Bundle bundle = intent.getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE);
if (bundle != null) {
options = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS);
}
}
if (options == null) {
options = new CropImageOptions();
if (attrs != null) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CropImageView, 0, 0);
try {
options.fixAspectRatio =
ta.getBoolean(R.styleable.CropImageView_cropFixAspectRatio, options.fixAspectRatio);
options.aspectRatioX =
ta.getInteger(R.styleable.CropImageView_cropAspectRatioX, options.aspectRatioX);
options.aspectRatioY =
ta.getInteger(R.styleable.CropImageView_cropAspectRatioY, options.aspectRatioY);
options.scaleType =
ScaleType.values()[
ta.getInt(R.styleable.CropImageView_cropScaleType, options.scaleType.ordinal())];
options.autoZoomEnabled =
ta.getBoolean(R.styleable.CropImageView_cropAutoZoomEnabled, options.autoZoomEnabled);
options.multiTouchEnabled =
ta.getBoolean(
R.styleable.CropImageView_cropMultiTouchEnabled, options.multiTouchEnabled);
options.maxZoom = ta.getInteger(R.styleable.CropImageView_cropMaxZoom, options.maxZoom);
options.cropShape =
CropShape.values()[
ta.getInt(R.styleable.CropImageView_cropShape, options.cropShape.ordinal())];
options.guidelines =
Guidelines.values()[
ta.getInt(
R.styleable.CropImageView_cropGuidelines, options.guidelines.ordinal())];
options.snapRadius =
ta.getDimension(R.styleable.CropImageView_cropSnapRadius, options.snapRadius);
options.touchRadius =
ta.getDimension(R.styleable.CropImageView_cropTouchRadius, options.touchRadius);
options.initialCropWindowPaddingRatio =
ta.getFloat(
R.styleable.CropImageView_cropInitialCropWindowPaddingRatio,
options.initialCropWindowPaddingRatio);
options.borderLineThickness =
ta.getDimension(
R.styleable.CropImageView_cropBorderLineThickness, options.borderLineThickness);
options.borderLineColor =
ta.getInteger(R.styleable.CropImageView_cropBorderLineColor, options.borderLineColor);
options.borderCornerThickness =
ta.getDimension(
R.styleable.CropImageView_cropBorderCornerThickness,
options.borderCornerThickness);
options.borderCornerOffset =
ta.getDimension(
R.styleable.CropImageView_cropBorderCornerOffset, options.borderCornerOffset);
options.borderCornerLength =
ta.getDimension(
R.styleable.CropImageView_cropBorderCornerLength, options.borderCornerLength);
options.borderCornerColor =
ta.getInteger(
R.styleable.CropImageView_cropBorderCornerColor, options.borderCornerColor);
options.guidelinesThickness =
ta.getDimension(
R.styleable.CropImageView_cropGuidelinesThickness, options.guidelinesThickness);
options.guidelinesColor =
ta.getInteger(R.styleable.CropImageView_cropGuidelinesColor, options.guidelinesColor);
options.backgroundColor =
ta.getInteger(R.styleable.CropImageView_cropBackgroundColor, options.backgroundColor);
options.showCropOverlay =
ta.getBoolean(R.styleable.CropImageView_cropShowCropOverlay, mShowCropOverlay);
options.showProgressBar =
ta.getBoolean(R.styleable.CropImageView_cropShowProgressBar, mShowProgressBar);
options.borderCornerThickness =
ta.getDimension(
R.styleable.CropImageView_cropBorderCornerThickness,
options.borderCornerThickness);
options.minCropWindowWidth =
(int)
ta.getDimension(
R.styleable.CropImageView_cropMinCropWindowWidth, options.minCropWindowWidth);
options.minCropWindowHeight =
(int)
ta.getDimension(
R.styleable.CropImageView_cropMinCropWindowHeight,
options.minCropWindowHeight);
options.minCropResultWidth =
(int)
ta.getFloat(
R.styleable.CropImageView_cropMinCropResultWidthPX,
options.minCropResultWidth);
options.minCropResultHeight =
(int)
ta.getFloat(
R.styleable.CropImageView_cropMinCropResultHeightPX,
options.minCropResultHeight);
options.maxCropResultWidth =
(int)
ta.getFloat(
R.styleable.CropImageView_cropMaxCropResultWidthPX,
options.maxCropResultWidth);
options.maxCropResultHeight =
(int)
ta.getFloat(
R.styleable.CropImageView_cropMaxCropResultHeightPX,
options.maxCropResultHeight);
options.flipHorizontally =
ta.getBoolean(
R.styleable.CropImageView_cropFlipHorizontally, options.flipHorizontally);
options.flipVertically =
ta.getBoolean(R.styleable.CropImageView_cropFlipHorizontally, options.flipVertically);
mSaveBitmapToInstanceState =
ta.getBoolean(
R.styleable.CropImageView_cropSaveBitmapToInstanceState,
mSaveBitmapToInstanceState);
// if aspect ratio is set then set fixed to true
if (ta.hasValue(R.styleable.CropImageView_cropAspectRatioX)
&& ta.hasValue(R.styleable.CropImageView_cropAspectRatioX)
&& !ta.hasValue(R.styleable.CropImageView_cropFixAspectRatio)) {
options.fixAspectRatio = true;
}
} finally {
ta.recycle();
}
}
}
options.validate();
mScaleType = options.scaleType;
mAutoZoomEnabled = options.autoZoomEnabled;
mMaxZoom = options.maxZoom;
mShowCropOverlay = options.showCropOverlay;
mShowProgressBar = options.showProgressBar;
mFlipHorizontally = options.flipHorizontally;
mFlipVertically = options.flipVertically;
LayoutInflater inflater = LayoutInflater.from(context);
View v = inflater.inflate(R.layout.crop_image_view, this, true);
mImageView = v.findViewById(R.id.ImageView_image);
mImageView.setScaleType(ImageView.ScaleType.MATRIX);
mCropOverlayView = v.findViewById(R.id.CropOverlayView);
mCropOverlayView.setCropWindowChangeListener(
new CropOverlayView.CropWindowChangeListener() {
@Override
public void onCropWindowChanged(boolean inProgress) {
handleCropWindowChanged(inProgress, true);
OnSetCropOverlayReleasedListener listener = mOnCropOverlayReleasedListener;
if (listener != null && !inProgress) {
listener.onCropOverlayReleased(getCropRect());
}
OnSetCropOverlayMovedListener movedListener = mOnSetCropOverlayMovedListener;
if (movedListener != null && inProgress) {
movedListener.onCropOverlayMoved(getCropRect());
}
}
});
mCropOverlayView.setInitialAttributeValues(options);
mProgressBar = v.findViewById(R.id.CropProgressBar);
setProgressBarVisibility();
}
/**
* Determines the specs for the onMeasure function. Calculates the width or height depending on
* the mode.
*
* @param measureSpecMode The mode of the measured width or height.
* @param measureSpecSize The size of the measured width or height.
* @param desiredSize The desired size of the measured width or height.
* @return The final size of the width or height.
*/
private static int getOnMeasureSpec(int measureSpecMode, int measureSpecSize, int desiredSize) {
// Measure Width
int spec;
if (measureSpecMode == MeasureSpec.EXACTLY) {
// Must be this size
spec = measureSpecSize;
} else if (measureSpecMode == MeasureSpec.AT_MOST) {
// Can't be bigger than...; match_parent value
spec = Math.min(desiredSize, measureSpecSize);
} else {
// Be whatever you want; wrap_content
spec = desiredSize;
}
return spec;
}
/**
* Get the scale type of the image in the crop view.
*/
public ScaleType getScaleType() {
return mScaleType;
}
/**
* Set the scale type of the image in the crop view
*/
public void setScaleType(ScaleType scaleType) {
if (scaleType != mScaleType) {
mScaleType = scaleType;
mZoom = 1;
mZoomOffsetX = mZoomOffsetY = 0;
mCropOverlayView.resetCropOverlayView();
requestLayout();
}
}
/**
* The shape of the cropping area - rectangle/circular.
*/
public CropShape getCropShape() {
return mCropOverlayView.getCropShape();
}
/**
* The shape of the cropping area - rectangle/circular.<br>
* To set square/circle crop shape set aspect ratio to 1:1.
*/
public void setCropShape(CropShape cropShape) {
mCropOverlayView.setCropShape(cropShape);
}
/**
* if auto-zoom functionality is enabled. default: true.
*/
public boolean isAutoZoomEnabled() {
return mAutoZoomEnabled;
}
/**
* Set auto-zoom functionality to enabled/disabled.
*/
public void setAutoZoomEnabled(boolean autoZoomEnabled) {
if (mAutoZoomEnabled != autoZoomEnabled) {
mAutoZoomEnabled = autoZoomEnabled;
handleCropWindowChanged(false, false);
mCropOverlayView.invalidate();
}
}
/**
* Set multi touch functionality to enabled/disabled.
*/
public void setMultiTouchEnabled(boolean multiTouchEnabled) {
if (mCropOverlayView.setMultiTouchEnabled(multiTouchEnabled)) {
handleCropWindowChanged(false, false);
mCropOverlayView.invalidate();
}
}
/**
* The max zoom allowed during cropping.
*/
public int getMaxZoom() {
return mMaxZoom;
}
/**
* The max zoom allowed during cropping.
*/
public void setMaxZoom(int maxZoom) {
if (mMaxZoom != maxZoom && maxZoom > 0) {
mMaxZoom = maxZoom;
handleCropWindowChanged(false, false);
mCropOverlayView.invalidate();
}
}
/**
* the min size the resulting cropping image is allowed to be, affects the cropping window limits
* (in pixels).<br>
*/
public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
mCropOverlayView.setMinCropResultSize(minCropResultWidth, minCropResultHeight);
}
/**
* the max size the resulting cropping image is allowed to be, affects the cropping window limits
* (in pixels).<br>
*/
public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
mCropOverlayView.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight);
}
/**
* Get the amount of degrees the cropping image is rotated cloackwise.<br>
*
* @return 0-360
*/
public int getRotatedDegrees() {
return mDegreesRotated;
}
/**
* Set the amount of degrees the cropping image is rotated cloackwise.<br>
*
* @param degrees 0-360
*/
public void setRotatedDegrees(int degrees) {
if (mDegreesRotated != degrees) {
rotateImage(degrees - mDegreesRotated);
}
}
/**
* whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to
* be changed.
*/
public boolean isFixAspectRatio() {
return mCropOverlayView.isFixAspectRatio();
}
/**
* Sets whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows
* it to be changed.
*/
public void setFixedAspectRatio(boolean fixAspectRatio) {
mCropOverlayView.setFixedAspectRatio(fixAspectRatio);
}
/**
* whether the image should be flipped horizontally
*/
public boolean isFlippedHorizontally() {
return mFlipHorizontally;
}
/**
* Sets whether the image should be flipped horizontally
*/
public void setFlippedHorizontally(boolean flipHorizontally) {
if (mFlipHorizontally != flipHorizontally) {
mFlipHorizontally = flipHorizontally;
applyImageMatrix(getWidth(), getHeight(), true, false);
}
}
/**
* whether the image should be flipped vertically
*/
public boolean isFlippedVertically() {
return mFlipVertically;
}
/**
* Sets whether the image should be flipped vertically
*/
public void setFlippedVertically(boolean flipVertically) {
if (mFlipVertically != flipVertically) {
mFlipVertically = flipVertically;
applyImageMatrix(getWidth(), getHeight(), true, false);
}
}
/**
* Get the current guidelines option set.
*/
public Guidelines getGuidelines() {
return mCropOverlayView.getGuidelines();
}
/**
* Sets the guidelines for the CropOverlayView to be either on, off, or to show when resizing the
* application.
*/
public void setGuidelines(Guidelines guidelines) {
mCropOverlayView.setGuidelines(guidelines);
}
/**
* both the X and Y values of the aspectRatio.
*/
public Pair<Integer, Integer> getAspectRatio() {
return new Pair<>(mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY());
}
/**
* Sets the both the X and Y values of the aspectRatio.<br>
* Sets fixed aspect ratio to TRUE.
*
* @param aspectRatioX int that specifies the new X value of the aspect ratio
* @param aspectRatioY int that specifies the new Y value of the aspect ratio
*/
public void setAspectRatio(int aspectRatioX, int aspectRatioY) {
mCropOverlayView.setAspectRatioX(aspectRatioX);
mCropOverlayView.setAspectRatioY(aspectRatioY);
setFixedAspectRatio(true);
}
/**
* Clears set aspect ratio values and sets fixed aspect ratio to FALSE.
*/
public void clearAspectRatio() {
mCropOverlayView.setAspectRatioX(1);
mCropOverlayView.setAspectRatioY(1);
setFixedAspectRatio(false);
}
/**
* An edge of the crop window will snap to the corresponding edge of a specified bounding box when
* the crop window edge is less than or equal to this distance (in pixels) away from the bounding
* box edge. (default: 3dp)
*/
public void setSnapRadius(float snapRadius) {
if (snapRadius >= 0) {
mCropOverlayView.setSnapRadius(snapRadius);
}
}
/**
* if to show progress bar when image async loading/cropping is in progress.<br>
* default: true, disable to provide custom progress bar UI.
*/
public boolean isShowProgressBar() {
return mShowProgressBar;
}
/**
* if to show progress bar when image async loading/cropping is in progress.<br>
* default: true, disable to provide custom progress bar UI.
*/
public void setShowProgressBar(boolean showProgressBar) {
if (mShowProgressBar != showProgressBar) {
mShowProgressBar = showProgressBar;
setProgressBarVisibility();
}
}
/**
* if to show crop overlay UI what contains the crop window UI surrounded by background over the
* cropping image.<br>
* default: true, may disable for animation or frame transition.
*/
public boolean isShowCropOverlay() {
return mShowCropOverlay;
}
/**
* if to show crop overlay UI what contains the crop window UI surrounded by background over the
* cropping image.<br>
* default: true, may disable for animation or frame transition.
*/
public void setShowCropOverlay(boolean showCropOverlay) {
if (mShowCropOverlay != showCropOverlay) {
mShowCropOverlay = showCropOverlay;
setCropOverlayVisibility();
}
}
/**
* if to save bitmap on save instance state.<br>
* It is best to avoid it by using URI in setting image for cropping.<br>
* If false the bitmap is not saved and if restore is required to view will be empty, storing the
* bitmap requires saving it to file which can be expensive. default: false.
*/
public boolean isSaveBitmapToInstanceState() {
return mSaveBitmapToInstanceState;
}
/**
* if to save bitmap on save instance state.<br>
* It is best to avoid it by using URI in setting image for cropping.<br>
* If false the bitmap is not saved and if restore is required to view will be empty, storing the
* bitmap requires saving it to file which can be expensive. default: false.
*/
public void setSaveBitmapToInstanceState(boolean saveBitmapToInstanceState) {
mSaveBitmapToInstanceState = saveBitmapToInstanceState;
}
/**
* Returns the integer of the imageResource
*/
public int getImageResource() {
return mImageResource;
}
/**
* Sets a Drawable as the content of the CropImageView.
*
* @param resId the drawable resource ID to set
*/
public void setImageResource(int resId) {
if (resId != 0) {
mCropOverlayView.setInitialCropWindowRect(null);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
setBitmap(bitmap, resId, null, 1, 0);
}
}
/**
* Get the URI of an image that was set by URI, null otherwise.
*/
public Uri getImageUri() {
return mLoadedImageUri;
}
/**
* Gets the source Bitmap's dimensions. This represents the largest possible crop rectangle.
*
* @return a Rect instance dimensions of the source Bitmap
*/
public Rect getWholeImageRect() {
int loadedSampleSize = mLoadedSampleSize;
Bitmap bitmap = mBitmap;
if (bitmap == null) {
return null;
}
int orgWidth = bitmap.getWidth() * loadedSampleSize;
int orgHeight = bitmap.getHeight() * loadedSampleSize;
return new Rect(0, 0, orgWidth, orgHeight);
}
/**
* Gets the crop window's position relative to the source Bitmap (not the image displayed in the
* CropImageView) using the original image rotation.
*
* @return a Rect instance containing cropped area boundaries of the source Bitmap
*/
public Rect getCropRect() {
int loadedSampleSize = mLoadedSampleSize;
Bitmap bitmap = mBitmap;
if (bitmap == null) {
return null;
}
// get the points of the crop rectangle adjusted to source bitmap
float[] points = getCropPoints();
int orgWidth = bitmap.getWidth() * loadedSampleSize;
int orgHeight = bitmap.getHeight() * loadedSampleSize;
// get the rectangle for the points (it may be larger than original if rotation is not stright)
return BitmapUtils.getRectFromPoints(
points,
orgWidth,
orgHeight,
mCropOverlayView.isFixAspectRatio(),
mCropOverlayView.getAspectRatioX(),
mCropOverlayView.getAspectRatioY());
}
/**
* Set the crop window position and size to the given rectangle.<br>
* Image to crop must be first set before invoking this, for async - after complete callback.
*
* @param rect window rectangle (position and size) relative to source bitmap
*/
public void setCropRect(Rect rect) {
mCropOverlayView.setInitialCropWindowRect(rect);
}
/**
* Gets the crop window's position relative to the parent's view at screen.
*
* @return a Rect instance containing cropped area boundaries of the source Bitmap
*/
public RectF getCropWindowRect() {
if (mCropOverlayView == null) {
return null;
}
return mCropOverlayView.getCropWindowRect();
}
/**
* Gets the 4 points of crop window's position relative to the source Bitmap (not the image
* displayed in the CropImageView) using the original image rotation.<br>
* Note: the 4 points may not be a rectangle if the image was rotates to NOT stright angle (!=
* 90/180/270).
*
* @return 4 points (x0,y0,x1,y1,x2,y2,x3,y3) of cropped area boundaries
*/
public float[] getCropPoints() {
// Get crop window position relative to the displayed image.
RectF cropWindowRect = mCropOverlayView.getCropWindowRect();
float[] points =
new float[]{
cropWindowRect.left,
cropWindowRect.top,
cropWindowRect.right,
cropWindowRect.top,
cropWindowRect.right,
cropWindowRect.bottom,
cropWindowRect.left,
cropWindowRect.bottom
};
mImageMatrix.invert(mImageInverseMatrix);
mImageInverseMatrix.mapPoints(points);
for (int i = 0; i < points.length; i++) {
points[i] *= mLoadedSampleSize;
}
return points;
}
/**
* Reset crop window to initial rectangle.
*/
public void resetCropRect() {
mZoom = 1;
mZoomOffsetX = 0;
mZoomOffsetY = 0;
mDegreesRotated = mInitialDegreesRotated;
mFlipHorizontally = false;
mFlipVertically = false;
applyImageMatrix(getWidth(), getHeight(), false, false);
mCropOverlayView.resetCropWindowRect();
}
/**
* Gets the cropped image based on the current crop window.
*
* @return a new Bitmap representing the cropped image
*/
public Bitmap getCroppedImage() {
return getCroppedImage(0, 0, RequestSizeOptions.NONE);
}
/**
* Gets the cropped image based on the current crop window.<br>
* Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
*
* @param reqWidth the width to resize the cropped image to
* @param reqHeight the height to resize the cropped image to
* @return a new Bitmap representing the cropped image
*/
public Bitmap getCroppedImage(int reqWidth, int reqHeight) {
return getCroppedImage(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
}
/**
* Gets the cropped image based on the current crop window.<br>
*
* @param reqWidth the width to resize the cropped image to (see options)
* @param reqHeight the height to resize the cropped image to (see options)
* @param options the resize method to use, see its documentation
* @return a new Bitmap representing the cropped image
*/
public Bitmap getCroppedImage(int reqWidth, int reqHeight, RequestSizeOptions options) {
Bitmap croppedBitmap = null;
if (mBitmap != null) {
mImageView.clearAnimation();
reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;
if (mLoadedImageUri != null
&& (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
int orgWidth = mBitmap.getWidth() * mLoadedSampleSize;
int orgHeight = mBitmap.getHeight() * mLoadedSampleSize;
BitmapUtils.BitmapSampled bitmapSampled =
BitmapUtils.cropBitmap(
getContext(),
mLoadedImageUri,
getCropPoints(),
mDegreesRotated,
orgWidth,
orgHeight,
mCropOverlayView.isFixAspectRatio(),
mCropOverlayView.getAspectRatioX(),
mCropOverlayView.getAspectRatioY(),
reqWidth,
reqHeight,
mFlipHorizontally,
mFlipVertically);
croppedBitmap = bitmapSampled.bitmap;
} else {
croppedBitmap =
BitmapUtils.cropBitmapObjectHandleOOM(
mBitmap,
getCropPoints(),
mDegreesRotated,
mCropOverlayView.isFixAspectRatio(),
mCropOverlayView.getAspectRatioX(),
mCropOverlayView.getAspectRatioY(),
mFlipHorizontally,
mFlipVertically)
.bitmap;
}
croppedBitmap = BitmapUtils.resizeBitmap(croppedBitmap, reqWidth, reqHeight, options);
}
return croppedBitmap;
}
/**
* Gets the cropped image based on the current crop window.<br>
* The result will be invoked to listener set by {@link
* #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
*/
public void getCroppedImageAsync() {
getCroppedImageAsync(0, 0, RequestSizeOptions.NONE);
}
/**
* Gets the cropped image based on the current crop window.<br>
* Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.<br>
* The result will be invoked to listener set by {@link
* #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
*
* @param reqWidth the width to resize the cropped image to
* @param reqHeight the height to resize the cropped image to
*/
public void getCroppedImageAsync(int reqWidth, int reqHeight) {
getCroppedImageAsync(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
}
/**
* Gets the cropped image based on the current crop window.<br>
* The result will be invoked to listener set by {@link
* #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
*
* @param reqWidth the width to resize the cropped image to (see options)
* @param reqHeight the height to resize the cropped image to (see options)
* @param options the resize method to use, see its documentation
*/
public void getCroppedImageAsync(int reqWidth, int reqHeight, RequestSizeOptions options) {
if (mOnCropImageCompleteListener == null) {
throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
}
startCropWorkerTask(reqWidth, reqHeight, options, null, null, 0);
}
/**
* Save the cropped image based on the current crop window to the given uri.<br>
* Uses JPEG image compression with 90 compression quality.<br>
* The result will be invoked to listener set by {@link
* #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
*
* @param saveUri the Android Uri to save the cropped image to
*/
public void saveCroppedImageAsync(Uri saveUri) {
saveCroppedImageAsync(saveUri, Bitmap.CompressFormat.JPEG, 90, 0, 0, RequestSizeOptions.NONE);
}
/**
* Save the cropped image based on the current crop window to the given uri.<br>
* The result will be invoked to listener set by {@link
* #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
*
* @param saveUri the Android Uri to save the cropped image to
* @param saveCompressFormat the compression format to use when writing the image
* @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
*/
public void saveCroppedImageAsync(
Uri saveUri, Bitmap.CompressFormat saveCompressFormat, int saveCompressQuality) {
saveCroppedImageAsync(
saveUri, saveCompressFormat, saveCompressQuality, 0, 0, RequestSizeOptions.NONE);
}
/**
* Save the cropped image based on the current crop window to the given uri.<br>
* Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.<br>
* The result will be invoked to listener set by {@link
* #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
*
* @param saveUri the Android Uri to save the cropped image to
* @param saveCompressFormat the compression format to use when writing the image
* @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
* @param reqWidth the width to resize the cropped image to
* @param reqHeight the height to resize the cropped image to
*/
public void saveCroppedImageAsync(
Uri saveUri,
Bitmap.CompressFormat saveCompressFormat,
int saveCompressQuality,
int reqWidth,
int reqHeight) {
saveCroppedImageAsync(
saveUri,
saveCompressFormat,
saveCompressQuality,
reqWidth,
reqHeight,
RequestSizeOptions.RESIZE_INSIDE);
}
/**
* Save the cropped image based on the current crop window to the given uri.<br>
* The result will be invoked to listener set by {@link
* #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
*
* @param saveUri the Android Uri to save the cropped image to
* @param saveCompressFormat the compression format to use when writing the image
* @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
* @param reqWidth the width to resize the cropped image to (see options)
* @param reqHeight the height to resize the cropped image to (see options)
* @param options the resize method to use, see its documentation
*/
public void saveCroppedImageAsync(
Uri saveUri,
Bitmap.CompressFormat saveCompressFormat,
int saveCompressQuality,
int reqWidth,
int reqHeight,
RequestSizeOptions options) {
if (mOnCropImageCompleteListener == null) {
throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
}
startCropWorkerTask(
reqWidth, reqHeight, options, saveUri, saveCompressFormat, saveCompressQuality);
}
/**
* Set the callback t
*/
public void setOnSetCropOverlayReleasedListener(OnSetCropOverlayReleasedListener listener) {
mOnCropOverlayReleasedListener = listener;
}
/**
* Set the callback when the cropping is moved
*/
public void setOnSetCropOverlayMovedListener(OnSetCropOverlayMovedListener listener) {
mOnSetCropOverlayMovedListener = listener;
}
/**
* Set the callback when the crop window is changed
*/
public void setOnCropWindowChangedListener(OnSetCropWindowChangeListener listener) {
mOnSetCropWindowChangeListener = listener;
}
/**
* Set the callback to be invoked when image async loading ({@link #setImageUriAsync(Uri)}) is
* complete (successful or failed).
*/
public void setOnSetImageUriCompleteListener(OnSetImageUriCompleteListener listener) {
mOnSetImageUriCompleteListener = listener;
}
/**
* Set the callback to be invoked when image async cropping image ({@link #getCroppedImageAsync()}
* or {@link #saveCroppedImageAsync(Uri)}) is complete (successful or failed).
*/
public void setOnCropImageCompleteListener(OnCropImageCompleteListener listener) {
mOnCropImageCompleteListener = listener;
}
/**
* Sets a Bitmap as the content of the CropImageView.
*
* @param bitmap the Bitmap to set
*/
public void setImageBitmap(Bitmap bitmap) {
mCropOverlayView.setInitialCropWindowRect(null);
setBitmap(bitmap, 0, null, 1, 0);
}
/**
* Sets a Bitmap and initializes the image rotation according to the EXIT data.<br>
* <br>
* The EXIF can be retrieved by doing the following: <code>
* ExifInterface exif = new ExifInterface(path);</code>
*
* @param bitmap the original bitmap to set; if null, this
* @param exif the EXIF information about this bitmap; may be null
*/
public void setImageBitmap(Bitmap bitmap, ExifInterface exif) {
Bitmap setBitmap;
int degreesRotated = 0;
if (bitmap != null && exif != null) {
BitmapUtils.RotateBitmapResult result = BitmapUtils.rotateBitmapByExif(bitmap, exif);
setBitmap = result.bitmap;
degreesRotated = result.degrees;
mInitialDegreesRotated = result.degrees;
} else {
setBitmap = bitmap;
}
mCropOverlayView.setInitialCropWindowRect(null);
setBitmap(setBitmap, 0, null, 1, degreesRotated);
}
/**
* Sets a bitmap loaded from the given Android URI as the content of the CropImageView.<br>
* Can be used with URI from gallery or camera source.<br>
* Will rotate the image by exif data.<br>
*
* @param uri the URI to load the image from
*/
public void setImageUriAsync(Uri uri) {
if (uri != null) {
BitmapLoadingWorkerTask currentTask =
mBitmapLoadingWorkerTask != null ? mBitmapLoadingWorkerTask.get() : null;
if (currentTask != null) {
// cancel previous loading (no check if the same URI because camera URI can be the same for
// different images)
currentTask.cancel(true);
}
// either no existing task is working or we canceled it, need to load new URI
clearImageInt();
mRestoreCropWindowRect = null;
mRestoreDegreesRotated = 0;
mCropOverlayView.setInitialCropWindowRect(null);
mBitmapLoadingWorkerTask = new WeakReference<>(new BitmapLoadingWorkerTask(this, uri));
mBitmapLoadingWorkerTask.get().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
setProgressBarVisibility();
}
}
/**
* Clear the current image set for cropping.
*/
public void clearImage() {
clearImageInt();
mCropOverlayView.setInitialCropWindowRect(null);
}
/**
* Rotates image by the specified number of degrees clockwise.<br>
* Negative values represent counter-clockwise rotations.
*
* @param degrees Integer specifying the number of degrees to rotate.
*/
public void rotateImage(int degrees) {
if (mBitmap != null) {
// Force degrees to be a non-zero value between 0 and 360 (inclusive)
if (degrees < 0) {
degrees = (degrees % 360) + 360;
} else {
degrees = degrees % 360;
}
boolean flipAxes =
!mCropOverlayView.isFixAspectRatio()
&& ((degrees > 45 && degrees < 135) || (degrees > 215 && degrees < 305));
BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());
float halfWidth = (flipAxes ? BitmapUtils.RECT.height() : BitmapUtils.RECT.width()) / 2f;
float halfHeight = (flipAxes ? BitmapUtils.RECT.width() : BitmapUtils.RECT.height()) / 2f;
if (flipAxes) {
boolean isFlippedHorizontally = mFlipHorizontally;
mFlipHorizontally = mFlipVertically;
mFlipVertically = isFlippedHorizontally;
}
mImageMatrix.invert(mImageInverseMatrix);
BitmapUtils.POINTS[0] = BitmapUtils.RECT.centerX();
BitmapUtils.POINTS[1] = BitmapUtils.RECT.centerY();
BitmapUtils.POINTS[2] = 0;
BitmapUtils.POINTS[3] = 0;
BitmapUtils.POINTS[4] = 1;
BitmapUtils.POINTS[5] = 0;
mImageInverseMatrix.mapPoints(BitmapUtils.POINTS);
// This is valid because degrees is not negative.
mDegreesRotated = (mDegreesRotated + degrees) % 360;
applyImageMatrix(getWidth(), getHeight(), true, false);
// adjust the zoom so the crop window size remains the same even after image scale change
mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);
mZoom /=
Math.sqrt(
Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2)
+ Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
mZoom = Math.max(mZoom, 1);
applyImageMatrix(getWidth(), getHeight(), true, false);
mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);
// adjust the width/height by the changes in scaling to the image
double change =
Math.sqrt(
Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2)
+ Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
halfWidth *= change;
halfHeight *= change;
// calculate the new crop window rectangle to center in the same location and have proper
// width/height
BitmapUtils.RECT.set(
BitmapUtils.POINTS2[0] - halfWidth,
BitmapUtils.POINTS2[1] - halfHeight,
BitmapUtils.POINTS2[0] + halfWidth,
BitmapUtils.POINTS2[1] + halfHeight);
mCropOverlayView.resetCropOverlayView();
mCropOverlayView.setCropWindowRect(BitmapUtils.RECT);
applyImageMatrix(getWidth(), getHeight(), true, false);
handleCropWindowChanged(false, false);
// make sure the crop window rectangle is within the cropping image bounds after all the
// changes
mCropOverlayView.fixCurrentCropWindowRect();
}
}
/**
* Flips the image horizontally.
*/
public void flipImageHorizontally() {
mFlipHorizontally = !mFlipHorizontally;
applyImageMatrix(getWidth(), getHeight(), true, false);
}
// region: Private methods
/**
* Flips the image vertically.
*/
public void flipImageVertically() {
mFlipVertically = !mFlipVertically;
applyImageMatrix(getWidth(), getHeight(), true, false);
}
/**
* On complete of the async bitmap loading by {@link #setImageUriAsync(Uri)} set the result to the
* widget if still relevant and call listener if set.
*
* @param result the result of bitmap loading
*/
void onSetImageUriAsyncComplete(BitmapLoadingWorkerTask.Result result) {
mBitmapLoadingWorkerTask = null;
setProgressBarVisibility();
if (result.error == null) {
mInitialDegreesRotated = result.degreesRotated;
setBitmap(result.bitmap, 0, result.uri, result.loadSampleSize, result.degreesRotated);
}
OnSetImageUriCompleteListener listener = mOnSetImageUriCompleteListener;
if (listener != null) {
listener.onSetImageUriComplete(this, result.uri, result.error);
}
}
/**
* On complete of the async bitmap cropping by {@link #getCroppedImageAsync()} call listener if
* set.
*
* @param result the result of bitmap cropping
*/
void onImageCroppingAsyncComplete(BitmapCroppingWorkerTask.Result result) {
mBitmapCroppingWorkerTask = null;
setProgressBarVisibility();
OnCropImageCompleteListener listener = mOnCropImageCompleteListener;
if (listener != null) {
CropResult cropResult =
new CropResult(
mBitmap,
mLoadedImageUri,
result.bitmap,
result.uri,
result.error,
getCropPoints(),
getCropRect(),
getWholeImageRect(),
getRotatedDegrees(),
result.sampleSize);
listener.onCropImageComplete(this, cropResult);
}
}
/**
* Set the given bitmap to be used in for cropping<br>
* Optionally clear full if the bitmap is new, or partial clear if the bitmap has been
* manipulated.
*/
private void setBitmap(
Bitmap bitmap, int imageResource, Uri imageUri, int loadSampleSize, int degreesRotated) {
if (mBitmap == null || !mBitmap.equals(bitmap)) {
mImageView.clearAnimation();
clearImageInt();
mBitmap = bitmap;
mImageView.setImageBitmap(mBitmap);
mLoadedImageUri = imageUri;
mImageResource = imageResource;
mLoadedSampleSize = loadSampleSize;
mDegreesRotated = degreesRotated;
applyImageMatrix(getWidth(), getHeight(), true, false);
if (mCropOverlayView != null) {
mCropOverlayView.resetCropOverlayView();
setCropOverlayVisibility();
}
}
}
/**
* Clear the current image set for cropping.<br>
* Full clear will also clear the data of the set image like Uri or Resource id while partial
* clear will only clear the bitmap and recycle if required.
*/
private void clearImageInt() {
// if we allocated the bitmap, release it as fast as possible
if (mBitmap != null && (mImageResource > 0 || mLoadedImageUri != null)) {
mBitmap.recycle();
}
mBitmap = null;
// clean the loaded image flags for new image
mImageResource = 0;
mLoadedImageUri = null;
mLoadedSampleSize = 1;
mDegreesRotated = 0;
mZoom = 1;
mZoomOffsetX = 0;
mZoomOffsetY = 0;
mImageMatrix.reset();
mSaveInstanceStateBitmapUri = null;
mImageView.setImageBitmap(null);
setCropOverlayVisibility();
}
/**
* Gets the cropped image based on the current crop window.<br>
* If (reqWidth,reqHeight) is given AND image is loaded from URI cropping will try to use sample
* size to fit in the requested width and height down-sampling if possible - optimization to get
* best size to quality.<br>
* The result will be invoked to listener set by {@link
* #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
*
* @param reqWidth the width to resize the cropped image to (see options)
* @param reqHeight the height to resize the cropped image to (see options)
* @param options the resize method to use on the cropped bitmap
* @param saveUri optional: to save the cropped image to
* @param saveCompressFormat if saveUri is given, the given compression will be used for saving
* the image
* @param saveCompressQuality if saveUri is given, the given quality will be used for the
* compression.
*/
public void startCropWorkerTask(
int reqWidth,
int reqHeight,
RequestSizeOptions options,
Uri saveUri,
Bitmap.CompressFormat saveCompressFormat,
int saveCompressQuality) {
Bitmap bitmap = mBitmap;
if (bitmap != null) {
mImageView.clearAnimation();
BitmapCroppingWorkerTask currentTask =
mBitmapCroppingWorkerTask != null ? mBitmapCroppingWorkerTask.get() : null;
if (currentTask != null) {
// cancel previous cropping
currentTask.cancel(true);
}
reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;
int orgWidth = bitmap.getWidth() * mLoadedSampleSize;
int orgHeight = bitmap.getHeight() * mLoadedSampleSize;
if (mLoadedImageUri != null
&& (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
mBitmapCroppingWorkerTask =
new WeakReference<>(
new BitmapCroppingWorkerTask(
this,
mLoadedImageUri,
getCropPoints(),
mDegreesRotated,
orgWidth,
orgHeight,
mCropOverlayView.isFixAspectRatio(),
mCropOverlayView.getAspectRatioX(),
mCropOverlayView.getAspectRatioY(),
reqWidth,
reqHeight,
mFlipHorizontally,
mFlipVertically,
options,
saveUri,
saveCompressFormat,
saveCompressQuality));
} else {
mBitmapCroppingWorkerTask =
new WeakReference<>(
new BitmapCroppingWorkerTask(
this,
bitmap,
getCropPoints(),
mDegreesRotated,
mCropOverlayView.isFixAspectRatio(),
mCropOverlayView.getAspectRatioX(),
mCropOverlayView.getAspectRatioY(),
reqWidth,
reqHeight,
mFlipHorizontally,
mFlipVertically,
options,
saveUri,
saveCompressFormat,
saveCompressQuality));
}
mBitmapCroppingWorkerTask.get().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
setProgressBarVisibility();
}
}
@Override
public Parcelable onSaveInstanceState() {
if (mLoadedImageUri == null && mBitmap == null && mImageResource < 1) {
return super.onSaveInstanceState();
}
Bundle bundle = new Bundle();
Uri imageUri = mLoadedImageUri;
if (mSaveBitmapToInstanceState && imageUri == null && mImageResource < 1) {
mSaveInstanceStateBitmapUri =
imageUri =
BitmapUtils.writeTempStateStoreBitmap(
getContext(), mBitmap, mSaveInstanceStateBitmapUri);
}
if (imageUri != null && mBitmap != null) {
String key = UUID.randomUUID().toString();
BitmapUtils.mStateBitmap = new Pair<>(key, new WeakReference<>(mBitmap));
bundle.putString("LOADED_IMAGE_STATE_BITMAP_KEY", key);
}
if (mBitmapLoadingWorkerTask != null) {
BitmapLoadingWorkerTask task = mBitmapLoadingWorkerTask.get();
if (task != null) {
bundle.putParcelable("LOADING_IMAGE_URI", task.getUri());
}
}
bundle.putParcelable("instanceState", super.onSaveInstanceState());
bundle.putParcelable("LOADED_IMAGE_URI", imageUri);
bundle.putInt("LOADED_IMAGE_RESOURCE", mImageResource);
bundle.putInt("LOADED_SAMPLE_SIZE", mLoadedSampleSize);
bundle.putInt("DEGREES_ROTATED", mDegreesRotated);
bundle.putParcelable("INITIAL_CROP_RECT", mCropOverlayView.getInitialCropWindowRect());
BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());
mImageMatrix.invert(mImageInverseMatrix);
mImageInverseMatrix.mapRect(BitmapUtils.RECT);
bundle.putParcelable("CROP_WINDOW_RECT", BitmapUtils.RECT);
bundle.putString("CROP_SHAPE", mCropOverlayView.getCropShape().name());
bundle.putBoolean("CROP_AUTO_ZOOM_ENABLED", mAutoZoomEnabled);
bundle.putInt("CROP_MAX_ZOOM", mMaxZoom);
bundle.putBoolean("CROP_FLIP_HORIZONTALLY", mFlipHorizontally);
bundle.putBoolean("CROP_FLIP_VERTICALLY", mFlipVertically);
return bundle;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
// prevent restoring state if already set by outside code
if (mBitmapLoadingWorkerTask == null
&& mLoadedImageUri == null
&& mBitmap == null
&& mImageResource == 0) {
Uri uri = bundle.getParcelable("LOADED_IMAGE_URI");
if (uri != null) {
String key = bundle.getString("LOADED_IMAGE_STATE_BITMAP_KEY");
if (key != null) {
Bitmap stateBitmap =
BitmapUtils.mStateBitmap != null && BitmapUtils.mStateBitmap.first.equals(key)
? BitmapUtils.mStateBitmap.second.get()
: null;
BitmapUtils.mStateBitmap = null;
if (stateBitmap != null && !stateBitmap.isRecycled()) {
setBitmap(stateBitmap, 0, uri, bundle.getInt("LOADED_SAMPLE_SIZE"), 0);
}
}
if (mLoadedImageUri == null) {
setImageUriAsync(uri);
}
} else {
int resId = bundle.getInt("LOADED_IMAGE_RESOURCE");
if (resId > 0) {
setImageResource(resId);
} else {
uri = bundle.getParcelable("LOADING_IMAGE_URI");
if (uri != null) {
setImageUriAsync(uri);
}
}
}
mDegreesRotated = mRestoreDegreesRotated = bundle.getInt("DEGREES_ROTATED");
Rect initialCropRect = bundle.getParcelable("INITIAL_CROP_RECT");
if (initialCropRect != null
&& (initialCropRect.width() > 0 || initialCropRect.height() > 0)) {
mCropOverlayView.setInitialCropWindowRect(initialCropRect);
}
RectF cropWindowRect = bundle.getParcelable("CROP_WINDOW_RECT");
if (cropWindowRect != null && (cropWindowRect.width() > 0 || cropWindowRect.height() > 0)) {
mRestoreCropWindowRect = cropWindowRect;
}
mCropOverlayView.setCropShape(CropShape.valueOf(bundle.getString("CROP_SHAPE")));
mAutoZoomEnabled = bundle.getBoolean("CROP_AUTO_ZOOM_ENABLED");
mMaxZoom = bundle.getInt("CROP_MAX_ZOOM");
mFlipHorizontally = bundle.getBoolean("CROP_FLIP_HORIZONTALLY");
mFlipVertically = bundle.getBoolean("CROP_FLIP_VERTICALLY");
}
super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
} else {
super.onRestoreInstanceState(state);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (mBitmap != null) {
// Bypasses a baffling bug when used within a ScrollView, where heightSize is set to 0.
if (heightSize == 0) {
heightSize = mBitmap.getHeight();
}
int desiredWidth;
int desiredHeight;
double viewToBitmapWidthRatio = Double.POSITIVE_INFINITY;
double viewToBitmapHeightRatio = Double.POSITIVE_INFINITY;
// Checks if either width or height needs to be fixed
if (widthSize < mBitmap.getWidth()) {
viewToBitmapWidthRatio = (double) widthSize / (double) mBitmap.getWidth();
}
if (heightSize < mBitmap.getHeight()) {
viewToBitmapHeightRatio = (double) heightSize / (double) mBitmap.getHeight();
}
// If either needs to be fixed, choose smallest ratio and calculate from there
if (viewToBitmapWidthRatio != Double.POSITIVE_INFINITY
|| viewToBitmapHeightRatio != Double.POSITIVE_INFINITY) {
if (viewToBitmapWidthRatio <= viewToBitmapHeightRatio) {
desiredWidth = widthSize;
desiredHeight = (int) (mBitmap.getHeight() * viewToBitmapWidthRatio);
} else {
desiredHeight = heightSize;
desiredWidth = (int) (mBitmap.getWidth() * viewToBitmapHeightRatio);
}
} else {
// Otherwise, the picture is within frame layout bounds. Desired width is simply picture
// size
desiredWidth = mBitmap.getWidth();
desiredHeight = mBitmap.getHeight();
}
int width = getOnMeasureSpec(widthMode, widthSize, desiredWidth);
int height = getOnMeasureSpec(heightMode, heightSize, desiredHeight);
mLayoutWidth = width;
mLayoutHeight = height;
setMeasuredDimension(mLayoutWidth, mLayoutHeight);
} else {
setMeasuredDimension(widthSize, heightSize);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (mLayoutWidth > 0 && mLayoutHeight > 0) {
// Gets original parameters, and creates the new parameters
ViewGroup.LayoutParams origParams = this.getLayoutParams();
origParams.width = mLayoutWidth;
origParams.height = mLayoutHeight;
setLayoutParams(origParams);
if (mBitmap != null) {
applyImageMatrix(r - l, b - t, true, false);
// after state restore we want to restore the window crop, possible only after widget size
// is known
if (mRestoreCropWindowRect != null) {
if (mRestoreDegreesRotated != mInitialDegreesRotated) {
mDegreesRotated = mRestoreDegreesRotated;
applyImageMatrix(r - l, b - t, true, false);
}
mImageMatrix.mapRect(mRestoreCropWindowRect);
mCropOverlayView.setCropWindowRect(mRestoreCropWindowRect);
handleCropWindowChanged(false, false);
mCropOverlayView.fixCurrentCropWindowRect();
mRestoreCropWindowRect = null;
} else if (mSizeChanged) {
mSizeChanged = false;
handleCropWindowChanged(false, false);
}
} else {
updateImageBounds(true);
}
} else {
updateImageBounds(true);
}
}
/**
* Detect size change to handle auto-zoom using {@link #handleCropWindowChanged(boolean, boolean)}
* in {@link #layout(int, int, int, int)}.
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mSizeChanged = oldw > 0 && oldh > 0;
}
/**
* Handle crop window change to:<br>
* 1. Execute auto-zoom-in/out depending on the area covered of cropping window relative to the
* available view area.<br>
* 2. Slide the zoomed sub-area if the cropping window is outside of the visible view sub-area.
* <br>
*
* @param inProgress is the crop window change is still in progress by the user
* @param animate if to animate the change to the image matrix, or set it directly
*/
private void handleCropWindowChanged(boolean inProgress, boolean animate) {
int width = getWidth();
int height = getHeight();
if (mBitmap != null && width > 0 && height > 0) {
RectF cropRect = mCropOverlayView.getCropWindowRect();
if (inProgress) {
if (cropRect.left < 0
|| cropRect.top < 0
|| cropRect.right > width
|| cropRect.bottom > height) {
applyImageMatrix(width, height, false, false);
}
} else if (mAutoZoomEnabled || mZoom > 1) {
float newZoom = 0;
// keep the cropping window covered area to 50%-65% of zoomed sub-area
if (mZoom < mMaxZoom
&& cropRect.width() < width * 0.5f
&& cropRect.height() < height * 0.5f) {
newZoom =
Math.min(
mMaxZoom,
Math.min(
width / (cropRect.width() / mZoom / 0.64f),
height / (cropRect.height() / mZoom / 0.64f)));
}
if (mZoom > 1 && (cropRect.width() > width * 0.65f || cropRect.height() > height * 0.65f)) {
newZoom =
Math.max(
1,
Math.min(
width / (cropRect.width() / mZoom / 0.51f),
height / (cropRect.height() / mZoom / 0.51f)));
}
if (!mAutoZoomEnabled) {
newZoom = 1;
}
if (newZoom > 0 && newZoom != mZoom) {
if (animate) {
if (mAnimation == null) {
// lazy create animation single instance
mAnimation = new CropImageAnimation(mImageView, mCropOverlayView);
}
// set the state for animation to start from
mAnimation.setStartState(mImagePoints, mImageMatrix);
}
mZoom = newZoom;
applyImageMatrix(width, height, true, animate);
}
}
if (mOnSetCropWindowChangeListener != null && !inProgress) {
mOnSetCropWindowChangeListener.onCropWindowChanged();
}
}
}
/**
* Apply matrix to handle the image inside the image view.
*
* @param width the width of the image view
* @param height the height of the image view
*/
private void applyImageMatrix(float width, float height, boolean center, boolean animate) {
if (mBitmap != null && width > 0 && height > 0) {
mImageMatrix.invert(mImageInverseMatrix);
RectF cropRect = mCropOverlayView.getCropWindowRect();
mImageInverseMatrix.mapRect(cropRect);
mImageMatrix.reset();
// move the image to the center of the image view first so we can manipulate it from there
mImageMatrix.postTranslate(
(width - mBitmap.getWidth()) / 2, (height - mBitmap.getHeight()) / 2);
mapImagePointsByImageMatrix();
// rotate the image the required degrees from center of image
if (mDegreesRotated > 0) {
mImageMatrix.postRotate(
mDegreesRotated,
BitmapUtils.getRectCenterX(mImagePoints),
BitmapUtils.getRectCenterY(mImagePoints));
mapImagePointsByImageMatrix();
}
// scale the image to the image view, image rect transformed to know new width/height
float scale =
Math.min(
width / BitmapUtils.getRectWidth(mImagePoints),
height / BitmapUtils.getRectHeight(mImagePoints));
if (mScaleType == ScaleType.FIT_CENTER
|| (mScaleType == ScaleType.CENTER_INSIDE && scale < 1)
|| (scale > 1 && mAutoZoomEnabled)) {
mImageMatrix.postScale(
scale,
scale,
BitmapUtils.getRectCenterX(mImagePoints),
BitmapUtils.getRectCenterY(mImagePoints));
mapImagePointsByImageMatrix();
}
// scale by the current zoom level
float scaleX = mFlipHorizontally ? -mZoom : mZoom;
float scaleY = mFlipVertically ? -mZoom : mZoom;
mImageMatrix.postScale(
scaleX,
scaleY,
BitmapUtils.getRectCenterX(mImagePoints),
BitmapUtils.getRectCenterY(mImagePoints));
mapImagePointsByImageMatrix();
mImageMatrix.mapRect(cropRect);
if (center) {
// set the zoomed area to be as to the center of cropping window as possible
mZoomOffsetX =
width > BitmapUtils.getRectWidth(mImagePoints)
? 0
: Math.max(
Math.min(
width / 2 - cropRect.centerX(), -BitmapUtils.getRectLeft(mImagePoints)),
getWidth() - BitmapUtils.getRectRight(mImagePoints))
/ scaleX;
mZoomOffsetY =
height > BitmapUtils.getRectHeight(mImagePoints)
? 0
: Math.max(
Math.min(
height / 2 - cropRect.centerY(), -BitmapUtils.getRectTop(mImagePoints)),
getHeight() - BitmapUtils.getRectBottom(mImagePoints))
/ scaleY;
} else {
// adjust the zoomed area so the crop window rectangle will be inside the area in case it
// was moved outside
mZoomOffsetX =
Math.min(Math.max(mZoomOffsetX * scaleX, -cropRect.left), -cropRect.right + width)
/ scaleX;
mZoomOffsetY =
Math.min(Math.max(mZoomOffsetY * scaleY, -cropRect.top), -cropRect.bottom + height)
/ scaleY;
}
// apply to zoom offset translate and update the crop rectangle to offset correctly
mImageMatrix.postTranslate(mZoomOffsetX * scaleX, mZoomOffsetY * scaleY);
cropRect.offset(mZoomOffsetX * scaleX, mZoomOffsetY * scaleY);
mCropOverlayView.setCropWindowRect(cropRect);
mapImagePointsByImageMatrix();
mCropOverlayView.invalidate();
// set matrix to apply
if (animate) {
// set the state for animation to end in, start animation now
mAnimation.setEndState(mImagePoints, mImageMatrix);
mImageView.startAnimation(mAnimation);
} else {
mImageView.setImageMatrix(mImageMatrix);
}
// update the image rectangle in the crop overlay
updateImageBounds(false);
}
}
/**
* Adjust the given image rectangle by image transformation matrix to know the final rectangle of
* the image.<br>
* To get the proper rectangle it must be first reset to original image rectangle.
*/
private void mapImagePointsByImageMatrix() {
mImagePoints[0] = 0;
mImagePoints[1] = 0;
mImagePoints[2] = mBitmap.getWidth();
mImagePoints[3] = 0;
mImagePoints[4] = mBitmap.getWidth();
mImagePoints[5] = mBitmap.getHeight();
mImagePoints[6] = 0;
mImagePoints[7] = mBitmap.getHeight();
mImageMatrix.mapPoints(mImagePoints);
mScaleImagePoints[0] = 0;
mScaleImagePoints[1] = 0;
mScaleImagePoints[2] = 100;
mScaleImagePoints[3] = 0;
mScaleImagePoints[4] = 100;
mScaleImagePoints[5] = 100;
mScaleImagePoints[6] = 0;
mScaleImagePoints[7] = 100;
mImageMatrix.mapPoints(mScaleImagePoints);
}
/**
* Set visibility of crop overlay to hide it when there is no image or specificly set by client.
*/
private void setCropOverlayVisibility() {
if (mCropOverlayView != null) {
mCropOverlayView.setVisibility(mShowCropOverlay && mBitmap != null ? VISIBLE : INVISIBLE);
}
}
/**
* Set visibility of progress bar when async loading/cropping is in process and show is enabled.
*/
private void setProgressBarVisibility() {
boolean visible =
mShowProgressBar
&& (mBitmap == null && mBitmapLoadingWorkerTask != null
|| mBitmapCroppingWorkerTask != null);
mProgressBar.setVisibility(visible ? VISIBLE : INVISIBLE);
}
/**
* Update the scale factor between the actual image bitmap and the shown image.<br>
*/
private void updateImageBounds(boolean clear) {
if (mBitmap != null && !clear) {
// Get the scale factor between the actual Bitmap dimensions and the displayed dimensions for
// width/height.
float scaleFactorWidth =
100f * mLoadedSampleSize / BitmapUtils.getRectWidth(mScaleImagePoints);
float scaleFactorHeight =
100f * mLoadedSampleSize / BitmapUtils.getRectHeight(mScaleImagePoints);
mCropOverlayView.setCropWindowLimits(
getWidth(), getHeight(), scaleFactorWidth, scaleFactorHeight);
}
// set the bitmap rectangle and update the crop window after scale factor is set
mCropOverlayView.setBounds(clear ? null : mImagePoints, getWidth(), getHeight());
}
// endregion
// region: Inner class: CropShape
/**
* The possible cropping area shape.<br>
* To set square/circle crop shape set aspect ratio to 1:1.
*/
public enum CropShape {
RECTANGLE,
OVAL
}
// endregion
// region: Inner class: ScaleType
/**
* Options for scaling the bounds of cropping image to the bounds of Crop Image View.<br>
* Note: Some options are affected by auto-zoom, if enabled.
*/
public enum ScaleType {
/**
* Scale the image uniformly (maintain the image's aspect ratio) to fit in crop image view.<br>
* The largest dimension will be equals to crop image view and the second dimension will be
* smaller.
*/
FIT_CENTER,
/**
* Center the image in the view, but perform no scaling.<br>
* Note: If auto-zoom is enabled and the source image is smaller than crop image view then it
* will be scaled uniformly to fit the crop image view.
*/
CENTER,
/**
* Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
* and height) of the image will be equal to or <b>larger</b> than the corresponding dimension
* of the view (minus padding).<br>
* The image is then centered in the view.
*/
CENTER_CROP,
/**
* Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
* and height) of the image will be equal to or <b>less</b> than the corresponding dimension of
* the view (minus padding).<br>
* The image is then centered in the view.<br>
* Note: If auto-zoom is enabled and the source image is smaller than crop image view then it
* will be scaled uniformly to fit the crop image view.
*/
CENTER_INSIDE
}
// endregion
// region: Inner class: Guidelines
/**
* The possible guidelines showing types.
*/
public enum Guidelines {
/**
* Never show
*/
OFF,
/**
* Show when crop move action is live
*/
ON_TOUCH,
/**
* Always show
*/
ON
}
// endregion
// region: Inner class: RequestSizeOptions
/**
* Possible options for handling requested width/height for cropping.
*/
public enum RequestSizeOptions {
/**
* No resize/sampling is done unless required for memory management (OOM).
*/
NONE,
/**
* Only sample the image during loading (if image set using URI) so the smallest of the image
* dimensions will be between the requested size and x2 requested size.<br>
* NOTE: resulting image will not be exactly requested width/height see: <a
* href="http://developer.android.com/training/displaying-bitmaps/load-bitmap.html">Loading
* Large Bitmaps Efficiently</a>.
*/
SAMPLING,
/**
* Resize the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
* and height) of the image will be equal to or <b>less</b> than the corresponding requested
* dimension.<br>
* If the image is smaller than the requested size it will NOT change.
*/
RESIZE_INSIDE,
/**
* Resize the image uniformly (maintain the image's aspect ratio) to fit in the given
* width/height.<br>
* The largest dimension will be equals to the requested and the second dimension will be
* smaller.<br>
* If the image is smaller than the requested size it will enlarge it.
*/
RESIZE_FIT,
/**
* Resize the image to fit exactly in the given width/height.<br>
* This resize method does NOT preserve aspect ratio.<br>
* If the image is smaller than the requested size it will enlarge it.
*/
RESIZE_EXACT
}
// endregion
// region: Inner class: OnSetImageUriCompleteListener
/**
* Interface definition for a callback to be invoked when the crop overlay is released.
*/
public interface OnSetCropOverlayReleasedListener {
/**
* Called when the crop overlay changed listener is called and inProgress is false.
*
* @param rect The rect coordinates of the cropped overlay
*/
void onCropOverlayReleased(Rect rect);
}
/**
* Interface definition for a callback to be invoked when the crop overlay is released.
*/
public interface OnSetCropOverlayMovedListener {
/**
* Called when the crop overlay is moved
*
* @param rect The rect coordinates of the cropped overlay
*/
void onCropOverlayMoved(Rect rect);
}
/**
* Interface definition for a callback to be invoked when the crop overlay is released.
*/
public interface OnSetCropWindowChangeListener {
/**
* Called when the crop window is changed
*/
void onCropWindowChanged();
}
/**
* Interface definition for a callback to be invoked when image async loading is complete.
*/
public interface OnSetImageUriCompleteListener {
/**
* Called when a crop image view has completed loading image for cropping.<br>
* If loading failed error parameter will contain the error.
*
* @param view The crop image view that loading of image was complete.
* @param uri the URI of the image that was loading
* @param error if error occurred during loading will contain the error, otherwise null.
*/
void onSetImageUriComplete(CropImageView view, Uri uri, Exception error);
}
// endregion
// region: Inner class: OnGetCroppedImageCompleteListener
/**
* Interface definition for a callback to be invoked when image async crop is complete.
*/
public interface OnCropImageCompleteListener {
/**
* Called when a crop image view has completed cropping image.<br>
* Result object contains the cropped bitmap, saved cropped image uri, crop points data or the
* error occured during cropping.
*
* @param view The crop image view that cropping of image was complete.
* @param result the crop image result data (with cropped image or error)
*/
void onCropImageComplete(CropImageView view, CropResult result);
}
// endregion
// region: Inner class: ActivityResult
/**
* Result data of crop image.
*/
public static class CropResult {
/**
* The image bitmap of the original image loaded for cropping.<br>
* Null if uri used to load image or activity result is used.
*/
private final Bitmap mOriginalBitmap;
/**
* The Android uri of the original image loaded for cropping.<br>
* Null if bitmap was used to load image.
*/
private final Uri mOriginalUri;
/**
* The cropped image bitmap result.<br>
* Null if save cropped image was executed, no output requested or failure.
*/
private final Bitmap mBitmap;
/**
* The Android uri of the saved cropped image result.<br>
* Null if get cropped image was executed, no output requested or failure.
*/
private final Uri mUri;
/**
* The error that failed the loading/cropping (null if successful)
*/
private final Exception mError;
/**
* The 4 points of the cropping window in the source image
*/
private final float[] mCropPoints;
/**
* The rectangle of the cropping window in the source image
*/
private final Rect mCropRect;
/**
* The rectangle of the source image dimensions
*/
private final Rect mWholeImageRect;
/**
* The final rotation of the cropped image relative to source
*/
private final int mRotation;
/**
* sample size used creating the crop bitmap to lower its size
*/
private final int mSampleSize;
CropResult(
Bitmap originalBitmap,
Uri originalUri,
Bitmap bitmap,
Uri uri,
Exception error,
float[] cropPoints,
Rect cropRect,
Rect wholeImageRect,
int rotation,
int sampleSize) {
mOriginalBitmap = originalBitmap;
mOriginalUri = originalUri;
mBitmap = bitmap;
mUri = uri;
mError = error;
mCropPoints = cropPoints;
mCropRect = cropRect;
mWholeImageRect = wholeImageRect;
mRotation = rotation;
mSampleSize = sampleSize;
}
/**
* The image bitmap of the original image loaded for cropping.<br>
* Null if uri used to load image or activity result is used.
*/
public Bitmap getOriginalBitmap() {
return mOriginalBitmap;
}
/**
* The Android uri of the original image loaded for cropping.<br>
* Null if bitmap was used to load image.
*/
public Uri getOriginalUri() {
return mOriginalUri;
}
/**
* Is the result is success or error.
*/
public boolean isSuccessful() {
return mError == null;
}
/**
* The cropped image bitmap result.<br>
* Null if save cropped image was executed, no output requested or failure.
*/
public Bitmap getBitmap() {
return mBitmap;
}
/**
* The Android uri of the saved cropped image result Null if get cropped image was executed, no
* output requested or failure.
*/
public Uri getUri() {
return mUri;
}
/**
* The error that failed the loading/cropping (null if successful)
*/
public Exception getError() {
return mError;
}
/**
* The 4 points of the cropping window in the source image
*/
public float[] getCropPoints() {
return mCropPoints;
}
/**
* The rectangle of the cropping window in the source image
*/
public Rect getCropRect() {
return mCropRect;
}
/**
* The rectangle of the source image dimensions
*/
public Rect getWholeImageRect() {
return mWholeImageRect;
}
/**
* The final rotation of the cropped image relative to source
*/
public int getRotation() {
return mRotation;
}
/**
* sample size used creating the crop bitmap to lower its size
*/
public int getSampleSize() {
return mSampleSize;
}
}
// endregion
}