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