// "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.graphics.Matrix; import android.graphics.PointF; import android.graphics.RectF; /** * Handler to update crop window edges by the move type - Horizontal, Vertical, Corner or Center. *
*/ final class CropWindowMoveHandler { // region: Fields and Consts /** * Matrix used for rectangle rotation handling */ private static final Matrix MATRIX = new Matrix(); /** * Minimum width in pixels that the crop window can get. */ private final float mMinCropWidth; /** * Minimum width in pixels that the crop window can get. */ private final float mMinCropHeight; /** * Maximum height in pixels that the crop window can get. */ private final float mMaxCropWidth; /** * Maximum height in pixels that the crop window can get. */ private final float mMaxCropHeight; /** * The type of crop window move that is handled. */ private final Type mType; /** * Holds the x and y offset between the exact touch location and the exact handle location that is * activated. There may be an offset because we allow for some leeway (specified by mHandleRadius) * in activating a handle. However, we want to maintain these offset values while the handle is * being dragged so that the handle doesn't jump. */ private final PointF mTouchOffset = new PointF(); // endregion /** * @param edgeMoveType the type of move this handler is executing * @param horizontalEdge the primary edge associated with this handle; may be null * @param verticalEdge the secondary edge associated with this handle; may be null * @param cropWindowHandler main crop window handle to get and update the crop window edges * @param touchX the location of the initial toch possition to measure move distance * @param touchY the location of the initial toch possition to measure move distance */ public CropWindowMoveHandler( Type type, CropWindowHandler cropWindowHandler, float touchX, float touchY) { mType = type; mMinCropWidth = cropWindowHandler.getMinCropWidth(); mMinCropHeight = cropWindowHandler.getMinCropHeight(); mMaxCropWidth = cropWindowHandler.getMaxCropWidth(); mMaxCropHeight = cropWindowHandler.getMaxCropHeight(); calculateTouchOffset(cropWindowHandler.getRect(), touchX, touchY); } /** * Calculates the aspect ratio given a rectangle. */ private static float calculateAspectRatio(float left, float top, float right, float bottom) { return (right - left) / (bottom - top); } // region: Private methods /** * Updates the crop window by change in the toch location.
* Move type handled by this instance, as initialized in creation, affects how the change in toch * location changes the crop window position and size.
* After the crop window position/size is changed by toch move it may result in values that * vialate contraints: outside the bounds of the shown bitmap, smaller/larger than min/max size or * missmatch in aspect ratio. So a series of fixes is executed on "secondary" edges to adjust it * by the "primary" edge movement.
* Primary is the edge directly affected by move type, secondary is the other edge.
* The crop window is changed by directly setting the Edge coordinates. * * @param x the new x-coordinate of this handle * @param y the new y-coordinate of this handle * @param bounds the bounding rectangle of the image * @param viewWidth The bounding image view width used to know the crop overlay is at view edges. * @param viewHeight The bounding image view height used to know the crop overlay is at view * edges. * @param parentView the parent View containing the image * @param snapMargin the maximum distance (in pixels) at which the crop window should snap to the * image * @param fixedAspectRatio is the aspect ration fixed and 'targetAspectRatio' should be used * @param aspectRatio the aspect ratio to maintain */ public void move( RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapMargin, boolean fixedAspectRatio, float aspectRatio) { // Adjust the coordinates for the finger position's offset (i.e. the // distance from the initial touch to the precise handle location). // We want to maintain the initial touch's distance to the pressed // handle so that the crop window size does not "jump". float adjX = x + mTouchOffset.x; float adjY = y + mTouchOffset.y; if (mType == Type.CENTER) { moveCenter(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin); } else { if (fixedAspectRatio) { moveSizeWithFixedAspectRatio( rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin, aspectRatio); } else { moveSizeWithFreeAspectRatio(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin); } } } /** * Calculates the offset of the touch point from the precise location of the specified handle.
* Save these values in a member variable since we want to maintain this offset as we drag the * handle. */ private void calculateTouchOffset(RectF rect, float touchX, float touchY) { float touchOffsetX = 0; float touchOffsetY = 0; // Calculate the offset from the appropriate handle. switch (mType) { case TOP_LEFT: touchOffsetX = rect.left - touchX; touchOffsetY = rect.top - touchY; break; case TOP_RIGHT: touchOffsetX = rect.right - touchX; touchOffsetY = rect.top - touchY; break; case BOTTOM_LEFT: touchOffsetX = rect.left - touchX; touchOffsetY = rect.bottom - touchY; break; case BOTTOM_RIGHT: touchOffsetX = rect.right - touchX; touchOffsetY = rect.bottom - touchY; break; case LEFT: touchOffsetX = rect.left - touchX; touchOffsetY = 0; break; case TOP: touchOffsetX = 0; touchOffsetY = rect.top - touchY; break; case RIGHT: touchOffsetX = rect.right - touchX; touchOffsetY = 0; break; case BOTTOM: touchOffsetX = 0; touchOffsetY = rect.bottom - touchY; break; case CENTER: touchOffsetX = rect.centerX() - touchX; touchOffsetY = rect.centerY() - touchY; break; default: break; } mTouchOffset.x = touchOffsetX; mTouchOffset.y = touchOffsetY; } /** * Center move only changes the position of the crop window without changing the size. */ private void moveCenter( RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapRadius) { float dx = x - rect.centerX(); float dy = y - rect.centerY(); if (rect.left + dx < 0 || rect.right + dx > viewWidth || rect.left + dx < bounds.left || rect.right + dx > bounds.right) { dx /= 1.05f; mTouchOffset.x -= dx / 2; } if (rect.top + dy < 0 || rect.bottom + dy > viewHeight || rect.top + dy < bounds.top || rect.bottom + dy > bounds.bottom) { dy /= 1.05f; mTouchOffset.y -= dy / 2; } rect.offset(dx, dy); snapEdgesToBounds(rect, bounds, snapRadius); } /** * Change the size of the crop window on the required edge (or edges for corner size move) without * affecting "secondary" edges.
* Only the primary edge(s) are fixed to stay within limits. */ private void moveSizeWithFreeAspectRatio( RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapMargin) { switch (mType) { case TOP_LEFT: adjustTop(rect, y, bounds, snapMargin, 0, false, false); adjustLeft(rect, x, bounds, snapMargin, 0, false, false); break; case TOP_RIGHT: adjustTop(rect, y, bounds, snapMargin, 0, false, false); adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false); break; case BOTTOM_LEFT: adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false); adjustLeft(rect, x, bounds, snapMargin, 0, false, false); break; case BOTTOM_RIGHT: adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false); adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false); break; case LEFT: adjustLeft(rect, x, bounds, snapMargin, 0, false, false); break; case TOP: adjustTop(rect, y, bounds, snapMargin, 0, false, false); break; case RIGHT: adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false); break; case BOTTOM: adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false); break; default: break; } } /** * Change the size of the crop window on the required "primary" edge WITH affect to relevant * "secondary" edge via aspect ratio.
* Example: change in the left edge (primary) will affect top and bottom edges (secondary) to * preserve the given aspect ratio. */ private void moveSizeWithFixedAspectRatio( RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapMargin, float aspectRatio) { switch (mType) { case TOP_LEFT: if (calculateAspectRatio(x, y, rect.right, rect.bottom) < aspectRatio) { adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, false); adjustLeftByAspectRatio(rect, aspectRatio); } else { adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, false); adjustTopByAspectRatio(rect, aspectRatio); } break; case TOP_RIGHT: if (calculateAspectRatio(rect.left, y, x, rect.bottom) < aspectRatio) { adjustTop(rect, y, bounds, snapMargin, aspectRatio, false, true); adjustRightByAspectRatio(rect, aspectRatio); } else { adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, false); adjustTopByAspectRatio(rect, aspectRatio); } break; case BOTTOM_LEFT: if (calculateAspectRatio(x, rect.top, rect.right, y) < aspectRatio) { adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, false); adjustLeftByAspectRatio(rect, aspectRatio); } else { adjustLeft(rect, x, bounds, snapMargin, aspectRatio, false, true); adjustBottomByAspectRatio(rect, aspectRatio); } break; case BOTTOM_RIGHT: if (calculateAspectRatio(rect.left, rect.top, x, y) < aspectRatio) { adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, false, true); adjustRightByAspectRatio(rect, aspectRatio); } else { adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, false, true); adjustBottomByAspectRatio(rect, aspectRatio); } break; case LEFT: adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, true); adjustTopBottomByAspectRatio(rect, bounds, aspectRatio); break; case TOP: adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, true); adjustLeftRightByAspectRatio(rect, bounds, aspectRatio); break; case RIGHT: adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, true); adjustTopBottomByAspectRatio(rect, bounds, aspectRatio); break; case BOTTOM: adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, true); adjustLeftRightByAspectRatio(rect, bounds, aspectRatio); break; default: break; } } /** * Check if edges have gone out of bounds (including snap margin), and fix if needed. */ private void snapEdgesToBounds(RectF edges, RectF bounds, float margin) { if (edges.left < bounds.left + margin) { edges.offset(bounds.left - edges.left, 0); } if (edges.top < bounds.top + margin) { edges.offset(0, bounds.top - edges.top); } if (edges.right > bounds.right - margin) { edges.offset(bounds.right - edges.right, 0); } if (edges.bottom > bounds.bottom - margin) { edges.offset(0, bounds.bottom - edges.bottom); } } /** * Get the resulting x-position of the left edge of the crop window given the handle's position * and the image's bounding box and snap radius. * * @param left the position that the left edge is dragged to * @param bounds the bounding box of the image that is being cropped * @param snapMargin the snap distance to the image edge (in pixels) */ private void adjustLeft( RectF rect, float left, RectF bounds, float snapMargin, float aspectRatio, boolean topMoves, boolean bottomMoves) { float newLeft = left; if (newLeft < 0) { newLeft /= 1.05f; mTouchOffset.x -= newLeft / 1.1f; } if (newLeft < bounds.left) { mTouchOffset.x -= (newLeft - bounds.left) / 2f; } if (newLeft - bounds.left < snapMargin) { newLeft = bounds.left; } // Checks if the window is too small horizontally if (rect.right - newLeft < mMinCropWidth) { newLeft = rect.right - mMinCropWidth; } // Checks if the window is too large horizontally if (rect.right - newLeft > mMaxCropWidth) { newLeft = rect.right - mMaxCropWidth; } if (newLeft - bounds.left < snapMargin) { newLeft = bounds.left; } // check vertical bounds if aspect ratio is in play if (aspectRatio > 0) { float newHeight = (rect.right - newLeft) / aspectRatio; // Checks if the window is too small vertically if (newHeight < mMinCropHeight) { newLeft = Math.max(bounds.left, rect.right - mMinCropHeight * aspectRatio); newHeight = (rect.right - newLeft) / aspectRatio; } // Checks if the window is too large vertically if (newHeight > mMaxCropHeight) { newLeft = Math.max(bounds.left, rect.right - mMaxCropHeight * aspectRatio); newHeight = (rect.right - newLeft) / aspectRatio; } // if top AND bottom edge moves by aspect ratio check that it is within full height bounds if (topMoves && bottomMoves) { newLeft = Math.max(newLeft, Math.max(bounds.left, rect.right - bounds.height() * aspectRatio)); } else { // if top edge moves by aspect ratio check that it is within bounds if (topMoves && rect.bottom - newHeight < bounds.top) { newLeft = Math.max(bounds.left, rect.right - (rect.bottom - bounds.top) * aspectRatio); newHeight = (rect.right - newLeft) / aspectRatio; } // if bottom edge moves by aspect ratio check that it is within bounds if (bottomMoves && rect.top + newHeight > bounds.bottom) { newLeft = Math.max( newLeft, Math.max(bounds.left, rect.right - (bounds.bottom - rect.top) * aspectRatio)); } } } rect.left = newLeft; } /** * Get the resulting x-position of the right edge of the crop window given the handle's position * and the image's bounding box and snap radius. * * @param right the position that the right edge is dragged to * @param bounds the bounding box of the image that is being cropped * @param viewWidth * @param snapMargin the snap distance to the image edge (in pixels) */ private void adjustRight( RectF rect, float right, RectF bounds, int viewWidth, float snapMargin, float aspectRatio, boolean topMoves, boolean bottomMoves) { float newRight = right; if (newRight > viewWidth) { newRight = viewWidth + (newRight - viewWidth) / 1.05f; mTouchOffset.x -= (newRight - viewWidth) / 1.1f; } if (newRight > bounds.right) { mTouchOffset.x -= (newRight - bounds.right) / 2f; } // If close to the edge if (bounds.right - newRight < snapMargin) { newRight = bounds.right; } // Checks if the window is too small horizontally if (newRight - rect.left < mMinCropWidth) { newRight = rect.left + mMinCropWidth; } // Checks if the window is too large horizontally if (newRight - rect.left > mMaxCropWidth) { newRight = rect.left + mMaxCropWidth; } // If close to the edge if (bounds.right - newRight < snapMargin) { newRight = bounds.right; } // check vertical bounds if aspect ratio is in play if (aspectRatio > 0) { float newHeight = (newRight - rect.left) / aspectRatio; // Checks if the window is too small vertically if (newHeight < mMinCropHeight) { newRight = Math.min(bounds.right, rect.left + mMinCropHeight * aspectRatio); newHeight = (newRight - rect.left) / aspectRatio; } // Checks if the window is too large vertically if (newHeight > mMaxCropHeight) { newRight = Math.min(bounds.right, rect.left + mMaxCropHeight * aspectRatio); newHeight = (newRight - rect.left) / aspectRatio; } // if top AND bottom edge moves by aspect ratio check that it is within full height bounds if (topMoves && bottomMoves) { newRight = Math.min(newRight, Math.min(bounds.right, rect.left + bounds.height() * aspectRatio)); } else { // if top edge moves by aspect ratio check that it is within bounds if (topMoves && rect.bottom - newHeight < bounds.top) { newRight = Math.min(bounds.right, rect.left + (rect.bottom - bounds.top) * aspectRatio); newHeight = (newRight - rect.left) / aspectRatio; } // if bottom edge moves by aspect ratio check that it is within bounds if (bottomMoves && rect.top + newHeight > bounds.bottom) { newRight = Math.min( newRight, Math.min(bounds.right, rect.left + (bounds.bottom - rect.top) * aspectRatio)); } } } rect.right = newRight; } /** * Get the resulting y-position of the top edge of the crop window given the handle's position and * the image's bounding box and snap radius. * * @param top the x-position that the top edge is dragged to * @param bounds the bounding box of the image that is being cropped * @param snapMargin the snap distance to the image edge (in pixels) */ private void adjustTop( RectF rect, float top, RectF bounds, float snapMargin, float aspectRatio, boolean leftMoves, boolean rightMoves) { float newTop = top; if (newTop < 0) { newTop /= 1.05f; mTouchOffset.y -= newTop / 1.1f; } if (newTop < bounds.top) { mTouchOffset.y -= (newTop - bounds.top) / 2f; } if (newTop - bounds.top < snapMargin) { newTop = bounds.top; } // Checks if the window is too small vertically if (rect.bottom - newTop < mMinCropHeight) { newTop = rect.bottom - mMinCropHeight; } // Checks if the window is too large vertically if (rect.bottom - newTop > mMaxCropHeight) { newTop = rect.bottom - mMaxCropHeight; } if (newTop - bounds.top < snapMargin) { newTop = bounds.top; } // check horizontal bounds if aspect ratio is in play if (aspectRatio > 0) { float newWidth = (rect.bottom - newTop) * aspectRatio; // Checks if the crop window is too small horizontally due to aspect ratio adjustment if (newWidth < mMinCropWidth) { newTop = Math.max(bounds.top, rect.bottom - (mMinCropWidth / aspectRatio)); newWidth = (rect.bottom - newTop) * aspectRatio; } // Checks if the crop window is too large horizontally due to aspect ratio adjustment if (newWidth > mMaxCropWidth) { newTop = Math.max(bounds.top, rect.bottom - (mMaxCropWidth / aspectRatio)); newWidth = (rect.bottom - newTop) * aspectRatio; } // if left AND right edge moves by aspect ratio check that it is within full width bounds if (leftMoves && rightMoves) { newTop = Math.max(newTop, Math.max(bounds.top, rect.bottom - bounds.width() / aspectRatio)); } else { // if left edge moves by aspect ratio check that it is within bounds if (leftMoves && rect.right - newWidth < bounds.left) { newTop = Math.max(bounds.top, rect.bottom - (rect.right - bounds.left) / aspectRatio); newWidth = (rect.bottom - newTop) * aspectRatio; } // if right edge moves by aspect ratio check that it is within bounds if (rightMoves && rect.left + newWidth > bounds.right) { newTop = Math.max( newTop, Math.max(bounds.top, rect.bottom - (bounds.right - rect.left) / aspectRatio)); } } } rect.top = newTop; } /** * Get the resulting y-position of the bottom edge of the crop window given the handle's position * and the image's bounding box and snap radius. * * @param bottom the position that the bottom edge is dragged to * @param bounds the bounding box of the image that is being cropped * @param viewHeight * @param snapMargin the snap distance to the image edge (in pixels) */ private void adjustBottom( RectF rect, float bottom, RectF bounds, int viewHeight, float snapMargin, float aspectRatio, boolean leftMoves, boolean rightMoves) { float newBottom = bottom; if (newBottom > viewHeight) { newBottom = viewHeight + (newBottom - viewHeight) / 1.05f; mTouchOffset.y -= (newBottom - viewHeight) / 1.1f; } if (newBottom > bounds.bottom) { mTouchOffset.y -= (newBottom - bounds.bottom) / 2f; } if (bounds.bottom - newBottom < snapMargin) { newBottom = bounds.bottom; } // Checks if the window is too small vertically if (newBottom - rect.top < mMinCropHeight) { newBottom = rect.top + mMinCropHeight; } // Checks if the window is too small vertically if (newBottom - rect.top > mMaxCropHeight) { newBottom = rect.top + mMaxCropHeight; } if (bounds.bottom - newBottom < snapMargin) { newBottom = bounds.bottom; } // check horizontal bounds if aspect ratio is in play if (aspectRatio > 0) { float newWidth = (newBottom - rect.top) * aspectRatio; // Checks if the window is too small horizontally if (newWidth < mMinCropWidth) { newBottom = Math.min(bounds.bottom, rect.top + mMinCropWidth / aspectRatio); newWidth = (newBottom - rect.top) * aspectRatio; } // Checks if the window is too large horizontally if (newWidth > mMaxCropWidth) { newBottom = Math.min(bounds.bottom, rect.top + mMaxCropWidth / aspectRatio); newWidth = (newBottom - rect.top) * aspectRatio; } // if left AND right edge moves by aspect ratio check that it is within full width bounds if (leftMoves && rightMoves) { newBottom = Math.min(newBottom, Math.min(bounds.bottom, rect.top + bounds.width() / aspectRatio)); } else { // if left edge moves by aspect ratio check that it is within bounds if (leftMoves && rect.right - newWidth < bounds.left) { newBottom = Math.min(bounds.bottom, rect.top + (rect.right - bounds.left) / aspectRatio); newWidth = (newBottom - rect.top) * aspectRatio; } // if right edge moves by aspect ratio check that it is within bounds if (rightMoves && rect.left + newWidth > bounds.right) { newBottom = Math.min( newBottom, Math.min(bounds.bottom, rect.top + (bounds.right - rect.left) / aspectRatio)); } } } rect.bottom = newBottom; } /** * Adjust left edge by current crop window height and the given aspect ratio, the right edge * remains in possition while the left adjusts to keep aspect ratio to the height. */ private void adjustLeftByAspectRatio(RectF rect, float aspectRatio) { rect.left = rect.right - rect.height() * aspectRatio; } /** * Adjust top edge by current crop window width and the given aspect ratio, the bottom edge * remains in possition while the top adjusts to keep aspect ratio to the width. */ private void adjustTopByAspectRatio(RectF rect, float aspectRatio) { rect.top = rect.bottom - rect.width() / aspectRatio; } /** * Adjust right edge by current crop window height and the given aspect ratio, the left edge * remains in possition while the left adjusts to keep aspect ratio to the height. */ private void adjustRightByAspectRatio(RectF rect, float aspectRatio) { rect.right = rect.left + rect.height() * aspectRatio; } /** * Adjust bottom edge by current crop window width and the given aspect ratio, the top edge * remains in possition while the top adjusts to keep aspect ratio to the width. */ private void adjustBottomByAspectRatio(RectF rect, float aspectRatio) { rect.bottom = rect.top + rect.width() / aspectRatio; } /** * Adjust left and right edges by current crop window height and the given aspect ratio, both * right and left edges adjusts equally relative to center to keep aspect ratio to the height. */ private void adjustLeftRightByAspectRatio(RectF rect, RectF bounds, float aspectRatio) { rect.inset((rect.width() - rect.height() * aspectRatio) / 2, 0); if (rect.left < bounds.left) { rect.offset(bounds.left - rect.left, 0); } if (rect.right > bounds.right) { rect.offset(bounds.right - rect.right, 0); } } /** * Adjust top and bottom edges by current crop window width and the given aspect ratio, both top * and bottom edges adjusts equally relative to center to keep aspect ratio to the width. */ private void adjustTopBottomByAspectRatio(RectF rect, RectF bounds, float aspectRatio) { rect.inset(0, (rect.height() - rect.width() / aspectRatio) / 2); if (rect.top < bounds.top) { rect.offset(0, bounds.top - rect.top); } if (rect.bottom > bounds.bottom) { rect.offset(0, bounds.bottom - rect.bottom); } } // endregion // region: Inner class: Type /** * The type of crop window move that is handled. */ public enum Type { TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, LEFT, TOP, RIGHT, BOTTOM, CENTER } // endregion }