// "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.
* It is best to avoid it by using URI in setting image for cropping.
* 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.
* 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.
* default: true, disable to provide custom progress bar UI. */ private boolean mShowProgressBar = true; /** * if auto-zoom functionality is enabled.
* 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 mBitmapLoadingWorkerTask; /** * Task used to crop bitmap async from UI thread */ private WeakReference 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.
* 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).
*/ 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).
*/ public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) { mCropOverlayView.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight); } /** * Get the amount of degrees the cropping image is rotated cloackwise.
* * @return 0-360 */ public int getRotatedDegrees() { return mDegreesRotated; } /** * Set the amount of degrees the cropping image is rotated cloackwise.
* * @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 getAspectRatio() { return new Pair<>(mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY()); } /** * Sets the both the X and Y values of the aspectRatio.
* 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.
* 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.
* 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.
* 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.
* 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.
* It is best to avoid it by using URI in setting image for cropping.
* 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.
* It is best to avoid it by using URI in setting image for cropping.
* 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.
* 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.
* 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.
* 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.
* * @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.
* 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.
* Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
* 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.
* 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.
* Uses JPEG image compression with 90 compression quality.
* 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.
* 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.
* Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
* 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.
* 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.
*
* The EXIF can be retrieved by doing the following: * ExifInterface exif = new ExifInterface(path); * * @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.
* Can be used with URI from gallery or camera source.
* Will rotate the image by exif data.
* * @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.
* 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
* 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.
* 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.
* 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.
* 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:
* 1. Execute auto-zoom-in/out depending on the area covered of cropping window relative to the * available view area.
* 2. Slide the zoomed sub-area if the cropping window is outside of the visible view sub-area. *
* * @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.
* 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.
*/ 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.
* 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.
* 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.
* 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.
* 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 larger than the corresponding dimension * of the view (minus padding).
* 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 less than the corresponding dimension of * the view (minus padding).
* The image is then centered in the view.
* 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.
* NOTE: resulting image will not be exactly requested width/height see: Loading * Large Bitmaps Efficiently. */ 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 less than the corresponding requested * dimension.
* 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.
* The largest dimension will be equals to the requested and the second dimension will be * smaller.
* 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.
* This resize method does NOT preserve aspect ratio.
* 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.
* 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.
* 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.
* 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.
* Null if bitmap was used to load image. */ private final Uri mOriginalUri; /** * The cropped image bitmap result.
* 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.
* 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.
* 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.
* 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.
* 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 }