content: /*
   * Copyright (C) 2011 Google Inc.
   * Licensed to The Android Open Source Project.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.ex.photo.views;
  
  import android.content.Context;
  import android.content.res.Resources;
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.Matrix;
  import android.graphics.Paint;
  import android.graphics.Paint.Style;
  import android.graphics.Rect;
  import android.graphics.RectF;
  import android.graphics.drawable.BitmapDrawable;
  import android.graphics.drawable.Drawable;
  import androidx.core.view.GestureDetectorCompat;
  import androidx.core.view.ScaleGestureDetectorCompat;
  import android.util.AttributeSet;
  import android.view.GestureDetector.OnDoubleTapListener;
  import android.view.GestureDetector.OnGestureListener;
  import android.view.MotionEvent;
  import android.view.ScaleGestureDetector;
  import android.view.View;
  import android.view.ViewConfiguration;
  
  import com.android.ex.photo.R;
  import com.android.ex.photo.fragments.PhotoViewFragment.HorizontallyScrollable;
  
  /**
   * Layout for the photo list view header.
   */
  public class PhotoView extends View implements OnGestureListener,
          OnDoubleTapListener, ScaleGestureDetector.OnScaleGestureListener,
          HorizontallyScrollable {
  
      public static final int TRANSLATE_NONE = 0;
      public static final int TRANSLATE_X_ONLY = 1;
      public static final int TRANSLATE_Y_ONLY = 2;
      public static final int TRANSLATE_BOTH = 3;
  
      /** Zoom animation duration; in milliseconds */
      private final static long ZOOM_ANIMATION_DURATION = 200L;
      /** Amount of time to wait after over-zooming before the zoom out animation; in milliseconds */
      private static final long ZOOM_CORRECTION_DELAY = 600L;
      /** Rotate animation duration; in milliseconds */
      private final static long ROTATE_ANIMATION_DURATION = 500L;
      /** Snap animation duration; in milliseconds */
      private static final long SNAP_DURATION = 100L;
      /** Amount of time to wait before starting snap animation; in milliseconds */
      private static final long SNAP_DELAY = 250L;
      /** By how much to scale the image when double click occurs */
      private final static float DOUBLE_TAP_SCALE_FACTOR = 2.0f;
      /** Amount which can be zoomed in past the maximum scale, and then scaled back */
      private final static float SCALE_OVERZOOM_FACTOR = 1.5f;
      /** Amount of translation needed before starting a snap animation */
      private final static float SNAP_THRESHOLD = 20.0f;
      /** The width & height of the bitmap returned by {@link #getCroppedPhoto()} */
      private final static float CROPPED_SIZE = 256.0f;
  
      /**
       * Touch slop used to determine if this double tap is valid for starting a scale or should be
       * ignored.
       */
      private static int sTouchSlopSquare;
  
      /** If {@code true}, the static values have been initialized */
      private static boolean sInitialized;
  
      // Various dimensions
      /** Width & height of the crop region */
      private static int sCropSize;
  
      // Bitmaps
      /** Video icon */
      private static Bitmap sVideoImage;
      /** Video icon */
      private static Bitmap sVideoNotReadyImage;
  
      // Paints
      /** Paint to partially dim the photo during crop */
      private static Paint sCropDimPaint;
      /** Paint to highlight the cropped portion of the photo */
      private static Paint sCropPaint;
  
      /** The photo to display */
      private Drawable mDrawable;
      /** The matrix used for drawing; this may be {@code null} */
      private Matrix mDrawMatrix;
      /** A matrix to apply the scaling of the photo */
      private Matrix mMatrix = new Matrix();
      /** The original matrix for this image; used to reset any transformations applied by the user */
      private Matrix mOriginalMatrix = new Matrix();
  
      /** The fixed height of this view. If {@code -1}, calculate the height */
      private int mFixedHeight = -1;
      /** When {@code true}, the view has been laid out */
      private boolean mHaveLayout;
      /** Whether or not the photo is full-screen */
      private boolean mFullScreen;
      /** Whether or not this is a still image of a video */
      private byte[] mVideoBlob;
      /** Whether or not this is a still image of a video */
      private boolean mVideoReady;
  
      /** Whether or not crop is allowed */
      private boolean mAllowCrop;
      /** The crop region */
      private Rect mCropRect = new Rect();
      /** Actual crop size; may differ from {@link #sCropSize} if the screen is smaller */
      private int mCropSize;
      /** The maximum amount of scaling to apply to images */
      private float mMaxInitialScaleFactor;
  
      /** Gesture detector */
      private GestureDetectorCompat mGestureDetector;
      /** Gesture detector that detects pinch gestures */
      private ScaleGestureDetector mScaleGetureDetector;
      /** An external click listener */
      private OnClickListener mExternalClickListener;
      /** When {@code true}, allows gestures to scale / pan the image */
      private boolean mTransformsEnabled;
  
      // To support zooming
      /** When {@code true}, a double tap scales the image by {@link #DOUBLE_TAP_SCALE_FACTOR} */
      private boolean mDoubleTapToZoomEnabled = true;
      /** When {@code true}, prevents scale end gesture from falsely triggering a double click. */
      private boolean mDoubleTapDebounce;
      /** When {@code false}, event is a scale gesture. Otherwise, event is a double touch. */
      private boolean mIsDoubleTouch;
      /** Runnable that scales the image */
      private ScaleRunnable mScaleRunnable;
      /** Minimum scale the image can have. */
      private float mMinScale;
      /** Maximum scale to limit scaling to, 0 means no limit. */
      private float mMaxScale;
  
      // To support translation [i.e. panning]
      /** Runnable that can move the image */
      private TranslateRunnable mTranslateRunnable;
      private SnapRunnable mSnapRunnable;
  
      // To support rotation
      /** The rotate runnable used to animate rotations of the image */
      private RotateRunnable mRotateRunnable;
      /** The current rotation amount, in degrees */
      private float mRotation;
  
      // Convenience fields
      // These are declared here not because they are important properties of the view. Rather, we
      // declare them here to avoid object allocation during critical graphics operations; such as
      // layout or drawing.
      /** Source (i.e. the photo size) bounds */
      private RectF mTempSrc = new RectF();
      /** Destination (i.e. the display) bounds. The image is scaled to this size. */
      private RectF mTempDst = new RectF();
      /** Rectangle to handle translations */
      private RectF mTranslateRect = new RectF();
      /** Array to store a copy of the matrix values */
      private float[] mValues = new float[9];
  
      /**
       * Track whether a double tap event occurred.
       */
      private boolean mDoubleTapOccurred;
  
      /**
       * X and Y coordinates for the current down event. Since mDoubleTapOccurred only contains the
       * information that there was a double tap event, use these to get the secondary tap
       * information to determine if a user has moved beyond touch slop.
       */
      private float mDownFocusX;
      private float mDownFocusY;
  
      /**
       * Whether the QuickSale gesture is enabled.
       */
      private boolean mQuickScaleEnabled;
  
      public PhotoView(Context context) {
          super(context);
          initialize();
      }
  
      public PhotoView(Context context, AttributeSet attrs) {
          super(context, attrs);
          initialize();
      }
  
      public PhotoView(Context context, AttributeSet attrs, int defStyle) {
          super(context, attrs, defStyle);
          initialize();
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent event) {
          if (mScaleGetureDetector == null || mGestureDetector == null) {
              // We're being destroyed; ignore any touch events
              return true;
          }
  
          mScaleGetureDetector.onTouchEvent(event);
          mGestureDetector.onTouchEvent(event);
          final int action = event.getAction();
  
          switch (action) {
              case MotionEvent.ACTION_UP:
              case MotionEvent.ACTION_CANCEL:
                  if (!mTranslateRunnable.mRunning) {
                      snap();
                  }
                  break;
          }
  
          return true;
      }
  
      @Override
      public boolean onDoubleTap(MotionEvent e) {
          mDoubleTapOccurred = true;
          if (!mQuickScaleEnabled) {
              return scale(e);
          }
          return false;
      }
  
      @Override
      public boolean onDoubleTapEvent(MotionEvent e) {
          final int action = e.getAction();
          boolean handled = false;
  
          switch (action) {
              case MotionEvent.ACTION_DOWN:
                  if (mQuickScaleEnabled) {
                      mDownFocusX = e.getX();
                      mDownFocusY = e.getY();
                  }
                  break;
              case MotionEvent.ACTION_UP:
                  if (mQuickScaleEnabled) {
                      handled = scale(e);
                  }
                  break;
              case MotionEvent.ACTION_MOVE:
                  if (mQuickScaleEnabled && mDoubleTapOccurred) {
                      final int deltaX = (int) (e.getX() - mDownFocusX);
                      final int deltaY = (int) (e.getY() - mDownFocusY);
                      int distance = (deltaX * deltaX) + (deltaY * deltaY);
                      if (distance > sTouchSlopSquare) {
                          mDoubleTapOccurred = false;
                      }
                  }
                  break;
  
          }
          return handled;
      }
  
      private boolean scale(MotionEvent e) {
          boolean handled = false;
          if (mDoubleTapToZoomEnabled && mTransformsEnabled && mDoubleTapOccurred) {
              if (!mDoubleTapDebounce) {
                  float currentScale = getScale();
                  float targetScale;
                  float centerX, centerY;
  
                  // Zoom out if not default scale, otherwise zoom in
                  if (currentScale > mMinScale) {
                      targetScale = mMinScale;
                      float relativeScale = targetScale / currentScale;
                      // Find the apparent origin for scaling that equals this scale and translate
                      centerX = (getWidth() / 2 - relativeScale * mTranslateRect.centerX()) /
                              (1 - relativeScale);
                      centerY = (getHeight() / 2 - relativeScale * mTranslateRect.centerY()) /
                              (1 - relativeScale);
                  } else {
                       targetScale = currentScale * DOUBLE_TAP_SCALE_FACTOR;
                       // Ensure the target scale is within our bounds
                       targetScale = Math.max(mMinScale, targetScale);
                       targetScale = Math.min(mMaxScale, targetScale);
                       float relativeScale = targetScale / currentScale;
                       float widthBuffer = (getWidth() - mTranslateRect.width()) / relativeScale;
                       float heightBuffer = (getHeight() - mTranslateRect.height()) / relativeScale;
                       // Clamp the center if it would result in uneven borders
                       if (mTranslateRect.width() <= widthBuffer * 2) {
                           centerX = mTranslateRect.centerX();
                       } else {
                           centerX = Math.min(Math.max(mTranslateRect.left + widthBuffer,
                                   e.getX()), mTranslateRect.right - widthBuffer);
                       }
                       if (mTranslateRect.height() <= heightBuffer * 2) {
                           centerY = mTranslateRect.centerY();
                       } else {
                           centerY = Math.min(Math.max(mTranslateRect.top + heightBuffer,
                                   e.getY()), mTranslateRect.bottom - heightBuffer);
                       }
                  }
  
                  mScaleRunnable.start(currentScale, targetScale, centerX, centerY);
                  handled = true;
              }
              mDoubleTapDebounce = false;
          }
          mDoubleTapOccurred = false;
          return handled;
      }
  
      @Override
      public boolean onSingleTapConfirmed(MotionEvent e) {
          if (mExternalClickListener != null && !mIsDoubleTouch) {
              mExternalClickListener.onClick(this);
          }
          mIsDoubleTouch = false;
          return true;
      }
  
      @Override
      public boolean onSingleTapUp(MotionEvent e) {
          return false;
      }
  
      @Override
      public void onLongPress(MotionEvent e) {
      }
  
      @Override
      public void onShowPress(MotionEvent e) {
      }
  
      @Override
      public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
          if (mTransformsEnabled && !mScaleRunnable.mRunning) {
              translate(-distanceX, -distanceY);
          }
          return true;
      }
  
      @Override
      public boolean onDown(MotionEvent e) {
          if (mTransformsEnabled) {
              mTranslateRunnable.stop();
              mSnapRunnable.stop();
          }
          return true;
      }
  
      @Override
      public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
          if (mTransformsEnabled && !mScaleRunnable.mRunning) {
              mTranslateRunnable.start(velocityX, velocityY);
          }
          return true;
      }
  
      @Override
      public boolean onScale(ScaleGestureDetector detector) {
          if (mTransformsEnabled && !mScaleRunnable.mRunning) {
              mIsDoubleTouch = false;
              float currentScale = getScale();
              float newScale = currentScale * detector.getScaleFactor();
              scale(newScale, detector.getFocusX(), detector.getFocusY());
          }
          return true;
      }
  
      @Override
      public boolean onScaleBegin(ScaleGestureDetector detector) {
          if (mTransformsEnabled && !mScaleRunnable.mRunning) {
              mScaleRunnable.stop();
              mIsDoubleTouch = true;
          }
          return true;
      }
  
      @Override
      public void onScaleEnd(ScaleGestureDetector detector) {
          if (mTransformsEnabled && mIsDoubleTouch) {
              mDoubleTapDebounce = true;
              resetTransformations();
          }
      }
  
      @Override
      public void setOnClickListener(OnClickListener listener) {
          mExternalClickListener = listener;
      }
  
      @Override
      public boolean interceptMoveLeft(float origX, float origY) {
          if (!mTransformsEnabled) {
              // Allow intercept if we're not in transform mode
              return false;
          } else if (mTranslateRunnable.mRunning) {
              // Don't allow touch intercept until we've stopped flinging
              return true;
          } else {
              mMatrix.getValues(mValues);
              mTranslateRect.set(mTempSrc);
              mMatrix.mapRect(mTranslateRect);
  
              final float viewWidth = getWidth();
              final float transX = mValues[Matrix.MTRANS_X];
              final float drawWidth = mTranslateRect.right - mTranslateRect.left;
  
              if (!mTransformsEnabled || drawWidth <= viewWidth) {
                  // Allow intercept if not in transform mode or the image is smaller than the view
                  return false;
              } else if (transX == 0) {
                  // We're at the left-side of the image; allow intercepting movements to the right
                  return false;
              } else if (viewWidth >= drawWidth + transX) {
                  // We're at the right-side of the image; allow intercepting movements to the left
                  return true;
              } else {
                  // We're in the middle of the image; don't allow touch intercept
                  return true;
              }
          }
      }
  
      @Override
      public boolean interceptMoveRight(float origX, float origY) {
          if (!mTransformsEnabled) {
              // Allow intercept if we're not in transform mode
              return false;
          } else if (mTranslateRunnable.mRunning) {
              // Don't allow touch intercept until we've stopped flinging
              return true;
          } else {
              mMatrix.getValues(mValues);
              mTranslateRect.set(mTempSrc);
              mMatrix.mapRect(mTranslateRect);
  
              final float viewWidth = getWidth();
              final float transX = mValues[Matrix.MTRANS_X];
              final float drawWidth = mTranslateRect.right - mTranslateRect.left;
  
              if (!mTransformsEnabled || drawWidth <= viewWidth) {
                  // Allow intercept if not in transform mode or the image is smaller than the view
                  return false;
              } else if (transX == 0) {
                  // We're at the left-side of the image; allow intercepting movements to the right
                  return true;
              } else if (viewWidth >= drawWidth + transX) {
                  // We're at the right-side of the image; allow intercepting movements to the left
                  return false;
              } else {
                  // We're in the middle of the image; don't allow touch intercept
                  return true;
              }
          }
      }
  
      /**
       * Free all resources held by this view.
       * The view is on its way to be collected and will not be reused.
       */
      public void clear() {
          mGestureDetector = null;
          mScaleGetureDetector = null;
          mDrawable = null;
          mScaleRunnable.stop();
          mScaleRunnable = null;
          mTranslateRunnable.stop();
          mTranslateRunnable = null;
          mSnapRunnable.stop();
          mSnapRunnable = null;
          mRotateRunnable.stop();
          mRotateRunnable = null;
          setOnClickListener(null);
          mExternalClickListener = null;
          mDoubleTapOccurred = false;
      }
  
      public void bindDrawable(Drawable drawable) {
          boolean changed = false;
          if (drawable != null && drawable != mDrawable) {
              // Clear previous state.
              if (mDrawable != null) {
                  mDrawable.setCallback(null);
              }
  
              mDrawable = drawable;
  
              // Reset mMinScale to ensure the bounds / matrix are recalculated
              mMinScale = 0f;
  
              // Set a callback?
              mDrawable.setCallback(this);
  
              changed = true;
          }
  
          configureBounds(changed);
          invalidate();
      }
  
      /**
       * Binds a bitmap to the view.
       *
       * @param photoBitmap the bitmap to bind.
       */
      public void bindPhoto(Bitmap photoBitmap) {
          boolean currentDrawableIsBitmapDrawable = mDrawable instanceof BitmapDrawable;
          boolean changed = !(currentDrawableIsBitmapDrawable);
          if (mDrawable != null && currentDrawableIsBitmapDrawable) {
              final Bitmap drawableBitmap = ((BitmapDrawable) mDrawable).getBitmap();
              if (photoBitmap == drawableBitmap) {
                  // setting the same bitmap; do nothing
                  return;
              }
  
              changed = photoBitmap != null &&
                      (mDrawable.getIntrinsicWidth() != photoBitmap.getWidth() ||
                      mDrawable.getIntrinsicHeight() != photoBitmap.getHeight());
  
              // Reset mMinScale to ensure the bounds / matrix are recalculated
              mMinScale = 0f;
              mDrawable = null;
          }
  
          if (mDrawable == null && photoBitmap != null) {
              mDrawable = new BitmapDrawable(getResources(), photoBitmap);
          }
  
          configureBounds(changed);
          invalidate();
      }
  
      /**
       * Returns the bound photo data if set. Otherwise, {@code null}.
       */
      public Bitmap getPhoto() {
          if (mDrawable != null && mDrawable instanceof BitmapDrawable) {
              return ((BitmapDrawable) mDrawable).getBitmap();
          }
          return null;
      }
  
      /**
       * Returns the bound drawable. May be {@code null} if no drawable is bound.
       */
      public Drawable getDrawable() {
          return mDrawable;
      }
  
      /**
       * Gets video data associated with this item. Returns {@code null} if this is not a video.
       */
      public byte[] getVideoData() {
          return mVideoBlob;
      }
  
      /**
       * Returns {@code true} if the photo represents a video. Otherwise, {@code false}.
       */
      public boolean isVideo() {
          return mVideoBlob != null;
      }
  
      /**
       * Returns {@code true} if the video is ready to play. Otherwise, {@code false}.
       */
      public boolean isVideoReady() {
          return mVideoBlob != null && mVideoReady;
      }
  
      /**
       * Returns {@code true} if a photo has been bound. Otherwise, {@code false}.
       */
      public boolean isPhotoBound() {
          return mDrawable != null;
      }
  
      /**
       * Hides the photo info portion of the header. As a side effect, this automatically enables
       * or disables image transformations [eg zoom, pan, etc...] depending upon the value of
       * fullScreen. If this is not desirable, enable / disable image transformations manually.
       */
      public void setFullScreen(boolean fullScreen, boolean animate) {
          if (fullScreen != mFullScreen) {
              mFullScreen = fullScreen;
              requestLayout();
              invalidate();
          }
      }
  
      /**
       * Enable or disable cropping of the displayed image. Cropping can only be enabled
       * <em>before</em> the view has been laid out. Additionally, once cropping has been
       * enabled, it cannot be disabled.
       */
      public void enableAllowCrop(boolean allowCrop) {
          if (allowCrop && mHaveLayout) {
              throw new IllegalArgumentException("Cannot set crop after view has been laid out");
          }
          if (!allowCrop && mAllowCrop) {
              throw new IllegalArgumentException("Cannot unset crop mode");
          }
          mAllowCrop = allowCrop;
      }
  
      /**
       * Gets a bitmap of the cropped region. If cropping is not enabled, returns {@code null}.
       */
      public Bitmap getCroppedPhoto() {
          if (!mAllowCrop) {
              return null;
          }
  
          final Bitmap croppedBitmap = Bitmap.createBitmap(
                  (int) CROPPED_SIZE, (int) CROPPED_SIZE, Bitmap.Config.ARGB_8888);
          final Canvas croppedCanvas = new Canvas(croppedBitmap);
  
          // scale for the final dimensions
          final int cropWidth = mCropRect.right - mCropRect.left;
          final float scaleWidth = CROPPED_SIZE / cropWidth;
          final float scaleHeight = CROPPED_SIZE / cropWidth;
  
          // translate to the origin & scale
          final Matrix matrix = new Matrix(mDrawMatrix);
          matrix.postTranslate(-mCropRect.left, -mCropRect.top);
          matrix.postScale(scaleWidth, scaleHeight);
  
          // draw the photo
          if (mDrawable != null) {
              croppedCanvas.concat(matrix);
              mDrawable.draw(croppedCanvas);
          }
          return croppedBitmap;
      }
  
      /**
       * Resets the image transformation to its original value.
       */
      public void resetTransformations() {
          // snap transformations; we don't animate
          mMatrix.set(mOriginalMatrix);
  
          // Invalidate the view because if you move off this PhotoView
          // to another one and come back, you want it to draw from scratch
          // in case you were zoomed in or translated (since those settings
          // are not preserved and probably shouldn't be).
          invalidate();
      }
  
      /**
       * Rotates the image 90 degrees, clockwise.
       */
      public void rotateClockwise() {
          rotate(90, true);
      }
  
      /**
       * Rotates the image 90 degrees, counter clockwise.
       */
      public void rotateCounterClockwise() {
          rotate(-90, true);
      }
  
      @Override
      protected void onDraw(Canvas canvas) {
          super.onDraw(canvas);
  
          // draw the photo
          if (mDrawable != null) {
              int saveCount = canvas.getSaveCount();
              canvas.save();
  
              if (mDrawMatrix != null) {
                  canvas.concat(mDrawMatrix);
              }
              mDrawable.draw(canvas);
  
              canvas.restoreToCount(saveCount);
  
              if (mVideoBlob != null) {
                  final Bitmap videoImage = (mVideoReady ? sVideoImage : sVideoNotReadyImage);
                  final int drawLeft = (getWidth() - videoImage.getWidth()) / 2;
                  final int drawTop = (getHeight() - videoImage.getHeight()) / 2;
                  canvas.drawBitmap(videoImage, drawLeft, drawTop, null);
              }
  
              // Extract the drawable's bounds (in our own copy, to not alter the image)
              mTranslateRect.set(mDrawable.getBounds());
              if (mDrawMatrix != null) {
                  mDrawMatrix.mapRect(mTranslateRect);
              }
  
              if (mAllowCrop) {
                  int previousSaveCount = canvas.getSaveCount();
                  canvas.drawRect(0, 0, getWidth(), getHeight(), sCropDimPaint);
                  canvas.save();
                  canvas.clipRect(mCropRect);
  
                  if (mDrawMatrix != null) {
                      canvas.concat(mDrawMatrix);
                  }
  
                  mDrawable.draw(canvas);
                  canvas.restoreToCount(previousSaveCount);
                  canvas.drawRect(mCropRect, sCropPaint);
              }
          }
      }
  
      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
          super.onLayout(changed, left, top, right, bottom);
          mHaveLayout = true;
          final int layoutWidth = getWidth();
          final int layoutHeight = getHeight();
  
          if (mAllowCrop) {
              mCropSize = Math.min(sCropSize, Math.min(layoutWidth, layoutHeight));
              final int cropLeft = (layoutWidth - mCropSize) / 2;
              final int cropTop = (layoutHeight - mCropSize) / 2;
              final int cropRight = cropLeft + mCropSize;
              final int cropBottom =  cropTop + mCropSize;
  
              // Create a crop region overlay. We need a separate canvas to be able to "punch
              // a hole" through to the underlying image.
              mCropRect.set(cropLeft, cropTop, cropRight, cropBottom);
          }
          configureBounds(changed);
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          if (mFixedHeight != -1) {
              super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mFixedHeight,
                      MeasureSpec.AT_MOST));
              setMeasuredDimension(getMeasuredWidth(), mFixedHeight);
          } else {
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
          }
      }
  
      @Override
      public boolean verifyDrawable(Drawable drawable) {
          return mDrawable == drawable || super.verifyDrawable(drawable);
      }
  
      @Override
      /**
       * {@inheritDoc}
       */
      public void invalidateDrawable(Drawable drawable) {
          // Only invalidate this view if the passed in drawable is displayed within this view. If
          // another drawable is passed in, have the parent view handle invalidation.
          if (mDrawable == drawable) {
              invalidate();
          } else {
              super.invalidateDrawable(drawable);
          }
      }
  
      /**
       * Forces a fixed height for this view.
       *
       * @param fixedHeight The height. If {@code -1}, use the measured height.
       */
      public void setFixedHeight(int fixedHeight) {
          final boolean adjustBounds = (fixedHeight != mFixedHeight);
          mFixedHeight = fixedHeight;
          setMeasuredDimension(getMeasuredWidth(), mFixedHeight);
          if (adjustBounds) {
              configureBounds(true);
              requestLayout();
          }
      }
  
      /**
       * Enable or disable image transformations. When transformations are enabled, this view
       * consumes all touch events.
       */
      public void enableImageTransforms(boolean enable) {
          mTransformsEnabled = enable;
          if (!mTransformsEnabled) {
              resetTransformations();
          }
      }
  
      /**
       * Configures the bounds of the photo. The photo will always be scaled to fit center.
       */
      private void configureBounds(boolean changed) {
          if (mDrawable == null || !mHaveLayout) {
              return;
          }
          final int dwidth = mDrawable.getIntrinsicWidth();
          final int dheight = mDrawable.getIntrinsicHeight();
  
          final int vwidth = getWidth();
          final int vheight = getHeight();
  
          final boolean fits = (dwidth < 0 || vwidth == dwidth) &&
                  (dheight < 0 || vheight == dheight);
  
          // We need to do the scaling ourself, so have the drawable use its native size.
          mDrawable.setBounds(0, 0, dwidth, dheight);
  
          // Create a matrix with the proper transforms
          if (changed || (mMinScale == 0 && mDrawable != null && mHaveLayout)) {
              generateMatrix();
              generateScale();
          }
  
          if (fits || mMatrix.isIdentity()) {
              // The bitmap fits exactly, no transform needed.
              mDrawMatrix = null;
          } else {
              mDrawMatrix = mMatrix;
          }
      }
  
      /**
       * Generates the initial transformation matrix for drawing. Additionally, it sets the
       * minimum and maximum scale values.
       */
      private void generateMatrix() {
          final int dwidth = mDrawable.getIntrinsicWidth();
          final int dheight = mDrawable.getIntrinsicHeight();
  
          final int vwidth = mAllowCrop ? sCropSize : getWidth();
          final int vheight = mAllowCrop ? sCropSize : getHeight();
  
          final boolean fits = (dwidth < 0 || vwidth == dwidth) &&
                  (dheight < 0 || vheight == dheight);
  
          if (fits && !mAllowCrop) {
              mMatrix.reset();
          } else {
              // Generate the required transforms for the photo
              mTempSrc.set(0, 0, dwidth, dheight);
              if (mAllowCrop) {
                  mTempDst.set(mCropRect);
              } else {
                  mTempDst.set(0, 0, vwidth, vheight);
              }
              RectF scaledDestination = new RectF(
                      (vwidth / 2) - (dwidth * mMaxInitialScaleFactor / 2),
                      (vheight / 2) - (dheight * mMaxInitialScaleFactor / 2),
                      (vwidth / 2) + (dwidth * mMaxInitialScaleFactor / 2),
                      (vheight / 2) + (dheight * mMaxInitialScaleFactor / 2));
              if(mTempDst.contains(scaledDestination)) {
                  mMatrix.setRectToRect(mTempSrc, scaledDestination, Matrix.ScaleToFit.CENTER);
              } else {
                  mMatrix.setRectToRect(mTempSrc, mTempDst, Matrix.ScaleToFit.CENTER);
              }
          }
          mOriginalMatrix.set(mMatrix);
      }
  
      private void generateScale() {
          final int dwidth = mDrawable.getIntrinsicWidth();
          final int dheight = mDrawable.getIntrinsicHeight();
  
          final int vwidth = mAllowCrop ? getCropSize() : getWidth();
          final int vheight = mAllowCrop ? getCropSize() : getHeight();
  
          if (dwidth < vwidth && dheight < vheight && !mAllowCrop) {
              mMinScale = 1.0f;
          } else {
              mMinScale = getScale();
          }
          mMaxScale = Math.max(mMinScale * 4, 4);
      }
  
      /**
       * @return the size of the crop regions
       */
      private int getCropSize() {
          return mCropSize > 0 ? mCropSize : sCropSize;
      }
  
      /**
       * Returns the currently applied scale factor for the image.
       * <p>
       * NOTE: This method overwrites any values stored in {@link #mValues}.
       */
      private float getScale() {
          mMatrix.getValues(mValues);
          return mValues[Matrix.MSCALE_X];
      }
  
      /**
       * Scales the image while keeping the aspect ratio.
       *
       * The given scale is capped so that the resulting scale of the image always remains
       * between {@link #mMinScale} and {@link #mMaxScale}.
       *
       * If the image is smaller than the viewable area, it will be centered.
       *
       * @param newScale the new scale
       * @param centerX the center horizontal point around which to scale
       * @param centerY the center vertical point around which to scale
       */
      private void scale(float newScale, float centerX, float centerY) {
          // Rotate back to the original orientation
          mMatrix.postRotate(-mRotation, getWidth() / 2, getHeight() / 2);
  
          // Ensure that mMinScale <= newScale <= mMaxScale
          newScale = Math.max(newScale, mMinScale);
          newScale = Math.min(newScale, mMaxScale * SCALE_OVERZOOM_FACTOR);
  
          float currentScale = getScale();
  
          // Prepare to animate zoom out if over-zooming
          if (newScale > mMaxScale && currentScale <= mMaxScale) {
              Runnable zoomBackRunnable = new Runnable() {
                  @Override
                  public void run() {
                      // Scale back to the maximum if over-zoomed
                      float currentScale = getScale();
                      if (currentScale > mMaxScale) {
                          // The number of times the crop amount pulled in can fit on the screen
                          float marginFit = 1 / (1 - mMaxScale / currentScale);
                          // The (negative) relative maximum distance from an image edge such that
                          // when scaled this far from the edge, all of the image off-screen in that
                          // direction is pulled in
                          float relativeDistance = 1 - marginFit;
                          float finalCenterX = getWidth() / 2;
                          float finalCenterY = getHeight() / 2;
                          // This center will pull all of the margin from the lesser side, over will
                          // expose trim
                          float maxX = mTranslateRect.left * relativeDistance;
                          float maxY = mTranslateRect.top * relativeDistance;
                          // This center will pull all of the margin from the greater side, over will
                          // expose trim
                          float minX = getWidth() * marginFit + mTranslateRect.right *
                                  relativeDistance;
                          float minY = getHeight() * marginFit + mTranslateRect.bottom *
                                  relativeDistance;
                          // Adjust center according to bounds to avoid bad crop
                          if (minX > maxX) {
                              // Border is inevitable due to small image size, so we split the crop
                              finalCenterX = (minX + maxX) / 2;
                          } else {
                              finalCenterX = Math.min(Math.max(minX, finalCenterX), maxX);
                          }
                          if (minY > maxY) {
                              // Border is inevitable due to small image size, so we split the crop
                              finalCenterY = (minY + maxY) / 2;
                          } else {
                              finalCenterY = Math.min(Math.max(minY, finalCenterY), maxY);
                          }
                          mScaleRunnable.start(currentScale, mMaxScale, finalCenterX, finalCenterY);
                      }
                  }
              };
              postDelayed(zoomBackRunnable, ZOOM_CORRECTION_DELAY);
          }
  
          float factor = newScale / currentScale;
  
          // Apply the scale factor
          mMatrix.postScale(factor, factor, centerX, centerY);
  
          // Re-apply any rotation
          mMatrix.postRotate(mRotation, getWidth() / 2, getHeight() / 2);
  
          invalidate();
      }
  
      /**
       * Translates the image.
       *
       * This method will not allow the image to be translated outside of the visible area.
       *
       * @param tx how many pixels to translate horizontally
       * @param ty how many pixels to translate vertically
       * @return result of the translation, represented as either {@link TRANSLATE_NONE},
       * {@link TRANSLATE_X_ONLY}, {@link TRANSLATE_Y_ONLY}, or {@link TRANSLATE_BOTH}
       */
      private int translate(float tx, float ty) {
          mTranslateRect.set(mTempSrc);
          mMatrix.mapRect(mTranslateRect);
  
          final float maxLeft = mAllowCrop ? mCropRect.left : 0.0f;
          final float maxRight = mAllowCrop ? mCropRect.right : getWidth();
          float l = mTranslateRect.left;
          float r = mTranslateRect.right;
  
          final float translateX;
          if (mAllowCrop) {
              // If we're cropping, allow the image to scroll off the edge of the screen
              translateX = Math.max(maxLeft - mTranslateRect.right,
                      Math.min(maxRight - mTranslateRect.left, tx));
          } else {
              // Otherwise, ensure the image never leaves the screen
              if (r - l < maxRight - maxLeft) {
                  translateX = maxLeft + ((maxRight - maxLeft) - (r + l)) / 2;
              } else {
                  translateX = Math.max(maxRight - r, Math.min(maxLeft - l, tx));
              }
          }
  
          float maxTop = mAllowCrop ? mCropRect.top: 0.0f;
          float maxBottom = mAllowCrop ? mCropRect.bottom : getHeight();
          float t = mTranslateRect.top;
          float b = mTranslateRect.bottom;
  
          final float translateY;
          if (mAllowCrop) {
              // If we're cropping, allow the image to scroll off the edge of the screen
              translateY = Math.max(maxTop - mTranslateRect.bottom,
                      Math.min(maxBottom - mTranslateRect.top, ty));
          } else {
              // Otherwise, ensure the image never leaves the screen
              if (b - t < maxBottom - maxTop) {
                  translateY = maxTop + ((maxBottom - maxTop) - (b + t)) / 2;
              } else {
                  translateY = Math.max(maxBottom - b, Math.min(maxTop - t, ty));
              }
          }
  
          // Do the translation
          mMatrix.postTranslate(translateX, translateY);
          invalidate();
  
          boolean didTranslateX = translateX == tx;
          boolean didTranslateY = translateY == ty;
          if (didTranslateX && didTranslateY) {
              return TRANSLATE_BOTH;
          } else if (didTranslateX) {
              return TRANSLATE_X_ONLY;
          } else if (didTranslateY) {
              return TRANSLATE_Y_ONLY;
          }
          return TRANSLATE_NONE;
      }
  
      /**
       * Snaps the image so it touches all edges of the view.
       */
      private void snap() {
          mTranslateRect.set(mTempSrc);
          mMatrix.mapRect(mTranslateRect);
  
          // Determine how much to snap in the horizontal direction [if any]
          float maxLeft = mAllowCrop ? mCropRect.left : 0.0f;
          float maxRight = mAllowCrop ? mCropRect.right : getWidth();
          float l = mTranslateRect.left;
          float r = mTranslateRect.right;
  
          final float translateX;
          if (r - l < maxRight - maxLeft) {
              // Image is narrower than view; translate to the center of the view
              translateX = maxLeft + ((maxRight - maxLeft) - (r + l)) / 2;
          } else if (l > maxLeft) {
              // Image is off right-edge of screen; bring it into view
              translateX = maxLeft - l;
          } else if (r < maxRight) {
              // Image is off left-edge of screen; bring it into view
              translateX = maxRight - r;
          } else {
              translateX = 0.0f;
          }
  
          // Determine how much to snap in the vertical direction [if any]
          float maxTop = mAllowCrop ? mCropRect.top : 0.0f;
          float maxBottom = mAllowCrop ? mCropRect.bottom : getHeight();
          float t = mTranslateRect.top;
          float b = mTranslateRect.bottom;
  
          final float translateY;
          if (b - t < maxBottom - maxTop) {
              // Image is shorter than view; translate to the bottom edge of the view
              translateY = maxTop + ((maxBottom - maxTop) - (b + t)) / 2;
          } else if (t > maxTop) {
              // Image is off bottom-edge of screen; bring it into view
              translateY = maxTop - t;
          } else if (b < maxBottom) {
              // Image is off top-edge of screen; bring it into view
              translateY = maxBottom - b;
          } else {
              translateY = 0.0f;
          }
  
          if (Math.abs(translateX) > SNAP_THRESHOLD || Math.abs(translateY) > SNAP_THRESHOLD) {
              mSnapRunnable.start(translateX, translateY);
          } else {
              mMatrix.postTranslate(translateX, translateY);
              invalidate();
          }
      }
  
      /**
       * Rotates the image, either instantly or gradually
       *
       * @param degrees how many degrees to rotate the image, positive rotates clockwise
       * @param animate if {@code true}, animate during the rotation. Otherwise, snap rotate.
       */
      private void rotate(float degrees, boolean animate) {
          if (animate) {
              mRotateRunnable.start(degrees);
          } else {
              mRotation += degrees;
              mMatrix.postRotate(degrees, getWidth() / 2, getHeight() / 2);
              invalidate();
          }
      }
  
      /**
       * Initializes the header and any static values
       */
      private void initialize() {
          Context context = getContext();
  
          if (!sInitialized) {
              sInitialized = true;
  
              Resources resources = context.getApplicationContext().getResources();
  
              sCropSize = resources.getDimensionPixelSize(R.dimen.photo_crop_width);
  
              sCropDimPaint = new Paint();
              sCropDimPaint.setAntiAlias(true);
              sCropDimPaint.setColor(resources.getColor(R.color.photo_crop_dim_color));
              sCropDimPaint.setStyle(Style.FILL);
  
              sCropPaint = new Paint();
              sCropPaint.setAntiAlias(true);
              sCropPaint.setColor(resources.getColor(R.color.photo_crop_highlight_color));
              sCropPaint.setStyle(Style.STROKE);
              sCropPaint.setStrokeWidth(resources.getDimension(R.dimen.photo_crop_stroke_width));
  
              final ViewConfiguration configuration = ViewConfiguration.get(context);
              final int touchSlop = configuration.getScaledTouchSlop();
              sTouchSlopSquare = touchSlop * touchSlop;
          }
  
          mGestureDetector = new GestureDetectorCompat(context, this, null);
          mScaleGetureDetector = new ScaleGestureDetector(context, this);
          mQuickScaleEnabled = ScaleGestureDetectorCompat.isQuickScaleEnabled(mScaleGetureDetector);
          mScaleRunnable = new ScaleRunnable(this);
          mTranslateRunnable = new TranslateRunnable(this);
          mSnapRunnable = new SnapRunnable(this);
          mRotateRunnable = new RotateRunnable(this);
      }
  
      /**
       * Runnable that animates an image scale operation.
       */
      private static class ScaleRunnable implements Runnable {
  
          private final PhotoView mHeader;
  
          private float mCenterX;
          private float mCenterY;
  
          private boolean mZoomingIn;
  
          private float mTargetScale;
          private float mStartScale;
          private float mVelocity;
          private long mStartTime;
  
          private boolean mRunning;
          private boolean mStop;
  
          public ScaleRunnable(PhotoView header) {
              mHeader = header;
          }
  
          /**
           * Starts the animation. There is no target scale bounds check.
           */
          public boolean start(float startScale, float targetScale, float centerX, float centerY) {
              if (mRunning) {
                  return false;
              }
  
              mCenterX = centerX;
              mCenterY = centerY;
  
              // Ensure the target scale is within the min/max bounds
              mTargetScale = targetScale;
              mStartTime = System.currentTimeMillis();
              mStartScale = startScale;
              mZoomingIn = mTargetScale > mStartScale;
              mVelocity = (mTargetScale - mStartScale) / ZOOM_ANIMATION_DURATION;
              mRunning = true;
              mStop = false;
              mHeader.post(this);
              return true;
          }
  
          /**
           * Stops the animation in place. It does not snap the image to its final zoom.
           */
          public void stop() {
              mRunning = false;
              mStop = true;
          }
  
          @Override
          public void run() {
              if (mStop) {
                  return;
              }
  
              // Scale
              long now = System.currentTimeMillis();
              long ellapsed = now - mStartTime;
              float newScale = (mStartScale + mVelocity * ellapsed);
              mHeader.scale(newScale, mCenterX, mCenterY);
  
              // Stop when done
              if (newScale == mTargetScale || (mZoomingIn == (newScale > mTargetScale))) {
                  mHeader.scale(mTargetScale, mCenterX, mCenterY);
                  stop();
              }
  
              if (!mStop) {
                  mHeader.post(this);
              }
          }
      }
  
      /**
       * Runnable that animates an image translation operation.
       */
      private static class TranslateRunnable implements Runnable {
  
          private static final float DECELERATION_RATE = 20000f;
          private static final long NEVER = -1L;
  
          private final PhotoView mHeader;
  
          private float mVelocityX;
          private float mVelocityY;
  
          private float mDecelerationX;
          private float mDecelerationY;
  
          private long mLastRunTime;
          private boolean mRunning;
          private boolean mStop;
  
          public TranslateRunnable(PhotoView header) {
              mLastRunTime = NEVER;
              mHeader = header;
          }
  
          /**
           * Starts the animation.
           */
          public boolean start(float velocityX, float velocityY) {
              if (mRunning) {
                  return false;
              }
              mLastRunTime = NEVER;
              mVelocityX = velocityX;
              mVelocityY = velocityY;
  
              float angle = (float) Math.atan2(mVelocityY, mVelocityX);
              mDecelerationX = (float) (DECELERATION_RATE * Math.cos(angle));
              mDecelerationY = (float) (DECELERATION_RATE * Math.sin(angle));
  
              mStop = false;
              mRunning = true;
              mHeader.post(this);
              return true;
          }
  
          /**
           * Stops the animation in place. It does not snap the image to its final translation.
           */
          public void stop() {
              mRunning = false;
              mStop = true;
          }
  
          @Override
          public void run() {
              // See if we were told to stop:
              if (mStop) {
                  return;
              }
  
              // Translate according to current velocities and time delta:
              long now = System.currentTimeMillis();
              float delta = (mLastRunTime != NEVER) ? (now - mLastRunTime) / 1000f : 0f;
              final int translateResult = mHeader.translate(mVelocityX * delta, mVelocityY * delta);
              mLastRunTime = now;
              // Slow down:
              float slowDownX = mDecelerationX * delta;
              if (Math.abs(mVelocityX) > Math.abs(slowDownX)) {
                  mVelocityX -= slowDownX;
              } else {
                  mVelocityX = 0f;
              }
              float slowDownY = mDecelerationY * delta;
              if (Math.abs(mVelocityY) > Math.abs(slowDownY)) {
                  mVelocityY -= slowDownY;
              } else {
                  mVelocityY = 0f;
              }
  
              // Stop when done
              if ((mVelocityX == 0f && mVelocityY == 0f)
                      || translateResult == TRANSLATE_NONE) {
                  stop();
                  mHeader.snap();
              } else if (translateResult == TRANSLATE_X_ONLY) {
                  mDecelerationX = (mVelocityX > 0) ? DECELERATION_RATE : -DECELERATION_RATE;
                  mDecelerationY = 0;
                  mVelocityY = 0f;
              } else if (translateResult == TRANSLATE_Y_ONLY) {
                  mDecelerationX = 0;
                  mDecelerationY = (mVelocityY > 0) ? DECELERATION_RATE : -DECELERATION_RATE;
                  mVelocityX = 0f;
              }
  
              // See if we need to continue flinging:
              if (mStop) {
                  return;
              }
              mHeader.post(this);
          }
      }
  
      /**
       * Runnable that animates an image translation operation.
       */
      private static class SnapRunnable implements Runnable {
  
          private static final long NEVER = -1L;
  
          private final PhotoView mHeader;
  
          private float mTranslateX;
          private float mTranslateY;
  
          private long mStartRunTime;
          private boolean mRunning;
          private boolean mStop;
  
          public SnapRunnable(PhotoView header) {
              mStartRunTime = NEVER;
              mHeader = header;
          }
  
          /**
           * Starts the animation.
           */
          public boolean start(float translateX, float translateY) {
              if (mRunning) {
                  return false;
              }
              mStartRunTime = NEVER;
              mTranslateX = translateX;
              mTranslateY = translateY;
              mStop = false;
              mRunning = true;
              mHeader.postDelayed(this, SNAP_DELAY);
              return true;
          }
  
          /**
           * Stops the animation in place. It does not snap the image to its final translation.
           */
          public void stop() {
              mRunning = false;
              mStop = true;
          }
  
          @Override
          public void run() {
              // See if we were told to stop:
              if (mStop) {
                  return;
              }
  
              // Translate according to current velocities and time delta:
              long now = System.currentTimeMillis();
              float delta = (mStartRunTime != NEVER) ? (now - mStartRunTime) : 0f;
  
              if (mStartRunTime == NEVER) {
                  mStartRunTime = now;
              }
  
              float transX;
              float transY;
              if (delta >= SNAP_DURATION) {
                  transX = mTranslateX;
                  transY = mTranslateY;
              } else {
                  transX = (mTranslateX / (SNAP_DURATION - delta)) * 10f;
                  transY = (mTranslateY / (SNAP_DURATION - delta)) * 10f;
                  if (Math.abs(transX) > Math.abs(mTranslateX) || Float.isNaN(transX)) {
                      transX = mTranslateX;
                  }
                  if (Math.abs(transY) > Math.abs(mTranslateY) || Float.isNaN(transY)) {
                      transY = mTranslateY;
                  }
              }
  
              mHeader.translate(transX, transY);
              mTranslateX -= transX;
              mTranslateY -= transY;
  
              if (mTranslateX == 0 && mTranslateY == 0) {
                  stop();
              }
  
              // See if we need to continue flinging:
              if (mStop) {
                  return;
              }
              mHeader.post(this);
          }
      }
  
      /**
       * Runnable that animates an image rotation operation.
       */
      private static class RotateRunnable implements Runnable {
  
          private static final long NEVER = -1L;
  
          private final PhotoView mHeader;
  
          private float mTargetRotation;
          private float mAppliedRotation;
          private float mVelocity;
          private long mLastRuntime;
  
          private boolean mRunning;
          private boolean mStop;
  
          public RotateRunnable(PhotoView header) {
              mHeader = header;
          }
  
          /**
           * Starts the animation.
           */
          public void start(float rotation) {
              if (mRunning) {
                  return;
              }
  
              mTargetRotation = rotation;
              mVelocity = mTargetRotation / ROTATE_ANIMATION_DURATION;
              mAppliedRotation = 0f;
              mLastRuntime = NEVER;
              mStop = false;
              mRunning = true;
              mHeader.post(this);
          }
  
          /**
           * Stops the animation in place. It does not snap the image to its final rotation.
           */
          public void stop() {
              mRunning = false;
              mStop = true;
          }
  
          @Override
          public void run() {
              if (mStop) {
                  return;
              }
  
              if (mAppliedRotation != mTargetRotation) {
                  long now = System.currentTimeMillis();
                  long delta = mLastRuntime != NEVER ? now - mLastRuntime : 0L;
                  float rotationAmount = mVelocity * delta;
                  if (mAppliedRotation < mTargetRotation
                          && mAppliedRotation + rotationAmount > mTargetRotation
                          || mAppliedRotation > mTargetRotation
                          && mAppliedRotation + rotationAmount < mTargetRotation) {
                      rotationAmount = mTargetRotation - mAppliedRotation;
                  }
                  mHeader.rotate(rotationAmount, false);
                  mAppliedRotation += rotationAmount;
                  if (mAppliedRotation == mTargetRotation) {
                      stop();
                  }
                  mLastRuntime = now;
              }
  
              if (mStop) {
                  return;
              }
              mHeader.post(this);
          }
      }
  
      public void setMaxInitialScale(float f) {
          mMaxInitialScaleFactor = f;
      }
  }
  