mirror of
https://gitlab.shinice.net/pixeldroid/PixelDroid
synced 2024-12-18 19:22:50 +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.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
import android.content.ContentUris
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@ -59,6 +61,9 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
|||||||
val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
|
val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
|
||||||
|
|
||||||
val uri = intent.getParcelableExtra<Uri>(PhotoEditActivity.PICTURE_URI)!!
|
val uri = intent.getParcelableExtra<Uri>(PhotoEditActivity.PICTURE_URI)!!
|
||||||
|
|
||||||
|
binding.cropImageView.setImageUriAsync(uri)
|
||||||
|
|
||||||
videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
|
videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
|
||||||
|
|
||||||
val inputVideoPath = ffmpegCompliantUri(uri)
|
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_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="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
|
<ImageView
|
||||||
android:id="@+id/muter"
|
android:id="@+id/muter"
|
||||||
android:layout_width="60dp"
|
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