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