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

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
}
}
}