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