content: package com.bihe0832.android.common.crop.view;
  
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.Bitmap;
  import android.graphics.BitmapFactory;
  import android.graphics.Matrix;
  import android.graphics.RectF;
  import android.graphics.drawable.Drawable;
  import android.net.Uri;
  import android.util.AttributeSet;
  import androidx.annotation.IntRange;
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.exifinterface.media.ExifInterface;
  import com.bihe0832.android.common.crop.R;
  import com.bihe0832.android.common.crop.callback.BitmapCropCallback;
  import com.bihe0832.android.common.crop.callback.CropBoundsChangeListener;
  import com.bihe0832.android.common.crop.model.ExifInfo;
  import com.bihe0832.android.common.crop.util.CubicEasing;
  import com.bihe0832.android.common.crop.model.ImageState;
  import com.bihe0832.android.lib.crop.jni.CropAction;
  import com.bihe0832.android.lib.file.FileUtils;
  import com.bihe0832.android.lib.log.ZLog;
  import com.bihe0832.android.lib.media.image.ImageHeaderParser;
  import com.bihe0832.android.lib.media.image.RectUtils;
  import com.bihe0832.android.lib.thread.ThreadManager;
  import java.io.File;
  import java.io.IOException;
  import java.lang.ref.WeakReference;
  import java.util.Arrays;
  
  public class CropImageView extends TransformImageView {
  
      public static final int DEFAULT_MAX_BITMAP_SIZE = 0;
      public static final int DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION = 500;
      public static final float DEFAULT_MAX_SCALE_MULTIPLIER = 20.0f;
      public static final float SOURCE_IMAGE_ASPECT_RATIO = 0f;
      public static final float DEFAULT_ASPECT_RATIO = SOURCE_IMAGE_ASPECT_RATIO;
  
      private final RectF mCropRect = new RectF();
  
      private final Matrix mTempMatrix = new Matrix();
  
      private float mTargetAspectRatio;
      private float mMaxScaleMultiplier = DEFAULT_MAX_SCALE_MULTIPLIER;
  
      private CropBoundsChangeListener mCropBoundsChangeListener;
  
      private Runnable mWrapCropBoundsRunnable, mZoomImageToPositionRunnable = null;
  
      private float mMaxScale, mMinScale;
      private int mMaxResultImageSizeX = 0, mMaxResultImageSizeY = 0;
      private long mImageToWrapCropBoundsAnimDuration = DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION;
  
      public CropImageView(Context context) {
          this(context, null);
      }
  
      public CropImageView(Context context, AttributeSet attrs) {
          this(context, attrs, 0);
      }
  
      public CropImageView(Context context, AttributeSet attrs, int defStyle) {
          super(context, attrs, defStyle);
      }
  
      /**
       * Cancels all current animations and sets image to fill crop area (without animation).
       * Then creates and executes {@link CropAction} with proper parameters.
       */
      public void cropAndSaveImage(@NonNull final Bitmap.CompressFormat compressFormat, final int compressQuality,
              @Nullable final BitmapCropCallback cropCallback) {
          cancelAllAnimations();
          setImageToWrapCropBounds(false);
  
          final ImageState imageState = new ImageState(
                  mCropRect, RectUtils.trapToRect(mCurrentImageCorners),
                  getCurrentScale(), getCurrentAngle());
          final RectF mCurrentImageRect = RectUtils.trapToRect(mCurrentImageCorners);
  
          ThreadManager.getInstance().start(new Runnable() {
              @Override
              public void run() {
  
                  Bitmap mViewBitmap = getViewBitmap();
                  if (mViewBitmap == null) {
                      new NullPointerException("ViewBitmap is null");
                  } else if (mViewBitmap.isRecycled()) {
                      new NullPointerException("ViewBitmap is recycled");
                  } else if (imageState.getCurrentImageRect().isEmpty()) {
                      new NullPointerException("CurrentImageRect is empty");
                  }
  
                  mCurrentScale = imageState.getCurrentScale();
                  float resizeScale = resize(mViewBitmap, getImageInputPath(), getExifInfo());
  
                  try {
                      crop(resizeScale, mCropRect, mCurrentImageRect, imageState.getCurrentAngle(), compressFormat,
                              getExifInfo(), compressQuality);
                      Uri uri = Uri.fromFile(new File(getImageOutputPath()));
                      cropCallback.onBitmapCropped(uri, cropOffsetX, cropOffsetY, mCroppedImageWidth,
                              mCroppedImageHeight);
                      return;
                  } catch (Throwable throwable) {
                      throwable.printStackTrace();
                  }
                  cropCallback.onCropFailure(new Throwable());
              }
          });
      }
  
      private int mCroppedImageWidth, mCroppedImageHeight;
      private int cropOffsetX, cropOffsetY;
  
      private boolean crop(float resizeScale, RectF mCropRect, RectF mCurrentImageRect, float mCurrentAngle,
              Bitmap.CompressFormat mCompressFormat, ExifInfo mExifInfo, int mCompressQuality)
              throws IOException {
          ExifInterface originalExif = new ExifInterface(getImageInputPath());
  
          cropOffsetX = Math.round((mCropRect.left - mCurrentImageRect.left) / mCurrentScale);
          cropOffsetY = Math.round((mCropRect.top - mCurrentImageRect.top) / mCurrentScale);
          mCroppedImageWidth = Math.round(mCropRect.width() / mCurrentScale);
          mCroppedImageHeight = Math.round(mCropRect.height() / mCurrentScale);
  
          boolean shouldCrop = shouldCrop(mCroppedImageWidth, mCroppedImageHeight, mCurrentImageRect, mCurrentAngle);
          ZLog.i("Should crop: " + shouldCrop);
  
          if (shouldCrop) {
              boolean cropped = CropAction.crop(getImageInputPath(), getImageOutputPath(),
                      cropOffsetX, cropOffsetY, mCroppedImageWidth, mCroppedImageHeight,
                      mCurrentAngle, resizeScale, mCompressFormat.ordinal(), mCompressQuality,
                      mExifInfo.getExifDegrees(), mExifInfo.getExifTranslation());
              if (cropped && mCompressFormat.equals(Bitmap.CompressFormat.JPEG)) {
                  ImageHeaderParser.copyExif(originalExif, mCroppedImageWidth, mCroppedImageHeight, getImageOutputPath());
              }
              return cropped;
          } else {
              FileUtils.INSTANCE.copyFile(new File(getImageInputPath()), new File(getImageOutputPath()), false);
              return false;
          }
      }
  
      /**
       * Check whether an image should be cropped at all or just file can be copied to the destination path.
       * For each 1000 pixels there is one pixel of error due to matrix calculations etc.
       *
       * @param width - crop area width
       * @param height - crop area height
       * @return - true if image must be cropped, false - if original image fits requirements
       */
      private boolean shouldCrop(int width, int height, RectF mCurrentImageRect, float mCurrentAngle) {
          int pixelError = 1;
          pixelError += Math.round(Math.max(width, height) / 1000f);
          return (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0)
                  || Math.abs(mCropRect.left - mCurrentImageRect.left) > pixelError
                  || Math.abs(mCropRect.top - mCurrentImageRect.top) > pixelError
                  || Math.abs(mCropRect.bottom - mCurrentImageRect.bottom) > pixelError
                  || Math.abs(mCropRect.right - mCurrentImageRect.right) > pixelError
                  || mCurrentAngle != 0;
      }
  
  
      private float mCurrentScale = 0f;
  
      private float resize(Bitmap mViewBitmap, String mImageInputPath, ExifInfo mExifInfo) {
          final BitmapFactory.Options options = new BitmapFactory.Options();
          options.inJustDecodeBounds = true;
          BitmapFactory.decodeFile(mImageInputPath, options);
  
          boolean swapSides = mExifInfo.getExifDegrees() == 90 || mExifInfo.getExifDegrees() == 270;
          float scaleX = (swapSides ? options.outHeight : options.outWidth) / (float) mViewBitmap.getWidth();
          float scaleY = (swapSides ? options.outWidth : options.outHeight) / (float) mViewBitmap.getHeight();
  
          float resizeScale = Math.min(scaleX, scaleY);
  
          mCurrentScale /= resizeScale;
  
          resizeScale = 1;
          if (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) {
              float cropWidth = mCropRect.width() / mCurrentScale;
              float cropHeight = mCropRect.height() / mCurrentScale;
  
              if (cropWidth > mMaxResultImageSizeX || cropHeight > mMaxResultImageSizeY) {
  
                  scaleX = mMaxResultImageSizeX / cropWidth;
                  scaleY = mMaxResultImageSizeY / cropHeight;
                  resizeScale = Math.min(scaleX, scaleY);
  
                  mCurrentScale /= resizeScale;
              }
          }
          return resizeScale;
      }
  
      /**
       * @return - maximum scale value for current image and crop ratio
       */
      public float getMaxScale() {
          return mMaxScale;
      }
  
      /**
       * @return - minimum scale value for current image and crop ratio
       */
      public float getMinScale() {
          return mMinScale;
      }
  
      /**
       * @return - aspect ratio for crop bounds
       */
      public float getTargetAspectRatio() {
          return mTargetAspectRatio;
      }
  
      /**
       * Updates current crop rectangle with given. Also recalculates image properties and position
       * to fit new crop rectangle.
       *
       * @param cropRect - new crop rectangle
       */
      public void setCropRect(RectF cropRect) {
          mTargetAspectRatio = cropRect.width() / cropRect.height();
          mCropRect.set(cropRect.left - getPaddingLeft(), cropRect.top - getPaddingTop(),
                  cropRect.right - getPaddingRight(), cropRect.bottom - getPaddingBottom());
          calculateImageScaleBounds();
          setImageToWrapCropBounds();
      }
  
      /**
       * This method sets aspect ratio for crop bounds.
       * If {@link #SOURCE_IMAGE_ASPECT_RATIO} value is passed - aspect ratio is calculated
       * based on current image width and height.
       *
       * @param targetAspectRatio - aspect ratio for image crop (e.g. 1.77(7) for 16:9)
       */
      public void setTargetAspectRatio(float targetAspectRatio) {
          final Drawable drawable = getDrawable();
          if (drawable == null) {
              mTargetAspectRatio = targetAspectRatio;
              return;
          }
  
          if (targetAspectRatio == SOURCE_IMAGE_ASPECT_RATIO) {
              mTargetAspectRatio = drawable.getIntrinsicWidth() / (float) drawable.getIntrinsicHeight();
          } else {
              mTargetAspectRatio = targetAspectRatio;
          }
  
          if (mCropBoundsChangeListener != null) {
              mCropBoundsChangeListener.onCropAspectRatioChanged(mTargetAspectRatio);
          }
      }
  
      @Nullable
      public CropBoundsChangeListener getCropBoundsChangeListener() {
          return mCropBoundsChangeListener;
      }
  
      public void setCropBoundsChangeListener(@Nullable CropBoundsChangeListener cropBoundsChangeListener) {
          mCropBoundsChangeListener = cropBoundsChangeListener;
      }
  
      /**
       * This method sets maximum width for resulting cropped image
       *
       * @param maxResultImageSizeX - size in pixels
       */
      public void setMaxResultImageSizeX(@IntRange(from = 10) int maxResultImageSizeX) {
          mMaxResultImageSizeX = maxResultImageSizeX;
      }
  
      /**
       * This method sets maximum width for resulting cropped image
       *
       * @param maxResultImageSizeY - size in pixels
       */
      public void setMaxResultImageSizeY(@IntRange(from = 10) int maxResultImageSizeY) {
          mMaxResultImageSizeY = maxResultImageSizeY;
      }
  
      /**
       * This method sets animation duration for image to wrap the crop bounds
       *
       * @param imageToWrapCropBoundsAnimDuration - duration in milliseconds
       */
      public void setImageToWrapCropBoundsAnimDuration(@IntRange(from = 100) long imageToWrapCropBoundsAnimDuration) {
          if (imageToWrapCropBoundsAnimDuration > 0) {
              mImageToWrapCropBoundsAnimDuration = imageToWrapCropBoundsAnimDuration;
          } else {
              throw new IllegalArgumentException("Animation duration cannot be negative value.");
          }
      }
  
      /**
       * This method sets multiplier that is used to calculate max image scale from min image scale.
       *
       * @param maxScaleMultiplier - (minScale * maxScaleMultiplier) = maxScale
       */
      public void setMaxScaleMultiplier(float maxScaleMultiplier) {
          mMaxScaleMultiplier = maxScaleMultiplier;
      }
  
      /**
       * This method scales image down for given value related to image center.
       */
      public void zoomOutImage(float deltaScale) {
          zoomOutImage(deltaScale, mCropRect.centerX(), mCropRect.centerY());
      }
  
      /**
       * This method scales image down for given value related given coords (x, y).
       */
      public void zoomOutImage(float scale, float centerX, float centerY) {
          if (scale >= getMinScale()) {
              postScale(scale / getCurrentScale(), centerX, centerY);
          }
      }
  
      /**
       * This method scales image up for given value related to image center.
       */
      public void zoomInImage(float deltaScale) {
          zoomInImage(deltaScale, mCropRect.centerX(), mCropRect.centerY());
      }
  
      /**
       * This method scales image up for given value related to given coords (x, y).
       */
      public void zoomInImage(float scale, float centerX, float centerY) {
          if (scale <= getMaxScale()) {
              postScale(scale / getCurrentScale(), centerX, centerY);
          }
      }
  
      /**
       * This method changes image scale for given value related to point (px, py) but only if
       * resulting scale is in min/max bounds.
       *
       * @param deltaScale - scale value
       * @param px - scale center X
       * @param py - scale center Y
       */
      public void postScale(float deltaScale, float px, float py) {
          if (deltaScale > 1 && getCurrentScale() * deltaScale <= getMaxScale()) {
              super.postScale(deltaScale, px, py);
          } else if (deltaScale < 1 && getCurrentScale() * deltaScale >= getMinScale()) {
              super.postScale(deltaScale, px, py);
          }
      }
  
      /**
       * This method rotates image for given angle related to the image center.
       *
       * @param deltaAngle - angle to rotate
       */
      public void postRotate(float deltaAngle) {
          postRotate(deltaAngle, mCropRect.centerX(), mCropRect.centerY());
      }
  
      /**
       * This method cancels all current Runnable objects that represent animations.
       */
      public void cancelAllAnimations() {
          removeCallbacks(mWrapCropBoundsRunnable);
          removeCallbacks(mZoomImageToPositionRunnable);
      }
  
      public void setImageToWrapCropBounds() {
          setImageToWrapCropBounds(true);
      }
  
      /**
       * If image doesn't fill the crop bounds it must be translated and scaled properly to fill those.
       * <p/>
       * Therefore this method calculates delta X, Y and scale values and passes them to the
       * {@link WrapCropBoundsRunnable} which animates image.
       * Scale value must be calculated only if image won't fill the crop bounds after it's translated to the
       * crop bounds rectangle center. Using temporary variables this method checks this case.
       */
      public void setImageToWrapCropBounds(boolean animate) {
          if (mBitmapLaidOut && !isImageWrapCropBounds()) {
  
              float currentX = mCurrentImageCenter[0];
              float currentY = mCurrentImageCenter[1];
              float currentScale = getCurrentScale();
  
              float deltaX = mCropRect.centerX() - currentX;
              float deltaY = mCropRect.centerY() - currentY;
              float deltaScale = 0;
  
              mTempMatrix.reset();
              mTempMatrix.setTranslate(deltaX, deltaY);
  
              final float[] tempCurrentImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length);
              mTempMatrix.mapPoints(tempCurrentImageCorners);
  
              boolean willImageWrapCropBoundsAfterTranslate = isImageWrapCropBounds(tempCurrentImageCorners);
  
              if (willImageWrapCropBoundsAfterTranslate) {
                  final float[] imageIndents = calculateImageIndents();
                  deltaX = -(imageIndents[0] + imageIndents[2]);
                  deltaY = -(imageIndents[1] + imageIndents[3]);
              } else {
                  RectF tempCropRect = new RectF(mCropRect);
                  mTempMatrix.reset();
                  mTempMatrix.setRotate(getCurrentAngle());
                  mTempMatrix.mapRect(tempCropRect);
  
                  final float[] currentImageSides = RectUtils.getRectSidesFromCorners(mCurrentImageCorners);
  
                  deltaScale = Math.max(tempCropRect.width() / currentImageSides[0],
                          tempCropRect.height() / currentImageSides[1]);
                  deltaScale = deltaScale * currentScale - currentScale;
              }
  
              if (animate) {
                  post(mWrapCropBoundsRunnable = new WrapCropBoundsRunnable(
                          CropImageView.this, mImageToWrapCropBoundsAnimDuration, currentX, currentY, deltaX, deltaY,
                          currentScale, deltaScale, willImageWrapCropBoundsAfterTranslate));
              } else {
                  postTranslate(deltaX, deltaY);
                  if (!willImageWrapCropBoundsAfterTranslate) {
                      zoomInImage(currentScale + deltaScale, mCropRect.centerX(), mCropRect.centerY());
                  }
              }
          }
      }
  
      /**
       * First, un-rotate image and crop rectangles (make image rectangle axis-aligned).
       * Second, calculate deltas between those rectangles sides.
       * Third, depending on delta (its sign) put them or zero inside an array.
       * Fourth, using Matrix, rotate back those points (indents).
       *
       * @return - the float array of image indents (4 floats) - in this order [left, top, right, bottom]
       */
      private float[] calculateImageIndents() {
          mTempMatrix.reset();
          mTempMatrix.setRotate(-getCurrentAngle());
  
          float[] unrotatedImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length);
          float[] unrotatedCropBoundsCorners = RectUtils.getCornersFromRect(mCropRect);
  
          mTempMatrix.mapPoints(unrotatedImageCorners);
          mTempMatrix.mapPoints(unrotatedCropBoundsCorners);
  
          RectF unrotatedImageRect = RectUtils.trapToRect(unrotatedImageCorners);
          RectF unrotatedCropRect = RectUtils.trapToRect(unrotatedCropBoundsCorners);
  
          float deltaLeft = unrotatedImageRect.left - unrotatedCropRect.left;
          float deltaTop = unrotatedImageRect.top - unrotatedCropRect.top;
          float deltaRight = unrotatedImageRect.right - unrotatedCropRect.right;
          float deltaBottom = unrotatedImageRect.bottom - unrotatedCropRect.bottom;
  
          float indents[] = new float[4];
          indents[0] = (deltaLeft > 0) ? deltaLeft : 0;
          indents[1] = (deltaTop > 0) ? deltaTop : 0;
          indents[2] = (deltaRight < 0) ? deltaRight : 0;
          indents[3] = (deltaBottom < 0) ? deltaBottom : 0;
  
          mTempMatrix.reset();
          mTempMatrix.setRotate(getCurrentAngle());
          mTempMatrix.mapPoints(indents);
  
          return indents;
      }
  
      /**
       * When image is laid out it must be centered properly to fit current crop bounds.
       */
      @Override
      protected void onImageLaidOut() {
          super.onImageLaidOut();
          final Drawable drawable = getDrawable();
          if (drawable == null) {
              return;
          }
  
          float drawableWidth = drawable.getIntrinsicWidth();
          float drawableHeight = drawable.getIntrinsicHeight();
  
          if (mTargetAspectRatio == SOURCE_IMAGE_ASPECT_RATIO) {
              mTargetAspectRatio = drawableWidth / drawableHeight;
          }
  
          int height = (int) (mThisWidth / mTargetAspectRatio);
          if (height > mThisHeight) {
              int width = (int) (mThisHeight * mTargetAspectRatio);
              int halfDiff = (mThisWidth - width) / 2;
              mCropRect.set(halfDiff, 0, width + halfDiff, mThisHeight);
          } else {
              int halfDiff = (mThisHeight - height) / 2;
              mCropRect.set(0, halfDiff, mThisWidth, height + halfDiff);
          }
  
          calculateImageScaleBounds(drawableWidth, drawableHeight);
          setupInitialImagePosition(drawableWidth, drawableHeight);
  
          if (mCropBoundsChangeListener != null) {
              mCropBoundsChangeListener.onCropAspectRatioChanged(mTargetAspectRatio);
          }
          if (mTransformImageListener != null) {
              mTransformImageListener.onScale(getCurrentScale());
              mTransformImageListener.onRotate(getCurrentAngle());
          }
      }
  
      /**
       * This method checks whether current image fills the crop bounds.
       */
      protected boolean isImageWrapCropBounds() {
          return isImageWrapCropBounds(mCurrentImageCorners);
      }
  
      /**
       * This methods checks whether a rectangle that is represented as 4 corner points (8 floats)
       * fills the crop bounds rectangle.
       *
       * @param imageCorners - corners of a rectangle
       * @return - true if it wraps crop bounds, false - otherwise
       */
      protected boolean isImageWrapCropBounds(float[] imageCorners) {
          mTempMatrix.reset();
          mTempMatrix.setRotate(-getCurrentAngle());
  
          float[] unrotatedImageCorners = Arrays.copyOf(imageCorners, imageCorners.length);
          mTempMatrix.mapPoints(unrotatedImageCorners);
  
          float[] unrotatedCropBoundsCorners = RectUtils.getCornersFromRect(mCropRect);
          mTempMatrix.mapPoints(unrotatedCropBoundsCorners);
  
          return RectUtils.trapToRect(unrotatedImageCorners).contains(RectUtils.trapToRect(unrotatedCropBoundsCorners));
      }
  
      /**
       * This method changes image scale (animating zoom for given duration), related to given center (x,y).
       *
       * @param scale - target scale
       * @param centerX - scale center X
       * @param centerY - scale center Y
       * @param durationMs - zoom animation duration
       */
      protected void zoomImageToPosition(float scale, float centerX, float centerY, long durationMs) {
          if (scale > getMaxScale()) {
              scale = getMaxScale();
          }
  
          final float oldScale = getCurrentScale();
          final float deltaScale = scale - oldScale;
  
          post(mZoomImageToPositionRunnable = new ZoomImageToPosition(CropImageView.this,
                  durationMs, oldScale, deltaScale, centerX, centerY));
      }
  
      private void calculateImageScaleBounds() {
          final Drawable drawable = getDrawable();
          if (drawable == null) {
              return;
          }
          calculateImageScaleBounds(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
      }
  
      /**
       * This method calculates image minimum and maximum scale values for current {@link #mCropRect}.
       *
       * @param drawableWidth - image width
       * @param drawableHeight - image height
       */
      private void calculateImageScaleBounds(float drawableWidth, float drawableHeight) {
          float widthScale = Math.min(mCropRect.width() / drawableWidth, mCropRect.width() / drawableHeight);
          float heightScale = Math.min(mCropRect.height() / drawableHeight, mCropRect.height() / drawableWidth);
  
          mMinScale = Math.min(widthScale, heightScale);
          mMaxScale = mMinScale * mMaxScaleMultiplier;
      }
  
      /**
       * This method calculates initial image position so it is positioned properly.
       * Then it sets those values to the current image matrix.
       *
       * @param drawableWidth - image width
       * @param drawableHeight - image height
       */
      private void setupInitialImagePosition(float drawableWidth, float drawableHeight) {
          float cropRectWidth = mCropRect.width();
          float cropRectHeight = mCropRect.height();
  
          float widthScale = mCropRect.width() / drawableWidth;
          float heightScale = mCropRect.height() / drawableHeight;
  
          float initialMinScale = Math.max(widthScale, heightScale);
  
          float tw = (cropRectWidth - drawableWidth * initialMinScale) / 2.0f + mCropRect.left;
          float th = (cropRectHeight - drawableHeight * initialMinScale) / 2.0f + mCropRect.top;
  
          mCurrentImageMatrix.reset();
          mCurrentImageMatrix.postScale(initialMinScale, initialMinScale);
          mCurrentImageMatrix.postTranslate(tw, th);
          setImageMatrix(mCurrentImageMatrix);
      }
  
      /**
       * This method extracts all needed values from the styled attributes.
       * Those are used to configure the view.
       */
      @SuppressWarnings("deprecation")
      protected void processStyledAttributes(@NonNull TypedArray a) {
          float targetAspectRatioX = Math.abs(
                  a.getFloat(R.styleable.CropView_aspect_ratio_x, DEFAULT_ASPECT_RATIO));
          float targetAspectRatioY = Math.abs(
                  a.getFloat(R.styleable.CropView_aspect_ratio_y, DEFAULT_ASPECT_RATIO));
  
          if (targetAspectRatioX == SOURCE_IMAGE_ASPECT_RATIO || targetAspectRatioY == SOURCE_IMAGE_ASPECT_RATIO) {
              mTargetAspectRatio = SOURCE_IMAGE_ASPECT_RATIO;
          } else {
              mTargetAspectRatio = targetAspectRatioX / targetAspectRatioY;
          }
      }
  
      /**
       * This Runnable is used to animate an image so it fills the crop bounds entirely.
       * Given values are interpolated during the animation time.
       * Runnable can be terminated either vie {@link #cancelAllAnimations()} method
       * or when certain conditions inside {@link WrapCropBoundsRunnable#run()} method are triggered.
       */
      private static class WrapCropBoundsRunnable implements Runnable {
  
          private final WeakReference<CropImageView> mCropImageView;
  
          private final long mDurationMs, mStartTime;
          private final float mOldX, mOldY;
          private final float mCenterDiffX, mCenterDiffY;
          private final float mOldScale;
          private final float mDeltaScale;
          private final boolean mWillBeImageInBoundsAfterTranslate;
  
          public WrapCropBoundsRunnable(CropImageView cropImageView,
                  long durationMs,
                  float oldX, float oldY,
                  float centerDiffX, float centerDiffY,
                  float oldScale, float deltaScale,
                  boolean willBeImageInBoundsAfterTranslate) {
  
              mCropImageView = new WeakReference<>(cropImageView);
  
              mDurationMs = durationMs;
              mStartTime = System.currentTimeMillis();
              mOldX = oldX;
              mOldY = oldY;
              mCenterDiffX = centerDiffX;
              mCenterDiffY = centerDiffY;
              mOldScale = oldScale;
              mDeltaScale = deltaScale;
              mWillBeImageInBoundsAfterTranslate = willBeImageInBoundsAfterTranslate;
          }
  
          @Override
          public void run() {
              CropImageView cropImageView = mCropImageView.get();
              if (cropImageView == null) {
                  return;
              }
  
              long now = System.currentTimeMillis();
              float currentMs = Math.min(mDurationMs, now - mStartTime);
  
              float newX = CubicEasing.easeOut(currentMs, 0, mCenterDiffX, mDurationMs);
              float newY = CubicEasing.easeOut(currentMs, 0, mCenterDiffY, mDurationMs);
              float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);
  
              if (currentMs < mDurationMs) {
                  cropImageView.postTranslate(newX - (cropImageView.mCurrentImageCenter[0] - mOldX),
                          newY - (cropImageView.mCurrentImageCenter[1] - mOldY));
                  if (!mWillBeImageInBoundsAfterTranslate) {
                      cropImageView.zoomInImage(mOldScale + newScale, cropImageView.mCropRect.centerX(),
                              cropImageView.mCropRect.centerY());
                  }
                  if (!cropImageView.isImageWrapCropBounds()) {
                      cropImageView.post(this);
                  }
              }
          }
      }
  
      /**
       * This Runnable is used to animate an image zoom.
       * Given values are interpolated during the animation time.
       * Runnable can be terminated either vie {@link #cancelAllAnimations()} method
       * or when certain conditions inside {@link ZoomImageToPosition#run()} method are triggered.
       */
      private static class ZoomImageToPosition implements Runnable {
  
          private final WeakReference<CropImageView> mCropImageView;
  
          private final long mDurationMs, mStartTime;
          private final float mOldScale;
          private final float mDeltaScale;
          private final float mDestX;
          private final float mDestY;
  
          public ZoomImageToPosition(CropImageView cropImageView,
                  long durationMs,
                  float oldScale, float deltaScale,
                  float destX, float destY) {
  
              mCropImageView = new WeakReference<>(cropImageView);
  
              mStartTime = System.currentTimeMillis();
              mDurationMs = durationMs;
              mOldScale = oldScale;
              mDeltaScale = deltaScale;
              mDestX = destX;
              mDestY = destY;
          }
  
          @Override
          public void run() {
              CropImageView cropImageView = mCropImageView.get();
              if (cropImageView == null) {
                  return;
              }
  
              long now = System.currentTimeMillis();
              float currentMs = Math.min(mDurationMs, now - mStartTime);
              float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);
  
              if (currentMs < mDurationMs) {
                  cropImageView.zoomInImage(mOldScale + newScale, mDestX, mDestY);
                  cropImageView.post(this);
              } else {
                  cropImageView.setImageToWrapCropBounds();
              }
          }
  
      }
  
  }
  