PixelDroid-App-Android/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowHandler.kt

269 lines
10 KiB
Kotlin

package org.pixeldroid.media_editor.photoEdit.cropper
// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is
// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid
// are under licensed under the GPLv3 or later, just like the rest of the PixelDroid project
import android.content.res.Resources
import android.graphics.RectF
import android.util.TypedValue
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
/** Handler from crop window stuff, moving and knowing position. */
internal class CropWindowHandler {
/** The 4 edges of the crop window defining its coordinates and size */
private val mEdges = RectF()
/**
* Rectangle used to return the edges rectangle without ability to change it and without
* creating new all the time.
*/
private val mGetEdges = RectF()
/** Maximum width in pixels that the crop window can CURRENTLY get. */
private var mMaxCropWindowWidth = 0f
/** Maximum height in pixels that the crop window can CURRENTLY get. */
private var mMaxCropWindowHeight = 0f
/** The left/top/right/bottom coordinates of the crop window. */
var rect: RectF
get() {
mGetEdges.set(mEdges)
return mGetEdges
}
set(rect) {
mEdges.set(rect)
}
/** Minimum width in pixels that the crop window can get. */
val minCropWidth: Float
get() {
val dm = Resources.getSystem().displayMetrics
val mMinCropResultWidth = 40f
return max(
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42f, dm).toInt().toFloat(),
mMinCropResultWidth
)
}
/** Minimum height in pixels that the crop window can get. */
val minCropHeight: Float
get() {
val dm = Resources.getSystem().displayMetrics
val mMinCropResultHeight = 40f
return max(
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42f, dm).toInt().toFloat(),
mMinCropResultHeight
)
}
/** Maximum width in pixels that the crop window can get. */
val maxCropWidth: Float
get() {
val mMaxCropResultWidth = 99999f
return min(mMaxCropWindowWidth, mMaxCropResultWidth)
}
/** Maximum height in pixels that the crop window can get. */
val maxCropHeight: Float
get() {
val mMaxCropResultHeight = 99999f
return min(mMaxCropWindowHeight, mMaxCropResultHeight)
}
/**
* Set the max width/height of the shown image to original image to scale the limits appropriately
*/
fun setCropWindowLimits(maxWidth: Float, maxHeight: Float) {
mMaxCropWindowWidth = maxWidth
mMaxCropWindowHeight = maxHeight
}
/**
* 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
*/
fun showGuidelines(): Boolean {
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
*/
fun getMoveHandler(x: Float, y: Float, targetRadius: Float): CropWindowMoveHandler? {
val type = getRectanglePressedMoveType(x, y, targetRadius)
return if (type != null) CropWindowMoveHandler(type, this, x, y) else 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 fun getRectanglePressedMoveType(
x: Float, y: Float, targetRadius: Float
): CropWindowMoveHandler.Type? {
var moveType: CropWindowMoveHandler.Type? = null
// Note: corner-handles take precedence, then side-handles, then center.
if (isInCornerTargetZone(x, y, mEdges.left, mEdges.top, targetRadius)) {
moveType = CropWindowMoveHandler.Type.TOP_LEFT
} else if (isInCornerTargetZone(
x, y, mEdges.right, mEdges.top, targetRadius
)
) {
moveType = CropWindowMoveHandler.Type.TOP_RIGHT
} else if (isInCornerTargetZone(
x, y, mEdges.left, mEdges.bottom, targetRadius
)
) {
moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT
} else if (isInCornerTargetZone(
x, y, mEdges.right, mEdges.bottom, targetRadius
)
) {
moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT
} else if (isInCenterTargetZone(
x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom
)
&& focusCenter()
) {
moveType = CropWindowMoveHandler.Type.CENTER
} else if (isInHorizontalTargetZone(
x, y, mEdges.left, mEdges.right, mEdges.top, targetRadius
)
) {
moveType = CropWindowMoveHandler.Type.TOP
} else if (isInHorizontalTargetZone(
x, y, mEdges.left, mEdges.right, mEdges.bottom, targetRadius
)
) {
moveType = CropWindowMoveHandler.Type.BOTTOM
} else if (isInVerticalTargetZone(
x, y, mEdges.left, mEdges.top, mEdges.bottom, targetRadius
)
) {
moveType = CropWindowMoveHandler.Type.LEFT
} else if (isInVerticalTargetZone(
x, y, mEdges.right, mEdges.top, mEdges.bottom, targetRadius
)
) {
moveType = CropWindowMoveHandler.Type.RIGHT
} else if (isInCenterTargetZone(
x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom
)
&& !focusCenter()
) {
moveType = CropWindowMoveHandler.Type.CENTER
}
return moveType
}
/**
* 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 fun focusCenter(): Boolean = !showGuidelines()
// endregion
companion object {
/**
* 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 fun isInCornerTargetZone(
x: Float, y: Float, handleX: Float, handleY: Float, targetRadius: Float
): Boolean {
return abs(x - handleX) <= targetRadius && 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 fun isInHorizontalTargetZone(
x: Float,
y: Float,
handleXStart: Float,
handleXEnd: Float,
handleY: Float,
targetRadius: Float
): Boolean {
return x > handleXStart && x < handleXEnd && 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 fun isInVerticalTargetZone(
x: Float,
y: Float,
handleX: Float,
handleYStart: Float,
handleYEnd: Float,
targetRadius: Float
): Boolean {
return 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 fun isInCenterTargetZone(
x: Float, y: Float, left: Float, top: Float, right: Float, bottom: Float
): Boolean {
return x > left && x < right && y > top && y < bottom
}
}
}