diff --git a/build.gradle b/build.gradle index b31e2b7f..6959fc9b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.0' + classpath 'com.android.tools.build:gradle:3.1.4' } } @@ -18,7 +18,7 @@ allprojects { } ext { - compileSdkVersion = 27 + compileSdkVersion = 28 buildToolsVersion = '27.0.3' supportLibraryVersion = '27.1.1' diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropOverlayView.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropOverlayView.java index 51abac43..8347707c 100644 --- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropOverlayView.java +++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropOverlayView.java @@ -29,1004 +29,1124 @@ import java.util.Arrays; -/** A custom View representing the crop window and the shaded background outside the crop window. */ +/** + * A custom View representing the crop window and the shaded background outside the crop window. + */ public class CropOverlayView extends View { - // region: Fields and Consts + // region: Fields and Consts - /** Gesture detector used for multi touch box scaling */ - private ScaleGestureDetector mScaleDetector; + /** + * Gesture detector used for multi touch box scaling + */ + private ScaleGestureDetector mScaleDetector; - /** Boolean to see if multi touch is enabled for the crop rectangle */ - private boolean mMultiTouchEnabled; + /** + * Boolean to see if multi touch is enabled for the crop rectangle + */ + private boolean mMultiTouchEnabled; - /** Handler from crop window stuff, moving and knowing possition. */ - private final CropWindowHandler mCropWindowHandler = new CropWindowHandler(); + /** + * Handler from crop window stuff, moving and knowing possition. + */ + private final CropWindowHandler mCropWindowHandler = new CropWindowHandler(); - /** Listener to publicj crop window changes */ - private CropWindowChangeListener mCropWindowChangeListener; + /** + * Listener to publicj crop window changes + */ + private CropWindowChangeListener mCropWindowChangeListener; - /** Rectangle used for drawing */ - private final RectF mDrawRect = new RectF(); + /** + * Rectangle used for drawing + */ + private final RectF mDrawRect = new RectF(); - /** The Paint used to draw the white rectangle around the crop area. */ - private Paint mBorderPaint; + /** + * The Paint used to draw the white rectangle around the crop area. + */ + private Paint mBorderPaint; - /** The Paint used to draw the corners of the Border */ - private Paint mBorderCornerPaint; + /** + * The Paint used to draw the corners of the Border + */ + private Paint mBorderCornerPaint; - /** The Paint used to draw the guidelines within the crop area when pressed. */ - private Paint mGuidelinePaint; + /** + * The Paint used to draw the guidelines within the crop area when pressed. + */ + private Paint mGuidelinePaint; - /** The Paint used to darken the surrounding areas outside the crop area. */ - private Paint mBackgroundPaint; + /** + * The Paint used to darken the surrounding areas outside the crop area. + */ + private Paint mBackgroundPaint; - /** Used for oval crop window shape or non-straight rotation drawing. */ - private Path mPath = new Path(); + /** + * Used for oval crop window shape or non-straight rotation drawing. + */ + private Path mPath = new Path(); - /** The bounding box around the Bitmap that we are cropping. */ - private final float[] mBoundsPoints = new float[8]; + /** + * The bounding box around the Bitmap that we are cropping. + */ + private final float[] mBoundsPoints = new float[8]; - /** The bounding box around the Bitmap that we are cropping. */ - private final RectF mCalcBounds = new RectF(); + /** + * The bounding box around the Bitmap that we are cropping. + */ + private final RectF mCalcBounds = new RectF(); - /** The bounding image view width used to know the crop overlay is at view edges. */ - private int mViewWidth; + /** + * The bounding image view width used to know the crop overlay is at view edges. + */ + private int mViewWidth; - /** The bounding image view height used to know the crop overlay is at view edges. */ - private int mViewHeight; + /** + * The bounding image view height used to know the crop overlay is at view edges. + */ + private int mViewHeight; - /** The offset to draw the border corener from the border */ - private float mBorderCornerOffset; + /** + * The offset to draw the border corener from the border + */ + private float mBorderCornerOffset; - /** the length of the border corner to draw */ - private float mBorderCornerLength; + /** + * the length of the border corner to draw + */ + private float mBorderCornerLength; - /** The initial crop window padding from image borders */ - private float mInitialCropWindowPaddingRatio; - - /** The radius of the touch zone (in pixels) around a given Handle. */ - private float mTouchRadius; - - /** - * 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. - */ - private float mSnapRadius; - - /** The Handle that is currently pressed; null if no Handle is pressed. */ - private CropWindowMoveHandler mMoveHandler; - - /** - * Flag indicating if the crop area should always be a certain aspect ratio (indicated by - * mTargetAspectRatio). - */ - private boolean mFixAspectRatio; - - /** save the current aspect ratio of the image */ - private int mAspectRatioX; - - /** save the current aspect ratio of the image */ - private int mAspectRatioY; - - /** - * The aspect ratio that the crop area should maintain; this variable is only used when - * mMaintainAspectRatio is true. - */ - private float mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY; - - /** Instance variables for customizable attributes */ - private CropImageView.Guidelines mGuidelines; - - /** The shape of the cropping area - rectangle/circular. */ - private CropImageView.CropShape mCropShape; - - /** the initial crop window rectangle to set */ - private final Rect mInitialCropWindowRect = new Rect(); - - /** Whether the Crop View has been initialized for the first time */ - private boolean initializedCropWindow; - - /** Used to set back LayerType after changing to software. */ - private Integer mOriginalLayerType; - // endregion - - public CropOverlayView(Context context) { - this(context, null); - } - - public CropOverlayView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - /** Set the crop window change listener. */ - public void setCropWindowChangeListener(CropWindowChangeListener listener) { - mCropWindowChangeListener = listener; - } - - /** Get the left/top/right/bottom coordinates of the crop window. */ - public RectF getCropWindowRect() { - return mCropWindowHandler.getRect(); - } - - /** Set the left/top/right/bottom coordinates of the crop window. */ - public void setCropWindowRect(RectF rect) { - mCropWindowHandler.setRect(rect); - } - - /** Fix the current crop window rectangle if it is outside of cropping image or view bounds. */ - public void fixCurrentCropWindowRect() { - RectF rect = getCropWindowRect(); - fixCropWindowRectByRules(rect); - mCropWindowHandler.setRect(rect); - } - - /** - * Informs the CropOverlayView of the image's position relative to the ImageView. This is - * necessary to call in order to draw the crop window. - * - * @param boundsPoints the image's bounding points - * @param viewWidth The bounding image view width. - * @param viewHeight The bounding image view height. - */ - public void setBounds(float[] boundsPoints, int viewWidth, int viewHeight) { - if (boundsPoints == null || !Arrays.equals(mBoundsPoints, boundsPoints)) { - if (boundsPoints == null) { - Arrays.fill(mBoundsPoints, 0); - } else { - System.arraycopy(boundsPoints, 0, mBoundsPoints, 0, boundsPoints.length); - } - mViewWidth = viewWidth; - mViewHeight = viewHeight; - RectF cropRect = mCropWindowHandler.getRect(); - if (cropRect.width() == 0 || cropRect.height() == 0) { - initCropWindow(); - } + /** + * The initial crop window padding from image borders + */ + private float mInitialCropWindowPaddingRatio; + + /** + * The radius of the touch zone (in pixels) around a given Handle. + */ + private float mTouchRadius; + + /** + * 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. + */ + private float mSnapRadius; + + /** + * The Handle that is currently pressed; null if no Handle is pressed. + */ + private CropWindowMoveHandler mMoveHandler; + + /** + * Flag indicating if the crop area should always be a certain aspect ratio (indicated by + * mTargetAspectRatio). + */ + private boolean mFixAspectRatio; + + /** + * save the current aspect ratio of the image + */ + private int mAspectRatioX; + + /** + * save the current aspect ratio of the image + */ + private int mAspectRatioY; + + /** + * The aspect ratio that the crop area should maintain; this variable is only used when + * mMaintainAspectRatio is true. + */ + private float mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY; + + /** + * Instance variables for customizable attributes + */ + private CropImageView.Guidelines mGuidelines; + + /** + * The shape of the cropping area - rectangle/circular. + */ + private CropImageView.CropShape mCropShape; + + /** + * the initial crop window rectangle to set + */ + private final Rect mInitialCropWindowRect = new Rect(); + + /** + * Whether the Crop View has been initialized for the first time + */ + private boolean initializedCropWindow; + + /** + * Used to set back LayerType after changing to software. + */ + private Integer mOriginalLayerType; + // endregion + + public CropOverlayView(Context context) { + this(context, null); } - } - - /** Resets the crop overlay view. */ - public void resetCropOverlayView() { - if (initializedCropWindow) { - setCropWindowRect(BitmapUtils.EMPTY_RECT_F); - initCropWindow(); - invalidate(); + + public CropOverlayView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * Set the crop window change listener. + */ + public void setCropWindowChangeListener(CropWindowChangeListener listener) { + mCropWindowChangeListener = listener; } - } - - /** The shape of the cropping area - rectangle/circular. */ - public CropImageView.CropShape getCropShape() { - return mCropShape; - } - - /** The shape of the cropping area - rectangle/circular. */ - public void setCropShape(CropImageView.CropShape cropShape) { - if (mCropShape != cropShape) { - mCropShape = cropShape; - if (Build.VERSION.SDK_INT <= 17) { - if (mCropShape == CropImageView.CropShape.OVAL) { - mOriginalLayerType = getLayerType(); - if (mOriginalLayerType != View.LAYER_TYPE_SOFTWARE) { - // TURN off hardware acceleration - setLayerType(View.LAYER_TYPE_SOFTWARE, null); - } else { - mOriginalLayerType = null; - } - } else if (mOriginalLayerType != null) { - // return hardware acceleration back - setLayerType(mOriginalLayerType, null); - mOriginalLayerType = null; + + /** + * Get the left/top/right/bottom coordinates of the crop window. + */ + public RectF getCropWindowRect() { + return mCropWindowHandler.getRect(); + } + + /** + * Set the left/top/right/bottom coordinates of the crop window. + */ + public void setCropWindowRect(RectF rect) { + mCropWindowHandler.setRect(rect); + } + + /** + * Fix the current crop window rectangle if it is outside of cropping image or view bounds. + */ + public void fixCurrentCropWindowRect() { + RectF rect = getCropWindowRect(); + fixCropWindowRectByRules(rect); + mCropWindowHandler.setRect(rect); + } + + /** + * Informs the CropOverlayView of the image's position relative to the ImageView. This is + * necessary to call in order to draw the crop window. + * + * @param boundsPoints the image's bounding points + * @param viewWidth The bounding image view width. + * @param viewHeight The bounding image view height. + */ + public void setBounds(float[] boundsPoints, int viewWidth, int viewHeight) { + if (boundsPoints == null || !Arrays.equals(mBoundsPoints, boundsPoints)) { + if (boundsPoints == null) { + Arrays.fill(mBoundsPoints, 0); + } else { + System.arraycopy(boundsPoints, 0, mBoundsPoints, 0, boundsPoints.length); + } + mViewWidth = viewWidth; + mViewHeight = viewHeight; + RectF cropRect = mCropWindowHandler.getRect(); + if (cropRect.width() == 0 || cropRect.height() == 0) { + initCropWindow(); + } } - } - invalidate(); } - } - - /** Get the current guidelines option set. */ - public CropImageView.Guidelines getGuidelines() { - return mGuidelines; - } - - /** - * Sets the guidelines for the CropOverlayView to be either on, off, or to show when resizing the - * application. - */ - public void setGuidelines(CropImageView.Guidelines guidelines) { - if (mGuidelines != guidelines) { - mGuidelines = guidelines; - if (initializedCropWindow) { - invalidate(); - } + + /** + * Resets the crop overlay view. + */ + public void resetCropOverlayView() { + if (initializedCropWindow) { + setCropWindowRect(BitmapUtils.EMPTY_RECT_F); + initCropWindow(); + invalidate(); + } } - } - - /** - * whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to - * be changed. - */ - public boolean isFixAspectRatio() { - return mFixAspectRatio; - } - - /** - * 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) { - if (mFixAspectRatio != fixAspectRatio) { - mFixAspectRatio = fixAspectRatio; - if (initializedCropWindow) { - initCropWindow(); - invalidate(); - } + + /** + * The shape of the cropping area - rectangle/circular. + */ + public CropImageView.CropShape getCropShape() { + return mCropShape; } - } - - /** the X value of the aspect ratio; */ - public int getAspectRatioX() { - return mAspectRatioX; - } - - /** Sets the X value of the aspect ratio; is defaulted to 1. */ - public void setAspectRatioX(int aspectRatioX) { - if (aspectRatioX <= 0) { - throw new IllegalArgumentException( - "Cannot set aspect ratio value to a number less than or equal to 0."); - } else if (mAspectRatioX != aspectRatioX) { - mAspectRatioX = aspectRatioX; - mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY; - - if (initializedCropWindow) { - initCropWindow(); - invalidate(); - } + + /** + * The shape of the cropping area - rectangle/circular. + */ + public void setCropShape(CropImageView.CropShape cropShape) { + if (mCropShape != cropShape) { + mCropShape = cropShape; + if (Build.VERSION.SDK_INT <= 17) { + if (mCropShape == CropImageView.CropShape.OVAL) { + mOriginalLayerType = getLayerType(); + if (mOriginalLayerType != View.LAYER_TYPE_SOFTWARE) { + // TURN off hardware acceleration + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } else { + mOriginalLayerType = null; + } + } else if (mOriginalLayerType != null) { + // return hardware acceleration back + setLayerType(mOriginalLayerType, null); + mOriginalLayerType = null; + } + } + invalidate(); + } } - } - - /** the Y value of the aspect ratio; */ - public int getAspectRatioY() { - return mAspectRatioY; - } - - /** - * Sets the Y value of the aspect ratio; is defaulted to 1. - * - * @param aspectRatioY int that specifies the new Y value of the aspect ratio - */ - public void setAspectRatioY(int aspectRatioY) { - if (aspectRatioY <= 0) { - throw new IllegalArgumentException( - "Cannot set aspect ratio value to a number less than or equal to 0."); - } else if (mAspectRatioY != aspectRatioY) { - mAspectRatioY = aspectRatioY; - mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY; - - if (initializedCropWindow) { - initCropWindow(); - invalidate(); - } + + /** + * Get the current guidelines option set. + */ + public CropImageView.Guidelines getGuidelines() { + return mGuidelines; } - } - - /** - * 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: 3) - */ - public void setSnapRadius(float snapRadius) { - mSnapRadius = snapRadius; - } - - /** Set multi touch functionality to enabled/disabled. */ - public boolean setMultiTouchEnabled(boolean multiTouchEnabled) { - if (mMultiTouchEnabled != multiTouchEnabled) { - mMultiTouchEnabled = multiTouchEnabled; - if (mMultiTouchEnabled && mScaleDetector == null) { - mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener()); - } - return true; + + /** + * Sets the guidelines for the CropOverlayView to be either on, off, or to show when resizing the + * application. + */ + public void setGuidelines(CropImageView.Guidelines guidelines) { + if (mGuidelines != guidelines) { + mGuidelines = guidelines; + if (initializedCropWindow) { + invalidate(); + } + } } - return false; - } - - /** - * 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) { - mCropWindowHandler.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) { - mCropWindowHandler.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight); - } - - /** - * set the max width/height and scale factor of the shown image to original image to scale the - * limits appropriately. - */ - public void setCropWindowLimits( - float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) { - mCropWindowHandler.setCropWindowLimits( - maxWidth, maxHeight, scaleFactorWidth, scaleFactorHeight); - } - - /** Get crop window initial rectangle. */ - public Rect getInitialCropWindowRect() { - return mInitialCropWindowRect; - } - - /** Set crop window initial rectangle to be used instead of default. */ - public void setInitialCropWindowRect(Rect rect) { - mInitialCropWindowRect.set(rect != null ? rect : BitmapUtils.EMPTY_RECT); - if (initializedCropWindow) { - initCropWindow(); - invalidate(); - callOnCropWindowChanged(false); + + /** + * whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to + * be changed. + */ + public boolean isFixAspectRatio() { + return mFixAspectRatio; } - } - - /** Reset crop window to initial rectangle. */ - public void resetCropWindowRect() { - if (initializedCropWindow) { - initCropWindow(); - invalidate(); - callOnCropWindowChanged(false); + + /** + * 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) { + if (mFixAspectRatio != fixAspectRatio) { + mFixAspectRatio = fixAspectRatio; + if (initializedCropWindow) { + initCropWindow(); + invalidate(); + } + } } - } - /** - * Sets all initial values, but does not call initCropWindow to reset the views.
- * Used once at the very start to initialize the attributes. - */ - public void setInitialAttributeValues(CropImageOptions options) { + /** + * the X value of the aspect ratio; + */ + public int getAspectRatioX() { + return mAspectRatioX; + } - mCropWindowHandler.setInitialAttributeValues(options); + /** + * Sets the X value of the aspect ratio; is defaulted to 1. + */ + public void setAspectRatioX(int aspectRatioX) { + if (aspectRatioX <= 0) { + throw new IllegalArgumentException( + "Cannot set aspect ratio value to a number less than or equal to 0."); + } else if (mAspectRatioX != aspectRatioX) { + mAspectRatioX = aspectRatioX; + mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY; + + if (initializedCropWindow) { + initCropWindow(); + invalidate(); + } + } + } - setCropShape(options.cropShape); + /** + * the Y value of the aspect ratio; + */ + public int getAspectRatioY() { + return mAspectRatioY; + } - setSnapRadius(options.snapRadius); + /** + * Sets the Y value of the aspect ratio; is defaulted to 1. + * + * @param aspectRatioY int that specifies the new Y value of the aspect ratio + */ + public void setAspectRatioY(int aspectRatioY) { + if (aspectRatioY <= 0) { + throw new IllegalArgumentException( + "Cannot set aspect ratio value to a number less than or equal to 0."); + } else if (mAspectRatioY != aspectRatioY) { + mAspectRatioY = aspectRatioY; + mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY; + + if (initializedCropWindow) { + initCropWindow(); + invalidate(); + } + } + } + + /** + * 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: 3) + */ + public void setSnapRadius(float snapRadius) { + mSnapRadius = snapRadius; + } + + /** + * Set multi touch functionality to enabled/disabled. + */ + public boolean setMultiTouchEnabled(boolean multiTouchEnabled) { + if (mMultiTouchEnabled != multiTouchEnabled) { + mMultiTouchEnabled = multiTouchEnabled; + if (mMultiTouchEnabled && mScaleDetector == null) { + mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener()); + } + return true; + } + return false; + } + + /** + * 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) { + mCropWindowHandler.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) { + mCropWindowHandler.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight); + } + + /** + * set the max width/height and scale factor of the shown image to original image to scale the + * limits appropriately. + */ + public void setCropWindowLimits( + float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) { + mCropWindowHandler.setCropWindowLimits( + maxWidth, maxHeight, scaleFactorWidth, scaleFactorHeight); + } + + /** + * Get crop window initial rectangle. + */ + public Rect getInitialCropWindowRect() { + return mInitialCropWindowRect; + } + + /** + * Set crop window initial rectangle to be used instead of default. + */ + public void setInitialCropWindowRect(Rect rect) { + mInitialCropWindowRect.set(rect != null ? rect : BitmapUtils.EMPTY_RECT); + if (initializedCropWindow) { + initCropWindow(); + invalidate(); + callOnCropWindowChanged(false); + } + } + + /** + * Reset crop window to initial rectangle. + */ + public void resetCropWindowRect() { + if (initializedCropWindow) { + initCropWindow(); + invalidate(); + callOnCropWindowChanged(false); + } + } - setGuidelines(options.guidelines); + /** + * Sets all initial values, but does not call initCropWindow to reset the views.
+ * Used once at the very start to initialize the attributes. + */ + public void setInitialAttributeValues(CropImageOptions options) { - setFixedAspectRatio(options.fixAspectRatio); + mCropWindowHandler.setInitialAttributeValues(options); - setAspectRatioX(options.aspectRatioX); + setCropShape(options.cropShape); - setAspectRatioY(options.aspectRatioY); + setSnapRadius(options.snapRadius); - setMultiTouchEnabled(options.multiTouchEnabled); + setGuidelines(options.guidelines); - mTouchRadius = options.touchRadius; + setFixedAspectRatio(options.fixAspectRatio); - mInitialCropWindowPaddingRatio = options.initialCropWindowPaddingRatio; + setAspectRatioX(options.aspectRatioX); - mBorderPaint = getNewPaintOrNull(options.borderLineThickness, options.borderLineColor); + setAspectRatioY(options.aspectRatioY); - mBorderCornerOffset = options.borderCornerOffset; - mBorderCornerLength = options.borderCornerLength; - mBorderCornerPaint = - getNewPaintOrNull(options.borderCornerThickness, options.borderCornerColor); + setMultiTouchEnabled(options.multiTouchEnabled); - mGuidelinePaint = getNewPaintOrNull(options.guidelinesThickness, options.guidelinesColor); + mTouchRadius = options.touchRadius; - mBackgroundPaint = getNewPaint(options.backgroundColor); - } + mInitialCropWindowPaddingRatio = options.initialCropWindowPaddingRatio; - // region: Private methods + mBorderPaint = getNewPaintOrNull(options.borderLineThickness, options.borderLineColor); - /** - * Set the initial crop window size and position. This is dependent on the size and position of - * the image being cropped. - */ - private void initCropWindow() { + mBorderCornerOffset = options.borderCornerOffset; + mBorderCornerLength = options.borderCornerLength; + mBorderCornerPaint = + getNewPaintOrNull(options.borderCornerThickness, options.borderCornerColor); - float leftLimit = Math.max(BitmapUtils.getRectLeft(mBoundsPoints), 0); - float topLimit = Math.max(BitmapUtils.getRectTop(mBoundsPoints), 0); - float rightLimit = Math.min(BitmapUtils.getRectRight(mBoundsPoints), getWidth()); - float bottomLimit = Math.min(BitmapUtils.getRectBottom(mBoundsPoints), getHeight()); + mGuidelinePaint = getNewPaintOrNull(options.guidelinesThickness, options.guidelinesColor); - if (rightLimit <= leftLimit || bottomLimit <= topLimit) { - return; + mBackgroundPaint = getNewPaint(options.backgroundColor); } - RectF rect = new RectF(); + // region: Private methods - // Tells the attribute functions the crop window has already been initialized - initializedCropWindow = true; + /** + * Set the initial crop window size and position. This is dependent on the size and position of + * the image being cropped. + */ + private void initCropWindow() { - float horizontalPadding = mInitialCropWindowPaddingRatio * (rightLimit - leftLimit); - float verticalPadding = mInitialCropWindowPaddingRatio * (bottomLimit - topLimit); + float leftLimit = Math.max(BitmapUtils.getRectLeft(mBoundsPoints), 0); + float topLimit = Math.max(BitmapUtils.getRectTop(mBoundsPoints), 0); + float rightLimit = Math.min(BitmapUtils.getRectRight(mBoundsPoints), getWidth()); + float bottomLimit = Math.min(BitmapUtils.getRectBottom(mBoundsPoints), getHeight()); - if (mInitialCropWindowRect.width() > 0 && mInitialCropWindowRect.height() > 0) { - // Get crop window position relative to the displayed image. - rect.left = - leftLimit + mInitialCropWindowRect.left / mCropWindowHandler.getScaleFactorWidth(); - rect.top = topLimit + mInitialCropWindowRect.top / mCropWindowHandler.getScaleFactorHeight(); - rect.right = - rect.left + mInitialCropWindowRect.width() / mCropWindowHandler.getScaleFactorWidth(); - rect.bottom = - rect.top + mInitialCropWindowRect.height() / mCropWindowHandler.getScaleFactorHeight(); + if (rightLimit <= leftLimit || bottomLimit <= topLimit) { + return; + } - // Correct for floating point errors. Crop rect boundaries should not exceed the source Bitmap - // bounds. - rect.left = Math.max(leftLimit, rect.left); - rect.top = Math.max(topLimit, rect.top); - rect.right = Math.min(rightLimit, rect.right); - rect.bottom = Math.min(bottomLimit, rect.bottom); + RectF rect = new RectF(); - } else if (mFixAspectRatio && rightLimit > leftLimit && bottomLimit > topLimit) { + // Tells the attribute functions the crop window has already been initialized + initializedCropWindow = true; - // If the image aspect ratio is wider than the crop aspect ratio, - // then the image height is the determining initial length. Else, vice-versa. - float bitmapAspectRatio = (rightLimit - leftLimit) / (bottomLimit - topLimit); - if (bitmapAspectRatio > mTargetAspectRatio) { + float horizontalPadding = mInitialCropWindowPaddingRatio * (rightLimit - leftLimit); + float verticalPadding = mInitialCropWindowPaddingRatio * (bottomLimit - topLimit); - rect.top = topLimit + verticalPadding; - rect.bottom = bottomLimit - verticalPadding; + if (mInitialCropWindowRect.width() > 0 && mInitialCropWindowRect.height() > 0) { + // Get crop window position relative to the displayed image. + rect.left = + leftLimit + mInitialCropWindowRect.left / mCropWindowHandler.getScaleFactorWidth(); + rect.top = topLimit + mInitialCropWindowRect.top / mCropWindowHandler.getScaleFactorHeight(); + rect.right = + rect.left + mInitialCropWindowRect.width() / mCropWindowHandler.getScaleFactorWidth(); + rect.bottom = + rect.top + mInitialCropWindowRect.height() / mCropWindowHandler.getScaleFactorHeight(); - float centerX = getWidth() / 2f; + // Correct for floating point errors. Crop rect boundaries should not exceed the source Bitmap + // bounds. + rect.left = Math.max(leftLimit, rect.left); + rect.top = Math.max(topLimit, rect.top); + rect.right = Math.min(rightLimit, rect.right); + rect.bottom = Math.min(bottomLimit, rect.bottom); - // dirty fix for wrong crop overlay aspect ratio when using fixed aspect ratio - mTargetAspectRatio = (float) mAspectRatioX / mAspectRatioY; + } else if (mFixAspectRatio && rightLimit > leftLimit && bottomLimit > topLimit) { - // Limits the aspect ratio to no less than 40 wide or 40 tall - float cropWidth = - Math.max(mCropWindowHandler.getMinCropWidth(), rect.height() * mTargetAspectRatio); + // If the image aspect ratio is wider than the crop aspect ratio, + // then the image height is the determining initial length. Else, vice-versa. + float bitmapAspectRatio = (rightLimit - leftLimit) / (bottomLimit - topLimit); + if (bitmapAspectRatio > mTargetAspectRatio) { - float halfCropWidth = cropWidth / 2f; - rect.left = centerX - halfCropWidth; - rect.right = centerX + halfCropWidth; + rect.top = topLimit + verticalPadding; + rect.bottom = bottomLimit - verticalPadding; - } else { + float centerX = getWidth() / 2f; - rect.left = leftLimit + horizontalPadding; - rect.right = rightLimit - horizontalPadding; + // dirty fix for wrong crop overlay aspect ratio when using fixed aspect ratio + mTargetAspectRatio = (float) mAspectRatioX / mAspectRatioY; - float centerY = getHeight() / 2f; + // Limits the aspect ratio to no less than 40 wide or 40 tall + float cropWidth = + Math.max(mCropWindowHandler.getMinCropWidth(), rect.height() * mTargetAspectRatio); - // Limits the aspect ratio to no less than 40 wide or 40 tall - float cropHeight = - Math.max(mCropWindowHandler.getMinCropHeight(), rect.width() / mTargetAspectRatio); + float halfCropWidth = cropWidth / 2f; + rect.left = centerX - halfCropWidth; + rect.right = centerX + halfCropWidth; - float halfCropHeight = cropHeight / 2f; - rect.top = centerY - halfCropHeight; - rect.bottom = centerY + halfCropHeight; - } - } else { - // Initialize crop window to have 10% padding w/ respect to image. - rect.left = leftLimit + horizontalPadding; - rect.top = topLimit + verticalPadding; - rect.right = rightLimit - horizontalPadding; - rect.bottom = bottomLimit - verticalPadding; - } + } else { - fixCropWindowRectByRules(rect); + rect.left = leftLimit + horizontalPadding; + rect.right = rightLimit - horizontalPadding; - mCropWindowHandler.setRect(rect); - } + float centerY = getHeight() / 2f; - /** Fix the given rect to fit into bitmap rect and follow min, max and aspect ratio rules. */ - private void fixCropWindowRectByRules(RectF rect) { - if (rect.width() < mCropWindowHandler.getMinCropWidth()) { - float adj = (mCropWindowHandler.getMinCropWidth() - rect.width()) / 2; - rect.left -= adj; - rect.right += adj; - } - if (rect.height() < mCropWindowHandler.getMinCropHeight()) { - float adj = (mCropWindowHandler.getMinCropHeight() - rect.height()) / 2; - rect.top -= adj; - rect.bottom += adj; - } - if (rect.width() > mCropWindowHandler.getMaxCropWidth()) { - float adj = (rect.width() - mCropWindowHandler.getMaxCropWidth()) / 2; - rect.left += adj; - rect.right -= adj; - } - if (rect.height() > mCropWindowHandler.getMaxCropHeight()) { - float adj = (rect.height() - mCropWindowHandler.getMaxCropHeight()) / 2; - rect.top += adj; - rect.bottom -= adj; + // Limits the aspect ratio to no less than 40 wide or 40 tall + float cropHeight = + Math.max(mCropWindowHandler.getMinCropHeight(), rect.width() / mTargetAspectRatio); + + float halfCropHeight = cropHeight / 2f; + rect.top = centerY - halfCropHeight; + rect.bottom = centerY + halfCropHeight; + } + } else { + // Initialize crop window to have 10% padding w/ respect to image. + rect.left = leftLimit + horizontalPadding; + rect.top = topLimit + verticalPadding; + rect.right = rightLimit - horizontalPadding; + rect.bottom = bottomLimit - verticalPadding; + } + + fixCropWindowRectByRules(rect); + + mCropWindowHandler.setRect(rect); } - calculateBounds(rect); - if (mCalcBounds.width() > 0 && mCalcBounds.height() > 0) { - float leftLimit = Math.max(mCalcBounds.left, 0); - float topLimit = Math.max(mCalcBounds.top, 0); - float rightLimit = Math.min(mCalcBounds.right, getWidth()); - float bottomLimit = Math.min(mCalcBounds.bottom, getHeight()); - if (rect.left < leftLimit) { - rect.left = leftLimit; - } - if (rect.top < topLimit) { - rect.top = topLimit; - } - if (rect.right > rightLimit) { - rect.right = rightLimit; - } - if (rect.bottom > bottomLimit) { - rect.bottom = bottomLimit; - } + /** + * Fix the given rect to fit into bitmap rect and follow min, max and aspect ratio rules. + */ + private void fixCropWindowRectByRules(RectF rect) { + if (rect.width() < mCropWindowHandler.getMinCropWidth()) { + float adj = (mCropWindowHandler.getMinCropWidth() - rect.width()) / 2; + rect.left -= adj; + rect.right += adj; + } + if (rect.height() < mCropWindowHandler.getMinCropHeight()) { + float adj = (mCropWindowHandler.getMinCropHeight() - rect.height()) / 2; + rect.top -= adj; + rect.bottom += adj; + } + if (rect.width() > mCropWindowHandler.getMaxCropWidth()) { + float adj = (rect.width() - mCropWindowHandler.getMaxCropWidth()) / 2; + rect.left += adj; + rect.right -= adj; + } + if (rect.height() > mCropWindowHandler.getMaxCropHeight()) { + float adj = (rect.height() - mCropWindowHandler.getMaxCropHeight()) / 2; + rect.top += adj; + rect.bottom -= adj; + } + + calculateBounds(rect); + if (mCalcBounds.width() > 0 && mCalcBounds.height() > 0) { + float leftLimit = Math.max(mCalcBounds.left, 0); + float topLimit = Math.max(mCalcBounds.top, 0); + float rightLimit = Math.min(mCalcBounds.right, getWidth()); + float bottomLimit = Math.min(mCalcBounds.bottom, getHeight()); + if (rect.left < leftLimit) { + rect.left = leftLimit; + } + if (rect.top < topLimit) { + rect.top = topLimit; + } + if (rect.right > rightLimit) { + rect.right = rightLimit; + } + if (rect.bottom > bottomLimit) { + rect.bottom = bottomLimit; + } + } + if (mFixAspectRatio && Math.abs(rect.width() - rect.height() * mTargetAspectRatio) > 0.1) { + if (rect.width() > rect.height() * mTargetAspectRatio) { + float adj = Math.abs(rect.height() * mTargetAspectRatio - rect.width()) / 2; + rect.left += adj; + rect.right -= adj; + } else { + float adj = Math.abs(rect.width() / mTargetAspectRatio - rect.height()) / 2; + rect.top += adj; + rect.bottom -= adj; + } + } } - if (mFixAspectRatio && Math.abs(rect.width() - rect.height() * mTargetAspectRatio) > 0.1) { - if (rect.width() > rect.height() * mTargetAspectRatio) { - float adj = Math.abs(rect.height() * mTargetAspectRatio - rect.width()) / 2; - rect.left += adj; - rect.right -= adj; - } else { - float adj = Math.abs(rect.width() / mTargetAspectRatio - rect.height()) / 2; - rect.top += adj; - rect.bottom -= adj; - } + + /** + * Draw crop overview by drawing background over image not in the cripping area, then borders and + * guidelines. + */ + @Override + protected void onDraw(Canvas canvas) { + + super.onDraw(canvas); + + // Draw translucent background for the cropped area. + drawBackground(canvas); + + if (mCropWindowHandler.showGuidelines()) { + // Determines whether guidelines should be drawn or not + if (mGuidelines == CropImageView.Guidelines.ON) { + drawGuidelines(canvas); + } else if (mGuidelines == CropImageView.Guidelines.ON_TOUCH && mMoveHandler != null) { + // Draw only when resizing + drawGuidelines(canvas); + } + } + + drawBorders(canvas); + + drawCorners(canvas); } - } - - /** - * Draw crop overview by drawing background over image not in the cripping area, then borders and - * guidelines. - */ - @Override - protected void onDraw(Canvas canvas) { - - super.onDraw(canvas); - - // Draw translucent background for the cropped area. - drawBackground(canvas); - - if (mCropWindowHandler.showGuidelines()) { - // Determines whether guidelines should be drawn or not - if (mGuidelines == CropImageView.Guidelines.ON) { - drawGuidelines(canvas); - } else if (mGuidelines == CropImageView.Guidelines.ON_TOUCH && mMoveHandler != null) { - // Draw only when resizing - drawGuidelines(canvas); - } + + /** + * Draw shadow background over the image not including the crop area. + */ + private void drawBackground(Canvas canvas) { + + RectF rect = mCropWindowHandler.getRect(); + + float left = Math.max(BitmapUtils.getRectLeft(mBoundsPoints), 0); + float top = Math.max(BitmapUtils.getRectTop(mBoundsPoints), 0); + float right = Math.min(BitmapUtils.getRectRight(mBoundsPoints), getWidth()); + float bottom = Math.min(BitmapUtils.getRectBottom(mBoundsPoints), getHeight()); + + if (mCropShape == CropImageView.CropShape.RECTANGLE) { + if (!isNonStraightAngleRotated() || Build.VERSION.SDK_INT <= 17) { + canvas.drawRect(left, top, right, rect.top, mBackgroundPaint); + canvas.drawRect(left, rect.bottom, right, bottom, mBackgroundPaint); + canvas.drawRect(left, rect.top, rect.left, rect.bottom, mBackgroundPaint); + canvas.drawRect(rect.right, rect.top, right, rect.bottom, mBackgroundPaint); + } else { + mPath.reset(); + mPath.moveTo(mBoundsPoints[0], mBoundsPoints[1]); + mPath.lineTo(mBoundsPoints[2], mBoundsPoints[3]); + mPath.lineTo(mBoundsPoints[4], mBoundsPoints[5]); + mPath.lineTo(mBoundsPoints[6], mBoundsPoints[7]); + mPath.close(); + + canvas.save(); + canvas.clipPath(mPath, Region.Op.INTERSECT); + + //canvas.clipRect(rect, Region.Op.XOR); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipRect(rect); + } else { + canvas.clipRect(rect, Region.Op.INTERSECT); + } + + canvas.drawRect(left, top, right, bottom, mBackgroundPaint); + canvas.restore(); + } + } else { + mPath.reset(); + if (Build.VERSION.SDK_INT <= 17 && mCropShape == CropImageView.CropShape.OVAL) { + mDrawRect.set(rect.left + 2, rect.top + 2, rect.right - 2, rect.bottom - 2); + } else { + mDrawRect.set(rect.left, rect.top, rect.right, rect.bottom); + } + mPath.addOval(mDrawRect, Path.Direction.CW); + canvas.save(); + // canvas.clipPath(mPath, Region.Op.XOR); //Deprecated + // https://developer.android.com/reference/android/graphics/Canvas#clipPath(android.graphics.Path,%20android.graphics.Region.Op) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipOutPath(mPath); + } else { + canvas.clipPath(mPath, Region.Op.INTERSECT); + } + canvas.drawRect(left, top, right, bottom, mBackgroundPaint); + canvas.restore(); + } } - drawBorders(canvas); - - drawCorners(canvas); - } - - /** Draw shadow background over the image not including the crop area. */ - private void drawBackground(Canvas canvas) { - - RectF rect = mCropWindowHandler.getRect(); - - float left = Math.max(BitmapUtils.getRectLeft(mBoundsPoints), 0); - float top = Math.max(BitmapUtils.getRectTop(mBoundsPoints), 0); - float right = Math.min(BitmapUtils.getRectRight(mBoundsPoints), getWidth()); - float bottom = Math.min(BitmapUtils.getRectBottom(mBoundsPoints), getHeight()); - - if (mCropShape == CropImageView.CropShape.RECTANGLE) { - if (!isNonStraightAngleRotated() || Build.VERSION.SDK_INT <= 17) { - canvas.drawRect(left, top, right, rect.top, mBackgroundPaint); - canvas.drawRect(left, rect.bottom, right, bottom, mBackgroundPaint); - canvas.drawRect(left, rect.top, rect.left, rect.bottom, mBackgroundPaint); - canvas.drawRect(rect.right, rect.top, right, rect.bottom, mBackgroundPaint); - } else { - mPath.reset(); - mPath.moveTo(mBoundsPoints[0], mBoundsPoints[1]); - mPath.lineTo(mBoundsPoints[2], mBoundsPoints[3]); - mPath.lineTo(mBoundsPoints[4], mBoundsPoints[5]); - mPath.lineTo(mBoundsPoints[6], mBoundsPoints[7]); - mPath.close(); - - canvas.save(); - canvas.clipPath(mPath, Region.Op.INTERSECT); - canvas.clipRect(rect, Region.Op.XOR); - canvas.drawRect(left, top, right, bottom, mBackgroundPaint); - canvas.restore(); - } - } else { - mPath.reset(); - if (Build.VERSION.SDK_INT <= 17 && mCropShape == CropImageView.CropShape.OVAL) { - mDrawRect.set(rect.left + 2, rect.top + 2, rect.right - 2, rect.bottom - 2); - } else { - mDrawRect.set(rect.left, rect.top, rect.right, rect.bottom); - } - mPath.addOval(mDrawRect, Path.Direction.CW); - canvas.save(); - canvas.clipPath(mPath, Region.Op.XOR); - canvas.drawRect(left, top, right, bottom, mBackgroundPaint); - canvas.restore(); + /** + * Draw 2 veritcal and 2 horizontal guidelines inside the cropping area to split it into 9 equal + * parts. + */ + private void drawGuidelines(Canvas canvas) { + if (mGuidelinePaint != null) { + float sw = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0; + RectF rect = mCropWindowHandler.getRect(); + rect.inset(sw, sw); + + float oneThirdCropWidth = rect.width() / 3; + float oneThirdCropHeight = rect.height() / 3; + + if (mCropShape == CropImageView.CropShape.OVAL) { + + float w = rect.width() / 2 - sw; + float h = rect.height() / 2 - sw; + + // Draw vertical guidelines. + float x1 = rect.left + oneThirdCropWidth; + float x2 = rect.right - oneThirdCropWidth; + float yv = (float) (h * Math.sin(Math.acos((w - oneThirdCropWidth) / w))); + canvas.drawLine(x1, rect.top + h - yv, x1, rect.bottom - h + yv, mGuidelinePaint); + canvas.drawLine(x2, rect.top + h - yv, x2, rect.bottom - h + yv, mGuidelinePaint); + + // Draw horizontal guidelines. + float y1 = rect.top + oneThirdCropHeight; + float y2 = rect.bottom - oneThirdCropHeight; + float xv = (float) (w * Math.cos(Math.asin((h - oneThirdCropHeight) / h))); + canvas.drawLine(rect.left + w - xv, y1, rect.right - w + xv, y1, mGuidelinePaint); + canvas.drawLine(rect.left + w - xv, y2, rect.right - w + xv, y2, mGuidelinePaint); + } else { + + // Draw vertical guidelines. + float x1 = rect.left + oneThirdCropWidth; + float x2 = rect.right - oneThirdCropWidth; + canvas.drawLine(x1, rect.top, x1, rect.bottom, mGuidelinePaint); + canvas.drawLine(x2, rect.top, x2, rect.bottom, mGuidelinePaint); + + // Draw horizontal guidelines. + float y1 = rect.top + oneThirdCropHeight; + float y2 = rect.bottom - oneThirdCropHeight; + canvas.drawLine(rect.left, y1, rect.right, y1, mGuidelinePaint); + canvas.drawLine(rect.left, y2, rect.right, y2, mGuidelinePaint); + } + } } - } - - /** - * Draw 2 veritcal and 2 horizontal guidelines inside the cropping area to split it into 9 equal - * parts. - */ - private void drawGuidelines(Canvas canvas) { - if (mGuidelinePaint != null) { - float sw = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0; - RectF rect = mCropWindowHandler.getRect(); - rect.inset(sw, sw); - - float oneThirdCropWidth = rect.width() / 3; - float oneThirdCropHeight = rect.height() / 3; - - if (mCropShape == CropImageView.CropShape.OVAL) { - - float w = rect.width() / 2 - sw; - float h = rect.height() / 2 - sw; - - // Draw vertical guidelines. - float x1 = rect.left + oneThirdCropWidth; - float x2 = rect.right - oneThirdCropWidth; - float yv = (float) (h * Math.sin(Math.acos((w - oneThirdCropWidth) / w))); - canvas.drawLine(x1, rect.top + h - yv, x1, rect.bottom - h + yv, mGuidelinePaint); - canvas.drawLine(x2, rect.top + h - yv, x2, rect.bottom - h + yv, mGuidelinePaint); - - // Draw horizontal guidelines. - float y1 = rect.top + oneThirdCropHeight; - float y2 = rect.bottom - oneThirdCropHeight; - float xv = (float) (w * Math.cos(Math.asin((h - oneThirdCropHeight) / h))); - canvas.drawLine(rect.left + w - xv, y1, rect.right - w + xv, y1, mGuidelinePaint); - canvas.drawLine(rect.left + w - xv, y2, rect.right - w + xv, y2, mGuidelinePaint); - } else { - - // Draw vertical guidelines. - float x1 = rect.left + oneThirdCropWidth; - float x2 = rect.right - oneThirdCropWidth; - canvas.drawLine(x1, rect.top, x1, rect.bottom, mGuidelinePaint); - canvas.drawLine(x2, rect.top, x2, rect.bottom, mGuidelinePaint); - - // Draw horizontal guidelines. - float y1 = rect.top + oneThirdCropHeight; - float y2 = rect.bottom - oneThirdCropHeight; - canvas.drawLine(rect.left, y1, rect.right, y1, mGuidelinePaint); - canvas.drawLine(rect.left, y2, rect.right, y2, mGuidelinePaint); - } + + /** + * Draw borders of the crop area. + */ + private void drawBorders(Canvas canvas) { + if (mBorderPaint != null) { + float w = mBorderPaint.getStrokeWidth(); + RectF rect = mCropWindowHandler.getRect(); + rect.inset(w / 2, w / 2); + + if (mCropShape == CropImageView.CropShape.RECTANGLE) { + // Draw rectangle crop window border. + canvas.drawRect(rect, mBorderPaint); + } else { + // Draw circular crop window border + canvas.drawOval(rect, mBorderPaint); + } + } } - } - - /** Draw borders of the crop area. */ - private void drawBorders(Canvas canvas) { - if (mBorderPaint != null) { - float w = mBorderPaint.getStrokeWidth(); - RectF rect = mCropWindowHandler.getRect(); - rect.inset(w / 2, w / 2); - - if (mCropShape == CropImageView.CropShape.RECTANGLE) { - // Draw rectangle crop window border. - canvas.drawRect(rect, mBorderPaint); - } else { - // Draw circular crop window border - canvas.drawOval(rect, mBorderPaint); - } + + /** + * Draw the corner of crop overlay. + */ + private void drawCorners(Canvas canvas) { + if (mBorderCornerPaint != null) { + + float lineWidth = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0; + float cornerWidth = mBorderCornerPaint.getStrokeWidth(); + + // for rectangle crop shape we allow the corners to be offset from the borders + float w = + cornerWidth / 2 + + (mCropShape == CropImageView.CropShape.RECTANGLE ? mBorderCornerOffset : 0); + + RectF rect = mCropWindowHandler.getRect(); + rect.inset(w, w); + + float cornerOffset = (cornerWidth - lineWidth) / 2; + float cornerExtension = cornerWidth / 2 + cornerOffset; + + // Top left + canvas.drawLine( + rect.left - cornerOffset, + rect.top - cornerExtension, + rect.left - cornerOffset, + rect.top + mBorderCornerLength, + mBorderCornerPaint); + canvas.drawLine( + rect.left - cornerExtension, + rect.top - cornerOffset, + rect.left + mBorderCornerLength, + rect.top - cornerOffset, + mBorderCornerPaint); + + // Top right + canvas.drawLine( + rect.right + cornerOffset, + rect.top - cornerExtension, + rect.right + cornerOffset, + rect.top + mBorderCornerLength, + mBorderCornerPaint); + canvas.drawLine( + rect.right + cornerExtension, + rect.top - cornerOffset, + rect.right - mBorderCornerLength, + rect.top - cornerOffset, + mBorderCornerPaint); + + // Bottom left + canvas.drawLine( + rect.left - cornerOffset, + rect.bottom + cornerExtension, + rect.left - cornerOffset, + rect.bottom - mBorderCornerLength, + mBorderCornerPaint); + canvas.drawLine( + rect.left - cornerExtension, + rect.bottom + cornerOffset, + rect.left + mBorderCornerLength, + rect.bottom + cornerOffset, + mBorderCornerPaint); + + // Bottom left + canvas.drawLine( + rect.right + cornerOffset, + rect.bottom + cornerExtension, + rect.right + cornerOffset, + rect.bottom - mBorderCornerLength, + mBorderCornerPaint); + canvas.drawLine( + rect.right + cornerExtension, + rect.bottom + cornerOffset, + rect.right - mBorderCornerLength, + rect.bottom + cornerOffset, + mBorderCornerPaint); + } } - } - - /** Draw the corner of crop overlay. */ - private void drawCorners(Canvas canvas) { - if (mBorderCornerPaint != null) { - - float lineWidth = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0; - float cornerWidth = mBorderCornerPaint.getStrokeWidth(); - - // for rectangle crop shape we allow the corners to be offset from the borders - float w = - cornerWidth / 2 - + (mCropShape == CropImageView.CropShape.RECTANGLE ? mBorderCornerOffset : 0); - - RectF rect = mCropWindowHandler.getRect(); - rect.inset(w, w); - - float cornerOffset = (cornerWidth - lineWidth) / 2; - float cornerExtension = cornerWidth / 2 + cornerOffset; - - // Top left - canvas.drawLine( - rect.left - cornerOffset, - rect.top - cornerExtension, - rect.left - cornerOffset, - rect.top + mBorderCornerLength, - mBorderCornerPaint); - canvas.drawLine( - rect.left - cornerExtension, - rect.top - cornerOffset, - rect.left + mBorderCornerLength, - rect.top - cornerOffset, - mBorderCornerPaint); - - // Top right - canvas.drawLine( - rect.right + cornerOffset, - rect.top - cornerExtension, - rect.right + cornerOffset, - rect.top + mBorderCornerLength, - mBorderCornerPaint); - canvas.drawLine( - rect.right + cornerExtension, - rect.top - cornerOffset, - rect.right - mBorderCornerLength, - rect.top - cornerOffset, - mBorderCornerPaint); - - // Bottom left - canvas.drawLine( - rect.left - cornerOffset, - rect.bottom + cornerExtension, - rect.left - cornerOffset, - rect.bottom - mBorderCornerLength, - mBorderCornerPaint); - canvas.drawLine( - rect.left - cornerExtension, - rect.bottom + cornerOffset, - rect.left + mBorderCornerLength, - rect.bottom + cornerOffset, - mBorderCornerPaint); - - // Bottom left - canvas.drawLine( - rect.right + cornerOffset, - rect.bottom + cornerExtension, - rect.right + cornerOffset, - rect.bottom - mBorderCornerLength, - mBorderCornerPaint); - canvas.drawLine( - rect.right + cornerExtension, - rect.bottom + cornerOffset, - rect.right - mBorderCornerLength, - rect.bottom + cornerOffset, - mBorderCornerPaint); + + /** + * Creates the Paint object for drawing. + */ + private static Paint getNewPaint(int color) { + Paint paint = new Paint(); + paint.setColor(color); + return paint; } - } - - /** Creates the Paint object for drawing. */ - private static Paint getNewPaint(int color) { - Paint paint = new Paint(); - paint.setColor(color); - return paint; - } - - /** Creates the Paint object for given thickness and color, if thickness < 0 return null. */ - private static Paint getNewPaintOrNull(float thickness, int color) { - if (thickness > 0) { - Paint borderPaint = new Paint(); - borderPaint.setColor(color); - borderPaint.setStrokeWidth(thickness); - borderPaint.setStyle(Paint.Style.STROKE); - borderPaint.setAntiAlias(true); - return borderPaint; - } else { - return null; + + /** + * Creates the Paint object for given thickness and color, if thickness < 0 return null. + */ + private static Paint getNewPaintOrNull(float thickness, int color) { + if (thickness > 0) { + Paint borderPaint = new Paint(); + borderPaint.setColor(color); + borderPaint.setStrokeWidth(thickness); + borderPaint.setStyle(Paint.Style.STROKE); + borderPaint.setAntiAlias(true); + return borderPaint; + } else { + return null; + } } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - // If this View is not enabled, don't allow for touch interactions. - if (isEnabled()) { - if (mMultiTouchEnabled) { - mScaleDetector.onTouchEvent(event); - } - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - onActionDown(event.getX(), event.getY()); - return true; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - getParent().requestDisallowInterceptTouchEvent(false); - onActionUp(); - return true; - case MotionEvent.ACTION_MOVE: - onActionMove(event.getX(), event.getY()); - getParent().requestDisallowInterceptTouchEvent(true); - return true; - default: - return false; - } - } else { - return false; + + @Override + public boolean onTouchEvent(MotionEvent event) { + // If this View is not enabled, don't allow for touch interactions. + if (isEnabled()) { + if (mMultiTouchEnabled) { + mScaleDetector.onTouchEvent(event); + } + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + onActionDown(event.getX(), event.getY()); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + getParent().requestDisallowInterceptTouchEvent(false); + onActionUp(); + return true; + case MotionEvent.ACTION_MOVE: + onActionMove(event.getX(), event.getY()); + getParent().requestDisallowInterceptTouchEvent(true); + return true; + default: + return false; + } + } else { + return false; + } } - } - - /** - * On press down start crop window movment depending on the location of the press.
- * if press is far from crop window then no move handler is returned (null). - */ - private void onActionDown(float x, float y) { - mMoveHandler = mCropWindowHandler.getMoveHandler(x, y, mTouchRadius, mCropShape); - if (mMoveHandler != null) { - invalidate(); + + /** + * On press down start crop window movment depending on the location of the press.
+ * if press is far from crop window then no move handler is returned (null). + */ + private void onActionDown(float x, float y) { + mMoveHandler = mCropWindowHandler.getMoveHandler(x, y, mTouchRadius, mCropShape); + if (mMoveHandler != null) { + invalidate(); + } } - } - - /** Clear move handler starting in {@link #onActionDown(float, float)} if exists. */ - private void onActionUp() { - if (mMoveHandler != null) { - mMoveHandler = null; - callOnCropWindowChanged(false); - invalidate(); + + /** + * Clear move handler starting in {@link #onActionDown(float, float)} if exists. + */ + private void onActionUp() { + if (mMoveHandler != null) { + mMoveHandler = null; + callOnCropWindowChanged(false); + invalidate(); + } } - } - - /** - * Handle move of crop window using the move handler created in {@link #onActionDown(float, - * float)}.
- * The move handler will do the proper move/resize of the crop window. - */ - private void onActionMove(float x, float y) { - if (mMoveHandler != null) { - float snapRadius = mSnapRadius; - RectF rect = mCropWindowHandler.getRect(); - - if (calculateBounds(rect)) { - snapRadius = 0; - } - - mMoveHandler.move( - rect, - x, - y, - mCalcBounds, - mViewWidth, - mViewHeight, - snapRadius, - mFixAspectRatio, - mTargetAspectRatio); - mCropWindowHandler.setRect(rect); - callOnCropWindowChanged(true); - invalidate(); + + /** + * Handle move of crop window using the move handler created in {@link #onActionDown(float, + * float)}.
+ * The move handler will do the proper move/resize of the crop window. + */ + private void onActionMove(float x, float y) { + if (mMoveHandler != null) { + float snapRadius = mSnapRadius; + RectF rect = mCropWindowHandler.getRect(); + + if (calculateBounds(rect)) { + snapRadius = 0; + } + + mMoveHandler.move( + rect, + x, + y, + mCalcBounds, + mViewWidth, + mViewHeight, + snapRadius, + mFixAspectRatio, + mTargetAspectRatio); + mCropWindowHandler.setRect(rect); + callOnCropWindowChanged(true); + invalidate(); + } } - } - - /** - * Calculate the bounding rectangle for current crop window, handle non-straight rotation angles. - *
- * If the rotation angle is straight then the bounds rectangle is the bitmap rectangle, otherwsie - * we find the max rectangle that is within the image bounds starting from the crop window - * rectangle. - * - * @param rect the crop window rectangle to start finsing bounded rectangle from - * @return true - non straight rotation in place, false - otherwise. - */ - private boolean calculateBounds(RectF rect) { - - float left = BitmapUtils.getRectLeft(mBoundsPoints); - float top = BitmapUtils.getRectTop(mBoundsPoints); - float right = BitmapUtils.getRectRight(mBoundsPoints); - float bottom = BitmapUtils.getRectBottom(mBoundsPoints); - - if (!isNonStraightAngleRotated()) { - mCalcBounds.set(left, top, right, bottom); - return false; - } else { - float x0 = mBoundsPoints[0]; - float y0 = mBoundsPoints[1]; - float x2 = mBoundsPoints[4]; - float y2 = mBoundsPoints[5]; - float x3 = mBoundsPoints[6]; - float y3 = mBoundsPoints[7]; - - if (mBoundsPoints[7] < mBoundsPoints[1]) { - if (mBoundsPoints[1] < mBoundsPoints[3]) { - x0 = mBoundsPoints[6]; - y0 = mBoundsPoints[7]; - x2 = mBoundsPoints[2]; - y2 = mBoundsPoints[3]; - x3 = mBoundsPoints[4]; - y3 = mBoundsPoints[5]; + + /** + * Calculate the bounding rectangle for current crop window, handle non-straight rotation angles. + *
+ * If the rotation angle is straight then the bounds rectangle is the bitmap rectangle, otherwsie + * we find the max rectangle that is within the image bounds starting from the crop window + * rectangle. + * + * @param rect the crop window rectangle to start finsing bounded rectangle from + * @return true - non straight rotation in place, false - otherwise. + */ + private boolean calculateBounds(RectF rect) { + + float left = BitmapUtils.getRectLeft(mBoundsPoints); + float top = BitmapUtils.getRectTop(mBoundsPoints); + float right = BitmapUtils.getRectRight(mBoundsPoints); + float bottom = BitmapUtils.getRectBottom(mBoundsPoints); + + if (!isNonStraightAngleRotated()) { + mCalcBounds.set(left, top, right, bottom); + return false; } else { - x0 = mBoundsPoints[4]; - y0 = mBoundsPoints[5]; - x2 = mBoundsPoints[0]; - y2 = mBoundsPoints[1]; - x3 = mBoundsPoints[2]; - y3 = mBoundsPoints[3]; + float x0 = mBoundsPoints[0]; + float y0 = mBoundsPoints[1]; + float x2 = mBoundsPoints[4]; + float y2 = mBoundsPoints[5]; + float x3 = mBoundsPoints[6]; + float y3 = mBoundsPoints[7]; + + if (mBoundsPoints[7] < mBoundsPoints[1]) { + if (mBoundsPoints[1] < mBoundsPoints[3]) { + x0 = mBoundsPoints[6]; + y0 = mBoundsPoints[7]; + x2 = mBoundsPoints[2]; + y2 = mBoundsPoints[3]; + x3 = mBoundsPoints[4]; + y3 = mBoundsPoints[5]; + } else { + x0 = mBoundsPoints[4]; + y0 = mBoundsPoints[5]; + x2 = mBoundsPoints[0]; + y2 = mBoundsPoints[1]; + x3 = mBoundsPoints[2]; + y3 = mBoundsPoints[3]; + } + } else if (mBoundsPoints[1] > mBoundsPoints[3]) { + x0 = mBoundsPoints[2]; + y0 = mBoundsPoints[3]; + x2 = mBoundsPoints[6]; + y2 = mBoundsPoints[7]; + x3 = mBoundsPoints[0]; + y3 = mBoundsPoints[1]; + } + + float a0 = (y3 - y0) / (x3 - x0); + float a1 = -1f / a0; + float b0 = y0 - a0 * x0; + float b1 = y0 - a1 * x0; + float b2 = y2 - a0 * x2; + float b3 = y2 - a1 * x2; + + float c0 = (rect.centerY() - rect.top) / (rect.centerX() - rect.left); + float c1 = -c0; + float d0 = rect.top - c0 * rect.left; + float d1 = rect.top - c1 * rect.right; + + left = Math.max(left, (d0 - b0) / (a0 - c0) < rect.right ? (d0 - b0) / (a0 - c0) : left); + left = Math.max(left, (d0 - b1) / (a1 - c0) < rect.right ? (d0 - b1) / (a1 - c0) : left); + left = Math.max(left, (d1 - b3) / (a1 - c1) < rect.right ? (d1 - b3) / (a1 - c1) : left); + right = Math.min(right, (d1 - b1) / (a1 - c1) > rect.left ? (d1 - b1) / (a1 - c1) : right); + right = Math.min(right, (d1 - b2) / (a0 - c1) > rect.left ? (d1 - b2) / (a0 - c1) : right); + right = Math.min(right, (d0 - b2) / (a0 - c0) > rect.left ? (d0 - b2) / (a0 - c0) : right); + + top = Math.max(top, Math.max(a0 * left + b0, a1 * right + b1)); + bottom = Math.min(bottom, Math.min(a1 * left + b3, a0 * right + b2)); + + mCalcBounds.left = left; + mCalcBounds.top = top; + mCalcBounds.right = right; + mCalcBounds.bottom = bottom; + return true; } - } else if (mBoundsPoints[1] > mBoundsPoints[3]) { - x0 = mBoundsPoints[2]; - y0 = mBoundsPoints[3]; - x2 = mBoundsPoints[6]; - y2 = mBoundsPoints[7]; - x3 = mBoundsPoints[0]; - y3 = mBoundsPoints[1]; - } - - float a0 = (y3 - y0) / (x3 - x0); - float a1 = -1f / a0; - float b0 = y0 - a0 * x0; - float b1 = y0 - a1 * x0; - float b2 = y2 - a0 * x2; - float b3 = y2 - a1 * x2; - - float c0 = (rect.centerY() - rect.top) / (rect.centerX() - rect.left); - float c1 = -c0; - float d0 = rect.top - c0 * rect.left; - float d1 = rect.top - c1 * rect.right; - - left = Math.max(left, (d0 - b0) / (a0 - c0) < rect.right ? (d0 - b0) / (a0 - c0) : left); - left = Math.max(left, (d0 - b1) / (a1 - c0) < rect.right ? (d0 - b1) / (a1 - c0) : left); - left = Math.max(left, (d1 - b3) / (a1 - c1) < rect.right ? (d1 - b3) / (a1 - c1) : left); - right = Math.min(right, (d1 - b1) / (a1 - c1) > rect.left ? (d1 - b1) / (a1 - c1) : right); - right = Math.min(right, (d1 - b2) / (a0 - c1) > rect.left ? (d1 - b2) / (a0 - c1) : right); - right = Math.min(right, (d0 - b2) / (a0 - c0) > rect.left ? (d0 - b2) / (a0 - c0) : right); - - top = Math.max(top, Math.max(a0 * left + b0, a1 * right + b1)); - bottom = Math.min(bottom, Math.min(a1 * left + b3, a0 * right + b2)); - - mCalcBounds.left = left; - mCalcBounds.top = top; - mCalcBounds.right = right; - mCalcBounds.bottom = bottom; - return true; } - } - - /** Is the cropping image has been rotated by NOT 0,90,180 or 270 degrees. */ - private boolean isNonStraightAngleRotated() { - return mBoundsPoints[0] != mBoundsPoints[6] && mBoundsPoints[1] != mBoundsPoints[7]; - } - - /** Invoke on crop change listener safe, don't let the app crash on exception. */ - private void callOnCropWindowChanged(boolean inProgress) { - try { - if (mCropWindowChangeListener != null) { - mCropWindowChangeListener.onCropWindowChanged(inProgress); - } - } catch (Exception e) { - Log.e("AIC", "Exception in crop window changed", e); - } - } - // endregion - - // region: Inner class: CropWindowChangeListener - /** Interface definition for a callback to be invoked when crop window rectangle is changing. */ - public interface CropWindowChangeListener { + /** + * Is the cropping image has been rotated by NOT 0,90,180 or 270 degrees. + */ + private boolean isNonStraightAngleRotated() { + return mBoundsPoints[0] != mBoundsPoints[6] && mBoundsPoints[1] != mBoundsPoints[7]; + } /** - * Called after a change in crop window rectangle. - * - * @param inProgress is the crop window change operation is still in progress by user touch + * Invoke on crop change listener safe, don't let the app crash on exception. */ - void onCropWindowChanged(boolean inProgress); - } - // endregion + private void callOnCropWindowChanged(boolean inProgress) { + try { + if (mCropWindowChangeListener != null) { + mCropWindowChangeListener.onCropWindowChanged(inProgress); + } + } catch (Exception e) { + Log.e("AIC", "Exception in crop window changed", e); + } + } + // endregion - // region: Inner class: ScaleListener + // region: Inner class: CropWindowChangeListener - /** Handle scaling the rectangle based on two finger input */ - private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + /** + * Interface definition for a callback to be invoked when crop window rectangle is changing. + */ + public interface CropWindowChangeListener { + + /** + * Called after a change in crop window rectangle. + * + * @param inProgress is the crop window change operation is still in progress by user touch + */ + void onCropWindowChanged(boolean inProgress); + } + // endregion - @Override - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public boolean onScale(ScaleGestureDetector detector) { - RectF rect = mCropWindowHandler.getRect(); - - float x = detector.getFocusX(); - float y = detector.getFocusY(); - float dY = detector.getCurrentSpanY() / 2; - float dX = detector.getCurrentSpanX() / 2; - - float newTop = y - dY; - float newLeft = x - dX; - float newRight = x + dX; - float newBottom = y + dY; - - if (newLeft < newRight - && newTop <= newBottom - && newLeft >= 0 - && newRight <= mCropWindowHandler.getMaxCropWidth() - && newTop >= 0 - && newBottom <= mCropWindowHandler.getMaxCropHeight()) { - - rect.set(newLeft, newTop, newRight, newBottom); - mCropWindowHandler.setRect(rect); - invalidate(); - } + // region: Inner class: ScaleListener - return true; + /** + * Handle scaling the rectangle based on two finger input + */ + private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public boolean onScale(ScaleGestureDetector detector) { + RectF rect = mCropWindowHandler.getRect(); + + float x = detector.getFocusX(); + float y = detector.getFocusY(); + float dY = detector.getCurrentSpanY() / 2; + float dX = detector.getCurrentSpanX() / 2; + + float newTop = y - dY; + float newLeft = x - dX; + float newRight = x + dX; + float newBottom = y + dY; + + if (newLeft < newRight + && newTop <= newBottom + && newLeft >= 0 + && newRight <= mCropWindowHandler.getMaxCropWidth() + && newTop >= 0 + && newBottom <= mCropWindowHandler.getMaxCropHeight()) { + + rect.set(newLeft, newTop, newRight, newBottom); + mCropWindowHandler.setRect(rect); + invalidate(); + } + + return true; + } } - } - // endregion + // endregion }