mirror of
https://gitlab.shinice.net/pixeldroid/PixelDroid
synced 2024-12-18 11:19:21 +01:00
wip
This commit is contained in:
parent
b85fd4ef15
commit
9ad24f7157
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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"
|
||||
|
6
app/src/main/res/layout/crop_image_activity.xml
Normal file
6
app/src/main/res/layout/crop_image_activity.xml
Normal 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"/>
|
31
app/src/main/res/layout/crop_image_view.xml
Normal file
31
app/src/main/res/layout/crop_image_view.xml
Normal 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>
|
Loading…
Reference in New Issue
Block a user