405 lines
14 KiB
Kotlin
405 lines
14 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.graphics.PointF
|
|
import android.graphics.RectF
|
|
|
|
/**
|
|
* Handler to update crop window edges by the move type - Horizontal, Vertical, Corner or Center.
|
|
*/
|
|
internal class CropWindowMoveHandler(
|
|
/** The type of crop window move that is handled. */
|
|
private val mType: Type,
|
|
cropWindowHandler: CropWindowHandler, touchX: Float, touchY: Float
|
|
) {
|
|
/** Minimum width in pixels that the crop window can get. */
|
|
private val mMinCropWidth: Float
|
|
|
|
/** Minimum width in pixels that the crop window can get. */
|
|
private val mMinCropHeight: Float
|
|
|
|
/** Maximum height in pixels that the crop window can get. */
|
|
private val mMaxCropWidth: Float
|
|
|
|
/** Maximum height in pixels that the crop window can get. */
|
|
private val mMaxCropHeight: Float
|
|
|
|
/**
|
|
* 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 val mTouchOffset = PointF()
|
|
|
|
init {
|
|
mMinCropWidth = cropWindowHandler.minCropWidth
|
|
mMinCropHeight = cropWindowHandler.minCropHeight
|
|
mMaxCropWidth = cropWindowHandler.maxCropWidth
|
|
mMaxCropHeight = cropWindowHandler.maxCropHeight
|
|
calculateTouchOffset(cropWindowHandler.rect, touchX, touchY)
|
|
}
|
|
|
|
/**
|
|
* Updates the crop window by change in the touch location.
|
|
* Move type handled by this instance, as initialized in creation, affects how the change in
|
|
* touch location changes the crop window position and size.
|
|
* After the crop window position/size is changed by touch move it may result in values that
|
|
* violate constraints: outside the bounds of the shown bitmap, smaller/larger than min/max size or
|
|
* mismatch 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 snapMargin the maximum distance (in pixels) at which the crop window should snap to the
|
|
* image
|
|
*/
|
|
fun move(
|
|
rect: RectF,
|
|
x: Float,
|
|
y: Float,
|
|
bounds: RectF,
|
|
viewWidth: Int,
|
|
viewHeight: Int,
|
|
snapMargin: Float
|
|
) {
|
|
|
|
// 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".
|
|
val adjX = x + mTouchOffset.x
|
|
val adjY = y + mTouchOffset.y
|
|
if (mType == Type.CENTER) {
|
|
moveCenter(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin)
|
|
} else {
|
|
changeSize(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin)
|
|
}
|
|
}
|
|
// region: Private methods
|
|
/**
|
|
* Calculates the offset of the touch point from the precise location of the specified handle.<br></br>
|
|
* Save these values in a member variable since we want to maintain this offset as we drag the
|
|
* handle.
|
|
*/
|
|
private fun calculateTouchOffset(rect: RectF, touchX: Float, touchY: Float) {
|
|
var touchOffsetX = 0f
|
|
var touchOffsetY = 0f
|
|
when (mType) {
|
|
Type.TOP_LEFT -> {
|
|
touchOffsetX = rect.left - touchX
|
|
touchOffsetY = rect.top - touchY
|
|
}
|
|
|
|
Type.TOP_RIGHT -> {
|
|
touchOffsetX = rect.right - touchX
|
|
touchOffsetY = rect.top - touchY
|
|
}
|
|
|
|
Type.BOTTOM_LEFT -> {
|
|
touchOffsetX = rect.left - touchX
|
|
touchOffsetY = rect.bottom - touchY
|
|
}
|
|
|
|
Type.BOTTOM_RIGHT -> {
|
|
touchOffsetX = rect.right - touchX
|
|
touchOffsetY = rect.bottom - touchY
|
|
}
|
|
|
|
Type.LEFT -> {
|
|
touchOffsetX = rect.left - touchX
|
|
touchOffsetY = 0f
|
|
}
|
|
|
|
Type.TOP -> {
|
|
touchOffsetX = 0f
|
|
touchOffsetY = rect.top - touchY
|
|
}
|
|
|
|
Type.RIGHT -> {
|
|
touchOffsetX = rect.right - touchX
|
|
touchOffsetY = 0f
|
|
}
|
|
|
|
Type.BOTTOM -> {
|
|
touchOffsetX = 0f
|
|
touchOffsetY = rect.bottom - touchY
|
|
}
|
|
|
|
Type.CENTER -> {
|
|
touchOffsetX = rect.centerX() - touchX
|
|
touchOffsetY = rect.centerY() - touchY
|
|
}
|
|
}
|
|
mTouchOffset.x = touchOffsetX
|
|
mTouchOffset.y = touchOffsetY
|
|
}
|
|
|
|
/** Center move only changes the position of the crop window without changing the size. */
|
|
private fun moveCenter(
|
|
rect: RectF,
|
|
x: Float,
|
|
y: Float,
|
|
bounds: RectF,
|
|
viewWidth: Int,
|
|
viewHeight: Int,
|
|
snapRadius: Float
|
|
) {
|
|
var dx = x - rect.centerX()
|
|
var 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 in the case of a corner)
|
|
*/
|
|
private fun changeSize(
|
|
rect: RectF,
|
|
x: Float,
|
|
y: Float,
|
|
bounds: RectF,
|
|
viewWidth: Int,
|
|
viewHeight: Int,
|
|
snapMargin: Float
|
|
) {
|
|
when (mType) {
|
|
Type.TOP_LEFT -> {
|
|
adjustTop(rect, y, bounds, snapMargin)
|
|
adjustLeft(rect, x, bounds, snapMargin)
|
|
}
|
|
|
|
Type.TOP_RIGHT -> {
|
|
adjustTop(rect, y, bounds, snapMargin)
|
|
adjustRight(rect, x, bounds, viewWidth, snapMargin)
|
|
}
|
|
|
|
Type.BOTTOM_LEFT -> {
|
|
adjustBottom(rect, y, bounds, viewHeight, snapMargin)
|
|
adjustLeft(rect, x, bounds, snapMargin)
|
|
}
|
|
|
|
Type.BOTTOM_RIGHT -> {
|
|
adjustBottom(rect, y, bounds, viewHeight, snapMargin)
|
|
adjustRight(rect, x, bounds, viewWidth, snapMargin)
|
|
}
|
|
|
|
Type.LEFT -> adjustLeft(rect, x, bounds, snapMargin)
|
|
Type.TOP -> adjustTop(rect, y, bounds, snapMargin)
|
|
Type.RIGHT -> adjustRight(rect, x, bounds, viewWidth, snapMargin)
|
|
Type.BOTTOM -> adjustBottom(rect, y, bounds, viewHeight, snapMargin)
|
|
else -> {}
|
|
}
|
|
}
|
|
|
|
/** Check if edges have gone out of bounds (including snap margin), and fix if needed. */
|
|
private fun snapEdgesToBounds(edges: RectF, bounds: RectF, margin: Float) {
|
|
if (edges.left < bounds.left + margin) {
|
|
edges.offset(bounds.left - edges.left, 0f)
|
|
}
|
|
if (edges.top < bounds.top + margin) {
|
|
edges.offset(0f, bounds.top - edges.top)
|
|
}
|
|
if (edges.right > bounds.right - margin) {
|
|
edges.offset(bounds.right - edges.right, 0f)
|
|
}
|
|
if (edges.bottom > bounds.bottom - margin) {
|
|
edges.offset(0f, 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 notCropped
|
|
* @param snapMargin the snap distance to the image edge (in pixels)
|
|
*/
|
|
private fun adjustLeft(
|
|
rect: RectF,
|
|
left: Float,
|
|
bounds: RectF,
|
|
snapMargin: Float
|
|
) {
|
|
var 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
|
|
}
|
|
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 notCropped
|
|
* @param viewWidth
|
|
* @param snapMargin the snap distance to the image edge (in pixels)
|
|
*/
|
|
private fun adjustRight(
|
|
rect: RectF,
|
|
right: Float,
|
|
bounds: RectF,
|
|
viewWidth: Int,
|
|
snapMargin: Float
|
|
) {
|
|
var 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
|
|
}
|
|
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 notCropped
|
|
* @param snapMargin the snap distance to the image edge (in pixels)
|
|
*/
|
|
private fun adjustTop(
|
|
rect: RectF,
|
|
top: Float,
|
|
bounds: RectF,
|
|
snapMargin: Float
|
|
) {
|
|
var 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
|
|
}
|
|
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 notCropped
|
|
* @param viewHeight
|
|
* @param snapMargin the snap distance to the image edge (in pixels)
|
|
*/
|
|
private fun adjustBottom(
|
|
rect: RectF,
|
|
bottom: Float,
|
|
bounds: RectF,
|
|
viewHeight: Int,
|
|
snapMargin: Float
|
|
) {
|
|
var 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
|
|
}
|
|
rect.bottom = newBottom
|
|
}
|
|
// endregion
|
|
|
|
/** The type of crop window move that is handled. */
|
|
enum class Type {
|
|
TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, LEFT, TOP, RIGHT, BOTTOM, CENTER
|
|
}
|
|
} |