Convert crop classes to Kotlin

This commit is contained in:
Matthieu 2022-10-21 17:11:37 +02:00
parent 9f8e1f1e04
commit 8ade4fcded
8 changed files with 1171 additions and 1198 deletions

View File

@ -151,7 +151,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
binding.saveCropButton.setOnClickListener {
// This is the rectangle selected by the crop
val cropRect = binding.cropImageView.cropWindowRect ?: return@setOnClickListener
val cropRect = binding.cropImageView.cropWindowRect
// This is the rectangle of the whole image
val fullImageRect: Rect = binding.cropImageView.getInitialCropWindowRect()

View File

@ -1,5 +1,9 @@
package org.pixeldroid.app.postCreation.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.graphics.Rect
import android.graphics.RectF
@ -35,7 +39,7 @@ class CropImageView @JvmOverloads constructor(context: Context?, attrs: Attribut
*
* @return a Rect instance containing notCropped area boundaries of the source Bitmap
*/
val cropWindowRect: RectF?
val cropWindowRect: RectF
get() = binding.CropOverlayView.cropWindowRect
@ -53,7 +57,7 @@ class CropImageView @JvmOverloads constructor(context: Context?, attrs: Attribut
*/
fun setImageUriAsync(uri: Uri, cropRelativeDimensions: VideoEditActivity.RelativeCropPosition) {
// either no existing task is working or we canceled it, need to load new URI
binding.CropOverlayView.initialCropWindowRect = null
binding.CropOverlayView.initialCropWindowRect = Rect()
Glide.with(this).load(uri).fitCenter().listener(object : RequestListener<Drawable> {
override fun onLoadFailed(

View File

@ -1,518 +0,0 @@
package org.pixeldroid.app.postCreation.photoEdit.cropper;
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.DisplayMetrics;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity;
/** A custom View representing the crop window and the shaded background outside the crop window. */
public class CropOverlayView extends View {
// region: Fields and Consts
/** Handler from crop window stuff, moving and knowing position. */
private final CropWindowHandler mCropWindowHandler = new CropWindowHandler();
/** The Paint used to draw the white rectangle around the crop area. */
private Paint mBorderPaint;
/** The Paint used to draw the corners of the Border */
private Paint mBorderCornerPaint;
/** The Paint used to draw the guidelines within the crop area when pressed. */
private Paint mGuidelinePaint;
/** The bounding box around the Bitmap that we are cropping. */
private final RectF mCalcBounds = new RectF();
/** The bounding image view width used to know the crop overlay is at view edges. */
private int mViewWidth;
/** The bounding image view height used to know the crop overlay is at view edges. */
private int mViewHeight;
/** The Handle that is currently pressed; null if no Handle is pressed. */
private CropWindowMoveHandler mMoveHandler;
/** the initial crop window rectangle to set */
private final Rect mInitialCropWindowRect = new Rect();
/** Whether the Crop View has been initialized for the first time */
private boolean initializedCropWindow;
// endregion
public CropOverlayView(Context context) {
this(context, null);
}
public CropOverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/** Get the left/top/right/bottom coordinates of the crop window. */
public RectF getCropWindowRect() {
return mCropWindowHandler.getRect();
}
/** Set the left/top/right/bottom coordinates of the crop window. */
public void setCropWindowRect(RectF rect) {
mCropWindowHandler.setRect(rect);
}
/** Fix the current crop window rectangle if it is outside of cropping image or view bounds. */
public void fixCurrentCropWindowRect() {
RectF rect = getCropWindowRect();
fixCropWindowRectByRules(rect);
mCropWindowHandler.setRect(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.
*/
public void setBounds(int viewWidth, int viewHeight) {
mViewWidth = viewWidth;
mViewHeight = viewHeight;
RectF cropRect = mCropWindowHandler.getRect();
if (cropRect.width() == 0 || cropRect.height() == 0) {
initCropWindow();
}
}
/** Resets the crop overlay view. */
public void resetCropOverlayView() {
if (initializedCropWindow) {
setCropWindowRect(new RectF());
initCropWindow();
invalidate();
}
}
/**
* Set the max width/height and scale factor of the shown image to original image to scale the
* limits appropriately.
*/
public void setCropWindowLimits(float maxWidth, float maxHeight) {
mCropWindowHandler.setCropWindowLimits(maxWidth, maxHeight);
}
/** Get crop window initial rectangle. */
public Rect getInitialCropWindowRect() {
return mInitialCropWindowRect;
}
public void setRecordedCropWindowRect(@NonNull VideoEditActivity.RelativeCropPosition relativeCropPosition) {
RectF rect = new RectF(
mInitialCropWindowRect.left + relativeCropPosition.getRelativeX() * mInitialCropWindowRect.width(),
mInitialCropWindowRect.top + relativeCropPosition.getRelativeY() * mInitialCropWindowRect.height(),
relativeCropPosition.getRelativeWidth() * mInitialCropWindowRect.width()
+ mInitialCropWindowRect.left + relativeCropPosition.getRelativeX() * mInitialCropWindowRect.width(),
relativeCropPosition.getRelativeHeight() * mInitialCropWindowRect.height()
+ mInitialCropWindowRect.top + relativeCropPosition.getRelativeY() * mInitialCropWindowRect.height()
);
mCropWindowHandler.setRect(rect);
}
/** Set crop window initial rectangle to be used instead of default. */
public void setInitialCropWindowRect(Rect rect) {
mInitialCropWindowRect.set(rect != null ? rect : new Rect());
if (initializedCropWindow) {
initCropWindow();
invalidate();
}
}
/** Reset crop window to initial rectangle. */
public void resetCropWindowRect() {
if (initializedCropWindow) {
initCropWindow();
invalidate();
}
}
/**
* Sets all initial values, but does not call initCropWindow to reset the views.<br>
* Used once at the very start to initialize the attributes.
*/
public void setInitialAttributeValues() {
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
mBorderPaint =
getNewPaintOfThickness(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm), Color.argb(170, 255, 255, 255));
mBorderCornerPaint =
getNewPaintOfThickness(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, dm), Color.WHITE);
mGuidelinePaint =
getNewPaintOfThickness(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, 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 void initCropWindow() {
RectF rect = new 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;
rect.top = mInitialCropWindowRect.top;
rect.right = rect.left + mInitialCropWindowRect.width();
rect.bottom = rect.top + mInitialCropWindowRect.height();
}
fixCropWindowRectByRules(rect);
mCropWindowHandler.setRect(rect);
}
/** Fix the given rect to fit into bitmap rect and follow min, max and aspect ratio rules. */
private void fixCropWindowRectByRules(RectF rect) {
if (rect.width() < mCropWindowHandler.getMinCropWidth()) {
float adj = (mCropWindowHandler.getMinCropWidth() - rect.width()) / 2;
rect.left -= adj;
rect.right += adj;
}
if (rect.height() < mCropWindowHandler.getMinCropHeight()) {
float adj = (mCropWindowHandler.getMinCropHeight() - rect.height()) / 2;
rect.top -= adj;
rect.bottom += adj;
}
if (rect.width() > mCropWindowHandler.getMaxCropWidth()) {
float adj = (rect.width() - mCropWindowHandler.getMaxCropWidth()) / 2;
rect.left += adj;
rect.right -= adj;
}
if (rect.height() > mCropWindowHandler.getMaxCropHeight()) {
float adj = (rect.height() - mCropWindowHandler.getMaxCropHeight()) / 2;
rect.top += adj;
rect.bottom -= adj;
}
calculateBounds(rect);
if (mCalcBounds.width() > 0 && mCalcBounds.height() > 0) {
float leftLimit = Math.max(mCalcBounds.left, 0);
float topLimit = Math.max(mCalcBounds.top, 0);
float rightLimit = Math.min(mCalcBounds.right, getWidth());
float bottomLimit = Math.min(mCalcBounds.bottom, getHeight());
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
protected void 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 void drawBackground(Canvas canvas) {
RectF rect = mCropWindowHandler.getRect();
Paint background = getNewPaint(Color.argb(119, 0, 0, 0));
canvas.drawRect(
mInitialCropWindowRect.left,
mInitialCropWindowRect.top,
rect.left,
mInitialCropWindowRect.bottom,
background
);
canvas.drawRect(
rect.left,
rect.bottom,
mInitialCropWindowRect.right,
mInitialCropWindowRect.bottom,
background
);
canvas.drawRect(
rect.right,
mInitialCropWindowRect.top,
mInitialCropWindowRect.right,
rect.bottom,
background
);
canvas.drawRect(
rect.left,
mInitialCropWindowRect.top,
rect.right,
rect.top,
background
);
}
/**
* Draw 2 vertical and 2 horizontal guidelines inside the cropping area to split it into 9 equal
* parts.
*/
private void drawGuidelines(Canvas canvas) {
if (mGuidelinePaint != null) {
float sw = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0;
RectF rect = mCropWindowHandler.getRect();
rect.inset(sw, sw);
float oneThirdCropWidth = rect.width() / 3;
float oneThirdCropHeight = rect.height() / 3;
// Draw vertical guidelines.
float x1 = rect.left + oneThirdCropWidth;
float 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.
float y1 = rect.top + oneThirdCropHeight;
float 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 void drawBorders(Canvas canvas) {
if (mBorderPaint != null) {
float w = mBorderPaint.getStrokeWidth();
RectF rect = mCropWindowHandler.getRect();
// 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 void drawCorners(Canvas canvas) {
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
if (mBorderCornerPaint != null) {
float lineWidth = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0;
float cornerWidth = mBorderCornerPaint.getStrokeWidth();
// The corners should be a bit offset from the borders
float w = (cornerWidth / 2)
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, dm);
RectF rect = mCropWindowHandler.getRect();
rect.inset(w, w);
float cornerOffset = (cornerWidth - lineWidth) / 2;
float cornerExtension = cornerWidth / 2 + cornerOffset;
/* the length of the border corner to draw */
float mBorderCornerLength = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, 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);
}
}
/** Creates the Paint object for drawing. */
private static Paint getNewPaint(int color) {
Paint paint = new Paint();
paint.setColor(color);
return paint;
}
/** Creates the Paint object for given thickness and color */
private static Paint getNewPaintOfThickness(float thickness, int color) {
Paint borderPaint = new Paint();
borderPaint.setColor(color);
borderPaint.setStrokeWidth(thickness);
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setAntiAlias(true);
return borderPaint;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// If this View is not enabled, don't allow for touch interactions.
if (isEnabled()) {
/* Boolean to see if multi touch is enabled for the crop rectangle */
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
onActionDown(event.getX(), event.getY());
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
onActionUp();
return true;
case MotionEvent.ACTION_MOVE:
onActionMove(event.getX(), event.getY());
getParent().requestDisallowInterceptTouchEvent(true);
return true;
default:
return false;
}
} else {
return false;
}
}
/**
* On press down start crop window movement depending on the location of the press.<br>
* if press is far from crop window then no move handler is returned (null).
*/
private void onActionDown(float x, float y) {
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
mMoveHandler = mCropWindowHandler.getMoveHandler(x, y, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24, dm));
if (mMoveHandler != null) {
invalidate();
}
}
/** Clear move handler starting in {@link #onActionDown(float, float)} if exists. */
private void onActionUp() {
if (mMoveHandler != null) {
mMoveHandler = null;
invalidate();
}
}
/**
* Handle move of crop window using the move handler created in {@link #onActionDown(float,
* float)}.<br>
* The move handler will do the proper move/resize of the crop window.
*/
private void onActionMove(float x, float y) {
if (mMoveHandler != null) {
RectF rect = mCropWindowHandler.getRect();
calculateBounds(rect);
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
float snapRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm);
mMoveHandler.move(
rect,
x,
y,
mCalcBounds,
mViewWidth,
mViewHeight,
snapRadius
);
mCropWindowHandler.setRect(rect);
invalidate();
}
}
/**
* Calculate the bounding rectangle for current crop window
* The bounds rectangle is the bitmap rectangle
*/
private void calculateBounds(RectF rect) {
mCalcBounds.set(mInitialCropWindowRect);
}
// endregion
}

View File

@ -0,0 +1,490 @@
package org.pixeldroid.app.postCreation.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.app.postCreation.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
}
}
}

View File

@ -1,235 +0,0 @@
package org.pixeldroid.app.postCreation.photoEdit.cropper;
import android.content.res.Resources;
import android.graphics.RectF;
import android.util.DisplayMetrics;
import android.util.TypedValue;
/** Handler from crop window stuff, moving and knowing position. */
final class CropWindowHandler {
// region: Fields and Const
/** The 4 edges of the crop window defining its coordinates and size */
private final RectF mEdges = new RectF();
/**
* Rectangle used to return the edges rectangle without ability to change it and without creating
* new all the time.
*/
private final RectF mGetEdges = new RectF();
/** Maximum width in pixels that the crop window can CURRENTLY get. */
private float mMaxCropWindowWidth;
/** Maximum height in pixels that the crop window can CURRENTLY get. */
private float mMaxCropWindowHeight;
// endregion
/** Get the left/top/right/bottom coordinates of the crop window. */
public RectF getRect() {
mGetEdges.set(mEdges);
return mGetEdges;
}
/** Minimum width in pixels that the crop window can get. */
public float getMinCropWidth() {
/*
* Minimum width in pixels that the result of cropping an image can get, affects crop window width
* adjusted by width scale factor.
*/
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
float mMinCropResultWidth = 40;
return Math.max((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm), mMinCropResultWidth);
}
/** Minimum height in pixels that the crop window can get. */
public float getMinCropHeight() {
/*
* Minimum height in pixels that the result of cropping an image can get, affects crop window
* height adjusted by height scale factor.
*/
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
float mMinCropResultHeight = 40;
return Math.max((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm), mMinCropResultHeight);
}
/** Maximum width in pixels that the crop window can get. */
public float getMaxCropWidth() {
float mMaxCropResultWidth = 99999;
return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth);
}
/** Maximum height in pixels that the crop window can get. */
public float getMaxCropHeight() {
float mMaxCropResultHeight = 99999;
return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight);
}
/**
* Set the max width/height of the shown image to original image to scale the limits appropriately
*/
public void setCropWindowLimits(float maxWidth, float maxHeight) {
mMaxCropWindowWidth = maxWidth;
mMaxCropWindowHeight = maxHeight;
}
/** Set the left/top/right/bottom coordinates of the crop window. */
public void setRect(RectF rect) {
mEdges.set(rect);
}
/**
* 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
*/
public boolean showGuidelines() {
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
*/
public CropWindowMoveHandler getMoveHandler(float x, float y, float targetRadius) {
CropWindowMoveHandler.Type type = getRectanglePressedMoveType(x, y, targetRadius);
return type != null ? new CropWindowMoveHandler(type, this, x, y) : 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 CropWindowMoveHandler.Type getRectanglePressedMoveType(
float x, float y, float targetRadius) {
CropWindowMoveHandler.Type moveType = null;
// Note: corner-handles take precedence, then side-handles, then center.
if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.left, mEdges.top, targetRadius)) {
moveType = CropWindowMoveHandler.Type.TOP_LEFT;
} else if (CropWindowHandler.isInCornerTargetZone(
x, y, mEdges.right, mEdges.top, targetRadius)) {
moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
} else if (CropWindowHandler.isInCornerTargetZone(
x, y, mEdges.left, mEdges.bottom, targetRadius)) {
moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
} else if (CropWindowHandler.isInCornerTargetZone(
x, y, mEdges.right, mEdges.bottom, targetRadius)) {
moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
} else if (CropWindowHandler.isInCenterTargetZone(
x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
&& focusCenter()) {
moveType = CropWindowMoveHandler.Type.CENTER;
} else if (CropWindowHandler.isInHorizontalTargetZone(
x, y, mEdges.left, mEdges.right, mEdges.top, targetRadius)) {
moveType = CropWindowMoveHandler.Type.TOP;
} else if (CropWindowHandler.isInHorizontalTargetZone(
x, y, mEdges.left, mEdges.right, mEdges.bottom, targetRadius)) {
moveType = CropWindowMoveHandler.Type.BOTTOM;
} else if (CropWindowHandler.isInVerticalTargetZone(
x, y, mEdges.left, mEdges.top, mEdges.bottom, targetRadius)) {
moveType = CropWindowMoveHandler.Type.LEFT;
} else if (CropWindowHandler.isInVerticalTargetZone(
x, y, mEdges.right, mEdges.top, mEdges.bottom, targetRadius)) {
moveType = CropWindowMoveHandler.Type.RIGHT;
} else if (CropWindowHandler.isInCenterTargetZone(
x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
&& !focusCenter()) {
moveType = CropWindowMoveHandler.Type.CENTER;
}
return moveType;
}
/**
* 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 static boolean isInCornerTargetZone(
float x, float y, float handleX, float handleY, float targetRadius) {
return Math.abs(x - handleX) <= targetRadius && Math.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 static boolean isInHorizontalTargetZone(
float x, float y, float handleXStart, float handleXEnd, float handleY, float targetRadius) {
return x > handleXStart && x < handleXEnd && Math.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 static boolean isInVerticalTargetZone(
float x, float y, float handleX, float handleYStart, float handleYEnd, float targetRadius) {
return Math.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 static boolean isInCenterTargetZone(
float x, float y, float left, float top, float right, float bottom) {
return x > left && x < right && y > top && y < bottom;
}
/**
* 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 boolean focusCenter() {
return !showGuidelines();
}
// endregion
}

View File

@ -0,0 +1,269 @@
package org.pixeldroid.app.postCreation.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
}
}
}

View File

@ -1,442 +0,0 @@
package org.pixeldroid.app.postCreation.photoEdit.cropper;
import android.graphics.PointF;
import android.graphics.RectF;
/**
* Handler to update crop window edges by the move type - Horizontal, Vertical, Corner or Center.
* <br>
*/
final class CropWindowMoveHandler {
// region: Fields and Consts
/** Minimum width in pixels that the crop window can get. */
private final float mMinCropWidth;
/** Minimum width in pixels that the crop window can get. */
private final float mMinCropHeight;
/** Maximum height in pixels that the crop window can get. */
private final float mMaxCropWidth;
/** Maximum height in pixels that the crop window can get. */
private final float mMaxCropHeight;
/** The type of crop window move that is handled. */
private final Type mType;
/**
* 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 final PointF mTouchOffset = new PointF();
// endregion
/**
* @param cropWindowHandler main crop window handle to get and update the crop window edges
* @param touchX the location of the initial touch position to measure move distance
* @param touchY the location of the initial touch position to measure move distance
*/
public CropWindowMoveHandler(
Type type, CropWindowHandler cropWindowHandler, float touchX, float touchY) {
mType = type;
mMinCropWidth = cropWindowHandler.getMinCropWidth();
mMinCropHeight = cropWindowHandler.getMinCropHeight();
mMaxCropWidth = cropWindowHandler.getMaxCropWidth();
mMaxCropHeight = cropWindowHandler.getMaxCropHeight();
calculateTouchOffset(cropWindowHandler.getRect(), touchX, touchY);
}
/**
* Updates the crop window by change in the touch location.<br>
* Move type handled by this instance, as initialized in creation, affects how the change in toch
* location changes the crop window position and size.<br>
* 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.<br>
* Primary is the edge directly affected by move type, secondary is the other edge.<br>
* 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
*/
public void move(
RectF rect,
float x,
float y,
RectF bounds,
int viewWidth,
int viewHeight,
float snapMargin) {
// 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".
float adjX = x + mTouchOffset.x;
float adjY = y + mTouchOffset.y;
if (mType == Type.CENTER) {
moveCenter(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin);
} else {
moveSizeWithFreeAspectRatio(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>
* Save these values in a member variable since we want to maintain this offset as we drag the
* handle.
*/
private void calculateTouchOffset(RectF rect, float touchX, float touchY) {
float touchOffsetX = 0;
float touchOffsetY = 0;
// Calculate the offset from the appropriate handle.
switch (mType) {
case TOP_LEFT:
touchOffsetX = rect.left - touchX;
touchOffsetY = rect.top - touchY;
break;
case TOP_RIGHT:
touchOffsetX = rect.right - touchX;
touchOffsetY = rect.top - touchY;
break;
case BOTTOM_LEFT:
touchOffsetX = rect.left - touchX;
touchOffsetY = rect.bottom - touchY;
break;
case BOTTOM_RIGHT:
touchOffsetX = rect.right - touchX;
touchOffsetY = rect.bottom - touchY;
break;
case LEFT:
touchOffsetX = rect.left - touchX;
touchOffsetY = 0;
break;
case TOP:
touchOffsetX = 0;
touchOffsetY = rect.top - touchY;
break;
case RIGHT:
touchOffsetX = rect.right - touchX;
touchOffsetY = 0;
break;
case BOTTOM:
touchOffsetX = 0;
touchOffsetY = rect.bottom - touchY;
break;
case CENTER:
touchOffsetX = rect.centerX() - touchX;
touchOffsetY = rect.centerY() - touchY;
break;
default:
break;
}
mTouchOffset.x = touchOffsetX;
mTouchOffset.y = touchOffsetY;
}
/** Center move only changes the position of the crop window without changing the size. */
private void moveCenter(
RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapRadius) {
float dx = x - rect.centerX();
float 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 for corner size move) without
* affecting "secondary" edges.<br>
* Only the primary edge(s) are fixed to stay within limits.
*/
private void moveSizeWithFreeAspectRatio(
RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapMargin) {
switch (mType) {
case TOP_LEFT:
adjustTop(rect, y, bounds, snapMargin);
adjustLeft(rect, x, bounds, snapMargin);
break;
case TOP_RIGHT:
adjustTop(rect, y, bounds, snapMargin);
adjustRight(rect, x, bounds, viewWidth, snapMargin);
break;
case BOTTOM_LEFT:
adjustBottom(rect, y, bounds, viewHeight, snapMargin);
adjustLeft(rect, x, bounds, snapMargin);
break;
case BOTTOM_RIGHT:
adjustBottom(rect, y, bounds, viewHeight, snapMargin);
adjustRight(rect, x, bounds, viewWidth, snapMargin);
break;
case LEFT:
adjustLeft(rect, x, bounds, snapMargin);
break;
case TOP:
adjustTop(rect, y, bounds, snapMargin);
break;
case RIGHT:
adjustRight(rect, x, bounds, viewWidth, snapMargin);
break;
case BOTTOM:
adjustBottom(rect, y, bounds, viewHeight, snapMargin);
break;
default:
break;
}
}
/** Check if edges have gone out of bounds (including snap margin), and fix if needed. */
private void snapEdgesToBounds(RectF edges, RectF bounds, float margin) {
if (edges.left < bounds.left + margin) {
edges.offset(bounds.left - edges.left, 0);
}
if (edges.top < bounds.top + margin) {
edges.offset(0, bounds.top - edges.top);
}
if (edges.right > bounds.right - margin) {
edges.offset(bounds.right - edges.right, 0);
}
if (edges.bottom > bounds.bottom - margin) {
edges.offset(0, 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 void adjustLeft(
RectF rect,
float left,
RectF bounds,
float snapMargin) {
float 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 void adjustRight(
RectF rect,
float right,
RectF bounds,
int viewWidth,
float snapMargin) {
float 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 void adjustTop(
RectF rect,
float top,
RectF bounds,
float snapMargin) {
float 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 void adjustBottom(
RectF rect,
float bottom,
RectF bounds,
int viewHeight,
float snapMargin) {
float 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
// region: Inner class: Type
/** The type of crop window move that is handled. */
public enum Type {
TOP_LEFT,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM_RIGHT,
LEFT,
TOP,
RIGHT,
BOTTOM,
CENTER
}
// endregion
}

View File

@ -0,0 +1,405 @@
package org.pixeldroid.app.postCreation.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
}
}