269 lines
10 KiB
Kotlin
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
|
|
}
|
|
}
|
|
} |