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