490 lines
18 KiB
Kotlin
490 lines
18 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.Context
|
|
import android.content.res.Resources
|
|
import android.graphics.Canvas
|
|
import android.graphics.Color
|
|
import android.graphics.Paint
|
|
import android.graphics.Rect
|
|
import android.graphics.RectF
|
|
import android.util.AttributeSet
|
|
import android.util.TypedValue
|
|
import android.view.MotionEvent
|
|
import android.view.View
|
|
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity.RelativeCropPosition
|
|
import kotlin.math.max
|
|
import kotlin.math.min
|
|
|
|
/** A custom View representing the crop window and the shaded background outside the crop window. */
|
|
class CropOverlayView // endregion
|
|
@JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : View(context, attrs) {
|
|
// region: Fields and Consts
|
|
/** Handler from crop window stuff, moving and knowing position. */
|
|
private val mCropWindowHandler = CropWindowHandler()
|
|
|
|
/** The Paint used to draw the white rectangle around the crop area. */
|
|
private var mBorderPaint: Paint? = null
|
|
|
|
/** The Paint used to draw the corners of the Border */
|
|
private var mBorderCornerPaint: Paint? = null
|
|
|
|
/** The Paint used to draw the guidelines within the crop area when pressed. */
|
|
private var mGuidelinePaint: Paint? = null
|
|
|
|
/** The bounding box around the Bitmap that we are cropping. */
|
|
private val mCalcBounds = RectF()
|
|
|
|
/** The bounding image view width used to know the crop overlay is at view edges. */
|
|
private var mViewWidth = 0
|
|
|
|
/** The bounding image view height used to know the crop overlay is at view edges. */
|
|
private var mViewHeight = 0
|
|
|
|
/** The Handle that is currently pressed; null if no Handle is pressed. */
|
|
private var mMoveHandler: CropWindowMoveHandler? = null
|
|
|
|
/** the initial crop window rectangle to set */
|
|
private val mInitialCropWindowRect = Rect()
|
|
|
|
/** Whether the Crop View has been initialized for the first time */
|
|
private var initializedCropWindow = false
|
|
/** Get the left/top/right/bottom coordinates of the crop window. */
|
|
/** Set the left/top/right/bottom coordinates of the crop window. */
|
|
var cropWindowRect: RectF
|
|
get() = mCropWindowHandler.rect
|
|
set(rect) {
|
|
mCropWindowHandler.rect = rect
|
|
}
|
|
|
|
/**
|
|
* Informs the CropOverlayView of the image's position relative to the ImageView. This is
|
|
* necessary to call in order to draw the crop window.
|
|
*
|
|
* @param viewWidth The bounding image view width.
|
|
* @param viewHeight The bounding image view height.
|
|
*/
|
|
fun setBounds(viewWidth: Int, viewHeight: Int) {
|
|
mViewWidth = viewWidth
|
|
mViewHeight = viewHeight
|
|
val cropRect = mCropWindowHandler.rect
|
|
if (cropRect.width() == 0f || cropRect.height() == 0f) {
|
|
initCropWindow()
|
|
}
|
|
}
|
|
|
|
/** Resets the crop overlay view. */
|
|
fun resetCropOverlayView() {
|
|
if (initializedCropWindow) {
|
|
cropWindowRect = RectF()
|
|
initCropWindow()
|
|
invalidate()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the max width/height and scale factor of the shown image to original image to scale the
|
|
* limits appropriately.
|
|
*/
|
|
fun setCropWindowLimits(maxWidth: Float, maxHeight: Float) {
|
|
mCropWindowHandler.setCropWindowLimits(maxWidth, maxHeight)
|
|
}
|
|
/** Get crop window initial rectangle. */
|
|
/** Set crop window initial rectangle to be used instead of default. */
|
|
var initialCropWindowRect: Rect
|
|
get() = mInitialCropWindowRect
|
|
set(rect) {
|
|
mInitialCropWindowRect.set(rect)
|
|
if (initializedCropWindow) {
|
|
initCropWindow()
|
|
invalidate()
|
|
}
|
|
}
|
|
|
|
fun setRecordedCropWindowRect(relativeCropPosition: RelativeCropPosition) {
|
|
val rect = RectF(
|
|
mInitialCropWindowRect.left + relativeCropPosition.relativeX * mInitialCropWindowRect.width(),
|
|
mInitialCropWindowRect.top + relativeCropPosition.relativeY * mInitialCropWindowRect.height(),
|
|
relativeCropPosition.relativeWidth * mInitialCropWindowRect.width() + mInitialCropWindowRect.left + relativeCropPosition.relativeX * mInitialCropWindowRect.width(),
|
|
relativeCropPosition.relativeHeight * mInitialCropWindowRect.height() + mInitialCropWindowRect.top + relativeCropPosition.relativeY * mInitialCropWindowRect.height()
|
|
)
|
|
mCropWindowHandler.rect = rect
|
|
}
|
|
|
|
/** Reset crop window to initial rectangle. */
|
|
fun resetCropWindowRect() {
|
|
if (initializedCropWindow) {
|
|
initCropWindow()
|
|
invalidate()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets all initial values, but does not call initCropWindow to reset the views.<br></br>
|
|
* Used once at the very start to initialize the attributes.
|
|
*/
|
|
fun setInitialAttributeValues() {
|
|
val dm = Resources.getSystem().displayMetrics
|
|
mBorderPaint = getNewPaintOfThickness(
|
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3f, dm),
|
|
Color.argb(170, 255, 255, 255)
|
|
)
|
|
mBorderCornerPaint = getNewPaintOfThickness(
|
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, dm),
|
|
Color.WHITE
|
|
)
|
|
mGuidelinePaint = getNewPaintOfThickness(
|
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1f, dm),
|
|
Color.argb(170, 255, 255, 255)
|
|
)
|
|
}
|
|
// region: Private methods
|
|
/**
|
|
* Set the initial crop window size and position. This is dependent on the size and position of
|
|
* the image being cropped.
|
|
*/
|
|
private fun initCropWindow() {
|
|
val rect = RectF()
|
|
|
|
// Tells the attribute functions the crop window has already been initialized
|
|
initializedCropWindow = true
|
|
if (mInitialCropWindowRect.width() > 0 && mInitialCropWindowRect.height() > 0) {
|
|
// Get crop window position relative to the displayed image.
|
|
rect.left = mInitialCropWindowRect.left.toFloat()
|
|
rect.top = mInitialCropWindowRect.top.toFloat()
|
|
rect.right = rect.left + mInitialCropWindowRect.width()
|
|
rect.bottom = rect.top + mInitialCropWindowRect.height()
|
|
}
|
|
fixCropWindowRectByRules(rect)
|
|
mCropWindowHandler.rect = rect
|
|
}
|
|
|
|
/** Fix the given rect to fit into bitmap rect and follow min, max and aspect ratio rules. */
|
|
private fun fixCropWindowRectByRules(rect: RectF) {
|
|
if (rect.width() < mCropWindowHandler.minCropWidth) {
|
|
val adj = (mCropWindowHandler.minCropWidth - rect.width()) / 2
|
|
rect.left -= adj
|
|
rect.right += adj
|
|
}
|
|
if (rect.height() < mCropWindowHandler.minCropHeight) {
|
|
val adj = (mCropWindowHandler.minCropHeight - rect.height()) / 2
|
|
rect.top -= adj
|
|
rect.bottom += adj
|
|
}
|
|
if (rect.width() > mCropWindowHandler.maxCropWidth) {
|
|
val adj = (rect.width() - mCropWindowHandler.maxCropWidth) / 2
|
|
rect.left += adj
|
|
rect.right -= adj
|
|
}
|
|
if (rect.height() > mCropWindowHandler.maxCropHeight) {
|
|
val adj = (rect.height() - mCropWindowHandler.maxCropHeight) / 2
|
|
rect.top += adj
|
|
rect.bottom -= adj
|
|
}
|
|
setBounds()
|
|
if (mCalcBounds.width() > 0 && mCalcBounds.height() > 0) {
|
|
val leftLimit = max(mCalcBounds.left, 0f)
|
|
val topLimit = max(mCalcBounds.top, 0f)
|
|
val rightLimit = min(mCalcBounds.right, width.toFloat())
|
|
val bottomLimit = min(mCalcBounds.bottom, height.toFloat())
|
|
if (rect.left < leftLimit) {
|
|
rect.left = leftLimit
|
|
}
|
|
if (rect.top < topLimit) {
|
|
rect.top = topLimit
|
|
}
|
|
if (rect.right > rightLimit) {
|
|
rect.right = rightLimit
|
|
}
|
|
if (rect.bottom > bottomLimit) {
|
|
rect.bottom = bottomLimit
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw crop overview by drawing background over image not in the cropping area, then borders and
|
|
* guidelines.
|
|
*/
|
|
override fun onDraw(canvas: Canvas) {
|
|
super.onDraw(canvas)
|
|
|
|
// Draw translucent background for the notCropped area.
|
|
drawBackground(canvas)
|
|
if (mCropWindowHandler.showGuidelines()) {
|
|
// Determines whether guidelines should be drawn or not
|
|
if (mMoveHandler != null) {
|
|
// Draw only when resizing
|
|
drawGuidelines(canvas)
|
|
}
|
|
}
|
|
drawBorders(canvas)
|
|
drawCorners(canvas)
|
|
}
|
|
|
|
/** Draw shadow background over the image not including the crop area. */
|
|
private fun drawBackground(canvas: Canvas) {
|
|
val rect = mCropWindowHandler.rect
|
|
val background = getNewPaint(Color.argb(119, 0, 0, 0))
|
|
canvas.drawRect(
|
|
mInitialCropWindowRect.left.toFloat(),
|
|
mInitialCropWindowRect.top.toFloat(),
|
|
rect.left,
|
|
mInitialCropWindowRect.bottom.toFloat(),
|
|
background
|
|
)
|
|
canvas.drawRect(
|
|
rect.left,
|
|
rect.bottom,
|
|
mInitialCropWindowRect.right.toFloat(),
|
|
mInitialCropWindowRect.bottom.toFloat(),
|
|
background
|
|
)
|
|
canvas.drawRect(
|
|
rect.right,
|
|
mInitialCropWindowRect.top.toFloat(),
|
|
mInitialCropWindowRect.right.toFloat(),
|
|
rect.bottom,
|
|
background
|
|
)
|
|
canvas.drawRect(
|
|
rect.left,
|
|
mInitialCropWindowRect.top.toFloat(),
|
|
rect.right,
|
|
rect.top,
|
|
background
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Draw 2 vertical and 2 horizontal guidelines inside the cropping area to split it into 9 equal
|
|
* parts.
|
|
*/
|
|
private fun drawGuidelines(canvas: Canvas) {
|
|
if (mGuidelinePaint != null) {
|
|
val sw: Float = if (mBorderPaint != null) mBorderPaint!!.strokeWidth else 0f
|
|
val rect = mCropWindowHandler.rect
|
|
rect.inset(sw, sw)
|
|
val oneThirdCropWidth = rect.width() / 3
|
|
val oneThirdCropHeight = rect.height() / 3
|
|
|
|
// Draw vertical guidelines.
|
|
val x1 = rect.left + oneThirdCropWidth
|
|
val x2 = rect.right - oneThirdCropWidth
|
|
canvas.drawLine(x1, rect.top, x1, rect.bottom, mGuidelinePaint!!)
|
|
canvas.drawLine(x2, rect.top, x2, rect.bottom, mGuidelinePaint!!)
|
|
|
|
// Draw horizontal guidelines.
|
|
val y1 = rect.top + oneThirdCropHeight
|
|
val y2 = rect.bottom - oneThirdCropHeight
|
|
canvas.drawLine(rect.left, y1, rect.right, y1, mGuidelinePaint!!)
|
|
canvas.drawLine(rect.left, y2, rect.right, y2, mGuidelinePaint!!)
|
|
}
|
|
}
|
|
|
|
/** Draw borders of the crop area. */
|
|
private fun drawBorders(canvas: Canvas) {
|
|
if (mBorderPaint != null) {
|
|
val w = mBorderPaint!!.strokeWidth
|
|
val rect = mCropWindowHandler.rect
|
|
// Make the rectangle a bit smaller to accommodate for the border
|
|
rect.inset(w / 2, w / 2)
|
|
|
|
// Draw rectangle crop window border.
|
|
canvas.drawRect(rect, mBorderPaint!!)
|
|
}
|
|
}
|
|
|
|
/** Draw the corner of crop overlay. */
|
|
private fun drawCorners(canvas: Canvas) {
|
|
val dm = Resources.getSystem().displayMetrics
|
|
if (mBorderCornerPaint != null) {
|
|
val lineWidth: Float = if (mBorderPaint != null) mBorderPaint!!.strokeWidth else 0f
|
|
val cornerWidth = mBorderCornerPaint!!.strokeWidth
|
|
|
|
// The corners should be a bit offset from the borders
|
|
val w = (cornerWidth / 2
|
|
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, dm))
|
|
val rect = mCropWindowHandler.rect
|
|
rect.inset(w, w)
|
|
val cornerOffset = (cornerWidth - lineWidth) / 2
|
|
val cornerExtension = cornerWidth / 2 + cornerOffset
|
|
|
|
/* the length of the border corner to draw */
|
|
val mBorderCornerLength =
|
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14f, dm)
|
|
|
|
// Top left
|
|
canvas.drawLine(
|
|
rect.left - cornerOffset,
|
|
rect.top - cornerExtension,
|
|
rect.left - cornerOffset,
|
|
rect.top + mBorderCornerLength,
|
|
mBorderCornerPaint!!
|
|
)
|
|
canvas.drawLine(
|
|
rect.left - cornerExtension,
|
|
rect.top - cornerOffset,
|
|
rect.left + mBorderCornerLength,
|
|
rect.top - cornerOffset,
|
|
mBorderCornerPaint!!
|
|
)
|
|
|
|
// Top right
|
|
canvas.drawLine(
|
|
rect.right + cornerOffset,
|
|
rect.top - cornerExtension,
|
|
rect.right + cornerOffset,
|
|
rect.top + mBorderCornerLength,
|
|
mBorderCornerPaint!!
|
|
)
|
|
canvas.drawLine(
|
|
rect.right + cornerExtension,
|
|
rect.top - cornerOffset,
|
|
rect.right - mBorderCornerLength,
|
|
rect.top - cornerOffset,
|
|
mBorderCornerPaint!!
|
|
)
|
|
|
|
// Bottom left
|
|
canvas.drawLine(
|
|
rect.left - cornerOffset,
|
|
rect.bottom + cornerExtension,
|
|
rect.left - cornerOffset,
|
|
rect.bottom - mBorderCornerLength,
|
|
mBorderCornerPaint!!
|
|
)
|
|
canvas.drawLine(
|
|
rect.left - cornerExtension,
|
|
rect.bottom + cornerOffset,
|
|
rect.left + mBorderCornerLength,
|
|
rect.bottom + cornerOffset,
|
|
mBorderCornerPaint!!
|
|
)
|
|
|
|
// Bottom left
|
|
canvas.drawLine(
|
|
rect.right + cornerOffset,
|
|
rect.bottom + cornerExtension,
|
|
rect.right + cornerOffset,
|
|
rect.bottom - mBorderCornerLength,
|
|
mBorderCornerPaint!!
|
|
)
|
|
canvas.drawLine(
|
|
rect.right + cornerExtension,
|
|
rect.bottom + cornerOffset,
|
|
rect.right - mBorderCornerLength,
|
|
rect.bottom + cornerOffset,
|
|
mBorderCornerPaint!!
|
|
)
|
|
}
|
|
}
|
|
|
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
// If this View is not enabled, don't allow for touch interactions.
|
|
return if (isEnabled) {
|
|
/* Boolean to see if multi touch is enabled for the crop rectangle */
|
|
when (event.action) {
|
|
MotionEvent.ACTION_DOWN -> {
|
|
onActionDown(event.x, event.y)
|
|
true
|
|
}
|
|
|
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
parent.requestDisallowInterceptTouchEvent(false)
|
|
onActionUp()
|
|
true
|
|
}
|
|
|
|
MotionEvent.ACTION_MOVE -> {
|
|
onActionMove(event.x, event.y)
|
|
parent.requestDisallowInterceptTouchEvent(true)
|
|
true
|
|
}
|
|
|
|
else -> false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* On press down start crop window movement depending on the location of the press.<br></br>
|
|
* if press is far from crop window then no move handler is returned (null).
|
|
*/
|
|
private fun onActionDown(x: Float, y: Float) {
|
|
val dm = Resources.getSystem().displayMetrics
|
|
mMoveHandler = mCropWindowHandler.getMoveHandler(
|
|
x,
|
|
y,
|
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24f, dm)
|
|
)
|
|
if (mMoveHandler != null) {
|
|
invalidate()
|
|
}
|
|
}
|
|
|
|
/** Clear move handler starting in [.onActionDown] if exists. */
|
|
private fun onActionUp() {
|
|
if (mMoveHandler != null) {
|
|
mMoveHandler = null
|
|
invalidate()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle move of crop window using the move handler created in [.onActionDown].<br></br>
|
|
* The move handler will do the proper move/resize of the crop window.
|
|
*/
|
|
private fun onActionMove(x: Float, y: Float) {
|
|
if (mMoveHandler != null) {
|
|
val rect = mCropWindowHandler.rect
|
|
setBounds()
|
|
val dm = Resources.getSystem().displayMetrics
|
|
val snapRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3f, dm)
|
|
mMoveHandler!!.move(
|
|
rect,
|
|
x,
|
|
y,
|
|
mCalcBounds,
|
|
mViewWidth,
|
|
mViewHeight,
|
|
snapRadius
|
|
)
|
|
mCropWindowHandler.rect = rect
|
|
invalidate()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate the bounding rectangle for current crop window
|
|
* The bounds rectangle is the bitmap rectangle
|
|
*/
|
|
private fun setBounds() {
|
|
mCalcBounds.set(mInitialCropWindowRect)
|
|
}
|
|
|
|
companion object {
|
|
/** Creates the Paint object for drawing. */
|
|
private fun getNewPaint(color: Int): Paint {
|
|
val paint = Paint()
|
|
paint.color = color
|
|
return paint
|
|
}
|
|
|
|
/** Creates the Paint object for given thickness and color */
|
|
private fun getNewPaintOfThickness(thickness: Float, color: Int): Paint {
|
|
val borderPaint = Paint()
|
|
borderPaint.color = color
|
|
borderPaint.strokeWidth = thickness
|
|
borderPaint.style = Paint.Style.STROKE
|
|
borderPaint.isAntiAlias = true
|
|
return borderPaint
|
|
}
|
|
}
|
|
} |