This commit is contained in:
Matthieu 2022-10-17 13:23:02 +02:00
parent b85fd4ef15
commit 9ad24f7157
8 changed files with 1405 additions and 0 deletions

View File

@ -2,12 +2,14 @@ package org.pixeldroid.app.postCreation.photoEdit
import android.app.Activity
import android.app.AlertDialog
import android.content.ContentUris
import android.content.Intent
import android.media.AudioManager
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.text.format.DateUtils
import android.util.Log
import android.view.Menu
@ -59,6 +61,9 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
val uri = intent.getParcelableExtra<Uri>(PhotoEditActivity.PICTURE_URI)!!
binding.cropImageView.setImageUriAsync(uri)
videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
val inputVideoPath = ffmpegCompliantUri(uri)

View File

@ -0,0 +1,115 @@
package org.pixeldroid.app.postCreation.photoEdit.cropper
import android.content.Context
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.net.Uri
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.core.graphics.toRect
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import org.pixeldroid.app.R
/** Custom view that provides cropping capabilities to an image. */
class CropImageView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
FrameLayout(context!!, attrs) {
/** Image view widget used to show the image for cropping. */
private val mImageView: ImageView
/** Overlay over the image view to show cropping UI. */
private val mCropOverlayView: CropOverlayView?
/** The sample size the image was loaded by if was loaded by URI */
private var mLoadedSampleSize = 1
init {
val inflater = LayoutInflater.from(context)
val v = inflater.inflate(R.layout.crop_image_view, this, true)
mImageView = v.findViewById(R.id.ImageView_image)
mCropOverlayView = v.findViewById(R.id.CropOverlayView)
mCropOverlayView.setInitialAttributeValues()
}
/**
* Gets the crop window's position relative to the parent's view at screen.
*
* @return a Rect instance containing cropped area boundaries of the source Bitmap
*/
val cropWindowRect: RectF?
get() = mCropOverlayView?.cropWindowRect// Get crop window position relative to the displayed image.
/**
* Set the crop window position and size to the given rectangle.
* Image to crop must be first set before invoking this, for async - after complete callback.
*
* @param rect window rectangle (position and size) relative to source bitmap
*/
fun setCropRect(rect: Rect?) {
mCropOverlayView!!.initialCropWindowRect = rect
}
/** Reset crop window to initial rectangle. */
fun resetCropRect() {
mCropOverlayView!!.resetCropWindowRect()
}
/**
* Sets a bitmap loaded from the given Android URI as the content of the CropImageView.<br></br>
* Can be used with URI from gallery or camera source.<br></br>
* Will rotate the image by exif data.<br></br>
*
* @param uri the URI to load the image from
*/
fun setImageUriAsync(uri: Uri) {
// either no existing task is working or we canceled it, need to load new URI
mCropOverlayView!!.initialCropWindowRect = null
Glide.with(this).load(uri).fitCenter().listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {return false }
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
// Get width and height that the image will take on the screen
val drawnWidth = resource?.intrinsicWidth ?: width
val drawnHeight = resource?.intrinsicHeight ?: height
mCropOverlayView.cropWindowRect =
RectF((width - drawnWidth)/2f, (height - drawnHeight)/2f, (width + drawnWidth)/2f, (height + drawnHeight)/2f)
mCropOverlayView.initialCropWindowRect = mCropOverlayView.cropWindowRect.toRect()
mCropOverlayView.setCropWindowLimits(drawnWidth.toFloat(), drawnHeight.toFloat(), 1f, 1f)
setBitmap()
// Indicate to Glide that the image hasn't been set yet
return false
}
}).into(mImageView)
}
/**
* Set the given bitmap to be used in for cropping<br></br>
* Optionally clear full if the bitmap is new, or partial clear if the bitmap has been
* manipulated.
*/
private fun setBitmap() {
mLoadedSampleSize = 1
if (mCropOverlayView != null) {
mCropOverlayView.invalidate()
mCropOverlayView.setBounds(width, height)
mCropOverlayView.resetCropOverlayView()
mCropOverlayView.visibility = VISIBLE
}
}
}

View File

@ -0,0 +1,533 @@
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;
/** 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 Paint used to darken the surrounding areas outside the crop area. */
private final Paint mBackgroundPaint = getNewPaint(Color.argb(119, 0, 0, 0));
/** 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 initial crop window padding from image borders */
private float mInitialCropWindowPaddingRatio;
/** The Handle that is currently pressed; null if no Handle is pressed. */
private CropWindowMoveHandler mMoveHandler;
/** save the current aspect ratio of the image */
private int mAspectRatioX;
/** save the current aspect ratio of the image */
private int mAspectRatioY;
/** 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();
}
}
/** the X value of the aspect ratio; */
public int getAspectRatioX() {
return mAspectRatioX;
}
/** Sets the X value of the aspect ratio to 1. */
public void setAspectRatioX() {
if (mAspectRatioX != 1) {
mAspectRatioX = 1;
if (initializedCropWindow) {
initCropWindow();
invalidate();
}
}
}
/** the Y value of the aspect ratio; */
public int getAspectRatioY() {
return mAspectRatioY;
}
/**
* Sets the Y value of the aspect ratio to 1.
*
*/
public void setAspectRatioY() {
if (mAspectRatioY != 1) {
mAspectRatioY = 1;
if (initializedCropWindow) {
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, float scaleFactorWidth, float scaleFactorHeight) {
mCropWindowHandler.setCropWindowLimits(
maxWidth, maxHeight, scaleFactorWidth, scaleFactorHeight);
}
/** Get crop window initial rectangle. */
public Rect getInitialCropWindowRect() {
return mInitialCropWindowRect;
}
/** 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();
setAspectRatioX();
setAspectRatioY();
mInitialCropWindowPaddingRatio = 0.1f;
mBorderPaint = getNewPaintOrNull(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm), Color.argb(170, 255, 255, 255));
mBorderCornerPaint =
getNewPaintOrNull(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, dm), Color.WHITE);
mGuidelinePaint = getNewPaintOrNull(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 cropped 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();
canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom, mBackgroundPaint);
}
/**
* 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, if thickness < 0 return null. */
private static Paint getNewPaintOrNull(float thickness, int color) {
if (thickness > 0) {
Paint borderPaint = new Paint();
borderPaint.setColor(color);
borderPaint.setStrokeWidth(thickness);
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setAntiAlias(true);
return borderPaint;
} else {
return null;
}
}
@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
*
* @param rect the crop window rectangle to start finding bounded rectangle from
*/
private void calculateBounds(RectF rect) {
mCalcBounds.set(mInitialCropWindowRect);
}
// endregion
}

View File

@ -0,0 +1,263 @@
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;
/** The width scale factor of shown image and actual image */
private float mScaleFactorWidth = 1;
/** The height scale factor of shown image and actual image */
private float mScaleFactorHeight = 1;
// 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 / mScaleFactorWidth);
}
/** 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 / mScaleFactorHeight);
}
/** Maximum width in pixels that the crop window can get. */
public float getMaxCropWidth() {
/*
* Maximum width in pixels that the result of cropping an image can get, affects crop window width
* adjusted by width scale factor.
*/
float mMaxCropResultWidth = 99999;
return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth / mScaleFactorWidth);
}
/** Maximum height in pixels that the crop window can get. */
public float getMaxCropHeight() {
/*
* Maximum height in pixels that the result of cropping an image can get, affects crop window
* height adjusted by height scale factor.
*/
float mMaxCropResultHeight = 99999;
return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight / mScaleFactorHeight);
}
/** get the scale factor (on width) of the shown image to original image. */
public float getScaleFactorWidth() {
return mScaleFactorWidth;
}
/** get the scale factor (on height) of the shown image to original image. */
public float getScaleFactorHeight() {
return mScaleFactorHeight;
}
/**
* 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, float scaleFactorWidth, float scaleFactorHeight) {
mMaxCropWindowWidth = maxWidth;
mMaxCropWindowHeight = maxHeight;
mScaleFactorWidth = scaleFactorWidth;
mScaleFactorHeight = scaleFactorHeight;
}
/** 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,442 @@
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 cropped
* @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 cropped
* @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 cropped
* @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 cropped
* @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

@ -19,6 +19,16 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<org.pixeldroid.app.postCreation.photoEdit.cropper.CropImageView
android:id="@+id/cropImageView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/videoView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:visibility="visible"/>
<ImageView
android:id="@+id/muter"
android:layout_width="60dp"

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<org.pixeldroid.app.postCreation.photoEdit.cropper.CropImageView
android:id="@+id/cropImageView"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"
android:layout_width="match_parent">
<org.pixeldroid.app.postCreation.photoEdit.cropper.CropOverlayView
android:id="@+id/CropOverlayView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="2dp"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ImageView_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>