Home | History | Annotate | Download | only in views
      1 /*
      2  * Copyright (C) 2011 Google Inc.
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.ex.photo.views;
     19 
     20 import android.content.Context;
     21 import android.content.res.Resources;
     22 import android.graphics.Bitmap;
     23 import android.graphics.Canvas;
     24 import android.graphics.Matrix;
     25 import android.graphics.Paint;
     26 import android.graphics.Paint.Style;
     27 import android.graphics.Rect;
     28 import android.graphics.RectF;
     29 import android.graphics.drawable.BitmapDrawable;
     30 import android.graphics.drawable.Drawable;
     31 import android.support.v4.view.GestureDetectorCompat;
     32 import android.support.v4.view.ScaleGestureDetectorCompat;
     33 import android.util.AttributeSet;
     34 import android.view.GestureDetector.OnDoubleTapListener;
     35 import android.view.GestureDetector.OnGestureListener;
     36 import android.view.MotionEvent;
     37 import android.view.ScaleGestureDetector;
     38 import android.view.View;
     39 import android.view.ViewConfiguration;
     40 
     41 import com.android.ex.photo.R;
     42 import com.android.ex.photo.fragments.PhotoViewFragment.HorizontallyScrollable;
     43 
     44 /**
     45  * Layout for the photo list view header.
     46  */
     47 public class PhotoView extends View implements OnGestureListener,
     48         OnDoubleTapListener, ScaleGestureDetector.OnScaleGestureListener,
     49         HorizontallyScrollable {
     50 
     51     public static final int TRANSLATE_NONE = 0;
     52     public static final int TRANSLATE_X_ONLY = 1;
     53     public static final int TRANSLATE_Y_ONLY = 2;
     54     public static final int TRANSLATE_BOTH = 3;
     55 
     56     /** Zoom animation duration; in milliseconds */
     57     private final static long ZOOM_ANIMATION_DURATION = 200L;
     58     /** Rotate animation duration; in milliseconds */
     59     private final static long ROTATE_ANIMATION_DURATION = 500L;
     60     /** Snap animation duration; in milliseconds */
     61     private static final long SNAP_DURATION = 100L;
     62     /** Amount of time to wait before starting snap animation; in milliseconds */
     63     private static final long SNAP_DELAY = 250L;
     64     /** By how much to scale the image when double click occurs */
     65     private final static float DOUBLE_TAP_SCALE_FACTOR = 2.0f;
     66     /** Amount which can be zoomed in past the maximum scale, and then scaled back */
     67     private final static float SCALE_OVERZOOM_FACTOR = 1.5f;
     68     /** Amount of translation needed before starting a snap animation */
     69     private final static float SNAP_THRESHOLD = 20.0f;
     70     /** The width & height of the bitmap returned by {@link #getCroppedPhoto()} */
     71     private final static float CROPPED_SIZE = 256.0f;
     72 
     73     /**
     74      * Touch slop used to determine if this double tap is valid for starting a scale or should be
     75      * ignored.
     76      */
     77     private static int sTouchSlopSquare;
     78 
     79     /** If {@code true}, the static values have been initialized */
     80     private static boolean sInitialized;
     81 
     82     // Various dimensions
     83     /** Width & height of the crop region */
     84     private static int sCropSize;
     85 
     86     // Bitmaps
     87     /** Video icon */
     88     private static Bitmap sVideoImage;
     89     /** Video icon */
     90     private static Bitmap sVideoNotReadyImage;
     91 
     92     // Paints
     93     /** Paint to partially dim the photo during crop */
     94     private static Paint sCropDimPaint;
     95     /** Paint to highlight the cropped portion of the photo */
     96     private static Paint sCropPaint;
     97 
     98     /** The photo to display */
     99     private Drawable mDrawable;
    100     /** The matrix used for drawing; this may be {@code null} */
    101     private Matrix mDrawMatrix;
    102     /** A matrix to apply the scaling of the photo */
    103     private Matrix mMatrix = new Matrix();
    104     /** The original matrix for this image; used to reset any transformations applied by the user */
    105     private Matrix mOriginalMatrix = new Matrix();
    106 
    107     /** The fixed height of this view. If {@code -1}, calculate the height */
    108     private int mFixedHeight = -1;
    109     /** When {@code true}, the view has been laid out */
    110     private boolean mHaveLayout;
    111     /** Whether or not the photo is full-screen */
    112     private boolean mFullScreen;
    113     /** Whether or not this is a still image of a video */
    114     private byte[] mVideoBlob;
    115     /** Whether or not this is a still image of a video */
    116     private boolean mVideoReady;
    117 
    118     /** Whether or not crop is allowed */
    119     private boolean mAllowCrop;
    120     /** The crop region */
    121     private Rect mCropRect = new Rect();
    122     /** Actual crop size; may differ from {@link #sCropSize} if the screen is smaller */
    123     private int mCropSize;
    124     /** The maximum amount of scaling to apply to images */
    125     private float mMaxInitialScaleFactor;
    126 
    127     /** Gesture detector */
    128     private GestureDetectorCompat mGestureDetector;
    129     /** Gesture detector that detects pinch gestures */
    130     private ScaleGestureDetector mScaleGetureDetector;
    131     /** An external click listener */
    132     private OnClickListener mExternalClickListener;
    133     /** When {@code true}, allows gestures to scale / pan the image */
    134     private boolean mTransformsEnabled;
    135 
    136     // To support zooming
    137     /** When {@code true}, a double tap scales the image by {@link #DOUBLE_TAP_SCALE_FACTOR} */
    138     private boolean mDoubleTapToZoomEnabled = true;
    139     /** When {@code true}, prevents scale end gesture from falsely triggering a double click. */
    140     private boolean mDoubleTapDebounce;
    141     /** When {@code false}, event is a scale gesture. Otherwise, event is a double touch. */
    142     private boolean mIsDoubleTouch;
    143     /** Runnable that scales the image */
    144     private ScaleRunnable mScaleRunnable;
    145     /** Minimum scale the image can have. */
    146     private float mMinScale;
    147     /** Maximum scale to limit scaling to, 0 means no limit. */
    148     private float mMaxScale;
    149 
    150     // To support translation [i.e. panning]
    151     /** Runnable that can move the image */
    152     private TranslateRunnable mTranslateRunnable;
    153     private SnapRunnable mSnapRunnable;
    154 
    155     // To support rotation
    156     /** The rotate runnable used to animate rotations of the image */
    157     private RotateRunnable mRotateRunnable;
    158     /** The current rotation amount, in degrees */
    159     private float mRotation;
    160 
    161     // Convenience fields
    162     // These are declared here not because they are important properties of the view. Rather, we
    163     // declare them here to avoid object allocation during critical graphics operations; such as
    164     // layout or drawing.
    165     /** Source (i.e. the photo size) bounds */
    166     private RectF mTempSrc = new RectF();
    167     /** Destination (i.e. the display) bounds. The image is scaled to this size. */
    168     private RectF mTempDst = new RectF();
    169     /** Rectangle to handle translations */
    170     private RectF mTranslateRect = new RectF();
    171     /** Array to store a copy of the matrix values */
    172     private float[] mValues = new float[9];
    173 
    174     /**
    175      * Track whether a double tap event occurred.
    176      */
    177     private boolean mDoubleTapOccurred;
    178 
    179     /**
    180      * X and Y coordinates for the current down event. Since mDoubleTapOccurred only contains the
    181      * information that there was a double tap event, use these to get the secondary tap
    182      * information to determine if a user has moved beyond touch slop.
    183      */
    184     private float mDownFocusX;
    185     private float mDownFocusY;
    186 
    187     /**
    188      * Whether the QuickSale gesture is enabled.
    189      */
    190     private boolean mQuickScaleEnabled;
    191 
    192     public PhotoView(Context context) {
    193         super(context);
    194         initialize();
    195     }
    196 
    197     public PhotoView(Context context, AttributeSet attrs) {
    198         super(context, attrs);
    199         initialize();
    200     }
    201 
    202     public PhotoView(Context context, AttributeSet attrs, int defStyle) {
    203         super(context, attrs, defStyle);
    204         initialize();
    205     }
    206 
    207     @Override
    208     public boolean onTouchEvent(MotionEvent event) {
    209         if (mScaleGetureDetector == null || mGestureDetector == null) {
    210             // We're being destroyed; ignore any touch events
    211             return true;
    212         }
    213 
    214         mScaleGetureDetector.onTouchEvent(event);
    215         mGestureDetector.onTouchEvent(event);
    216         final int action = event.getAction();
    217 
    218         switch (action) {
    219             case MotionEvent.ACTION_UP:
    220             case MotionEvent.ACTION_CANCEL:
    221                 if (!mTranslateRunnable.mRunning) {
    222                     snap();
    223                 }
    224                 break;
    225         }
    226 
    227         return true;
    228     }
    229 
    230     @Override
    231     public boolean onDoubleTap(MotionEvent e) {
    232         mDoubleTapOccurred = true;
    233         if (!mQuickScaleEnabled) {
    234             return scale(e);
    235         }
    236         return false;
    237     }
    238 
    239     @Override
    240     public boolean onDoubleTapEvent(MotionEvent e) {
    241         final int action = e.getAction();
    242         boolean handled = false;
    243 
    244         switch (action) {
    245             case MotionEvent.ACTION_DOWN:
    246                 if (mQuickScaleEnabled) {
    247                     mDownFocusX = e.getX();
    248                     mDownFocusY = e.getY();
    249                 }
    250                 break;
    251             case MotionEvent.ACTION_UP:
    252                 if (mQuickScaleEnabled) {
    253                     handled = scale(e);
    254                 }
    255                 break;
    256             case MotionEvent.ACTION_MOVE:
    257                 if (mQuickScaleEnabled && mDoubleTapOccurred) {
    258                     final int deltaX = (int) (e.getX() - mDownFocusX);
    259                     final int deltaY = (int) (e.getY() - mDownFocusY);
    260                     int distance = (deltaX * deltaX) + (deltaY * deltaY);
    261                     if (distance > sTouchSlopSquare) {
    262                         mDoubleTapOccurred = false;
    263                     }
    264                 }
    265                 break;
    266 
    267         }
    268         return handled;
    269     }
    270 
    271     private boolean scale(MotionEvent e) {
    272         boolean handled = false;
    273         if (mDoubleTapToZoomEnabled && mTransformsEnabled && mDoubleTapOccurred) {
    274             if (!mDoubleTapDebounce) {
    275                 float currentScale = getScale();
    276                 float targetScale;
    277                 float centerX, centerY;
    278 
    279                 // Zoom out if not default scale, otherwise zoom in
    280                 if (currentScale > mMinScale) {
    281                     targetScale = mMinScale;
    282                     float relativeScale = targetScale / currentScale;
    283                     // Find the apparent origin for scaling that equals this scale and translate
    284                     centerX = (getWidth() / 2 - relativeScale * mTranslateRect.centerX()) /
    285                             (1 - relativeScale);
    286                     centerY = (getHeight() / 2 - relativeScale * mTranslateRect.centerY()) /
    287                             (1 - relativeScale);
    288                 } else {
    289                      targetScale = currentScale * DOUBLE_TAP_SCALE_FACTOR;
    290                      // Ensure the target scale is within our bounds
    291                      targetScale = Math.max(mMinScale, targetScale);
    292                      targetScale = Math.min(mMaxScale, targetScale);
    293                      float relativeScale = targetScale / currentScale;
    294                      float widthBuffer = (getWidth() - mTranslateRect.width()) / relativeScale;
    295                      float heightBuffer = (getHeight() - mTranslateRect.height()) / relativeScale;
    296                      // Clamp the center if it would result in uneven borders
    297                      if (mTranslateRect.width() <= widthBuffer * 2) {
    298                          centerX = mTranslateRect.centerX();
    299                      } else {
    300                          centerX = Math.min(Math.max(mTranslateRect.left + widthBuffer,
    301                                  e.getX()), mTranslateRect.right - widthBuffer);
    302                      }
    303                      if (mTranslateRect.height() <= heightBuffer * 2) {
    304                          centerY = mTranslateRect.centerY();
    305                      } else {
    306                          centerY = Math.min(Math.max(mTranslateRect.top + heightBuffer,
    307                                  e.getY()), mTranslateRect.bottom - heightBuffer);
    308                      }
    309                 }
    310 
    311                 mScaleRunnable.start(currentScale, targetScale, centerX, centerY);
    312                 handled = true;
    313             }
    314             mDoubleTapDebounce = false;
    315         }
    316         mDoubleTapOccurred = false;
    317         return handled;
    318     }
    319 
    320     @Override
    321     public boolean onSingleTapConfirmed(MotionEvent e) {
    322         if (mExternalClickListener != null && !mIsDoubleTouch) {
    323             mExternalClickListener.onClick(this);
    324         }
    325         mIsDoubleTouch = false;
    326         return true;
    327     }
    328 
    329     @Override
    330     public boolean onSingleTapUp(MotionEvent e) {
    331         return false;
    332     }
    333 
    334     @Override
    335     public void onLongPress(MotionEvent e) {
    336     }
    337 
    338     @Override
    339     public void onShowPress(MotionEvent e) {
    340     }
    341 
    342     @Override
    343     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    344         if (mTransformsEnabled && !mScaleRunnable.mRunning) {
    345             translate(-distanceX, -distanceY);
    346         }
    347         return true;
    348     }
    349 
    350     @Override
    351     public boolean onDown(MotionEvent e) {
    352         if (mTransformsEnabled) {
    353             mTranslateRunnable.stop();
    354             mSnapRunnable.stop();
    355         }
    356         return true;
    357     }
    358 
    359     @Override
    360     public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    361         if (mTransformsEnabled && !mScaleRunnable.mRunning) {
    362             mTranslateRunnable.start(velocityX, velocityY);
    363         }
    364         return true;
    365     }
    366 
    367     @Override
    368     public boolean onScale(ScaleGestureDetector detector) {
    369         if (mTransformsEnabled) {
    370             mIsDoubleTouch = false;
    371             float currentScale = getScale();
    372             float newScale = currentScale * detector.getScaleFactor();
    373             scale(newScale, detector.getFocusX(), detector.getFocusY());
    374         }
    375         return true;
    376     }
    377 
    378     @Override
    379     public boolean onScaleBegin(ScaleGestureDetector detector) {
    380         if (mTransformsEnabled) {
    381             mScaleRunnable.stop();
    382             mIsDoubleTouch = true;
    383         }
    384         return true;
    385     }
    386 
    387     @Override
    388     public void onScaleEnd(ScaleGestureDetector detector) {
    389         // Scale back to the maximum if over-zoomed
    390         float currentScale = getScale();
    391         if (currentScale > mMaxScale) {
    392             // The number of times the crop amount pulled in can fit on the screen
    393             float marginFit = 1 / (1 - mMaxScale / currentScale);
    394             // The (negative) relative maximum distance from an image edge such that when scaled
    395             // this far from the edge, all of the image off-screen in that direction is pulled in
    396             float relativeDistance = 1 - marginFit;
    397             float centerX = getWidth() / 2;
    398             float centerY = getHeight() / 2;
    399             // This center will pull all of the margin from the lesser side, over will expose trim
    400             float maxX = mTranslateRect.left * relativeDistance;
    401             float maxY = mTranslateRect.top * relativeDistance;
    402             // This center will pull all of the margin from the greater side, over will expose trim
    403             float minX = getWidth() * marginFit + mTranslateRect.right * relativeDistance;
    404             float minY = getHeight() * marginFit + mTranslateRect.bottom * relativeDistance;
    405             // Adjust center according to bounds to avoid bad crop
    406             if (minX > maxX) {
    407                 // Border is inevitable due to small image size, so we split the crop difference
    408                 centerX = (minX + maxX) / 2;
    409             } else {
    410                 centerX = Math.min(Math.max(minX, centerX), maxX);
    411             }
    412             if (minY > maxY) {
    413                 // Border is inevitable due to small image size, so we split the crop difference
    414                 centerY = (minY + maxY) / 2;
    415             } else {
    416                 centerY = Math.min(Math.max(minY, centerY), maxY);
    417             }
    418             mScaleRunnable.start(currentScale, mMaxScale, centerX, centerY);
    419         }
    420         if (mTransformsEnabled && mIsDoubleTouch) {
    421             mDoubleTapDebounce = true;
    422             resetTransformations();
    423         }
    424     }
    425 
    426     @Override
    427     public void setOnClickListener(OnClickListener listener) {
    428         mExternalClickListener = listener;
    429     }
    430 
    431     @Override
    432     public boolean interceptMoveLeft(float origX, float origY) {
    433         if (!mTransformsEnabled) {
    434             // Allow intercept if we're not in transform mode
    435             return false;
    436         } else if (mTranslateRunnable.mRunning) {
    437             // Don't allow touch intercept until we've stopped flinging
    438             return true;
    439         } else {
    440             mMatrix.getValues(mValues);
    441             mTranslateRect.set(mTempSrc);
    442             mMatrix.mapRect(mTranslateRect);
    443 
    444             final float viewWidth = getWidth();
    445             final float transX = mValues[Matrix.MTRANS_X];
    446             final float drawWidth = mTranslateRect.right - mTranslateRect.left;
    447 
    448             if (!mTransformsEnabled || drawWidth <= viewWidth) {
    449                 // Allow intercept if not in transform mode or the image is smaller than the view
    450                 return false;
    451             } else if (transX == 0) {
    452                 // We're at the left-side of the image; allow intercepting movements to the right
    453                 return false;
    454             } else if (viewWidth >= drawWidth + transX) {
    455                 // We're at the right-side of the image; allow intercepting movements to the left
    456                 return true;
    457             } else {
    458                 // We're in the middle of the image; don't allow touch intercept
    459                 return true;
    460             }
    461         }
    462     }
    463 
    464     @Override
    465     public boolean interceptMoveRight(float origX, float origY) {
    466         if (!mTransformsEnabled) {
    467             // Allow intercept if we're not in transform mode
    468             return false;
    469         } else if (mTranslateRunnable.mRunning) {
    470             // Don't allow touch intercept until we've stopped flinging
    471             return true;
    472         } else {
    473             mMatrix.getValues(mValues);
    474             mTranslateRect.set(mTempSrc);
    475             mMatrix.mapRect(mTranslateRect);
    476 
    477             final float viewWidth = getWidth();
    478             final float transX = mValues[Matrix.MTRANS_X];
    479             final float drawWidth = mTranslateRect.right - mTranslateRect.left;
    480 
    481             if (!mTransformsEnabled || drawWidth <= viewWidth) {
    482                 // Allow intercept if not in transform mode or the image is smaller than the view
    483                 return false;
    484             } else if (transX == 0) {
    485                 // We're at the left-side of the image; allow intercepting movements to the right
    486                 return true;
    487             } else if (viewWidth >= drawWidth + transX) {
    488                 // We're at the right-side of the image; allow intercepting movements to the left
    489                 return false;
    490             } else {
    491                 // We're in the middle of the image; don't allow touch intercept
    492                 return true;
    493             }
    494         }
    495     }
    496 
    497     /**
    498      * Free all resources held by this view.
    499      * The view is on its way to be collected and will not be reused.
    500      */
    501     public void clear() {
    502         mGestureDetector = null;
    503         mScaleGetureDetector = null;
    504         mDrawable = null;
    505         mScaleRunnable.stop();
    506         mScaleRunnable = null;
    507         mTranslateRunnable.stop();
    508         mTranslateRunnable = null;
    509         mSnapRunnable.stop();
    510         mSnapRunnable = null;
    511         mRotateRunnable.stop();
    512         mRotateRunnable = null;
    513         setOnClickListener(null);
    514         mExternalClickListener = null;
    515         mDoubleTapOccurred = false;
    516     }
    517 
    518     public void bindDrawable(Drawable drawable) {
    519         boolean changed = false;
    520         if (drawable != null && drawable != mDrawable) {
    521             // Clear previous state.
    522             if (mDrawable != null) {
    523                 mDrawable.setCallback(null);
    524             }
    525 
    526             mDrawable = drawable;
    527 
    528             // Reset mMinScale to ensure the bounds / matrix are recalculated
    529             mMinScale = 0f;
    530 
    531             // Set a callback?
    532             mDrawable.setCallback(this);
    533 
    534             changed = true;
    535         }
    536 
    537         configureBounds(changed);
    538         invalidate();
    539     }
    540 
    541     /**
    542      * Binds a bitmap to the view.
    543      *
    544      * @param photoBitmap the bitmap to bind.
    545      */
    546     public void bindPhoto(Bitmap photoBitmap) {
    547         boolean currentDrawableIsBitmapDrawable = mDrawable instanceof BitmapDrawable;
    548         boolean changed = !(currentDrawableIsBitmapDrawable);
    549         if (mDrawable != null && currentDrawableIsBitmapDrawable) {
    550             final Bitmap drawableBitmap = ((BitmapDrawable) mDrawable).getBitmap();
    551             if (photoBitmap == drawableBitmap) {
    552                 // setting the same bitmap; do nothing
    553                 return;
    554             }
    555 
    556             changed = photoBitmap != null &&
    557                     (mDrawable.getIntrinsicWidth() != photoBitmap.getWidth() ||
    558                     mDrawable.getIntrinsicHeight() != photoBitmap.getHeight());
    559 
    560             // Reset mMinScale to ensure the bounds / matrix are recalculated
    561             mMinScale = 0f;
    562             mDrawable = null;
    563         }
    564 
    565         if (mDrawable == null && photoBitmap != null) {
    566             mDrawable = new BitmapDrawable(getResources(), photoBitmap);
    567         }
    568 
    569         configureBounds(changed);
    570         invalidate();
    571     }
    572 
    573     /**
    574      * Returns the bound photo data if set. Otherwise, {@code null}.
    575      */
    576     public Bitmap getPhoto() {
    577         if (mDrawable != null && mDrawable instanceof BitmapDrawable) {
    578             return ((BitmapDrawable) mDrawable).getBitmap();
    579         }
    580         return null;
    581     }
    582 
    583     /**
    584      * Returns the bound drawable. May be {@code null} if no drawable is bound.
    585      */
    586     public Drawable getDrawable() {
    587         return mDrawable;
    588     }
    589 
    590     /**
    591      * Gets video data associated with this item. Returns {@code null} if this is not a video.
    592      */
    593     public byte[] getVideoData() {
    594         return mVideoBlob;
    595     }
    596 
    597     /**
    598      * Returns {@code true} if the photo represents a video. Otherwise, {@code false}.
    599      */
    600     public boolean isVideo() {
    601         return mVideoBlob != null;
    602     }
    603 
    604     /**
    605      * Returns {@code true} if the video is ready to play. Otherwise, {@code false}.
    606      */
    607     public boolean isVideoReady() {
    608         return mVideoBlob != null && mVideoReady;
    609     }
    610 
    611     /**
    612      * Returns {@code true} if a photo has been bound. Otherwise, {@code false}.
    613      */
    614     public boolean isPhotoBound() {
    615         return mDrawable != null;
    616     }
    617 
    618     /**
    619      * Hides the photo info portion of the header. As a side effect, this automatically enables
    620      * or disables image transformations [eg zoom, pan, etc...] depending upon the value of
    621      * fullScreen. If this is not desirable, enable / disable image transformations manually.
    622      */
    623     public void setFullScreen(boolean fullScreen, boolean animate) {
    624         if (fullScreen != mFullScreen) {
    625             mFullScreen = fullScreen;
    626             requestLayout();
    627             invalidate();
    628         }
    629     }
    630 
    631     /**
    632      * Enable or disable cropping of the displayed image. Cropping can only be enabled
    633      * <em>before</em> the view has been laid out. Additionally, once cropping has been
    634      * enabled, it cannot be disabled.
    635      */
    636     public void enableAllowCrop(boolean allowCrop) {
    637         if (allowCrop && mHaveLayout) {
    638             throw new IllegalArgumentException("Cannot set crop after view has been laid out");
    639         }
    640         if (!allowCrop && mAllowCrop) {
    641             throw new IllegalArgumentException("Cannot unset crop mode");
    642         }
    643         mAllowCrop = allowCrop;
    644     }
    645 
    646     /**
    647      * Gets a bitmap of the cropped region. If cropping is not enabled, returns {@code null}.
    648      */
    649     public Bitmap getCroppedPhoto() {
    650         if (!mAllowCrop) {
    651             return null;
    652         }
    653 
    654         final Bitmap croppedBitmap = Bitmap.createBitmap(
    655                 (int) CROPPED_SIZE, (int) CROPPED_SIZE, Bitmap.Config.ARGB_8888);
    656         final Canvas croppedCanvas = new Canvas(croppedBitmap);
    657 
    658         // scale for the final dimensions
    659         final int cropWidth = mCropRect.right - mCropRect.left;
    660         final float scaleWidth = CROPPED_SIZE / cropWidth;
    661         final float scaleHeight = CROPPED_SIZE / cropWidth;
    662 
    663         // translate to the origin & scale
    664         final Matrix matrix = new Matrix(mDrawMatrix);
    665         matrix.postTranslate(-mCropRect.left, -mCropRect.top);
    666         matrix.postScale(scaleWidth, scaleHeight);
    667 
    668         // draw the photo
    669         if (mDrawable != null) {
    670             croppedCanvas.concat(matrix);
    671             mDrawable.draw(croppedCanvas);
    672         }
    673         return croppedBitmap;
    674     }
    675 
    676     /**
    677      * Resets the image transformation to its original value.
    678      */
    679     public void resetTransformations() {
    680         // snap transformations; we don't animate
    681         mMatrix.set(mOriginalMatrix);
    682 
    683         // Invalidate the view because if you move off this PhotoView
    684         // to another one and come back, you want it to draw from scratch
    685         // in case you were zoomed in or translated (since those settings
    686         // are not preserved and probably shouldn't be).
    687         invalidate();
    688     }
    689 
    690     /**
    691      * Rotates the image 90 degrees, clockwise.
    692      */
    693     public void rotateClockwise() {
    694         rotate(90, true);
    695     }
    696 
    697     /**
    698      * Rotates the image 90 degrees, counter clockwise.
    699      */
    700     public void rotateCounterClockwise() {
    701         rotate(-90, true);
    702     }
    703 
    704     @Override
    705     protected void onDraw(Canvas canvas) {
    706         super.onDraw(canvas);
    707 
    708         // draw the photo
    709         if (mDrawable != null) {
    710             int saveCount = canvas.getSaveCount();
    711             canvas.save();
    712 
    713             if (mDrawMatrix != null) {
    714                 canvas.concat(mDrawMatrix);
    715             }
    716             mDrawable.draw(canvas);
    717 
    718             canvas.restoreToCount(saveCount);
    719 
    720             if (mVideoBlob != null) {
    721                 final Bitmap videoImage = (mVideoReady ? sVideoImage : sVideoNotReadyImage);
    722                 final int drawLeft = (getWidth() - videoImage.getWidth()) / 2;
    723                 final int drawTop = (getHeight() - videoImage.getHeight()) / 2;
    724                 canvas.drawBitmap(videoImage, drawLeft, drawTop, null);
    725             }
    726 
    727             // Extract the drawable's bounds (in our own copy, to not alter the image)
    728             mTranslateRect.set(mDrawable.getBounds());
    729             if (mDrawMatrix != null) {
    730                 mDrawMatrix.mapRect(mTranslateRect);
    731             }
    732 
    733             if (mAllowCrop) {
    734                 int previousSaveCount = canvas.getSaveCount();
    735                 canvas.drawRect(0, 0, getWidth(), getHeight(), sCropDimPaint);
    736                 canvas.save();
    737                 canvas.clipRect(mCropRect);
    738 
    739                 if (mDrawMatrix != null) {
    740                     canvas.concat(mDrawMatrix);
    741                 }
    742 
    743                 mDrawable.draw(canvas);
    744                 canvas.restoreToCount(previousSaveCount);
    745                 canvas.drawRect(mCropRect, sCropPaint);
    746             }
    747         }
    748     }
    749 
    750     @Override
    751     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    752         super.onLayout(changed, left, top, right, bottom);
    753         mHaveLayout = true;
    754         final int layoutWidth = getWidth();
    755         final int layoutHeight = getHeight();
    756 
    757         if (mAllowCrop) {
    758             mCropSize = Math.min(sCropSize, Math.min(layoutWidth, layoutHeight));
    759             final int cropLeft = (layoutWidth - mCropSize) / 2;
    760             final int cropTop = (layoutHeight - mCropSize) / 2;
    761             final int cropRight = cropLeft + mCropSize;
    762             final int cropBottom =  cropTop + mCropSize;
    763 
    764             // Create a crop region overlay. We need a separate canvas to be able to "punch
    765             // a hole" through to the underlying image.
    766             mCropRect.set(cropLeft, cropTop, cropRight, cropBottom);
    767         }
    768         configureBounds(changed);
    769     }
    770 
    771     @Override
    772     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    773         if (mFixedHeight != -1) {
    774             super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mFixedHeight,
    775                     MeasureSpec.AT_MOST));
    776             setMeasuredDimension(getMeasuredWidth(), mFixedHeight);
    777         } else {
    778             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    779         }
    780     }
    781 
    782     @Override
    783     public boolean verifyDrawable(Drawable drawable) {
    784         return mDrawable == drawable || super.verifyDrawable(drawable);
    785     }
    786 
    787     @Override
    788     /**
    789      * {@inheritDoc}
    790      */
    791     public void invalidateDrawable(Drawable drawable) {
    792         // Only invalidate this view if the passed in drawable is displayed within this view. If
    793         // another drawable is passed in, have the parent view handle invalidation.
    794         if (mDrawable == drawable) {
    795             invalidate();
    796         } else {
    797             super.invalidateDrawable(drawable);
    798         }
    799     }
    800 
    801     /**
    802      * Forces a fixed height for this view.
    803      *
    804      * @param fixedHeight The height. If {@code -1}, use the measured height.
    805      */
    806     public void setFixedHeight(int fixedHeight) {
    807         final boolean adjustBounds = (fixedHeight != mFixedHeight);
    808         mFixedHeight = fixedHeight;
    809         setMeasuredDimension(getMeasuredWidth(), mFixedHeight);
    810         if (adjustBounds) {
    811             configureBounds(true);
    812             requestLayout();
    813         }
    814     }
    815 
    816     /**
    817      * Enable or disable image transformations. When transformations are enabled, this view
    818      * consumes all touch events.
    819      */
    820     public void enableImageTransforms(boolean enable) {
    821         mTransformsEnabled = enable;
    822         if (!mTransformsEnabled) {
    823             resetTransformations();
    824         }
    825     }
    826 
    827     /**
    828      * Configures the bounds of the photo. The photo will always be scaled to fit center.
    829      */
    830     private void configureBounds(boolean changed) {
    831         if (mDrawable == null || !mHaveLayout) {
    832             return;
    833         }
    834         final int dwidth = mDrawable.getIntrinsicWidth();
    835         final int dheight = mDrawable.getIntrinsicHeight();
    836 
    837         final int vwidth = getWidth();
    838         final int vheight = getHeight();
    839 
    840         final boolean fits = (dwidth < 0 || vwidth == dwidth) &&
    841                 (dheight < 0 || vheight == dheight);
    842 
    843         // We need to do the scaling ourself, so have the drawable use its native size.
    844         mDrawable.setBounds(0, 0, dwidth, dheight);
    845 
    846         // Create a matrix with the proper transforms
    847         if (changed || (mMinScale == 0 && mDrawable != null && mHaveLayout)) {
    848             generateMatrix();
    849             generateScale();
    850         }
    851 
    852         if (fits || mMatrix.isIdentity()) {
    853             // The bitmap fits exactly, no transform needed.
    854             mDrawMatrix = null;
    855         } else {
    856             mDrawMatrix = mMatrix;
    857         }
    858     }
    859 
    860     /**
    861      * Generates the initial transformation matrix for drawing. Additionally, it sets the
    862      * minimum and maximum scale values.
    863      */
    864     private void generateMatrix() {
    865         final int dwidth = mDrawable.getIntrinsicWidth();
    866         final int dheight = mDrawable.getIntrinsicHeight();
    867 
    868         final int vwidth = mAllowCrop ? sCropSize : getWidth();
    869         final int vheight = mAllowCrop ? sCropSize : getHeight();
    870 
    871         final boolean fits = (dwidth < 0 || vwidth == dwidth) &&
    872                 (dheight < 0 || vheight == dheight);
    873 
    874         if (fits && !mAllowCrop) {
    875             mMatrix.reset();
    876         } else {
    877             // Generate the required transforms for the photo
    878             mTempSrc.set(0, 0, dwidth, dheight);
    879             if (mAllowCrop) {
    880                 mTempDst.set(mCropRect);
    881             } else {
    882                 mTempDst.set(0, 0, vwidth, vheight);
    883             }
    884             RectF scaledDestination = new RectF(
    885                     (vwidth / 2) - (dwidth * mMaxInitialScaleFactor / 2),
    886                     (vheight / 2) - (dheight * mMaxInitialScaleFactor / 2),
    887                     (vwidth / 2) + (dwidth * mMaxInitialScaleFactor / 2),
    888                     (vheight / 2) + (dheight * mMaxInitialScaleFactor / 2));
    889             if(mTempDst.contains(scaledDestination)) {
    890                 mMatrix.setRectToRect(mTempSrc, scaledDestination, Matrix.ScaleToFit.CENTER);
    891             } else {
    892                 mMatrix.setRectToRect(mTempSrc, mTempDst, Matrix.ScaleToFit.CENTER);
    893             }
    894         }
    895         mOriginalMatrix.set(mMatrix);
    896     }
    897 
    898     private void generateScale() {
    899         final int dwidth = mDrawable.getIntrinsicWidth();
    900         final int dheight = mDrawable.getIntrinsicHeight();
    901 
    902         final int vwidth = mAllowCrop ? getCropSize() : getWidth();
    903         final int vheight = mAllowCrop ? getCropSize() : getHeight();
    904 
    905         if (dwidth < vwidth && dheight < vheight && !mAllowCrop) {
    906             mMinScale = 1.0f;
    907         } else {
    908             mMinScale = getScale();
    909         }
    910         mMaxScale = Math.max(mMinScale * 4, 4);
    911     }
    912 
    913     /**
    914      * @return the size of the crop regions
    915      */
    916     private int getCropSize() {
    917         return mCropSize > 0 ? mCropSize : sCropSize;
    918     }
    919 
    920     /**
    921      * Returns the currently applied scale factor for the image.
    922      * <p>
    923      * NOTE: This method overwrites any values stored in {@link #mValues}.
    924      */
    925     private float getScale() {
    926         mMatrix.getValues(mValues);
    927         return mValues[Matrix.MSCALE_X];
    928     }
    929 
    930     /**
    931      * Scales the image while keeping the aspect ratio.
    932      *
    933      * The given scale is capped so that the resulting scale of the image always remains
    934      * between {@link #mMinScale} and {@link #mMaxScale}.
    935      *
    936      * If the image is smaller than the viewable area, it will be centered.
    937      *
    938      * @param newScale the new scale
    939      * @param centerX the center horizontal point around which to scale
    940      * @param centerY the center vertical point around which to scale
    941      */
    942     private void scale(float newScale, float centerX, float centerY) {
    943         // rotate back to the original orientation
    944         mMatrix.postRotate(-mRotation, getWidth() / 2, getHeight() / 2);
    945 
    946         // ensure that mMinScale <= newScale <= mMaxScale
    947         newScale = Math.max(newScale, mMinScale);
    948         newScale = Math.min(newScale, mMaxScale * SCALE_OVERZOOM_FACTOR);
    949 
    950         float currentScale = getScale();
    951         float factor = newScale / currentScale;
    952 
    953         // apply the scale factor
    954         mMatrix.postScale(factor, factor, centerX, centerY);
    955 
    956         // re-apply any rotation
    957         mMatrix.postRotate(mRotation, getWidth() / 2, getHeight() / 2);
    958 
    959         invalidate();
    960     }
    961 
    962     /**
    963      * Translates the image.
    964      *
    965      * This method will not allow the image to be translated outside of the visible area.
    966      *
    967      * @param tx how many pixels to translate horizontally
    968      * @param ty how many pixels to translate vertically
    969      * @return result of the translation, represented as either {@link TRANSLATE_NONE},
    970      * {@link TRANSLATE_X_ONLY}, {@link TRANSLATE_Y_ONLY}, or {@link TRANSLATE_BOTH}
    971      */
    972     private int translate(float tx, float ty) {
    973         mTranslateRect.set(mTempSrc);
    974         mMatrix.mapRect(mTranslateRect);
    975 
    976         final float maxLeft = mAllowCrop ? mCropRect.left : 0.0f;
    977         final float maxRight = mAllowCrop ? mCropRect.right : getWidth();
    978         float l = mTranslateRect.left;
    979         float r = mTranslateRect.right;
    980 
    981         final float translateX;
    982         if (mAllowCrop) {
    983             // If we're cropping, allow the image to scroll off the edge of the screen
    984             translateX = Math.max(maxLeft - mTranslateRect.right,
    985                     Math.min(maxRight - mTranslateRect.left, tx));
    986         } else {
    987             // Otherwise, ensure the image never leaves the screen
    988             if (r - l < maxRight - maxLeft) {
    989                 translateX = maxLeft + ((maxRight - maxLeft) - (r + l)) / 2;
    990             } else {
    991                 translateX = Math.max(maxRight - r, Math.min(maxLeft - l, tx));
    992             }
    993         }
    994 
    995         float maxTop = mAllowCrop ? mCropRect.top: 0.0f;
    996         float maxBottom = mAllowCrop ? mCropRect.bottom : getHeight();
    997         float t = mTranslateRect.top;
    998         float b = mTranslateRect.bottom;
    999 
   1000         final float translateY;
   1001 
   1002         if (mAllowCrop) {
   1003             // If we're cropping, allow the image to scroll off the edge of the screen
   1004             translateY = Math.max(maxTop - mTranslateRect.bottom,
   1005                     Math.min(maxBottom - mTranslateRect.top, ty));
   1006         } else {
   1007             // Otherwise, ensure the image never leaves the screen
   1008             if (b - t < maxBottom - maxTop) {
   1009                 translateY = maxTop + ((maxBottom - maxTop) - (b + t)) / 2;
   1010             } else {
   1011                 translateY = Math.max(maxBottom - b, Math.min(maxTop - t, ty));
   1012             }
   1013         }
   1014 
   1015         // Do the translation
   1016         mMatrix.postTranslate(translateX, translateY);
   1017         invalidate();
   1018 
   1019         boolean didTranslateX = translateX == tx;
   1020         boolean didTranslateY = translateY == ty;
   1021         if (didTranslateX && didTranslateY) {
   1022             return TRANSLATE_BOTH;
   1023         } else if (didTranslateX) {
   1024             return TRANSLATE_X_ONLY;
   1025         } else if (didTranslateY) {
   1026             return TRANSLATE_Y_ONLY;
   1027         }
   1028         return TRANSLATE_NONE;
   1029     }
   1030 
   1031     /**
   1032      * Snaps the image so it touches all edges of the view.
   1033      */
   1034     private void snap() {
   1035         mTranslateRect.set(mTempSrc);
   1036         mMatrix.mapRect(mTranslateRect);
   1037 
   1038         // Determine how much to snap in the horizontal direction [if any]
   1039         float maxLeft = mAllowCrop ? mCropRect.left : 0.0f;
   1040         float maxRight = mAllowCrop ? mCropRect.right : getWidth();
   1041         float l = mTranslateRect.left;
   1042         float r = mTranslateRect.right;
   1043 
   1044         final float translateX;
   1045         if (r - l < maxRight - maxLeft) {
   1046             // Image is narrower than view; translate to the center of the view
   1047             translateX = maxLeft + ((maxRight - maxLeft) - (r + l)) / 2;
   1048         } else if (l > maxLeft) {
   1049             // Image is off right-edge of screen; bring it into view
   1050             translateX = maxLeft - l;
   1051         } else if (r < maxRight) {
   1052             // Image is off left-edge of screen; bring it into view
   1053             translateX = maxRight - r;
   1054         } else {
   1055             translateX = 0.0f;
   1056         }
   1057 
   1058         // Determine how much to snap in the vertical direction [if any]
   1059         float maxTop = mAllowCrop ? mCropRect.top : 0.0f;
   1060         float maxBottom = mAllowCrop ? mCropRect.bottom : getHeight();
   1061         float t = mTranslateRect.top;
   1062         float b = mTranslateRect.bottom;
   1063 
   1064         final float translateY;
   1065         if (b - t < maxBottom - maxTop) {
   1066             // Image is shorter than view; translate to the bottom edge of the view
   1067             translateY = maxTop + ((maxBottom - maxTop) - (b + t)) / 2;
   1068         } else if (t > maxTop) {
   1069             // Image is off bottom-edge of screen; bring it into view
   1070             translateY = maxTop - t;
   1071         } else if (b < maxBottom) {
   1072             // Image is off top-edge of screen; bring it into view
   1073             translateY = maxBottom - b;
   1074         } else {
   1075             translateY = 0.0f;
   1076         }
   1077 
   1078         if (Math.abs(translateX) > SNAP_THRESHOLD || Math.abs(translateY) > SNAP_THRESHOLD) {
   1079             mSnapRunnable.start(translateX, translateY);
   1080         } else {
   1081             mMatrix.postTranslate(translateX, translateY);
   1082             invalidate();
   1083         }
   1084     }
   1085 
   1086     /**
   1087      * Rotates the image, either instantly or gradually
   1088      *
   1089      * @param degrees how many degrees to rotate the image, positive rotates clockwise
   1090      * @param animate if {@code true}, animate during the rotation. Otherwise, snap rotate.
   1091      */
   1092     private void rotate(float degrees, boolean animate) {
   1093         if (animate) {
   1094             mRotateRunnable.start(degrees);
   1095         } else {
   1096             mRotation += degrees;
   1097             mMatrix.postRotate(degrees, getWidth() / 2, getHeight() / 2);
   1098             invalidate();
   1099         }
   1100     }
   1101 
   1102     /**
   1103      * Initializes the header and any static values
   1104      */
   1105     private void initialize() {
   1106         Context context = getContext();
   1107 
   1108         if (!sInitialized) {
   1109             sInitialized = true;
   1110 
   1111             Resources resources = context.getApplicationContext().getResources();
   1112 
   1113             sCropSize = resources.getDimensionPixelSize(R.dimen.photo_crop_width);
   1114 
   1115             sCropDimPaint = new Paint();
   1116             sCropDimPaint.setAntiAlias(true);
   1117             sCropDimPaint.setColor(resources.getColor(R.color.photo_crop_dim_color));
   1118             sCropDimPaint.setStyle(Style.FILL);
   1119 
   1120             sCropPaint = new Paint();
   1121             sCropPaint.setAntiAlias(true);
   1122             sCropPaint.setColor(resources.getColor(R.color.photo_crop_highlight_color));
   1123             sCropPaint.setStyle(Style.STROKE);
   1124             sCropPaint.setStrokeWidth(resources.getDimension(R.dimen.photo_crop_stroke_width));
   1125 
   1126             final ViewConfiguration configuration = ViewConfiguration.get(context);
   1127             final int touchSlop = configuration.getScaledTouchSlop();
   1128             sTouchSlopSquare = touchSlop * touchSlop;
   1129         }
   1130 
   1131         mGestureDetector = new GestureDetectorCompat(context, this, null);
   1132         mScaleGetureDetector = new ScaleGestureDetector(context, this);
   1133         mQuickScaleEnabled = ScaleGestureDetectorCompat.isQuickScaleEnabled(mScaleGetureDetector);
   1134         mScaleRunnable = new ScaleRunnable(this);
   1135         mTranslateRunnable = new TranslateRunnable(this);
   1136         mSnapRunnable = new SnapRunnable(this);
   1137         mRotateRunnable = new RotateRunnable(this);
   1138     }
   1139 
   1140     /**
   1141      * Runnable that animates an image scale operation.
   1142      */
   1143     private static class ScaleRunnable implements Runnable {
   1144 
   1145         private final PhotoView mHeader;
   1146 
   1147         private float mCenterX;
   1148         private float mCenterY;
   1149 
   1150         private boolean mZoomingIn;
   1151 
   1152         private float mTargetScale;
   1153         private float mStartScale;
   1154         private float mVelocity;
   1155         private long mStartTime;
   1156 
   1157         private boolean mRunning;
   1158         private boolean mStop;
   1159 
   1160         public ScaleRunnable(PhotoView header) {
   1161             mHeader = header;
   1162         }
   1163 
   1164         /**
   1165          * Starts the animation. There is no target scale bounds check.
   1166          */
   1167         public boolean start(float startScale, float targetScale, float centerX, float centerY) {
   1168             if (mRunning) {
   1169                 return false;
   1170             }
   1171 
   1172             mCenterX = centerX;
   1173             mCenterY = centerY;
   1174 
   1175             // Ensure the target scale is within the min/max bounds
   1176             mTargetScale = targetScale;
   1177             mStartTime = System.currentTimeMillis();
   1178             mStartScale = startScale;
   1179             mZoomingIn = mTargetScale > mStartScale;
   1180             mVelocity = (mTargetScale - mStartScale) / ZOOM_ANIMATION_DURATION;
   1181             mRunning = true;
   1182             mStop = false;
   1183             mHeader.post(this);
   1184             return true;
   1185         }
   1186 
   1187         /**
   1188          * Stops the animation in place. It does not snap the image to its final zoom.
   1189          */
   1190         public void stop() {
   1191             mRunning = false;
   1192             mStop = true;
   1193         }
   1194 
   1195         @Override
   1196         public void run() {
   1197             if (mStop) {
   1198                 return;
   1199             }
   1200 
   1201             // Scale
   1202             long now = System.currentTimeMillis();
   1203             long ellapsed = now - mStartTime;
   1204             float newScale = (mStartScale + mVelocity * ellapsed);
   1205             mHeader.scale(newScale, mCenterX, mCenterY);
   1206 
   1207             // Stop when done
   1208             if (newScale == mTargetScale || (mZoomingIn == (newScale > mTargetScale))) {
   1209                 mHeader.scale(mTargetScale, mCenterX, mCenterY);
   1210                 stop();
   1211             }
   1212 
   1213             if (!mStop) {
   1214                 mHeader.post(this);
   1215             }
   1216         }
   1217     }
   1218 
   1219     /**
   1220      * Runnable that animates an image translation operation.
   1221      */
   1222     private static class TranslateRunnable implements Runnable {
   1223 
   1224         private static final float DECELERATION_RATE = 1000f;
   1225         private static final long NEVER = -1L;
   1226 
   1227         private final PhotoView mHeader;
   1228 
   1229         private float mVelocityX;
   1230         private float mVelocityY;
   1231 
   1232         private float mDecelerationX;
   1233         private float mDecelerationY;
   1234 
   1235         private long mLastRunTime;
   1236         private boolean mRunning;
   1237         private boolean mStop;
   1238 
   1239         public TranslateRunnable(PhotoView header) {
   1240             mLastRunTime = NEVER;
   1241             mHeader = header;
   1242         }
   1243 
   1244         /**
   1245          * Starts the animation.
   1246          */
   1247         public boolean start(float velocityX, float velocityY) {
   1248             if (mRunning) {
   1249                 return false;
   1250             }
   1251             mLastRunTime = NEVER;
   1252             mVelocityX = velocityX;
   1253             mVelocityY = velocityY;
   1254 
   1255             float angle = (float) Math.atan2(mVelocityY, mVelocityX);
   1256             mDecelerationX = (float) (DECELERATION_RATE * Math.cos(angle));
   1257             mDecelerationY = (float) (DECELERATION_RATE * Math.sin(angle));
   1258 
   1259             mStop = false;
   1260             mRunning = true;
   1261             mHeader.post(this);
   1262             return true;
   1263         }
   1264 
   1265         /**
   1266          * Stops the animation in place. It does not snap the image to its final translation.
   1267          */
   1268         public void stop() {
   1269             mRunning = false;
   1270             mStop = true;
   1271         }
   1272 
   1273         @Override
   1274         public void run() {
   1275             // See if we were told to stop:
   1276             if (mStop) {
   1277                 return;
   1278             }
   1279 
   1280             // Translate according to current velocities and time delta:
   1281             long now = System.currentTimeMillis();
   1282             float delta = (mLastRunTime != NEVER) ? (now - mLastRunTime) / 1000f : 0f;
   1283             final int translateResult = mHeader.translate(mVelocityX * delta, mVelocityY * delta);
   1284             mLastRunTime = now;
   1285             // Slow down:
   1286             float slowDownX = mDecelerationX * delta;
   1287             if (Math.abs(mVelocityX) > Math.abs(slowDownX)) {
   1288                 mVelocityX -= slowDownX;
   1289             } else {
   1290                 mVelocityX = 0f;
   1291             }
   1292             float slowDownY = mDecelerationY * delta;
   1293             if (Math.abs(mVelocityY) > Math.abs(slowDownY)) {
   1294                 mVelocityY -= slowDownY;
   1295             } else {
   1296                 mVelocityY = 0f;
   1297             }
   1298 
   1299             // Stop when done
   1300             if ((mVelocityX == 0f && mVelocityY == 0f)
   1301                     || translateResult == TRANSLATE_NONE) {
   1302                 stop();
   1303                 mHeader.snap();
   1304             } else if (translateResult == TRANSLATE_X_ONLY) {
   1305                 mDecelerationX = (mVelocityX > 0) ? DECELERATION_RATE : -DECELERATION_RATE;
   1306                 mDecelerationY = 0;
   1307                 mVelocityY = 0f;
   1308             } else if (translateResult == TRANSLATE_Y_ONLY) {
   1309                 mDecelerationX = 0;
   1310                 mDecelerationY = (mVelocityY > 0) ? DECELERATION_RATE : -DECELERATION_RATE;
   1311                 mVelocityX = 0f;
   1312             }
   1313 
   1314             // See if we need to continue flinging:
   1315             if (mStop) {
   1316                 return;
   1317             }
   1318             mHeader.post(this);
   1319         }
   1320     }
   1321 
   1322     /**
   1323      * Runnable that animates an image translation operation.
   1324      */
   1325     private static class SnapRunnable implements Runnable {
   1326 
   1327         private static final long NEVER = -1L;
   1328 
   1329         private final PhotoView mHeader;
   1330 
   1331         private float mTranslateX;
   1332         private float mTranslateY;
   1333 
   1334         private long mStartRunTime;
   1335         private boolean mRunning;
   1336         private boolean mStop;
   1337 
   1338         public SnapRunnable(PhotoView header) {
   1339             mStartRunTime = NEVER;
   1340             mHeader = header;
   1341         }
   1342 
   1343         /**
   1344          * Starts the animation.
   1345          */
   1346         public boolean start(float translateX, float translateY) {
   1347             if (mRunning) {
   1348                 return false;
   1349             }
   1350             mStartRunTime = NEVER;
   1351             mTranslateX = translateX;
   1352             mTranslateY = translateY;
   1353             mStop = false;
   1354             mRunning = true;
   1355             mHeader.postDelayed(this, SNAP_DELAY);
   1356             return true;
   1357         }
   1358 
   1359         /**
   1360          * Stops the animation in place. It does not snap the image to its final translation.
   1361          */
   1362         public void stop() {
   1363             mRunning = false;
   1364             mStop = true;
   1365         }
   1366 
   1367         @Override
   1368         public void run() {
   1369             // See if we were told to stop:
   1370             if (mStop) {
   1371                 return;
   1372             }
   1373 
   1374             // Translate according to current velocities and time delta:
   1375             long now = System.currentTimeMillis();
   1376             float delta = (mStartRunTime != NEVER) ? (now - mStartRunTime) : 0f;
   1377 
   1378             if (mStartRunTime == NEVER) {
   1379                 mStartRunTime = now;
   1380             }
   1381 
   1382             float transX;
   1383             float transY;
   1384             if (delta >= SNAP_DURATION) {
   1385                 transX = mTranslateX;
   1386                 transY = mTranslateY;
   1387             } else {
   1388                 transX = (mTranslateX / (SNAP_DURATION - delta)) * 10f;
   1389                 transY = (mTranslateY / (SNAP_DURATION - delta)) * 10f;
   1390                 if (Math.abs(transX) > Math.abs(mTranslateX) || Float.isNaN(transX)) {
   1391                     transX = mTranslateX;
   1392                 }
   1393                 if (Math.abs(transY) > Math.abs(mTranslateY) || Float.isNaN(transY)) {
   1394                     transY = mTranslateY;
   1395                 }
   1396             }
   1397 
   1398             mHeader.translate(transX, transY);
   1399             mTranslateX -= transX;
   1400             mTranslateY -= transY;
   1401 
   1402             if (mTranslateX == 0 && mTranslateY == 0) {
   1403                 stop();
   1404             }
   1405 
   1406             // See if we need to continue flinging:
   1407             if (mStop) {
   1408                 return;
   1409             }
   1410             mHeader.post(this);
   1411         }
   1412     }
   1413 
   1414     /**
   1415      * Runnable that animates an image rotation operation.
   1416      */
   1417     private static class RotateRunnable implements Runnable {
   1418 
   1419         private static final long NEVER = -1L;
   1420 
   1421         private final PhotoView mHeader;
   1422 
   1423         private float mTargetRotation;
   1424         private float mAppliedRotation;
   1425         private float mVelocity;
   1426         private long mLastRuntime;
   1427 
   1428         private boolean mRunning;
   1429         private boolean mStop;
   1430 
   1431         public RotateRunnable(PhotoView header) {
   1432             mHeader = header;
   1433         }
   1434 
   1435         /**
   1436          * Starts the animation.
   1437          */
   1438         public void start(float rotation) {
   1439             if (mRunning) {
   1440                 return;
   1441             }
   1442 
   1443             mTargetRotation = rotation;
   1444             mVelocity = mTargetRotation / ROTATE_ANIMATION_DURATION;
   1445             mAppliedRotation = 0f;
   1446             mLastRuntime = NEVER;
   1447             mStop = false;
   1448             mRunning = true;
   1449             mHeader.post(this);
   1450         }
   1451 
   1452         /**
   1453          * Stops the animation in place. It does not snap the image to its final rotation.
   1454          */
   1455         public void stop() {
   1456             mRunning = false;
   1457             mStop = true;
   1458         }
   1459 
   1460         @Override
   1461         public void run() {
   1462             if (mStop) {
   1463                 return;
   1464             }
   1465 
   1466             if (mAppliedRotation != mTargetRotation) {
   1467                 long now = System.currentTimeMillis();
   1468                 long delta = mLastRuntime != NEVER ? now - mLastRuntime : 0L;
   1469                 float rotationAmount = mVelocity * delta;
   1470                 if (mAppliedRotation < mTargetRotation
   1471                         && mAppliedRotation + rotationAmount > mTargetRotation
   1472                         || mAppliedRotation > mTargetRotation
   1473                         && mAppliedRotation + rotationAmount < mTargetRotation) {
   1474                     rotationAmount = mTargetRotation - mAppliedRotation;
   1475                 }
   1476                 mHeader.rotate(rotationAmount, false);
   1477                 mAppliedRotation += rotationAmount;
   1478                 if (mAppliedRotation == mTargetRotation) {
   1479                     stop();
   1480                 }
   1481                 mLastRuntime = now;
   1482             }
   1483 
   1484             if (mStop) {
   1485                 return;
   1486             }
   1487             mHeader.post(this);
   1488         }
   1489     }
   1490 
   1491     public void setMaxInitialScale(float f) {
   1492         mMaxInitialScaleFactor = f;
   1493     }
   1494 }
   1495