From 9ad24f71574e563fb738f92895839ab02a104a2c Mon Sep 17 00:00:00 2001
From: Matthieu <24-artectrex@users.noreply.shinice.net>
Date: Mon, 17 Oct 2022 13:23:02 +0200
Subject: [PATCH] wip

---
 .../photoEdit/VideoEditActivity.kt            |   5 +
 .../photoEdit/cropper/CropImageView.kt        | 115 ++++
 .../photoEdit/cropper/CropOverlayView.java    | 533 ++++++++++++++++++
 .../photoEdit/cropper/CropWindowHandler.java  | 263 +++++++++
 .../cropper/CropWindowMoveHandler.java        | 442 +++++++++++++++
 .../main/res/layout/activity_video_edit.xml   |  10 +
 .../main/res/layout/crop_image_activity.xml   |   6 +
 app/src/main/res/layout/crop_image_view.xml   |  31 +
 8 files changed, 1405 insertions(+)
 create mode 100644 app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropImageView.kt
 create mode 100644 app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropOverlayView.java
 create mode 100644 app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowHandler.java
 create mode 100644 app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowMoveHandler.java
 create mode 100644 app/src/main/res/layout/crop_image_activity.xml
 create mode 100644 app/src/main/res/layout/crop_image_view.xml

diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt
index 3981b82f..868e9409 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt
@@ -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)
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropImageView.kt b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropImageView.kt
new file mode 100644
index 00000000..21616b97
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropImageView.kt
@@ -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
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropOverlayView.java b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropOverlayView.java
new file mode 100644
index 00000000..82205c8d
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropOverlayView.java
@@ -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
+}
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowHandler.java b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowHandler.java
new file mode 100644
index 00000000..6226811b
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowHandler.java
@@ -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
+}
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowMoveHandler.java b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowMoveHandler.java
new file mode 100644
index 00000000..41f5409b
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowMoveHandler.java
@@ -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
+}
diff --git a/app/src/main/res/layout/activity_video_edit.xml b/app/src/main/res/layout/activity_video_edit.xml
index fe3feb98..a3e1faef 100644
--- a/app/src/main/res/layout/activity_video_edit.xml
+++ b/app/src/main/res/layout/activity_video_edit.xml
@@ -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"
diff --git a/app/src/main/res/layout/crop_image_activity.xml b/app/src/main/res/layout/crop_image_activity.xml
new file mode 100644
index 00000000..b8336538
--- /dev/null
+++ b/app/src/main/res/layout/crop_image_activity.xml
@@ -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"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/crop_image_view.xml b/app/src/main/res/layout/crop_image_view.xml
new file mode 100644
index 00000000..48b90bb4
--- /dev/null
+++ b/app/src/main/res/layout/crop_image_view.xml
@@ -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>
\ No newline at end of file