Convert crop classes to Kotlin
This commit is contained in:
parent
9f8e1f1e04
commit
8ade4fcded
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue