All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.theartofdev.edmodo.cropper.CropOverlayView Maven / Gradle / Ivy

There is a newer version: 2.5.0
Show newest version
// "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.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;

import java.util.Arrays;

/**
 * A custom View representing the crop window and the shaded background outside the crop window.
 */
public class CropOverlayView extends View {

    //region: Fields and Consts

    /**
     * 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;

    /**
     * Handler from crop window stuff, moving and knowing possition.
     */
    private final CropWindowHandler mCropWindowHandler = new CropWindowHandler();

    /**
     * Listener to publicj crop window changes
     */
    private CropWindowChangeListener mCropWindowChangeListener;

    /**
     * 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 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 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();

    /**
     * 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 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 offset to draw the border corener from the border
     */
    private float mBorderCornerOffset;

    /**
     * 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();
            }
        }
    }

    /**
     * Resets the crop overlay view.
     */
    public void resetCropOverlayView() {
        if (initializedCropWindow) {
            setCropWindowRect(BitmapUtils.EMPTY_RECT_F);
            initCropWindow();
            invalidate();
        }
    }

    /**
     * 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 >= 11 && 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();
        }
    }

    /**
     * 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();
            }
        }
    }

    /**
     * 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 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 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();
            }
        }
    }

    /**
     * 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && 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); } } /** * 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) { mCropWindowHandler.setInitialAttributeValues(options); setCropShape(options.cropShape); setSnapRadius(options.snapRadius); setGuidelines(options.guidelines); setFixedAspectRatio(options.fixAspectRatio); setAspectRatioX(options.aspectRatioX); setAspectRatioY(options.aspectRatioY); setMultiTouchEnabled(options.multiTouchEnabled); mTouchRadius = options.touchRadius; mInitialCropWindowPaddingRatio = options.initialCropWindowPaddingRatio; mBorderPaint = getNewPaintOrNull(options.borderLineThickness, options.borderLineColor); mBorderCornerOffset = options.borderCornerOffset; mBorderCornerLength = options.borderCornerLength; mBorderCornerPaint = getNewPaintOrNull(options.borderCornerThickness, options.borderCornerColor); mGuidelinePaint = getNewPaintOrNull(options.guidelinesThickness, options.guidelinesColor); mBackgroundPaint = getNewPaint(options.backgroundColor); } //region: Private methods /** * 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 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 (rightLimit <= leftLimit || bottomLimit <= topLimit) { return; } RectF rect = new RectF(); // Tells the attribute functions the crop window has already been initialized initializedCropWindow = true; float horizontalPadding = mInitialCropWindowPaddingRatio * (rightLimit - leftLimit); float verticalPadding = mInitialCropWindowPaddingRatio * (bottomLimit - topLimit); 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(); // 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); } else if (mFixAspectRatio && rightLimit > leftLimit && bottomLimit > topLimit) { // 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) { rect.top = topLimit + verticalPadding; rect.bottom = bottomLimit - verticalPadding; float centerX = getWidth() / 2f; //dirty fix for wrong crop overlay aspect ratio when using fixed aspect ratio mTargetAspectRatio = (float) mAspectRatioX / mAspectRatioY; // Limits the aspect ratio to no less than 40 wide or 40 tall float cropWidth = Math.max(mCropWindowHandler.getMinCropWidth(), rect.height() * mTargetAspectRatio); float halfCropWidth = cropWidth / 2f; rect.left = centerX - halfCropWidth; rect.right = centerX + halfCropWidth; } else { rect.left = leftLimit + horizontalPadding; rect.right = rightLimit - horizontalPadding; float centerY = getHeight() / 2f; // 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); } /** * 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; } } } /** * 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 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 >= 11 && 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 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); } } /** * 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; } } @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(); } } /** * 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(); } } /** * 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]; } 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; } } /** * 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 { /** * 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 //region: Inner class: ScaleListener /** * 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 }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy