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 android.content.Context;
     20 import android.graphics.Rect;
     21 import android.util.Log;
     22 import android.widget.OverScroller;
     23 
     24 import com.android.gallery3d.common.Utils;
     25 import com.android.gallery3d.util.GalleryUtils;
     26 import com.android.gallery3d.util.RangeArray;
     27 import com.android.gallery3d.util.RangeIntArray;
     28 import com.android.gallery3d.ui.PhotoView.Size;
     29 
     30 class PositionController {
     31     private static final String TAG = "PositionController";
     32 
     33     public static final int IMAGE_AT_LEFT_EDGE = 1;
     34     public static final int IMAGE_AT_RIGHT_EDGE = 2;
     35     public static final int IMAGE_AT_TOP_EDGE = 4;
     36     public static final int IMAGE_AT_BOTTOM_EDGE = 8;
     37 
     38     public static final int CAPTURE_ANIMATION_TIME = 700;
     39     public static final int SNAPBACK_ANIMATION_TIME = 600;
     40 
     41     // Special values for animation time.
     42     private static final long NO_ANIMATION = -1;
     43     private static final long LAST_ANIMATION = -2;
     44 
     45     private static final int ANIM_KIND_NONE = -1;
     46     private static final int ANIM_KIND_SCROLL = 0;
     47     private static final int ANIM_KIND_SCALE = 1;
     48     private static final int ANIM_KIND_SNAPBACK = 2;
     49     private static final int ANIM_KIND_SLIDE = 3;
     50     private static final int ANIM_KIND_ZOOM = 4;
     51     private static final int ANIM_KIND_OPENING = 5;
     52     private static final int ANIM_KIND_FLING = 6;
     53     private static final int ANIM_KIND_FLING_X = 7;
     54     private static final int ANIM_KIND_DELETE = 8;
     55     private static final int ANIM_KIND_CAPTURE = 9;
     56 
     57     // Animation time in milliseconds. The order must match ANIM_KIND_* above.
     58     //
     59     // The values for ANIM_KIND_FLING_X does't matter because we use
     60     // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's
     61     // faster for Animatable.advanceAnimation() to calculate the progress
     62     // (always 1).
     63     private static final int ANIM_TIME[] = {
     64         0,    // ANIM_KIND_SCROLL
     65         0,    // ANIM_KIND_SCALE
     66         SNAPBACK_ANIMATION_TIME,  // ANIM_KIND_SNAPBACK
     67         400,  // ANIM_KIND_SLIDE
     68         300,  // ANIM_KIND_ZOOM
     69         400,  // ANIM_KIND_OPENING
     70         0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
     71         0,    // ANIM_KIND_FLING_X (see the comment above)
     72         0,    // ANIM_KIND_DELETE (the duration is calculated dynamically)
     73         CAPTURE_ANIMATION_TIME,  // ANIM_KIND_CAPTURE
     74     };
     75 
     76     // We try to scale up the image to fill the screen. But in order not to
     77     // scale too much for small icons, we limit the max up-scaling factor here.
     78     private static final float SCALE_LIMIT = 4;
     79 
     80     // For user's gestures, we give a temporary extra scaling range which goes
     81     // above or below the usual scaling limits.
     82     private static final float SCALE_MIN_EXTRA = 0.7f;
     83     private static final float SCALE_MAX_EXTRA = 1.4f;
     84 
     85     // Setting this true makes the extra scaling range permanent (until this is
     86     // set to false again).
     87     private boolean mExtraScalingRange = false;
     88 
     89     // Film Mode v.s. Page Mode: in film mode we show smaller pictures.
     90     private boolean mFilmMode = false;
     91 
     92     // These are the limits for width / height of the picture in film mode.
     93     private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f;
     94     private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f;
     95     private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f;
     96     private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f;
     97 
     98     // In addition to the focused box (index == 0). We also keep information
     99     // about this many boxes on each side.
    100     private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX;
    101     private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1];
    102 
    103     private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16);
    104     private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
    105 
    106     // These are constants for the delete gesture.
    107     private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms
    108     private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms
    109 
    110     private Listener mListener;
    111     private volatile Rect mOpenAnimationRect;
    112 
    113     // Use a large enough value, so we won't see the gray shadown in the beginning.
    114     private int mViewW = 1200;
    115     private int mViewH = 1200;
    116 
    117     // A scaling guesture is in progress.
    118     private boolean mInScale;
    119     // The focus point of the scaling gesture, relative to the center of the
    120     // picture in bitmap pixels.
    121     private float mFocusX, mFocusY;
    122 
    123     // whether there is a previous/next picture.
    124     private boolean mHasPrev, mHasNext;
    125 
    126     // This is used by the fling animation (page mode).
    127     private FlingScroller mPageScroller;
    128 
    129     // This is used by the fling animation (film mode).
    130     private OverScroller mFilmScroller;
    131 
    132     // The bound of the stable region that the focused box can stay, see the
    133     // comments above calculateStableBound() for details.
    134     private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
    135 
    136     // Constrained frame is a rectangle that the focused box should fit into if
    137     // it is constrained. It has two effects:
    138     //
    139     // (1) In page mode, if the focused box is constrained, scaling for the
    140     // focused box is adjusted to fit into the constrained frame, instead of the
    141     // whole view.
    142     //
    143     // (2) In page mode, if the focused box is constrained, the mPlatform's
    144     // default center (mDefaultX/Y) is moved to the center of the constrained
    145     // frame, instead of the view center.
    146     //
    147     private Rect mConstrainedFrame = new Rect();
    148 
    149     // Whether the focused box is constrained.
    150     //
    151     // Our current program's first call to moveBox() sets constrained = true, so
    152     // we set the initial value of this variable to true, and we will not see
    153     // see unwanted transition animation.
    154     private boolean mConstrained = true;
    155 
    156     //
    157     //  ___________________________________________________________
    158     // |   _____       _____       _____       _____       _____   |
    159     // |  |     |     |     |     |     |     |     |     |     |  |
    160     // |  | Box |     | Box |     | Box*|     | Box |     | Box |  |
    161     // |  |_____|.....|_____|.....|_____|.....|_____|.....|_____|  |
    162     // |          Gap         Gap         Gap         Gap          |
    163     // |___________________________________________________________|
    164     //
    165     //                       <--  Platform  -->
    166     //
    167     // The focused box (Box*) centers at mPlatform's (mCurrentX, mCurrentY)
    168 
    169     private Platform mPlatform = new Platform();
    170     private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
    171     // The gap at the right of a Box i is at index i. The gap at the left of a
    172     // Box i is at index i - 1.
    173     private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
    174     private FilmRatio mFilmRatio = new FilmRatio();
    175 
    176     // These are only used during moveBox().
    177     private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
    178     private RangeArray<Gap> mTempGaps =
    179         new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
    180 
    181     // The output of the PositionController. Available throught getPosition().
    182     private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX);
    183 
    184     // The direction of a new picture should appear. New pictures pop from top
    185     // if this value is true, or from bottom if this value is false.
    186     boolean mPopFromTop;
    187 
    188     public interface Listener {
    189         void invalidate();
    190         boolean isHoldingDown();
    191         boolean isHoldingDelete();
    192 
    193         // EdgeView
    194         void onPull(int offset, int direction);
    195         void onRelease();
    196         void onAbsorb(int velocity, int direction);
    197     }
    198 
    199     static {
    200         // Initialize the CENTER_OUT_INDEX array.
    201         // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX
    202         // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX
    203         for (int i = 0; i < CENTER_OUT_INDEX.length; i++) {
    204             int j = (i + 1) / 2;
    205             if ((i & 1) == 0) j = -j;
    206             CENTER_OUT_INDEX[i] = j;
    207         }
    208     }
    209 
    210     public PositionController(Context context, Listener listener) {
    211         mListener = listener;
    212         mPageScroller = new FlingScroller();
    213         mFilmScroller = new OverScroller(context,
    214                 null /* default interpolator */, false /* no flywheel */);
    215 
    216         // Initialize the areas.
    217         initPlatform();
    218         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
    219             mBoxes.put(i, new Box());
    220             initBox(i);
    221             mRects.put(i, new Rect());
    222         }
    223         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
    224             mGaps.put(i, new Gap());
    225             initGap(i);
    226         }
    227     }
    228 
    229     public void setOpenAnimationRect(Rect r) {
    230         mOpenAnimationRect = r;
    231     }
    232 
    233     public void setViewSize(int viewW, int viewH) {
    234         if (viewW == mViewW && viewH == mViewH) return;
    235 
    236         boolean wasMinimal = isAtMinimalScale();
    237 
    238         mViewW = viewW;
    239         mViewH = viewH;
    240         initPlatform();
    241 
    242         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
    243             setBoxSize(i, viewW, viewH, true);
    244         }
    245 
    246         updateScaleAndGapLimit();
    247 
    248         // If the focused box was at minimal scale, we try to make it the
    249         // minimal scale under the new view size.
    250         if (wasMinimal) {
    251             Box b = mBoxes.get(0);
    252             b.mCurrentScale = b.mScaleMin;
    253         }
    254 
    255         // If we have the opening animation, do it. Otherwise go directly to the
    256         // right position.
    257         if (!startOpeningAnimationIfNeeded()) {
    258             skipToFinalPosition();
    259         }
    260     }
    261 
    262     public void setConstrainedFrame(Rect cFrame) {
    263         if (mConstrainedFrame.equals(cFrame)) return;
    264         mConstrainedFrame.set(cFrame);
    265         mPlatform.updateDefaultXY();
    266         updateScaleAndGapLimit();
    267         snapAndRedraw();
    268     }
    269 
    270     public void forceImageSize(int index, Size s) {
    271         if (s.width == 0 || s.height == 0) return;
    272         Box b = mBoxes.get(index);
    273         b.mImageW = s.width;
    274         b.mImageH = s.height;
    275         return;
    276     }
    277 
    278     public void setImageSize(int index, Size s, Rect cFrame) {
    279         if (s.width == 0 || s.height == 0) return;
    280 
    281         boolean needUpdate = false;
    282         if (cFrame != null && !mConstrainedFrame.equals(cFrame)) {
    283             mConstrainedFrame.set(cFrame);
    284             mPlatform.updateDefaultXY();
    285             needUpdate = true;
    286         }
    287         needUpdate |= setBoxSize(index, s.width, s.height, false);
    288 
    289         if (!needUpdate) return;
    290         updateScaleAndGapLimit();
    291         snapAndRedraw();
    292     }
    293 
    294     // Returns false if the box size doesn't change.
    295     private boolean setBoxSize(int i, int width, int height, boolean isViewSize) {
    296         Box b = mBoxes.get(i);
    297         boolean wasViewSize = b.mUseViewSize;
    298 
    299         // If we already have an image size, we don't want to use the view size.
    300         if (!wasViewSize && isViewSize) return false;
    301 
    302         b.mUseViewSize = isViewSize;
    303 
    304         if (width == b.mImageW && height == b.mImageH) {
    305             return false;
    306         }
    307 
    308         // The ratio of the old size and the new size.
    309         //
    310         // If the aspect ratio changes, we don't know if it is because one side
    311         // grows or the other side shrinks. Currently we just assume the view
    312         // angle of the longer side doesn't change (so the aspect ratio change
    313         // is because the view angle of the shorter side changes). This matches
    314         // what camera preview does.
    315         float ratio = (width > height)
    316                 ? (float) b.mImageW / width
    317                 : (float) b.mImageH / height;
    318 
    319         b.mImageW = width;
    320         b.mImageH = height;
    321 
    322         // If this is the first time we receive an image size, we change the
    323         // scale directly. Otherwise adjust the scales by a ratio, and snapback
    324         // will animate the scale into the min/max bounds if necessary.
    325         if (wasViewSize && !isViewSize) {
    326             b.mCurrentScale = getMinimalScale(b);
    327             b.mAnimationStartTime = NO_ANIMATION;
    328         } else {
    329             b.mCurrentScale *= ratio;
    330             b.mFromScale *= ratio;
    331             b.mToScale *= ratio;
    332         }
    333 
    334         if (i == 0) {
    335             mFocusX /= ratio;
    336             mFocusY /= ratio;
    337         }
    338 
    339         return true;
    340     }
    341 
    342     private boolean startOpeningAnimationIfNeeded() {
    343         if (mOpenAnimationRect == null) return false;
    344         Box b = mBoxes.get(0);
    345         if (b.mUseViewSize) return false;
    346 
    347         // Start animation from the saved rectangle if we have one.
    348         Rect r = mOpenAnimationRect;
    349         mOpenAnimationRect = null;
    350 
    351         mPlatform.mCurrentX = r.centerX() - mViewW / 2;
    352         b.mCurrentY = r.centerY() - mViewH / 2;
    353         b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
    354                 r.height() / (float) b.mImageH);
    355         startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin,
    356                 ANIM_KIND_OPENING);
    357 
    358         // Animate from large gaps for neighbor boxes to avoid them
    359         // shown on the screen during opening animation.
    360         for (int i = -1; i < 1; i++) {
    361             Gap g = mGaps.get(i);
    362             g.mCurrentGap = mViewW;
    363             g.doAnimation(g.mDefaultSize, ANIM_KIND_OPENING);
    364         }
    365 
    366         return true;
    367     }
    368 
    369     public void setFilmMode(boolean enabled) {
    370         if (enabled == mFilmMode) return;
    371         mFilmMode = enabled;
    372 
    373         mPlatform.updateDefaultXY();
    374         updateScaleAndGapLimit();
    375         stopAnimation();
    376         snapAndRedraw();
    377     }
    378 
    379     public void setExtraScalingRange(boolean enabled) {
    380         if (mExtraScalingRange == enabled) return;
    381         mExtraScalingRange = enabled;
    382         if (!enabled) {
    383             snapAndRedraw();
    384         }
    385     }
    386 
    387     // This should be called whenever the scale range of boxes or the default
    388     // gap size may change. Currently this can happen due to change of view
    389     // size, image size, mFilmMode, mConstrained, and mConstrainedFrame.
    390     private void updateScaleAndGapLimit() {
    391         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
    392             Box b = mBoxes.get(i);
    393             b.mScaleMin = getMinimalScale(b);
    394             b.mScaleMax = getMaximalScale(b);
    395         }
    396 
    397         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
    398             Gap g = mGaps.get(i);
    399             g.mDefaultSize = getDefaultGapSize(i);
    400         }
    401     }
    402 
    403     // Returns the default gap size according the the size of the boxes around
    404     // the gap and the current mode.
    405     private int getDefaultGapSize(int i) {
    406         if (mFilmMode) return IMAGE_GAP;
    407         Box a = mBoxes.get(i);
    408         Box b = mBoxes.get(i + 1);
    409         return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b));
    410     }
    411 
    412     // Here is how we layout the boxes in the page mode.
    413     //
    414     //   previous             current             next
    415     //  ___________       ________________     __________
    416     // |  _______  |     |   __________   |   |  ______  |
    417     // | |       | |     |  |   right->|  |   | |      | |
    418     // | |       |<-------->|<--left   |  |   | |      | |
    419     // | |_______| |  |  |  |__________|  |   | |______| |
    420     // |___________|  |  |________________|   |__________|
    421     //                |  <--> gapToSide()
    422     //                |
    423     // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current))
    424     private int gapToSide(Box b) {
    425         return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f);
    426     }
    427 
    428     // Stop all animations at where they are now.
    429     public void stopAnimation() {
    430         mPlatform.mAnimationStartTime = NO_ANIMATION;
    431         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
    432             mBoxes.get(i).mAnimationStartTime = NO_ANIMATION;
    433         }
    434         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
    435             mGaps.get(i).mAnimationStartTime = NO_ANIMATION;
    436         }
    437     }
    438 
    439     public void skipAnimation() {
    440         if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
    441             mPlatform.mCurrentX = mPlatform.mToX;
    442             mPlatform.mCurrentY = mPlatform.mToY;
    443             mPlatform.mAnimationStartTime = NO_ANIMATION;
    444         }
    445         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
    446             Box b = mBoxes.get(i);
    447             if (b.mAnimationStartTime == NO_ANIMATION) continue;
    448             b.mCurrentY = b.mToY;
    449             b.mCurrentScale = b.mToScale;
    450             b.mAnimationStartTime = NO_ANIMATION;
    451         }
    452         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
    453             Gap g = mGaps.get(i);
    454             if (g.mAnimationStartTime == NO_ANIMATION) continue;
    455             g.mCurrentGap = g.mToGap;
    456             g.mAnimationStartTime = NO_ANIMATION;
    457         }
    458         redraw();
    459     }
    460 
    461     public void snapback() {
    462         snapAndRedraw();
    463     }
    464 
    465     public void skipToFinalPosition() {
    466         stopAnimation();
    467         snapAndRedraw();
    468         skipAnimation();
    469     }
    470 
    471     ////////////////////////////////////////////////////////////////////////////
    472     //  Start an animations for the focused box
    473     ////////////////////////////////////////////////////////////////////////////
    474 
    475     public void zoomIn(float tapX, float tapY, float targetScale) {
    476         tapX -= mViewW / 2;
    477         tapY -= mViewH / 2;
    478         Box b = mBoxes.get(0);
    479 
    480         // Convert the tap position to distance to center in bitmap coordinates
    481         float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale;
    482         float tempY = (tapY - b.mCurrentY) / b.mCurrentScale;
    483 
    484         int x = (int) (-tempX * targetScale + 0.5f);
    485         int y = (int) (-tempY * targetScale + 0.5f);
    486 
    487         calculateStableBound(targetScale);
    488         int targetX = Utils.clamp(x, mBoundLeft, mBoundRight);
    489         int targetY = Utils.clamp(y, mBoundTop, mBoundBottom);
    490         targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax);
    491 
    492         startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
    493     }
    494 
    495     public void resetToFullView() {
    496         Box b = mBoxes.get(0);
    497         startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_ZOOM);
    498     }
    499 
    500     public void beginScale(float focusX, float focusY) {
    501         focusX -= mViewW / 2;
    502         focusY -= mViewH / 2;
    503         Box b = mBoxes.get(0);
    504         Platform p = mPlatform;
    505         mInScale = true;
    506         mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f);
    507         mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f);
    508     }
    509 
    510     // Scales the image by the given factor.
    511     // Returns an out-of-range indicator:
    512     //   1 if the intended scale is too large for the stable range.
    513     //   0 if the intended scale is in the stable range.
    514     //  -1 if the intended scale is too small for the stable range.
    515     public int scaleBy(float s, float focusX, float focusY) {
    516         focusX -= mViewW / 2;
    517         focusY -= mViewH / 2;
    518         Box b = mBoxes.get(0);
    519         Platform p = mPlatform;
    520 
    521         // We want to keep the focus point (on the bitmap) the same as when we
    522         // begin the scale guesture, that is,
    523         //
    524         // (focusX' - currentX') / scale' = (focusX - currentX) / scale
    525         //
    526         s = b.clampScale(s * getTargetScale(b));
    527         int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f);
    528         int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f);
    529         startAnimation(x, y, s, ANIM_KIND_SCALE);
    530         if (s < b.mScaleMin) return -1;
    531         if (s > b.mScaleMax) return 1;
    532         return 0;
    533     }
    534 
    535     public void endScale() {
    536         mInScale = false;
    537         snapAndRedraw();
    538     }
    539 
    540     // Slide the focused box to the center of the view.
    541     public void startHorizontalSlide() {
    542         Box b = mBoxes.get(0);
    543         startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_SLIDE);
    544     }
    545 
    546     // Slide the focused box to the center of the view with the capture
    547     // animation. In addition to the sliding, the animation will also scale the
    548     // the focused box, the specified neighbor box, and the gap between the
    549     // two. The specified offset should be 1 or -1.
    550     public void startCaptureAnimationSlide(int offset) {
    551         Box b = mBoxes.get(0);
    552         Box n = mBoxes.get(offset);  // the neighbor box
    553         Gap g = mGaps.get(offset);  // the gap between the two boxes
    554 
    555         mPlatform.doAnimation(mPlatform.mDefaultX, mPlatform.mDefaultY,
    556                 ANIM_KIND_CAPTURE);
    557         b.doAnimation(0, b.mScaleMin, ANIM_KIND_CAPTURE);
    558         n.doAnimation(0, n.mScaleMin, ANIM_KIND_CAPTURE);
    559         g.doAnimation(g.mDefaultSize, ANIM_KIND_CAPTURE);
    560         redraw();
    561     }
    562 
    563     // Only allow scrolling when we are not currently in an animation or we
    564     // are in some animation with can be interrupted.
    565     private boolean canScroll() {
    566         Box b = mBoxes.get(0);
    567         if (b.mAnimationStartTime == NO_ANIMATION) return true;
    568         switch (b.mAnimationKind) {
    569             case ANIM_KIND_SCROLL:
    570             case ANIM_KIND_FLING:
    571             case ANIM_KIND_FLING_X:
    572                 return true;
    573         }
    574         return false;
    575     }
    576 
    577     public void scrollPage(int dx, int dy) {
    578         if (!canScroll()) return;
    579 
    580         Box b = mBoxes.get(0);
    581         Platform p = mPlatform;
    582 
    583         calculateStableBound(b.mCurrentScale);
    584 
    585         int x = p.mCurrentX + dx;
    586         int y = b.mCurrentY + dy;
    587 
    588         // Vertical direction: If we have space to move in the vertical
    589         // direction, we show the edge effect when scrolling reaches the edge.
    590         if (mBoundTop != mBoundBottom) {
    591             if (y < mBoundTop) {
    592                 mListener.onPull(mBoundTop - y, EdgeView.BOTTOM);
    593             } else if (y > mBoundBottom) {
    594                 mListener.onPull(y - mBoundBottom, EdgeView.TOP);
    595             }
    596         }
    597 
    598         y = Utils.clamp(y, mBoundTop, mBoundBottom);
    599 
    600         // Horizontal direction: we show the edge effect when the scrolling
    601         // tries to go left of the first image or go right of the last image.
    602         if (!mHasPrev && x > mBoundRight) {
    603             int pixels = x - mBoundRight;
    604             mListener.onPull(pixels, EdgeView.LEFT);
    605             x = mBoundRight;
    606         } else if (!mHasNext && x < mBoundLeft) {
    607             int pixels = mBoundLeft - x;
    608             mListener.onPull(pixels, EdgeView.RIGHT);
    609             x = mBoundLeft;
    610         }
    611 
    612         startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
    613     }
    614 
    615     public void scrollFilmX(int dx) {
    616         if (!canScroll()) return;
    617 
    618         Box b = mBoxes.get(0);
    619         Platform p = mPlatform;
    620 
    621         // Only allow scrolling when we are not currently in an animation or we
    622         // are in some animation with can be interrupted.
    623         if (b.mAnimationStartTime != NO_ANIMATION) {
    624             switch (b.mAnimationKind) {
    625                 case ANIM_KIND_SCROLL:
    626                 case ANIM_KIND_FLING:
    627                 case ANIM_KIND_FLING_X:
    628                     break;
    629                 default:
    630                     return;
    631             }
    632         }
    633 
    634         int x = p.mCurrentX + dx;
    635 
    636         // Horizontal direction: we show the edge effect when the scrolling
    637         // tries to go left of the first image or go right of the last image.
    638         x -= mPlatform.mDefaultX;
    639         if (!mHasPrev && x > 0) {
    640             mListener.onPull(x, EdgeView.LEFT);
    641             x = 0;
    642         } else if (!mHasNext && x < 0) {
    643             mListener.onPull(-x, EdgeView.RIGHT);
    644             x = 0;
    645         }
    646         x += mPlatform.mDefaultX;
    647         startAnimation(x, b.mCurrentY, b.mCurrentScale, ANIM_KIND_SCROLL);
    648     }
    649 
    650     public void scrollFilmY(int boxIndex, int dy) {
    651         if (!canScroll()) return;
    652 
    653         Box b = mBoxes.get(boxIndex);
    654         int y = b.mCurrentY + dy;
    655         b.doAnimation(y, b.mCurrentScale, ANIM_KIND_SCROLL);
    656         redraw();
    657     }
    658 
    659     public boolean flingPage(int velocityX, int velocityY) {
    660         Box b = mBoxes.get(0);
    661         Platform p = mPlatform;
    662 
    663         // We only want to do fling when the picture is zoomed-in.
    664         if (viewWiderThanScaledImage(b.mCurrentScale) &&
    665             viewTallerThanScaledImage(b.mCurrentScale)) {
    666             return false;
    667         }
    668 
    669         // We only allow flinging in the directions where it won't go over the
    670         // picture.
    671         int edges = getImageAtEdges();
    672         if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
    673             (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
    674             velocityX = 0;
    675         }
    676         if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
    677             (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
    678             velocityY = 0;
    679         }
    680 
    681         if (velocityX == 0 && velocityY == 0) return false;
    682 
    683         mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY,
    684                 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
    685         int targetX = mPageScroller.getFinalX();
    686         int targetY = mPageScroller.getFinalY();
    687         ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
    688         return startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
    689     }
    690 
    691     public boolean flingFilmX(int velocityX) {
    692         if (velocityX == 0) return false;
    693 
    694         Box b = mBoxes.get(0);
    695         Platform p = mPlatform;
    696 
    697         // If we are already at the edge, don't start the fling.
    698         int defaultX = p.mDefaultX;
    699         if ((!mHasPrev && p.mCurrentX >= defaultX)
    700                 || (!mHasNext && p.mCurrentX <= defaultX)) {
    701             return false;
    702         }
    703 
    704         mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
    705                 Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
    706         int targetX = mFilmScroller.getFinalX();
    707         return startAnimation(
    708                 targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING_X);
    709     }
    710 
    711     // Moves the specified box out of screen. If velocityY is 0, a default
    712     // velocity is used. Returns the time for the duration, or -1 if we cannot
    713     // not do the animation.
    714     public int flingFilmY(int boxIndex, int velocityY) {
    715         Box b = mBoxes.get(boxIndex);
    716 
    717         // Calculate targetY
    718         int h = heightOf(b);
    719         int targetY;
    720         int FUZZY = 3;  // TODO: figure out why this is needed.
    721         if (velocityY < 0 || (velocityY == 0 && b.mCurrentY <= 0)) {
    722             targetY = -mViewH / 2 - (h + 1) / 2 - FUZZY;
    723         } else {
    724             targetY = (mViewH + 1) / 2 + h / 2 + FUZZY;
    725         }
    726 
    727         // Calculate duration
    728         int duration;
    729         if (velocityY != 0) {
    730             duration = (int) (Math.abs(targetY - b.mCurrentY) * 1000f
    731                     / Math.abs(velocityY));
    732             duration = Math.min(MAX_DELETE_ANIMATION_DURATION, duration);
    733         } else {
    734             duration = DEFAULT_DELETE_ANIMATION_DURATION;
    735         }
    736 
    737         // Start animation
    738         ANIM_TIME[ANIM_KIND_DELETE] = duration;
    739         if (b.doAnimation(targetY, b.mCurrentScale, ANIM_KIND_DELETE)) {
    740             redraw();
    741             return duration;
    742         }
    743         return -1;
    744     }
    745 
    746     // Returns the index of the box which contains the given point (x, y)
    747     // Returns Integer.MAX_VALUE if there is no hit. There may be more than
    748     // one box contains the given point, and we want to give priority to the
    749     // one closer to the focused index (0).
    750     public int hitTest(int x, int y) {
    751         for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
    752             int j = CENTER_OUT_INDEX[i];
    753             Rect r = mRects.get(j);
    754             if (r.contains(x, y)) {
    755                 return j;
    756             }
    757         }
    758 
    759         return Integer.MAX_VALUE;
    760     }
    761 
    762     ////////////////////////////////////////////////////////////////////////////
    763     //  Redraw
    764     //
    765     //  If a method changes box positions directly, redraw()
    766     //  should be called.
    767     //
    768     //  If a method may also cause a snapback to happen, snapAndRedraw() should
    769     //  be called.
    770     //
    771     //  If a method starts an animation to change the position of focused box,
    772     //  startAnimation() should be called.
    773     //
    774     //  If time advances to change the box position, advanceAnimation() should
    775     //  be called.
    776     ////////////////////////////////////////////////////////////////////////////
    777     private void redraw() {
    778         layoutAndSetPosition();
    779         mListener.invalidate();
    780     }
    781 
    782     private void snapAndRedraw() {
    783         mPlatform.startSnapback();
    784         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
    785             mBoxes.get(i).startSnapback();
    786         }
    787         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
    788             mGaps.get(i).startSnapback();
    789         }
    790         mFilmRatio.startSnapback();
    791         redraw();
    792     }
    793 
    794     private boolean startAnimation(int targetX, int targetY, float targetScale,
    795             int kind) {
    796         boolean changed = false;
    797         changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind);
    798         changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
    799         if (changed) redraw();
    800         return changed;
    801     }
    802 
    803     public void advanceAnimation() {
    804         boolean changed = false;
    805         changed |= mPlatform.advanceAnimation();
    806         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
    807             changed |= mBoxes.get(i).advanceAnimation();
    808         }
    809         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
    810             changed |= mGaps.get(i).advanceAnimation();
    811         }
    812         changed |= mFilmRatio.advanceAnimation();
    813         if (changed) redraw();
    814     }
    815 
    816     public boolean inOpeningAnimation() {
    817         return (mPlatform.mAnimationKind == ANIM_KIND_OPENING &&
    818                 mPlatform.mAnimationStartTime != NO_ANIMATION) ||
    819                (mBoxes.get(0).mAnimationKind == ANIM_KIND_OPENING &&
    820                 mBoxes.get(0).mAnimationStartTime != NO_ANIMATION);
    821     }
    822 
    823     ////////////////////////////////////////////////////////////////////////////
    824     //  Layout
    825     ////////////////////////////////////////////////////////////////////////////
    826 
    827     // Returns the display width of this box.
    828     private int widthOf(Box b) {
    829         return (int) (b.mImageW * b.mCurrentScale + 0.5f);
    830     }
    831 
    832     // Returns the display height of this box.
    833     private int heightOf(Box b) {
    834         return (int) (b.mImageH * b.mCurrentScale + 0.5f);
    835     }
    836 
    837     // Returns the display width of this box, using the given scale.
    838     private int widthOf(Box b, float scale) {
    839         return (int) (b.mImageW * scale + 0.5f);
    840     }
    841 
    842     // Returns the display height of this box, using the given scale.
    843     private int heightOf(Box b, float scale) {
    844         return (int) (b.mImageH * scale + 0.5f);
    845     }
    846 
    847     // Convert the information in mPlatform and mBoxes to mRects, so the user
    848     // can get the position of each box by getPosition().
    849     //
    850     // Note we go from center-out because each box's X coordinate
    851     // is relative to its anchor box (except the focused box).
    852     private void layoutAndSetPosition() {
    853         for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
    854             convertBoxToRect(CENTER_OUT_INDEX[i]);
    855         }
    856         //dumpState();
    857     }
    858 
    859     private void dumpState() {
    860         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
    861             Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
    862         }
    863 
    864         for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
    865             dumpRect(CENTER_OUT_INDEX[i]);
    866         }
    867 
    868         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
    869             for (int j = i + 1; j <= BOX_MAX; j++) {
    870                 if (Rect.intersects(mRects.get(i), mRects.get(j))) {
    871                     Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
    872                 }
    873             }
    874         }
    875     }
    876 
    877     private void dumpRect(int i) {
    878         StringBuilder sb = new StringBuilder();
    879         Rect r = mRects.get(i);
    880         sb.append("Rect " + i + ":");
    881         sb.append("(");
    882         sb.append(r.centerX());
    883         sb.append(",");
    884         sb.append(r.centerY());
    885         sb.append(") [");
    886         sb.append(r.width());
    887         sb.append("x");
    888         sb.append(r.height());
    889         sb.append("]");
    890         Log.d(TAG, sb.toString());
    891     }
    892 
    893     private void convertBoxToRect(int i) {
    894         Box b = mBoxes.get(i);
    895         Rect r = mRects.get(i);
    896         int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2;
    897         int w = widthOf(b);
    898         int h = heightOf(b);
    899         if (i == 0) {
    900             int x = mPlatform.mCurrentX + mViewW / 2;
    901             r.left = x - w / 2;
    902             r.right = r.left + w;
    903         } else if (i > 0) {
    904             Rect a = mRects.get(i - 1);
    905             Gap g = mGaps.get(i - 1);
    906             r.left = a.right + g.mCurrentGap;
    907             r.right = r.left + w;
    908         } else {  // i < 0
    909             Rect a = mRects.get(i + 1);
    910             Gap g = mGaps.get(i);
    911             r.right = a.left - g.mCurrentGap;
    912             r.left = r.right - w;
    913         }
    914         r.top = y - h / 2;
    915         r.bottom = r.top + h;
    916     }
    917 
    918     // Returns the position of a box.
    919     public Rect getPosition(int index) {
    920         return mRects.get(index);
    921     }
    922 
    923     ////////////////////////////////////////////////////////////////////////////
    924     //  Box management
    925     ////////////////////////////////////////////////////////////////////////////
    926 
    927     // Initialize the platform to be at the view center.
    928     private void initPlatform() {
    929         mPlatform.updateDefaultXY();
    930         mPlatform.mCurrentX = mPlatform.mDefaultX;
    931         mPlatform.mCurrentY = mPlatform.mDefaultY;
    932         mPlatform.mAnimationStartTime = NO_ANIMATION;
    933     }
    934 
    935     // Initialize a box to have the size of the view.
    936     private void initBox(int index) {
    937         Box b = mBoxes.get(index);
    938         b.mImageW = mViewW;
    939         b.mImageH = mViewH;
    940         b.mUseViewSize = true;
    941         b.mScaleMin = getMinimalScale(b);
    942         b.mScaleMax = getMaximalScale(b);
    943         b.mCurrentY = 0;
    944         b.mCurrentScale = b.mScaleMin;
    945         b.mAnimationStartTime = NO_ANIMATION;
    946         b.mAnimationKind = ANIM_KIND_NONE;
    947     }
    948 
    949     // Initialize a box to a given size.
    950     private void initBox(int index, Size size) {
    951         if (size.width == 0 || size.height == 0) {
    952             initBox(index);
    953             return;
    954         }
    955         Box b = mBoxes.get(index);
    956         b.mImageW = size.width;
    957         b.mImageH = size.height;
    958         b.mUseViewSize = false;
    959         b.mScaleMin = getMinimalScale(b);
    960         b.mScaleMax = getMaximalScale(b);
    961         b.mCurrentY = 0;
    962         b.mCurrentScale = b.mScaleMin;
    963         b.mAnimationStartTime = NO_ANIMATION;
    964         b.mAnimationKind = ANIM_KIND_NONE;
    965     }
    966 
    967     // Initialize a gap. This can only be called after the boxes around the gap
    968     // has been initialized.
    969     private void initGap(int index) {
    970         Gap g = mGaps.get(index);
    971         g.mDefaultSize = getDefaultGapSize(index);
    972         g.mCurrentGap = g.mDefaultSize;
    973         g.mAnimationStartTime = NO_ANIMATION;
    974     }
    975 
    976     private void initGap(int index, int size) {
    977         Gap g = mGaps.get(index);
    978         g.mDefaultSize = getDefaultGapSize(index);
    979         g.mCurrentGap = size;
    980         g.mAnimationStartTime = NO_ANIMATION;
    981     }
    982 
    983     private void debugMoveBox(int fromIndex[]) {
    984         StringBuilder s = new StringBuilder("moveBox:");
    985         for (int i = 0; i < fromIndex.length; i++) {
    986             int j = fromIndex[i];
    987             if (j == Integer.MAX_VALUE) {
    988                 s.append(" N");
    989             } else {
    990                 s.append(" ");
    991                 s.append(fromIndex[i]);
    992             }
    993         }
    994         Log.d(TAG, s.toString());
    995     }
    996 
    997     // Move the boxes: it may indicate focus change, box deleted, box appearing,
    998     // box reordered, etc.
    999     //
   1000     // Each element in the fromIndex array indicates where each box was in the
   1001     // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
   1002     // means the box is new.
   1003     //
   1004     // For example:
   1005     // N N N N N N N -- all new boxes
   1006     // -3 -2 -1 0 1 2 3 -- nothing changed
   1007     // -2 -1 0 1 2 3 N -- focus goes to the next box
   1008     // N -3 -2 -1 0 1 2 -- focuse goes to the previous box
   1009     // -3 -2 -1 1 2 3 N -- the focused box was deleted.
   1010     //
   1011     // hasPrev/hasNext indicates if there are previous/next boxes for the
   1012     // focused box. constrained indicates whether the focused box should be put
   1013     // into the constrained frame.
   1014     public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext,
   1015             boolean constrained, Size[] sizes) {
   1016         //debugMoveBox(fromIndex);
   1017         mHasPrev = hasPrev;
   1018         mHasNext = hasNext;
   1019 
   1020         RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
   1021 
   1022         // 1. Get the absolute X coordiates for the boxes.
   1023         layoutAndSetPosition();
   1024         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
   1025             Box b = mBoxes.get(i);
   1026             Rect r = mRects.get(i);
   1027             b.mAbsoluteX = r.centerX() - mViewW / 2;
   1028         }
   1029 
   1030         // 2. copy boxes and gaps to temporary storage.
   1031         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
   1032             mTempBoxes.put(i, mBoxes.get(i));
   1033             mBoxes.put(i, null);
   1034         }
   1035         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
   1036             mTempGaps.put(i, mGaps.get(i));
   1037             mGaps.put(i, null);
   1038         }
   1039 
   1040         // 3. move back boxes that are used in the new array.
   1041         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
   1042             int j = from.get(i);
   1043             if (j == Integer.MAX_VALUE) continue;
   1044             mBoxes.put(i, mTempBoxes.get(j));
   1045             mTempBoxes.put(j, null);
   1046         }
   1047 
   1048         // 4. move back gaps if both boxes around it are kept together.
   1049         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
   1050             int j = from.get(i);
   1051             if (j == Integer.MAX_VALUE) continue;
   1052             int k = from.get(i + 1);
   1053             if (k == Integer.MAX_VALUE) continue;
   1054             if (j + 1 == k) {
   1055                 mGaps.put(i, mTempGaps.get(j));
   1056                 mTempGaps.put(j, null);
   1057             }
   1058         }
   1059 
   1060         // 5. recycle the boxes that are not used in the new array.
   1061         int k = -BOX_MAX;
   1062         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
   1063             if (mBoxes.get(i) != null) continue;
   1064             while (mTempBoxes.get(k) == null) {
   1065                 k++;
   1066             }
   1067             mBoxes.put(i, mTempBoxes.get(k++));
   1068             initBox(i, sizes[i + BOX_MAX]);
   1069         }
   1070 
   1071         // 6. Now give the recycled box a reasonable absolute X position.
   1072         //
   1073         // First try to find the first and the last box which the absolute X
   1074         // position is known.
   1075         int first, last;
   1076         for (first = -BOX_MAX; first <= BOX_MAX; first++) {
   1077             if (from.get(first) != Integer.MAX_VALUE) break;
   1078         }
   1079         for (last = BOX_MAX; last >= -BOX_MAX; last--) {
   1080             if (from.get(last) != Integer.MAX_VALUE) break;
   1081         }
   1082         // If there is no box has known X position at all, make the focused one
   1083         // as known.
   1084         if (first > BOX_MAX) {
   1085             mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
   1086             first = last = 0;
   1087         }
   1088         // Now for those boxes between first and last, assign their position to
   1089         // align to the previous box or the next box with known position. For
   1090         // the boxes before first or after last, we will use a new default gap
   1091         // size below.
   1092 
   1093         // Align to the previous box
   1094         for (int i = Math.max(0, first + 1); i < last; i++) {
   1095             if (from.get(i) != Integer.MAX_VALUE) continue;
   1096             Box a = mBoxes.get(i - 1);
   1097             Box b = mBoxes.get(i);
   1098             int wa = widthOf(a);
   1099             int wb = widthOf(b);
   1100             b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2
   1101                     + getDefaultGapSize(i);
   1102             if (mPopFromTop) {
   1103                 b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
   1104             } else {
   1105                 b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
   1106             }
   1107         }
   1108 
   1109         // Align to the next box
   1110         for (int i = Math.min(-1, last - 1); i > first; i--) {
   1111             if (from.get(i) != Integer.MAX_VALUE) continue;
   1112             Box a = mBoxes.get(i + 1);
   1113             Box b = mBoxes.get(i);
   1114             int wa = widthOf(a);
   1115             int wb = widthOf(b);
   1116             b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2)
   1117                     - getDefaultGapSize(i);
   1118             if (mPopFromTop) {
   1119                 b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
   1120             } else {
   1121                 b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
   1122             }
   1123         }
   1124 
   1125         // 7. recycle the gaps that are not used in the new array.
   1126         k = -BOX_MAX;
   1127         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
   1128             if (mGaps.get(i) != null) continue;
   1129             while (mTempGaps.get(k) == null) {
   1130                 k++;
   1131             }
   1132             mGaps.put(i, mTempGaps.get(k++));
   1133             Box a = mBoxes.get(i);
   1134             Box b = mBoxes.get(i + 1);
   1135             int wa = widthOf(a);
   1136             int wb = widthOf(b);
   1137             if (i >= first && i < last) {
   1138                 int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
   1139                 initGap(i, g);
   1140             } else {
   1141                 initGap(i);
   1142             }
   1143         }
   1144 
   1145         // 8. calculate the new absolute X coordinates for those box before
   1146         // first or after last.
   1147         for (int i = first - 1; i >= -BOX_MAX; i--) {
   1148             Box a = mBoxes.get(i + 1);
   1149             Box b = mBoxes.get(i);
   1150             int wa = widthOf(a);
   1151             int wb = widthOf(b);
   1152             Gap g = mGaps.get(i);
   1153             b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap;
   1154         }
   1155 
   1156         for (int i = last + 1; i <= BOX_MAX; i++) {
   1157             Box a = mBoxes.get(i - 1);
   1158             Box b = mBoxes.get(i);
   1159             int wa = widthOf(a);
   1160             int wb = widthOf(b);
   1161             Gap g = mGaps.get(i - 1);
   1162             b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap;
   1163         }
   1164 
   1165         // 9. offset the Platform position
   1166         int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
   1167         mPlatform.mCurrentX += dx;
   1168         mPlatform.mFromX += dx;
   1169         mPlatform.mToX += dx;
   1170         mPlatform.mFlingOffset += dx;
   1171 
   1172         if (mConstrained != constrained) {
   1173             mConstrained = constrained;
   1174             mPlatform.updateDefaultXY();
   1175             updateScaleAndGapLimit();
   1176         }
   1177 
   1178         snapAndRedraw();
   1179     }
   1180 
   1181     ////////////////////////////////////////////////////////////////////////////
   1182     //  Public utilities
   1183     ////////////////////////////////////////////////////////////////////////////
   1184 
   1185     public boolean isAtMinimalScale() {
   1186         Box b = mBoxes.get(0);
   1187         return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
   1188     }
   1189 
   1190     public boolean isCenter() {
   1191         Box b = mBoxes.get(0);
   1192         return mPlatform.mCurrentX == mPlatform.mDefaultX
   1193             && b.mCurrentY == 0;
   1194     }
   1195 
   1196     public int getImageWidth() {
   1197         Box b = mBoxes.get(0);
   1198         return b.mImageW;
   1199     }
   1200 
   1201     public int getImageHeight() {
   1202         Box b = mBoxes.get(0);
   1203         return b.mImageH;
   1204     }
   1205 
   1206     public float getImageScale() {
   1207         Box b = mBoxes.get(0);
   1208         return b.mCurrentScale;
   1209     }
   1210 
   1211     public int getImageAtEdges() {
   1212         Box b = mBoxes.get(0);
   1213         Platform p = mPlatform;
   1214         calculateStableBound(b.mCurrentScale);
   1215         int edges = 0;
   1216         if (p.mCurrentX <= mBoundLeft) {
   1217             edges |= IMAGE_AT_RIGHT_EDGE;
   1218         }
   1219         if (p.mCurrentX >= mBoundRight) {
   1220             edges |= IMAGE_AT_LEFT_EDGE;
   1221         }
   1222         if (b.mCurrentY <= mBoundTop) {
   1223             edges |= IMAGE_AT_BOTTOM_EDGE;
   1224         }
   1225         if (b.mCurrentY >= mBoundBottom) {
   1226             edges |= IMAGE_AT_TOP_EDGE;
   1227         }
   1228         return edges;
   1229     }
   1230 
   1231     public boolean isScrolling() {
   1232         return mPlatform.mAnimationStartTime != NO_ANIMATION
   1233                 && mPlatform.mCurrentX != mPlatform.mToX;
   1234     }
   1235 
   1236     public void stopScrolling() {
   1237         if (mPlatform.mAnimationStartTime == NO_ANIMATION) return;
   1238         if (mFilmMode) mFilmScroller.forceFinished(true);
   1239         mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX;
   1240     }
   1241 
   1242     public float getFilmRatio() {
   1243         return mFilmRatio.mCurrentRatio;
   1244     }
   1245 
   1246     public void setPopFromTop(boolean top) {
   1247         mPopFromTop = top;
   1248     }
   1249 
   1250     public boolean hasDeletingBox() {
   1251         for(int i = -BOX_MAX; i <= BOX_MAX; i++) {
   1252             if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) {
   1253                 return true;
   1254             }
   1255         }
   1256         return false;
   1257     }
   1258 
   1259     ////////////////////////////////////////////////////////////////////////////
   1260     //  Private utilities
   1261     ////////////////////////////////////////////////////////////////////////////
   1262 
   1263     private float getMinimalScale(Box b) {
   1264         float wFactor = 1.0f;
   1265         float hFactor = 1.0f;
   1266         int viewW, viewH;
   1267 
   1268         if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty()
   1269                 && b == mBoxes.get(0)) {
   1270             viewW = mConstrainedFrame.width();
   1271             viewH = mConstrainedFrame.height();
   1272         } else {
   1273             viewW = mViewW;
   1274             viewH = mViewH;
   1275         }
   1276 
   1277         if (mFilmMode) {
   1278             if (mViewH > mViewW) {  // portrait
   1279                 wFactor = FILM_MODE_PORTRAIT_WIDTH;
   1280                 hFactor = FILM_MODE_PORTRAIT_HEIGHT;
   1281             } else {  // landscape
   1282                 wFactor = FILM_MODE_LANDSCAPE_WIDTH;
   1283                 hFactor = FILM_MODE_LANDSCAPE_HEIGHT;
   1284             }
   1285         }
   1286 
   1287         float s = Math.min(wFactor * viewW / b.mImageW,
   1288                 hFactor * viewH / b.mImageH);
   1289         return Math.min(SCALE_LIMIT, s);
   1290     }
   1291 
   1292     private float getMaximalScale(Box b) {
   1293         if (mFilmMode) return getMinimalScale(b);
   1294         if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b);
   1295         return SCALE_LIMIT;
   1296     }
   1297 
   1298     private static boolean isAlmostEqual(float a, float b) {
   1299         float diff = a - b;
   1300         return (diff < 0 ? -diff : diff) < 0.02f;
   1301     }
   1302 
   1303     // Calculates the stable region of mPlatform.mCurrentX and
   1304     // mBoxes.get(0).mCurrentY, where "stable" means
   1305     //
   1306     // (1) If the dimension of scaled image >= view dimension, we will not
   1307     // see black region outside the image (at that dimension).
   1308     // (2) If the dimension of scaled image < view dimension, we will center
   1309     // the scaled image.
   1310     //
   1311     // We might temporarily go out of this stable during user interaction,
   1312     // but will "snap back" after user stops interaction.
   1313     //
   1314     // The results are stored in mBound{Left/Right/Top/Bottom}.
   1315     //
   1316     // An extra parameter "horizontalSlack" (which has the value of 0 usually)
   1317     // is used to extend the stable region by some pixels on each side
   1318     // horizontally.
   1319     private void calculateStableBound(float scale, int horizontalSlack) {
   1320         Box b = mBoxes.get(0);
   1321 
   1322         // The width and height of the box in number of view pixels
   1323         int w = widthOf(b, scale);
   1324         int h = heightOf(b, scale);
   1325 
   1326         // When the edge of the view is aligned with the edge of the box
   1327         mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack;
   1328         mBoundRight = w / 2 - mViewW / 2 + horizontalSlack;
   1329         mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2;
   1330         mBoundBottom = h / 2 - mViewH / 2;
   1331 
   1332         // If the scaled height is smaller than the view height,
   1333         // force it to be in the center.
   1334         if (viewTallerThanScaledImage(scale)) {
   1335             mBoundTop = mBoundBottom = 0;
   1336         }
   1337 
   1338         // Same for width
   1339         if (viewWiderThanScaledImage(scale)) {
   1340             mBoundLeft = mBoundRight = mPlatform.mDefaultX;
   1341         }
   1342     }
   1343 
   1344     private void calculateStableBound(float scale) {
   1345         calculateStableBound(scale, 0);
   1346     }
   1347 
   1348     private boolean viewTallerThanScaledImage(float scale) {
   1349         return mViewH >= heightOf(mBoxes.get(0), scale);
   1350     }
   1351 
   1352     private boolean viewWiderThanScaledImage(float scale) {
   1353         return mViewW >= widthOf(mBoxes.get(0), scale);
   1354     }
   1355 
   1356     private float getTargetScale(Box b) {
   1357         return b.mAnimationStartTime == NO_ANIMATION
   1358                 ? b.mCurrentScale : b.mToScale;
   1359     }
   1360 
   1361     ////////////////////////////////////////////////////////////////////////////
   1362     //  Animatable: an thing which can do animation.
   1363     ////////////////////////////////////////////////////////////////////////////
   1364     private abstract static class Animatable {
   1365         public long mAnimationStartTime;
   1366         public int mAnimationKind;
   1367         public int mAnimationDuration;
   1368 
   1369         // This should be overidden in subclass to change the animation values
   1370         // give the progress value in [0, 1].
   1371         protected abstract boolean interpolate(float progress);
   1372         public abstract boolean startSnapback();
   1373 
   1374         // Returns true if the animation values changes, so things need to be
   1375         // redrawn.
   1376         public boolean advanceAnimation() {
   1377             if (mAnimationStartTime == NO_ANIMATION) {
   1378                 return false;
   1379             }
   1380             if (mAnimationStartTime == LAST_ANIMATION) {
   1381                 mAnimationStartTime = NO_ANIMATION;
   1382                 return startSnapback();
   1383             }
   1384 
   1385             float progress;
   1386             if (mAnimationDuration == 0) {
   1387                 progress = 1;
   1388             } else {
   1389                 long now = AnimationTime.get();
   1390                 progress =
   1391                     (float) (now - mAnimationStartTime) / mAnimationDuration;
   1392             }
   1393 
   1394             if (progress >= 1) {
   1395                 progress = 1;
   1396             } else {
   1397                 progress = applyInterpolationCurve(mAnimationKind, progress);
   1398             }
   1399 
   1400             boolean done = interpolate(progress);
   1401 
   1402             if (done) {
   1403                 mAnimationStartTime = LAST_ANIMATION;
   1404             }
   1405 
   1406             return true;
   1407         }
   1408 
   1409         private static float applyInterpolationCurve(int kind, float progress) {
   1410             float f = 1 - progress;
   1411             switch (kind) {
   1412                 case ANIM_KIND_SCROLL:
   1413                 case ANIM_KIND_FLING:
   1414                 case ANIM_KIND_FLING_X:
   1415                 case ANIM_KIND_DELETE:
   1416                 case ANIM_KIND_CAPTURE:
   1417                     progress = 1 - f;  // linear
   1418                     break;
   1419                 case ANIM_KIND_SCALE:
   1420                     progress = 1 - f * f;  // quadratic
   1421                     break;
   1422                 case ANIM_KIND_OPENING:
   1423                     progress = 1 - f * f * f;  // x^3
   1424                     break;
   1425                 case ANIM_KIND_SNAPBACK:
   1426                 case ANIM_KIND_ZOOM:
   1427                 case ANIM_KIND_SLIDE:
   1428                     progress = 1 - f * f * f * f * f; // x^5
   1429                     break;
   1430             }
   1431             return progress;
   1432         }
   1433     }
   1434 
   1435     ////////////////////////////////////////////////////////////////////////////
   1436     //  Platform: captures the global X/Y movement.
   1437     ////////////////////////////////////////////////////////////////////////////
   1438     private class Platform extends Animatable {
   1439         public int mCurrentX, mFromX, mToX, mDefaultX;
   1440         public int mCurrentY, mFromY, mToY, mDefaultY;
   1441         public int mFlingOffset;
   1442 
   1443         @Override
   1444         public boolean startSnapback() {
   1445             if (mAnimationStartTime != NO_ANIMATION) return false;
   1446             if (mAnimationKind == ANIM_KIND_SCROLL
   1447                     && mListener.isHoldingDown()) return false;
   1448             if (mInScale) return false;
   1449 
   1450             Box b = mBoxes.get(0);
   1451             float scaleMin = mExtraScalingRange ?
   1452                 b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin;
   1453             float scaleMax = mExtraScalingRange ?
   1454                 b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax;
   1455             float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax);
   1456             int x = mCurrentX;
   1457             int y = mDefaultY;
   1458             if (mFilmMode) {
   1459                 int defaultX = mDefaultX;
   1460                 if (!mHasNext) x = Math.max(x, defaultX);
   1461                 if (!mHasPrev) x = Math.min(x, defaultX);
   1462             } else {
   1463                 calculateStableBound(scale, HORIZONTAL_SLACK);
   1464                 // If the picture is zoomed-in, we want to keep the focus point
   1465                 // stay in the same position on screen, so we need to adjust
   1466                 // target mCurrentX (which is the center of the focused
   1467                 // box). The position of the focus point on screen (relative the
   1468                 // the center of the view) is:
   1469                 //
   1470                 // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX
   1471                 // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX
   1472                 //
   1473                 if (!viewWiderThanScaledImage(scale)) {
   1474                     float scaleDiff = b.mCurrentScale - scale;
   1475                     x += (int) (mFocusX * scaleDiff + 0.5f);
   1476                 }
   1477                 x = Utils.clamp(x, mBoundLeft, mBoundRight);
   1478             }
   1479             if (mCurrentX != x || mCurrentY != y) {
   1480                 return doAnimation(x, y, ANIM_KIND_SNAPBACK);
   1481             }
   1482             return false;
   1483         }
   1484 
   1485         // The updateDefaultXY() should be called whenever these variables
   1486         // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4)
   1487         // mFilmMode
   1488         public void updateDefaultXY() {
   1489             // We don't check mFilmMode and return 0 for mDefaultX. Because
   1490             // otherwise if we decide to leave film mode because we are
   1491             // centered, we will immediately back into film mode because we find
   1492             // we are not centered.
   1493             if (mConstrained && !mConstrainedFrame.isEmpty()) {
   1494                 mDefaultX = mConstrainedFrame.centerX() - mViewW / 2;
   1495                 mDefaultY = mFilmMode ? 0 :
   1496                         mConstrainedFrame.centerY() - mViewH / 2;
   1497             } else {
   1498                 mDefaultX = 0;
   1499                 mDefaultY = 0;
   1500             }
   1501         }
   1502 
   1503         // Starts an animation for the platform.
   1504         private boolean doAnimation(int targetX, int targetY, int kind) {
   1505             if (mCurrentX == targetX && mCurrentY == targetY) return false;
   1506             mAnimationKind = kind;
   1507             mFromX = mCurrentX;
   1508             mFromY = mCurrentY;
   1509             mToX = targetX;
   1510             mToY = targetY;
   1511             mAnimationStartTime = AnimationTime.startTime();
   1512             mAnimationDuration = ANIM_TIME[kind];
   1513             mFlingOffset = 0;
   1514             advanceAnimation();
   1515             return true;
   1516         }
   1517 
   1518         @Override
   1519         protected boolean interpolate(float progress) {
   1520             if (mAnimationKind == ANIM_KIND_FLING) {
   1521                 return interpolateFlingPage(progress);
   1522             } else if (mAnimationKind == ANIM_KIND_FLING_X) {
   1523                 return interpolateFlingFilm(progress);
   1524             } else {
   1525                 return interpolateLinear(progress);
   1526             }
   1527         }
   1528 
   1529         private boolean interpolateFlingFilm(float progress) {
   1530             mFilmScroller.computeScrollOffset();
   1531             mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
   1532 
   1533             int dir = EdgeView.INVALID_DIRECTION;
   1534             if (mCurrentX < mDefaultX) {
   1535                 if (!mHasNext) {
   1536                     dir = EdgeView.RIGHT;
   1537                 }
   1538             } else if (mCurrentX > mDefaultX) {
   1539                 if (!mHasPrev) {
   1540                     dir = EdgeView.LEFT;
   1541                 }
   1542             }
   1543             if (dir != EdgeView.INVALID_DIRECTION) {
   1544                 int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
   1545                 mListener.onAbsorb(v, dir);
   1546                 mFilmScroller.forceFinished(true);
   1547                 mCurrentX = mDefaultX;
   1548             }
   1549             return mFilmScroller.isFinished();
   1550         }
   1551 
   1552         private boolean interpolateFlingPage(float progress) {
   1553             mPageScroller.computeScrollOffset(progress);
   1554             Box b = mBoxes.get(0);
   1555             calculateStableBound(b.mCurrentScale);
   1556 
   1557             int oldX = mCurrentX;
   1558             mCurrentX = mPageScroller.getCurrX();
   1559 
   1560             // Check if we hit the edges; show edge effects if we do.
   1561             if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
   1562                 int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
   1563                 mListener.onAbsorb(v, EdgeView.RIGHT);
   1564             } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
   1565                 int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
   1566                 mListener.onAbsorb(v, EdgeView.LEFT);
   1567             }
   1568 
   1569             return progress >= 1;
   1570         }
   1571 
   1572         private boolean interpolateLinear(float progress) {
   1573             // Other animations
   1574             if (progress >= 1) {
   1575                 mCurrentX = mToX;
   1576                 mCurrentY = mToY;
   1577                 return true;
   1578             } else {
   1579                 if (mAnimationKind == ANIM_KIND_CAPTURE) {
   1580                     progress = CaptureAnimation.calculateSlide(progress);
   1581                 }
   1582                 mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
   1583                 mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
   1584                 if (mAnimationKind == ANIM_KIND_CAPTURE) {
   1585                     return false;
   1586                 } else {
   1587                     return (mCurrentX == mToX && mCurrentY == mToY);
   1588                 }
   1589             }
   1590         }
   1591     }
   1592 
   1593     ////////////////////////////////////////////////////////////////////////////
   1594     //  Box: represents a rectangular area which shows a picture.
   1595     ////////////////////////////////////////////////////////////////////////////
   1596     private class Box extends Animatable {
   1597         // Size of the bitmap
   1598         public int mImageW, mImageH;
   1599 
   1600         // This is true if we assume the image size is the same as view size
   1601         // until we know the actual size of image. This is also used to
   1602         // determine if there is an image ready to show.
   1603         public boolean mUseViewSize;
   1604 
   1605         // The minimum and maximum scale we allow for this box.
   1606         public float mScaleMin, mScaleMax;
   1607 
   1608         // The X/Y value indicates where the center of the box is on the view
   1609         // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
   1610         // actual values used currently. Note that the X values are implicitly
   1611         // defined by Platform and Gaps.
   1612         public int mCurrentY, mFromY, mToY;
   1613         public float mCurrentScale, mFromScale, mToScale;
   1614 
   1615         // The absolute X coordinate of the center of the box. This is only used
   1616         // during moveBox().
   1617         public int mAbsoluteX;
   1618 
   1619         @Override
   1620         public boolean startSnapback() {
   1621             if (mAnimationStartTime != NO_ANIMATION) return false;
   1622             if (mAnimationKind == ANIM_KIND_SCROLL
   1623                     && mListener.isHoldingDown()) return false;
   1624             if (mAnimationKind == ANIM_KIND_DELETE
   1625                     && mListener.isHoldingDelete()) return false;
   1626             if (mInScale && this == mBoxes.get(0)) return false;
   1627 
   1628             int y = mCurrentY;
   1629             float scale;
   1630 
   1631             if (this == mBoxes.get(0)) {
   1632                 float scaleMin = mExtraScalingRange ?
   1633                     mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
   1634                 float scaleMax = mExtraScalingRange ?
   1635                     mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
   1636                 scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
   1637                 if (mFilmMode) {
   1638                     y = 0;
   1639                 } else {
   1640                     calculateStableBound(scale, HORIZONTAL_SLACK);
   1641                     // If the picture is zoomed-in, we want to keep the focus
   1642                     // point stay in the same position on screen. See the
   1643                     // comment in Platform.startSnapback for details.
   1644                     if (!viewTallerThanScaledImage(scale)) {
   1645                         float scaleDiff = mCurrentScale - scale;
   1646                         y += (int) (mFocusY * scaleDiff + 0.5f);
   1647                     }
   1648                     y = Utils.clamp(y, mBoundTop, mBoundBottom);
   1649                 }
   1650             } else {
   1651                 y = 0;
   1652                 scale = mScaleMin;
   1653             }
   1654 
   1655             if (mCurrentY != y || mCurrentScale != scale) {
   1656                 return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
   1657             }
   1658             return false;
   1659         }
   1660 
   1661         private boolean doAnimation(int targetY, float targetScale, int kind) {
   1662             targetScale = clampScale(targetScale);
   1663 
   1664             if (mCurrentY == targetY && mCurrentScale == targetScale
   1665                     && kind != ANIM_KIND_CAPTURE) {
   1666                 return false;
   1667             }
   1668 
   1669             // Now starts an animation for the box.
   1670             mAnimationKind = kind;
   1671             mFromY = mCurrentY;
   1672             mFromScale = mCurrentScale;
   1673             mToY = targetY;
   1674             mToScale = targetScale;
   1675             mAnimationStartTime = AnimationTime.startTime();
   1676             mAnimationDuration = ANIM_TIME[kind];
   1677             advanceAnimation();
   1678             return true;
   1679         }
   1680 
   1681         // Clamps the input scale to the range that doAnimation() can reach.
   1682         public float clampScale(float s) {
   1683             return Utils.clamp(s,
   1684                     SCALE_MIN_EXTRA * mScaleMin,
   1685                     SCALE_MAX_EXTRA * mScaleMax);
   1686         }
   1687 
   1688         @Override
   1689         protected boolean interpolate(float progress) {
   1690             if (mAnimationKind == ANIM_KIND_FLING) {
   1691                 return interpolateFlingPage(progress);
   1692             } else {
   1693                 return interpolateLinear(progress);
   1694             }
   1695         }
   1696 
   1697         private boolean interpolateFlingPage(float progress) {
   1698             mPageScroller.computeScrollOffset(progress);
   1699             calculateStableBound(mCurrentScale);
   1700 
   1701             int oldY = mCurrentY;
   1702             mCurrentY = mPageScroller.getCurrY();
   1703 
   1704             // Check if we hit the edges; show edge effects if we do.
   1705             if (oldY > mBoundTop && mCurrentY == mBoundTop) {
   1706                 int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
   1707                 mListener.onAbsorb(v, EdgeView.BOTTOM);
   1708             } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
   1709                 int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
   1710                 mListener.onAbsorb(v, EdgeView.TOP);
   1711             }
   1712 
   1713             return progress >= 1;
   1714         }
   1715 
   1716         private boolean interpolateLinear(float progress) {
   1717             if (progress >= 1) {
   1718                 mCurrentY = mToY;
   1719                 mCurrentScale = mToScale;
   1720                 return true;
   1721             } else {
   1722                 mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
   1723                 mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
   1724                 if (mAnimationKind == ANIM_KIND_CAPTURE) {
   1725                     float f = CaptureAnimation.calculateScale(progress);
   1726                     mCurrentScale *= f;
   1727                     return false;
   1728                 } else {
   1729                     return (mCurrentY == mToY && mCurrentScale == mToScale);
   1730                 }
   1731             }
   1732         }
   1733     }
   1734 
   1735     ////////////////////////////////////////////////////////////////////////////
   1736     //  Gap: represents a rectangular area which is between two boxes.
   1737     ////////////////////////////////////////////////////////////////////////////
   1738     private class Gap extends Animatable {
   1739         // The default gap size between two boxes. The value may vary for
   1740         // different image size of the boxes and for different modes (page or
   1741         // film).
   1742         public int mDefaultSize;
   1743 
   1744         // The gap size between the two boxes.
   1745         public int mCurrentGap, mFromGap, mToGap;
   1746 
   1747         @Override
   1748         public boolean startSnapback() {
   1749             if (mAnimationStartTime != NO_ANIMATION) return false;
   1750             return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK);
   1751         }
   1752 
   1753         // Starts an animation for a gap.
   1754         public boolean doAnimation(int targetSize, int kind) {
   1755             if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) {
   1756                 return false;
   1757             }
   1758             mAnimationKind = kind;
   1759             mFromGap = mCurrentGap;
   1760             mToGap = targetSize;
   1761             mAnimationStartTime = AnimationTime.startTime();
   1762             mAnimationDuration = ANIM_TIME[mAnimationKind];
   1763             advanceAnimation();
   1764             return true;
   1765         }
   1766 
   1767         @Override
   1768         protected boolean interpolate(float progress) {
   1769             if (progress >= 1) {
   1770                 mCurrentGap = mToGap;
   1771                 return true;
   1772             } else {
   1773                 mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
   1774                 if (mAnimationKind == ANIM_KIND_CAPTURE) {
   1775                     float f = CaptureAnimation.calculateScale(progress);
   1776                     mCurrentGap = (int) (mCurrentGap * f);
   1777                     return false;
   1778                 } else {
   1779                     return (mCurrentGap == mToGap);
   1780                 }
   1781             }
   1782         }
   1783     }
   1784 
   1785     ////////////////////////////////////////////////////////////////////////////
   1786     //  FilmRatio: represents the progress of film mode change.
   1787     ////////////////////////////////////////////////////////////////////////////
   1788     private class FilmRatio extends Animatable {
   1789         // The film ratio: 1 means switching to film mode is complete, 0 means
   1790         // switching to page mode is complete.
   1791         public float mCurrentRatio, mFromRatio, mToRatio;
   1792 
   1793         @Override
   1794         public boolean startSnapback() {
   1795             float target = mFilmMode ? 1f : 0f;
   1796             if (target == mToRatio) return false;
   1797             return doAnimation(target, ANIM_KIND_SNAPBACK);
   1798         }
   1799 
   1800         // Starts an animation for the film ratio.
   1801         private boolean doAnimation(float targetRatio, int kind) {
   1802             mAnimationKind = kind;
   1803             mFromRatio = mCurrentRatio;
   1804             mToRatio = targetRatio;
   1805             mAnimationStartTime = AnimationTime.startTime();
   1806             mAnimationDuration = ANIM_TIME[mAnimationKind];
   1807             advanceAnimation();
   1808             return true;
   1809         }
   1810 
   1811         @Override
   1812         protected boolean interpolate(float progress) {
   1813             if (progress >= 1) {
   1814                 mCurrentRatio = mToRatio;
   1815                 return true;
   1816             } else {
   1817                 mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio);
   1818                 return (mCurrentRatio == mToRatio);
   1819             }
   1820         }
   1821     }
   1822 }
   1823