Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.gallery3d.ui;
     18 
     19 import com.android.gallery3d.R;
     20 import com.android.gallery3d.app.GalleryActivity;
     21 import com.android.gallery3d.common.Utils;
     22 import com.android.gallery3d.data.Path;
     23 import com.android.gallery3d.ui.PositionRepository.Position;
     24 import com.android.gallery3d.util.GalleryUtils;
     25 
     26 import android.content.Context;
     27 import android.graphics.Bitmap;
     28 import android.graphics.Color;
     29 import android.graphics.RectF;
     30 import android.os.Message;
     31 import android.os.SystemClock;
     32 import android.view.GestureDetector;
     33 import android.view.MotionEvent;
     34 import android.view.ScaleGestureDetector;
     35 import android.widget.Scroller;
     36 
     37 class PositionController {
     38     private static final String TAG = "PositionController";
     39     private long mAnimationStartTime = NO_ANIMATION;
     40     private static final long NO_ANIMATION = -1;
     41     private static final long LAST_ANIMATION = -2;
     42 
     43     private int mAnimationKind;
     44     private float mAnimationDuration;
     45     private final static int ANIM_KIND_SCROLL = 0;
     46     private final static int ANIM_KIND_SCALE = 1;
     47     private final static int ANIM_KIND_SNAPBACK = 2;
     48     private final static int ANIM_KIND_SLIDE = 3;
     49     private final static int ANIM_KIND_ZOOM = 4;
     50     private final static int ANIM_KIND_FLING = 5;
     51 
     52     // Animation time in milliseconds. The order must match ANIM_KIND_* above.
     53     private final static int ANIM_TIME[] = {
     54         0,    // ANIM_KIND_SCROLL
     55         50,   // ANIM_KIND_SCALE
     56         600,  // ANIM_KIND_SNAPBACK
     57         400,  // ANIM_KIND_SLIDE
     58         300,  // ANIM_KIND_ZOOM
     59         0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
     60     };
     61 
     62     // We try to scale up the image to fill the screen. But in order not to
     63     // scale too much for small icons, we limit the max up-scaling factor here.
     64     private static final float SCALE_LIMIT = 4;
     65     private static final int sHorizontalSlack = GalleryUtils.dpToPixel(12);
     66 
     67     private PhotoView mViewer;
     68     private EdgeView mEdgeView;
     69     private int mImageW, mImageH;
     70     private int mViewW, mViewH;
     71 
     72     // The X, Y are the coordinate on bitmap which shows on the center of
     73     // the view. We always keep the mCurrent{X,Y,Scale} sync with the actual
     74     // values used currently.
     75     private int mCurrentX, mFromX, mToX;
     76     private int mCurrentY, mFromY, mToY;
     77     private float mCurrentScale, mFromScale, mToScale;
     78 
     79     // The focus point of the scaling gesture (in bitmap coordinates).
     80     private int mFocusBitmapX;
     81     private int mFocusBitmapY;
     82     private boolean mInScale;
     83 
     84     // The minimum and maximum scale we allow.
     85     private float mScaleMin, mScaleMax = SCALE_LIMIT;
     86 
     87     // This is used by the fling animation
     88     private FlingScroller mScroller;
     89 
     90     // The bound of the stable region, see the comments above
     91     // calculateStableBound() for details.
     92     private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
     93 
     94     // Assume the image size is the same as view size before we know the actual
     95     // size of image.
     96     private boolean mUseViewSize = true;
     97 
     98     private RectF mTempRect = new RectF();
     99     private float[] mTempPoints = new float[8];
    100 
    101     public PositionController(PhotoView viewer, Context context,
    102             EdgeView edgeView) {
    103         mViewer = viewer;
    104         mEdgeView = edgeView;
    105         mScroller = new FlingScroller();
    106     }
    107 
    108     public void setImageSize(int width, int height) {
    109 
    110         // If no image available, use view size.
    111         if (width == 0 || height == 0) {
    112             mUseViewSize = true;
    113             mImageW = mViewW;
    114             mImageH = mViewH;
    115             mCurrentX = mImageW / 2;
    116             mCurrentY = mImageH / 2;
    117             mCurrentScale = 1;
    118             mScaleMin = 1;
    119             mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
    120             return;
    121         }
    122 
    123         mUseViewSize = false;
    124 
    125         float ratio = Math.min(
    126                 (float) mImageW / width, (float) mImageH / height);
    127 
    128         // See the comment above translate() for details.
    129         mCurrentX = translate(mCurrentX, mImageW, width, ratio);
    130         mCurrentY = translate(mCurrentY, mImageH, height, ratio);
    131         mCurrentScale = mCurrentScale * ratio;
    132 
    133         mFromX = translate(mFromX, mImageW, width, ratio);
    134         mFromY = translate(mFromY, mImageH, height, ratio);
    135         mFromScale = mFromScale * ratio;
    136 
    137         mToX = translate(mToX, mImageW, width, ratio);
    138         mToY = translate(mToY, mImageH, height, ratio);
    139         mToScale = mToScale * ratio;
    140 
    141         mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio);
    142         mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio);
    143 
    144         mImageW = width;
    145         mImageH = height;
    146 
    147         mScaleMin = getMinimalScale(mImageW, mImageH);
    148 
    149         // Start animation from the saved position if we have one.
    150         Position position = mViewer.retrieveSavedPosition();
    151         if (position != null) {
    152             // The animation starts from 240 pixels and centers at the image
    153             // at the saved position.
    154             float scale = 240f / Math.min(width, height);
    155             mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2;
    156             mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2;
    157             mCurrentScale = scale;
    158             mViewer.openAnimationStarted();
    159             startSnapback();
    160         } else if (mAnimationStartTime == NO_ANIMATION) {
    161             mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
    162         }
    163         mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
    164     }
    165 
    166     public void zoomIn(float tapX, float tapY, float targetScale) {
    167         if (targetScale > mScaleMax) targetScale = mScaleMax;
    168 
    169         // Convert the tap position to image coordinate
    170         int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX);
    171         int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY);
    172 
    173         calculateStableBound(targetScale);
    174         int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight);
    175         int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom);
    176 
    177         startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
    178     }
    179 
    180     public void resetToFullView() {
    181         startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM);
    182     }
    183 
    184     public float getMinimalScale(int w, int h) {
    185         return Math.min(SCALE_LIMIT,
    186                 Math.min((float) mViewW / w, (float) mViewH / h));
    187     }
    188 
    189     // Translate a coordinate on bitmap if the bitmap size changes.
    190     // If the aspect ratio doesn't change, it's easy:
    191     //
    192     //         r  = w / w' (= h / h')
    193     //         x' = x / r
    194     //         y' = y / r
    195     //
    196     // However the aspect ratio may change. That happens when the user slides
    197     // a image before it's loaded, we don't know the actual aspect ratio, so
    198     // we will assume one. When we receive the actual bitmap size, we need to
    199     // translate the coordinate from the old bitmap into the new bitmap.
    200     //
    201     // What we want to do is center the bitmap at the original position.
    202     //
    203     //         ...+--+...
    204     //         .  |  |  .
    205     //         .  |  |  .
    206     //         ...+--+...
    207     //
    208     // First we scale down the new bitmap by a factor r = min(w/w', h/h').
    209     // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps
    210     // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of
    211     // the old bitmap maps to (x', y') in the new bitmap, where
    212     //         x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r
    213     //         y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r
    214     private static int translate(int value, int size, int newSize, float ratio) {
    215         return Math.round(newSize / 2f + (value - size / 2f) / ratio);
    216     }
    217 
    218     public void setViewSize(int viewW, int viewH) {
    219         boolean needLayout = mViewW == 0 || mViewH == 0;
    220 
    221         mViewW = viewW;
    222         mViewH = viewH;
    223 
    224         if (mUseViewSize) {
    225             mImageW = viewW;
    226             mImageH = viewH;
    227             mCurrentX = mImageW / 2;
    228             mCurrentY = mImageH / 2;
    229             mCurrentScale = 1;
    230             mScaleMin = 1;
    231             mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
    232             return;
    233         }
    234 
    235         // In most cases we want to keep the scaling factor intact when the
    236         // view size changes. The cases we want to reset the scaling factor
    237         // (to fit the view if possible) are (1) the scaling factor is too
    238         // small for the new view size (2) the scaling factor has not been
    239         // changed by the user.
    240         boolean wasMinScale = (mCurrentScale == mScaleMin);
    241         mScaleMin = getMinimalScale(mImageW, mImageH);
    242 
    243         if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
    244             mCurrentX = mImageW / 2;
    245             mCurrentY = mImageH / 2;
    246             mCurrentScale = mScaleMin;
    247             mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
    248         }
    249     }
    250 
    251     public void stopAnimation() {
    252         mAnimationStartTime = NO_ANIMATION;
    253     }
    254 
    255     public void skipAnimation() {
    256         if (mAnimationStartTime == NO_ANIMATION) return;
    257         mAnimationStartTime = NO_ANIMATION;
    258         mCurrentX = mToX;
    259         mCurrentY = mToY;
    260         mCurrentScale = mToScale;
    261     }
    262 
    263     public void beginScale(float focusX, float focusY) {
    264         mInScale = true;
    265         mFocusBitmapX = Math.round(mCurrentX +
    266                 (focusX - mViewW / 2f) / mCurrentScale);
    267         mFocusBitmapY = Math.round(mCurrentY +
    268                 (focusY - mViewH / 2f) / mCurrentScale);
    269     }
    270 
    271     public void scaleBy(float s, float focusX, float focusY) {
    272 
    273         // We want to keep the focus point (on the bitmap) the same as when
    274         // we begin the scale guesture, that is,
    275         //
    276         // mCurrentX' + (focusX - mViewW / 2f) / scale = mFocusBitmapX
    277         //
    278         s *= getTargetScale();
    279         int x = Math.round(mFocusBitmapX - (focusX - mViewW / 2f) / s);
    280         int y = Math.round(mFocusBitmapY - (focusY - mViewH / 2f) / s);
    281 
    282         startAnimation(x, y, s, ANIM_KIND_SCALE);
    283     }
    284 
    285     public void endScale() {
    286         mInScale = false;
    287         startSnapbackIfNeeded();
    288     }
    289 
    290     public float getCurrentScale() {
    291         return mCurrentScale;
    292     }
    293 
    294     public boolean isAtMinimalScale() {
    295         return isAlmostEquals(mCurrentScale, mScaleMin);
    296     }
    297 
    298     private static boolean isAlmostEquals(float a, float b) {
    299         float diff = a - b;
    300         return (diff < 0 ? -diff : diff) < 0.02f;
    301     }
    302 
    303     public void up() {
    304         startSnapback();
    305     }
    306 
    307     //             |<--| (1/2) * mImageW
    308     // +-------+-------+-------+
    309     // |       |       |       |
    310     // |       |   o   |       |
    311     // |       |       |       |
    312     // +-------+-------+-------+
    313     // |<----------| (3/2) * mImageW
    314     // Slide in the image from left or right.
    315     // Precondition: mCurrentScale = 1 (mView{W|H} == mImage{W|H}).
    316     // Sliding from left:  mCurrentX = (1/2) * mImageW
    317     //              right: mCurrentX = (3/2) * mImageW
    318     public void startSlideInAnimation(int direction) {
    319         int fromX = (direction == PhotoView.TRANS_SLIDE_IN_LEFT) ?
    320                 mImageW / 2 : 3 * mImageW / 2;
    321         mFromX = Math.round(fromX);
    322         mFromY = Math.round(mImageH / 2f);
    323         mCurrentX = mFromX;
    324         mCurrentY = mFromY;
    325         startAnimation(
    326                 mImageW / 2, mImageH / 2, mCurrentScale, ANIM_KIND_SLIDE);
    327     }
    328 
    329     public void startHorizontalSlide(int distance) {
    330         scrollBy(distance, 0, ANIM_KIND_SLIDE);
    331     }
    332 
    333     private void scrollBy(float dx, float dy, int type) {
    334         startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
    335                 getTargetY() + Math.round(dy / mCurrentScale),
    336                 mCurrentScale, type);
    337     }
    338 
    339     public void startScroll(float dx, float dy, boolean hasNext,
    340             boolean hasPrev) {
    341         int x = getTargetX() + Math.round(dx / mCurrentScale);
    342         int y = getTargetY() + Math.round(dy / mCurrentScale);
    343 
    344         calculateStableBound(mCurrentScale);
    345 
    346         // Vertical direction: If we have space to move in the vertical
    347         // direction, we show the edge effect when scrolling reaches the edge.
    348         if (mBoundTop != mBoundBottom) {
    349             if (y < mBoundTop) {
    350                 mEdgeView.onPull(mBoundTop - y, EdgeView.TOP);
    351             } else if (y > mBoundBottom) {
    352                 mEdgeView.onPull(y - mBoundBottom, EdgeView.BOTTOM);
    353             }
    354         }
    355 
    356         y = Utils.clamp(y, mBoundTop, mBoundBottom);
    357 
    358         // Horizontal direction: we show the edge effect when the scrolling
    359         // tries to go left of the first image or go right of the last image.
    360         if (!hasPrev && x < mBoundLeft) {
    361             int pixels = Math.round((mBoundLeft - x) * mCurrentScale);
    362             mEdgeView.onPull(pixels, EdgeView.LEFT);
    363             x = mBoundLeft;
    364         } else if (!hasNext && x > mBoundRight) {
    365             int pixels = Math.round((x - mBoundRight) * mCurrentScale);
    366             mEdgeView.onPull(pixels, EdgeView.RIGHT);
    367             x = mBoundRight;
    368         }
    369 
    370         startAnimation(x, y, mCurrentScale, ANIM_KIND_SCROLL);
    371     }
    372 
    373     public boolean fling(float velocityX, float velocityY) {
    374         // We only want to do fling when the picture is zoomed-in.
    375         if (mImageW * mCurrentScale <= mViewW &&
    376             mImageH * mCurrentScale <= mViewH) {
    377             return false;
    378         }
    379 
    380         calculateStableBound(mCurrentScale);
    381         mScroller.fling(mCurrentX, mCurrentY,
    382                 Math.round(-velocityX / mCurrentScale),
    383                 Math.round(-velocityY / mCurrentScale),
    384                 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
    385         int targetX = mScroller.getFinalX();
    386         int targetY = mScroller.getFinalY();
    387         mAnimationDuration = mScroller.getDuration();
    388         startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING);
    389         return true;
    390     }
    391 
    392     private void startAnimation(
    393             int targetX, int targetY, float scale, int kind) {
    394         if (targetX == mCurrentX && targetY == mCurrentY
    395                 && scale == mCurrentScale) return;
    396 
    397         mFromX = mCurrentX;
    398         mFromY = mCurrentY;
    399         mFromScale = mCurrentScale;
    400 
    401         mToX = targetX;
    402         mToY = targetY;
    403         mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax);
    404 
    405         // If the scaled height is smaller than the view height,
    406         // force it to be in the center.
    407         // (We do for height only, not width, because the user may
    408         // want to scroll to the previous/next image.)
    409         if (Math.floor(mImageH * mToScale) <= mViewH) {
    410             mToY = mImageH / 2;
    411         }
    412 
    413         mAnimationStartTime = SystemClock.uptimeMillis();
    414         mAnimationKind = kind;
    415         if (mAnimationKind != ANIM_KIND_FLING) {
    416             mAnimationDuration = ANIM_TIME[mAnimationKind];
    417         }
    418         if (advanceAnimation()) mViewer.invalidate();
    419     }
    420 
    421     // Returns true if redraw is needed.
    422     public boolean advanceAnimation() {
    423         if (mAnimationStartTime == NO_ANIMATION) {
    424             return false;
    425         } else if (mAnimationStartTime == LAST_ANIMATION) {
    426             mAnimationStartTime = NO_ANIMATION;
    427             if (mViewer.isInTransition()) {
    428                 mViewer.notifyTransitionComplete();
    429                 return false;
    430             } else {
    431                 return startSnapbackIfNeeded();
    432             }
    433         }
    434 
    435         long now = SystemClock.uptimeMillis();
    436         float progress;
    437         if (mAnimationDuration == 0) {
    438             progress = 1;
    439         } else {
    440             progress = (now - mAnimationStartTime) / mAnimationDuration;
    441         }
    442 
    443         if (progress >= 1) {
    444             progress = 1;
    445             mCurrentX = mToX;
    446             mCurrentY = mToY;
    447             mCurrentScale = mToScale;
    448             mAnimationStartTime = LAST_ANIMATION;
    449         } else {
    450             float f = 1 - progress;
    451             switch (mAnimationKind) {
    452                 case ANIM_KIND_SCROLL:
    453                 case ANIM_KIND_FLING:
    454                     progress = 1 - f;  // linear
    455                     break;
    456                 case ANIM_KIND_SCALE:
    457                     progress = 1 - f * f;  // quadratic
    458                     break;
    459                 case ANIM_KIND_SNAPBACK:
    460                 case ANIM_KIND_ZOOM:
    461                 case ANIM_KIND_SLIDE:
    462                     progress = 1 - f * f * f * f * f; // x^5
    463                     break;
    464             }
    465             if (mAnimationKind == ANIM_KIND_FLING) {
    466                 flingInterpolate(progress);
    467             } else {
    468                 linearInterpolate(progress);
    469             }
    470         }
    471         mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
    472         return true;
    473     }
    474 
    475     private void flingInterpolate(float progress) {
    476         mScroller.computeScrollOffset(progress);
    477         int oldX = mCurrentX;
    478         int oldY = mCurrentY;
    479         mCurrentX = mScroller.getCurrX();
    480         mCurrentY = mScroller.getCurrY();
    481 
    482         // Check if we hit the edges; show edge effects if we do.
    483         if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
    484             int v = Math.round(-mScroller.getCurrVelocityX() * mCurrentScale);
    485             mEdgeView.onAbsorb(v, EdgeView.LEFT);
    486         } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
    487             int v = Math.round(mScroller.getCurrVelocityX() * mCurrentScale);
    488             mEdgeView.onAbsorb(v, EdgeView.RIGHT);
    489         }
    490 
    491         if (oldY > mBoundTop && mCurrentY == mBoundTop) {
    492             int v = Math.round(-mScroller.getCurrVelocityY() * mCurrentScale);
    493             mEdgeView.onAbsorb(v, EdgeView.TOP);
    494         } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
    495             int v = Math.round(mScroller.getCurrVelocityY() * mCurrentScale);
    496             mEdgeView.onAbsorb(v, EdgeView.BOTTOM);
    497         }
    498     }
    499 
    500     // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1].
    501     private void linearInterpolate(float progress) {
    502         // To linearly interpolate the position on view coordinates, we do the
    503         // following steps:
    504         // (1) convert a bitmap position (x, y) to view coordinates:
    505         //     from: (x - mFromX) * mFromScale + mViewW / 2
    506         //     to: (x - mToX) * mToScale + mViewW / 2
    507         // (2) interpolate between the "from" and "to" coordinates:
    508         //     (x - mFromX) * mFromScale * (1 - p) + (x - mToX) * mToScale * p
    509         //     + mViewW / 2
    510         //     should be equal to
    511         //     (x - mCurrentX) * mCurrentScale + mViewW / 2
    512         // (3) The x-related terms in the above equation can be removed because
    513         //     mFromScale * (1 - p) + ToScale * p = mCurrentScale
    514         // (4) Solve for mCurrentX, we have mCurrentX =
    515         // (mFromX * mFromScale * (1 - p) + mToX * mToScale * p) / mCurrentScale
    516         float fromX = mFromX * mFromScale;
    517         float toX = mToX * mToScale;
    518         float currentX = fromX + progress * (toX - fromX);
    519 
    520         float fromY = mFromY * mFromScale;
    521         float toY = mToY * mToScale;
    522         float currentY = fromY + progress * (toY - fromY);
    523 
    524         mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
    525         mCurrentX = Math.round(currentX / mCurrentScale);
    526         mCurrentY = Math.round(currentY / mCurrentScale);
    527     }
    528 
    529     // Returns true if redraw is needed.
    530     private boolean startSnapbackIfNeeded() {
    531         if (mAnimationStartTime != NO_ANIMATION) return false;
    532         if (mInScale) return false;
    533         if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) {
    534             return false;
    535         }
    536         return startSnapback();
    537     }
    538 
    539     public boolean startSnapback() {
    540         boolean needAnimation = false;
    541         float scale = mCurrentScale;
    542 
    543         if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) {
    544             needAnimation = true;
    545             scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
    546         }
    547 
    548         calculateStableBound(scale, sHorizontalSlack);
    549         int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight);
    550         int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
    551 
    552         if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) {
    553             needAnimation = true;
    554         }
    555 
    556         if (needAnimation) {
    557             startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
    558         }
    559 
    560         return needAnimation;
    561     }
    562 
    563     // Calculates the stable region of mCurrent{X/Y}, where "stable" means
    564     //
    565     // (1) If the dimension of scaled image >= view dimension, we will not
    566     // see black region outside the image (at that dimension).
    567     // (2) If the dimension of scaled image < view dimension, we will center
    568     // the scaled image.
    569     //
    570     // We might temporarily go out of this stable during user interaction,
    571     // but will "snap back" after user stops interaction.
    572     //
    573     // The results are stored in mBound{Left/Right/Top/Bottom}.
    574     //
    575     // An extra parameter "horizontalSlack" (which has the value of 0 usually)
    576     // is used to extend the stable region by some pixels on each side
    577     // horizontally.
    578     private void calculateStableBound(float scale) {
    579         calculateStableBound(scale, 0f);
    580     }
    581 
    582     private void calculateStableBound(float scale, float horizontalSlack) {
    583         // The number of pixels between the center of the view
    584         // and the edge when the edge is aligned.
    585         mBoundLeft = (int) Math.ceil((mViewW - horizontalSlack) / (2 * scale));
    586         mBoundRight = mImageW - mBoundLeft;
    587         mBoundTop = (int) Math.ceil(mViewH / (2 * scale));
    588         mBoundBottom = mImageH - mBoundTop;
    589 
    590         // If the scaled height is smaller than the view height,
    591         // force it to be in the center.
    592         if (Math.floor(mImageH * scale) <= mViewH) {
    593             mBoundTop = mBoundBottom = mImageH / 2;
    594         }
    595 
    596         // Same for width
    597         if (Math.floor(mImageW * scale) <= mViewW) {
    598             mBoundLeft = mBoundRight = mImageW / 2;
    599         }
    600     }
    601 
    602     private boolean useCurrentValueAsTarget() {
    603         return mAnimationStartTime == NO_ANIMATION ||
    604                 mAnimationKind == ANIM_KIND_SNAPBACK ||
    605                 mAnimationKind == ANIM_KIND_FLING;
    606     }
    607 
    608     private float getTargetScale() {
    609         return useCurrentValueAsTarget() ? mCurrentScale : mToScale;
    610     }
    611 
    612     private int getTargetX() {
    613         return useCurrentValueAsTarget() ? mCurrentX : mToX;
    614     }
    615 
    616     private int getTargetY() {
    617         return useCurrentValueAsTarget() ? mCurrentY : mToY;
    618     }
    619 
    620     public RectF getImageBounds() {
    621         float points[] = mTempPoints;
    622 
    623         /*
    624          * (p0,p1)----------(p2,p3)
    625          *   |                  |
    626          *   |                  |
    627          * (p4,p5)----------(p6,p7)
    628          */
    629         points[0] = points[4] = -mCurrentX;
    630         points[1] = points[3] = -mCurrentY;
    631         points[2] = points[6] = mImageW - mCurrentX;
    632         points[5] = points[7] = mImageH - mCurrentY;
    633 
    634         RectF rect = mTempRect;
    635         rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
    636                 Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
    637 
    638         float scale = mCurrentScale;
    639         float offsetX = mViewW / 2;
    640         float offsetY = mViewH / 2;
    641         for (int i = 0; i < 4; ++i) {
    642             float x = points[i + i] * scale + offsetX;
    643             float y = points[i + i + 1] * scale + offsetY;
    644             if (x < rect.left) rect.left = x;
    645             if (x > rect.right) rect.right = x;
    646             if (y < rect.top) rect.top = y;
    647             if (y > rect.bottom) rect.bottom = y;
    648         }
    649         return rect;
    650     }
    651 
    652     public int getImageWidth() {
    653         return mImageW;
    654     }
    655 
    656     public int getImageHeight() {
    657         return mImageH;
    658     }
    659 
    660     public boolean isAtLeftEdge() {
    661         calculateStableBound(mCurrentScale);
    662         return mCurrentX <= mBoundLeft;
    663     }
    664 
    665     public boolean isAtRightEdge() {
    666         calculateStableBound(mCurrentScale);
    667         return mCurrentX >= mBoundRight;
    668     }
    669 }
    670