// "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.forfan.bigbang.cropper.handler; import android.graphics.RectF; /** * Handler from crop window stuff, moving and knowing possition. */ final class CropWindowHandler { //region: Fields and Consts /** * The 4 edges of the crop window defining its coordinates and size */ private final RectF mEdges = new RectF(); /** * Rectangle used to return the edges rectangle without ability to change it and without creating new all the time. */ private final RectF mGetEdges = new RectF(); /** * Minimum width in pixels that the crop window can get. */ private float mMinCropWindowWidth; /** * Minimum height in pixels that the crop window can get. */ private float mMinCropWindowHeight; /** * Maximum width in pixels that the crop window can CURRENTLY get. */ private float mMaxCropWindowWidth; /** * Maximum height in pixels that the crop window can CURRENTLY get. */ private float mMaxCropWindowHeight; /** * Minimum width in pixels that the result of cropping an image can get, * affects crop window width adjusted by width scale factor. */ private float mMinCropResultWidth; /** * Minimum height in pixels that the result of cropping an image can get, * affects crop window height adjusted by height scale factor. */ private float mMinCropResultHeight; /** * Maximum width in pixels that the result of cropping an image can get, * affects crop window width adjusted by width scale factor. */ private float mMaxCropResultWidth; /** * Maximum height in pixels that the result of cropping an image can get, * affects crop window height adjusted by height scale factor. */ private float mMaxCropResultHeight; /** * The width scale factor of shown image and actual image */ private float mScaleFactorWidth = 1; /** * The height scale factor of shown image and actual image */ private float mScaleFactorHeight = 1; //endregion /** * Get the left/top/right/bottom coordinates of the crop window. */ public RectF getRect() { mGetEdges.set(mEdges); return mGetEdges; } /** * Minimum width in pixels that the crop window can get. */ public float getMinCropWidth() { return Math.max(mMinCropWindowWidth, mMinCropResultWidth / mScaleFactorWidth); } /** * Minimum height in pixels that the crop window can get. */ public float getMinCropHeight() { return Math.max(mMinCropWindowHeight, mMinCropResultHeight / mScaleFactorHeight); } /** * Maximum width in pixels that the crop window can get. */ public float getMaxCropWidth() { return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth / mScaleFactorWidth); } /** * Maximum height in pixels that the crop window can get. */ public float getMaxCropHeight() { return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight / mScaleFactorHeight); } /** * get the scale factor (on width) of the showen image to original image. */ public float getScaleFactorWidth() { return mScaleFactorWidth; } /** * get the scale factor (on height) of the showen image to original image. */ public float getScaleFactorHeight() { return mScaleFactorHeight; } /** * the min size the resulting cropping image is allowed to be, affects the cropping window limits * (in pixels).<br> */ public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) { mMinCropResultWidth = minCropResultWidth; mMinCropResultHeight = minCropResultHeight; } /** * the max size the resulting cropping image is allowed to be, affects the cropping window limits * (in pixels).<br> */ public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) { mMaxCropResultWidth = maxCropResultWidth; mMaxCropResultHeight = maxCropResultHeight; } /** * set the max width/height and scale factor of the showen image to original image to scale the limits * appropriately. */ public void setCropWindowLimits(float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) { mMaxCropWindowWidth = maxWidth; mMaxCropWindowHeight = maxHeight; mScaleFactorWidth = scaleFactorWidth; mScaleFactorHeight = scaleFactorHeight; } /** * Set the variables to be used during crop window handling. */ public void setInitialAttributeValues(CropImageOptions options) { mMinCropWindowWidth = options.minCropWindowWidth; mMinCropWindowHeight = options.minCropWindowHeight; mMinCropResultWidth = options.minCropResultWidth; mMinCropResultHeight = options.minCropResultHeight; mMaxCropResultWidth = options.maxCropResultWidth; mMaxCropResultHeight = options.maxCropResultHeight; } /** * Set the left/top/right/bottom coordinates of the crop window. */ public void setRect(RectF rect) { mEdges.set(rect); } /** * Indicates whether the crop window is small enough that the guidelines * should be shown. Public because this function is also used to determine * if the center handle should be focused. * * @return boolean Whether the guidelines should be shown or not */ public boolean showGuidelines() { return !(mEdges.width() < 100 || mEdges.height() < 100); } /** * Determines which, if any, of the handles are pressed given the touch * coordinates, the bounding box, and the touch radius. * * @param x the x-coordinate of the touch point * @param y the y-coordinate of the touch point * @param targetRadius the target radius in pixels * @return the Handle that was pressed; null if no Handle was pressed */ public CropWindowMoveHandler getMoveHandler(float x, float y, float targetRadius, CropImageView.CropShape cropShape) { CropWindowMoveHandler.Type type = cropShape == CropImageView.CropShape.OVAL ? getOvalPressedMoveType(x, y) : getRectanglePressedMoveType(x, y, targetRadius); return type != null ? new CropWindowMoveHandler(type, this, x, y) : null; } //region: Private methods /** * Determines which, if any, of the handles are pressed given the touch * coordinates, the bounding box, and the touch radius. * * @param x the x-coordinate of the touch point * @param y the y-coordinate of the touch point * @param targetRadius the target radius in pixels * @return the Handle that was pressed; null if no Handle was pressed */ private CropWindowMoveHandler.Type getRectanglePressedMoveType(float x, float y, float targetRadius) { CropWindowMoveHandler.Type moveType = null; // Note: corner-handles take precedence, then side-handles, then center. if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.left, mEdges.top, targetRadius)) { moveType = CropWindowMoveHandler.Type.TOP_LEFT; } else if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.right, mEdges.top, targetRadius)) { moveType = CropWindowMoveHandler.Type.TOP_RIGHT; } else if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.left, mEdges.bottom, targetRadius)) { moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT; } else if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.right, mEdges.bottom, targetRadius)) { moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT; } else if (CropWindowHandler.isInCenterTargetZone(x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom) && focusCenter()) { moveType = CropWindowMoveHandler.Type.CENTER; } else if (CropWindowHandler.isInHorizontalTargetZone(x, y, mEdges.left, mEdges.right, mEdges.top, targetRadius)) { moveType = CropWindowMoveHandler.Type.TOP; } else if (CropWindowHandler.isInHorizontalTargetZone(x, y, mEdges.left, mEdges.right, mEdges.bottom, targetRadius)) { moveType = CropWindowMoveHandler.Type.BOTTOM; } else if (CropWindowHandler.isInVerticalTargetZone(x, y, mEdges.left, mEdges.top, mEdges.bottom, targetRadius)) { moveType = CropWindowMoveHandler.Type.LEFT; } else if (CropWindowHandler.isInVerticalTargetZone(x, y, mEdges.right, mEdges.top, mEdges.bottom, targetRadius)) { moveType = CropWindowMoveHandler.Type.RIGHT; } else if (CropWindowHandler.isInCenterTargetZone(x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom) && !focusCenter()) { moveType = CropWindowMoveHandler.Type.CENTER; } return moveType; } /** * Determines which, if any, of the handles are pressed given the touch * coordinates, the bounding box/oval, and the touch radius. * * @param x the x-coordinate of the touch point * @param y the y-coordinate of the touch point * @return the Handle that was pressed; null if no Handle was pressed */ private CropWindowMoveHandler.Type getOvalPressedMoveType(float x, float y) { /* Use a 6x6 grid system divided into 9 "handles", with the center the biggest region. While this is not perfect, it's a good quick-to-ship approach. TL T T T T TR L C C C C R L C C C C R L C C C C R L C C C C R BL B B B B BR */ float cellLength = mEdges.width() / 6; float leftCenter = mEdges.left + cellLength; float rightCenter = mEdges.left + (5 * cellLength); float cellHeight = mEdges.height() / 6; float topCenter = mEdges.top + cellHeight; float bottomCenter = mEdges.top + 5 * cellHeight; CropWindowMoveHandler.Type moveType; if (x < leftCenter) { if (y < topCenter) { moveType = CropWindowMoveHandler.Type.TOP_LEFT; } else if (y < bottomCenter) { moveType = CropWindowMoveHandler.Type.LEFT; } else { moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT; } } else if (x < rightCenter) { if (y < topCenter) { moveType = CropWindowMoveHandler.Type.TOP; } else if (y < bottomCenter) { moveType = CropWindowMoveHandler.Type.CENTER; } else { moveType = CropWindowMoveHandler.Type.BOTTOM; } } else { if (y < topCenter) { moveType = CropWindowMoveHandler.Type.TOP_RIGHT; } else if (y < bottomCenter) { moveType = CropWindowMoveHandler.Type.RIGHT; } else { moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT; } } return moveType; } /** * Determines if the specified coordinate is in the target touch zone for a * corner handle. * * @param x the x-coordinate of the touch point * @param y the y-coordinate of the touch point * @param handleX the x-coordinate of the corner handle * @param handleY the y-coordinate of the corner handle * @param targetRadius the target radius in pixels * @return true if the touch point is in the target touch zone; false * otherwise */ private static boolean isInCornerTargetZone(float x, float y, float handleX, float handleY, float targetRadius) { return Math.abs(x - handleX) <= targetRadius && Math.abs(y - handleY) <= targetRadius; } /** * Determines if the specified coordinate is in the target touch zone for a * horizontal bar handle. * * @param x the x-coordinate of the touch point * @param y the y-coordinate of the touch point * @param handleXStart the left x-coordinate of the horizontal bar handle * @param handleXEnd the right x-coordinate of the horizontal bar handle * @param handleY the y-coordinate of the horizontal bar handle * @param targetRadius the target radius in pixels * @return true if the touch point is in the target touch zone; false * otherwise */ private static boolean isInHorizontalTargetZone(float x, float y, float handleXStart, float handleXEnd, float handleY, float targetRadius) { return x > handleXStart && x < handleXEnd && Math.abs(y - handleY) <= targetRadius; } /** * Determines if the specified coordinate is in the target touch zone for a * vertical bar handle. * * @param x the x-coordinate of the touch point * @param y the y-coordinate of the touch point * @param handleX the x-coordinate of the vertical bar handle * @param handleYStart the top y-coordinate of the vertical bar handle * @param handleYEnd the bottom y-coordinate of the vertical bar handle * @param targetRadius the target radius in pixels * @return true if the touch point is in the target touch zone; false * otherwise */ private static boolean isInVerticalTargetZone(float x, float y, float handleX, float handleYStart, float handleYEnd, float targetRadius) { return Math.abs(x - handleX) <= targetRadius && y > handleYStart && y < handleYEnd; } /** * Determines if the specified coordinate falls anywhere inside the given * bounds. * * @param x the x-coordinate of the touch point * @param y the y-coordinate of the touch point * @param left the x-coordinate of the left bound * @param top the y-coordinate of the top bound * @param right the x-coordinate of the right bound * @param bottom the y-coordinate of the bottom bound * @return true if the touch point is inside the bounding rectangle; false * otherwise */ private static boolean isInCenterTargetZone(float x, float y, float left, float top, float right, float bottom) { return x > left && x < right && y > top && y < bottom; } /** * Determines if the cropper should focus on the center handle or the side * handles. If it is a small image, focus on the center handle so the user * can move it. If it is a large image, focus on the side handles so user * can grab them. Corresponds to the appearance of the * RuleOfThirdsGuidelines. * * @return true if it is small enough such that it should focus on the * center; less than show_guidelines limit */ private boolean focusCenter() { return !showGuidelines(); } //endregion }