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