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