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.Scroller;
     23 
     24 import com.android.gallery3d.app.PhotoPage;
     25 import com.android.gallery3d.common.Utils;
     26 import com.android.gallery3d.ui.PhotoView.Size;
     27 import com.android.gallery3d.util.GalleryUtils;
     28 import com.android.gallery3d.util.RangeArray;
     29 import com.android.gallery3d.util.RangeIntArray;
     30 
     31 class PositionController {
     32     private static final String TAG = "PositionController";
     33 
     34     public static final int IMAGE_AT_LEFT_EDGE = 1;
     35     public static final int IMAGE_AT_RIGHT_EDGE = 2;
     36     public static final int IMAGE_AT_TOP_EDGE = 4;
     37     public static final int IMAGE_AT_BOTTOM_EDGE = 8;
     38 
     39     public static final int CAPTURE_ANIMATION_TIME = 700;
     40     public static final int SNAPBACK_ANIMATION_TIME = 600;
     41 
     42     // Special values for animation time.
     43     private static final long NO_ANIMATION = -1;
     44     private static final long LAST_ANIMATION = -2;
     45 
     46     private static final int ANIM_KIND_NONE = -1;
     47     private static final int ANIM_KIND_SCROLL = 0;
     48     private static final int ANIM_KIND_SCALE = 1;
     49     private static final int ANIM_KIND_SNAPBACK = 2;
     50     private static final int ANIM_KIND_SLIDE = 3;
     51     private static final int ANIM_KIND_ZOOM = 4;
     52     private static final int ANIM_KIND_OPENING = 5;
     53     private static final int ANIM_KIND_FLING = 6;
     54     private static final int ANIM_KIND_FLING_X = 7;
     55     private static final int ANIM_KIND_DELETE = 8;
     56     private static final int ANIM_KIND_CAPTURE = 9;
     57 
     58     // Animation time in milliseconds. The order must match ANIM_KIND_* above.
     59     //
     60     // The values for ANIM_KIND_FLING_X does't matter because we use
     61     // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's
     62     // faster for Animatable.advanceAnimation() to calculate the progress
     63     // (always 1).
     64     private static final int ANIM_TIME[] = {
     65         0,    // ANIM_KIND_SCROLL
     66         0,    // ANIM_KIND_SCALE
     67         SNAPBACK_ANIMATION_TIME,  // ANIM_KIND_SNAPBACK
     68         400,  // ANIM_KIND_SLIDE
     69         300,  // ANIM_KIND_ZOOM
     70         300,  // ANIM_KIND_OPENING
     71         0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
     72         0,    // ANIM_KIND_FLING_X (see the comment above)
     73         0,    // ANIM_KIND_DELETE (the duration is calculated dynamically)
     74         CAPTURE_ANIMATION_TIME,  // ANIM_KIND_CAPTURE
     75     };
     76 
     77     // We try to scale up the image to fill the screen. But in order not to
     78     // scale too much for small icons, we limit the max up-scaling factor here.
     79     private static final float SCALE_LIMIT = 4;
     80 
     81     // For user's gestures, we give a temporary extra scaling range which goes
     82     // above or below the usual scaling limits.
     83     private static final float SCALE_MIN_EXTRA = 0.7f;
     84     private static final float SCALE_MAX_EXTRA = 1.4f;
     85 
     86     // Setting this true makes the extra scaling range permanent (until this is
     87     // set to false again).
     88     private boolean mExtraScalingRange = false;
     89 
     90     // Film Mode v.s. Page Mode: in film mode we show smaller pictures.
     91     private boolean mFilmMode = false;
     92 
     93     // These are the limits for width / height of the picture in film mode.
     94     private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f;
     95     private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f;
     96     private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f;
     97     private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f;
     98 
     99     // In addition to the focused box (index == 0). We also keep information
    100     // about this many boxes on each side.
    101     private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX;
    102     private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1];
    103 
    104     private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16);
    105     private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
    106 
    107     // These are constants for the delete gesture.
    108     private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms
    109     private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms
    110 
    111     private Listener mListener;
    112     private volatile Rect mOpenAnimationRect;
    113 
    114     // Use a large enough value, so we won't see the gray shadow in the beginning.
    115     private int mViewW = 1200;
    116     private int mViewH = 1200;
    117 
    118     // A scaling gesture is in progress.
    119     private boolean mInScale;
    120     // The focus point of the scaling gesture, relative to the center of the
    121     // picture in bitmap pixels.
    122     private float mFocusX, mFocusY;
    123 
    124     // whether there is a previous/next picture.
    125     private boolean mHasPrev, mHasNext;
    126 
    127     // This is used by the fling animation (page mode).
    128     private FlingScroller mPageScroller;
    129 
    130     // This is used by the fling animation (film mode).
    131     private Scroller mFilmScroller;
    132 
    133     // The bound of the stable region that the focused box can stay, see the
    134     // comments above calculateStableBound() for details.
    135     private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
    136 
    137     // Constrained frame is a rectangle that the focused box should fit into if
    138     // it is constrained. It has two effects:
    139     //
    140     // (1) In page mode, if the focused box is constrained, scaling for the
    141     // focused box is adjusted to fit into the constrained frame, instead of the
    142     // whole view.
    143     //
    144     // (2) In page mode, if the focused box is constrained, the mPlatform's
    145     // default center (mDefaultX/Y) is moved to the center of the constrained
    146     // frame, instead of the view center.
    147     //
    148     private Rect mConstrainedFrame = new Rect();
    149 
    150     // Whether the focused box is constrained.
    151     //
    152     // Our current program's first call to moveBox() sets constrained = true, so
    153     // we set the initial value of this variable to true, and we will not see
    154     // see unwanted transition animation.
    155     private boolean mConstrained = true;
    156 
    157     //
    158     //  ___________________________________________________________
    159     // |   _____       _____       _____       _____       _____   |
    160     // |  |     |     |     |     |     |     |     |     |     |  |
    161     // |  | Box |     | Box |     | Box*|     | Box |     | Box |  |
    162     // |  |_____|.....|_____|.....|_____|.....|_____|.....|_____|  |
    163     // |          Gap         Gap         Gap         Gap          |
    164     // |___________________________________________________________|
    165     //
    166     //                       <--  Platform  -->
    167     //
    168     // The focused box (Box*) centers at mPlatform's (mCurrentX, mCurrentY)
    169 
    170     private Platform mPlatform = new Platform();
    171     private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
    172     // The gap at the right of a Box i is at index i. The gap at the left of a
    173     // Box i is at index i - 1.
    174     private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
    175     private FilmRatio mFilmRatio = new FilmRatio();
    176 
    177     // These are only used during moveBox().
    178     private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
    179     private RangeArray<Gap> mTempGaps =
    180         new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
    181 
    182     // The output of the PositionController. Available through getPosition().
    183     private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX);
    184 
    185     // The direction of a new picture should appear. New pictures pop from top
    186     // if this value is true, or from bottom if this value is false.
    187     boolean mPopFromTop;
    188 
    189     public interface Listener {
    190         void invalidate();
    191         boolean isHoldingDown();
    192         boolean isHoldingDelete();
    193 
    194         // EdgeView
    195         void onPull(int offset, int direction);
    196         void onRelease();
    197         void onAbsorb(int velocity, int direction);
    198     }
    199 
    200     static {
    201         // Initialize the CENTER_OUT_INDEX array.
    202         // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX
    203         // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX
    204         for (int i = 0; i < CENTER_OUT_INDEX.length; i++) {
    205             int j = (i + 1) / 2;
    206             if ((i & 1) == 0) j = -j;
    207             CENTER_OUT_INDEX[i] = j;
    208         }
    209     }
    210 
    211     public PositionController(Context context, Listener listener) {
    212         mListener = listener;
    213         mPageScroller = new FlingScroller();
    214         mFilmScroller = new Scroller(context, null, false);
    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 or we are in fullscreen,
    323         // we change the scale directly. Otherwise adjust the scales by a ratio,
    324         // and snapback will animate the scale into the min/max bounds if necessary.
    325         if ((wasViewSize && !isViewSize) || !mFilmMode) {
    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 gesture, 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     @SuppressWarnings("unused")
    860     private void dumpState() {
    861         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
    862             Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
    863         }
    864 
    865         for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
    866             dumpRect(CENTER_OUT_INDEX[i]);
    867         }
    868 
    869         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
    870             for (int j = i + 1; j <= BOX_MAX; j++) {
    871                 if (Rect.intersects(mRects.get(i), mRects.get(j))) {
    872                     Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
    873                 }
    874             }
    875         }
    876     }
    877 
    878     private void dumpRect(int i) {
    879         StringBuilder sb = new StringBuilder();
    880         Rect r = mRects.get(i);
    881         sb.append("Rect " + i + ":");
    882         sb.append("(");
    883         sb.append(r.centerX());
    884         sb.append(",");
    885         sb.append(r.centerY());
    886         sb.append(") [");
    887         sb.append(r.width());
    888         sb.append("x");
    889         sb.append(r.height());
    890         sb.append("]");
    891         Log.d(TAG, sb.toString());
    892     }
    893 
    894     private void convertBoxToRect(int i) {
    895         Box b = mBoxes.get(i);
    896         Rect r = mRects.get(i);
    897         int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2;
    898         int w = widthOf(b);
    899         int h = heightOf(b);
    900         if (i == 0) {
    901             int x = mPlatform.mCurrentX + mViewW / 2;
    902             r.left = x - w / 2;
    903             r.right = r.left + w;
    904         } else if (i > 0) {
    905             Rect a = mRects.get(i - 1);
    906             Gap g = mGaps.get(i - 1);
    907             r.left = a.right + g.mCurrentGap;
    908             r.right = r.left + w;
    909         } else {  // i < 0
    910             Rect a = mRects.get(i + 1);
    911             Gap g = mGaps.get(i);
    912             r.right = a.left - g.mCurrentGap;
    913             r.left = r.right - w;
    914         }
    915         r.top = y - h / 2;
    916         r.bottom = r.top + h;
    917     }
    918 
    919     // Returns the position of a box.
    920     public Rect getPosition(int index) {
    921         return mRects.get(index);
    922     }
    923 
    924     ////////////////////////////////////////////////////////////////////////////
    925     //  Box management
    926     ////////////////////////////////////////////////////////////////////////////
    927 
    928     // Initialize the platform to be at the view center.
    929     private void initPlatform() {
    930         mPlatform.updateDefaultXY();
    931         mPlatform.mCurrentX = mPlatform.mDefaultX;
    932         mPlatform.mCurrentY = mPlatform.mDefaultY;
    933         mPlatform.mAnimationStartTime = NO_ANIMATION;
    934     }
    935 
    936     // Initialize a box to have the size of the view.
    937     private void initBox(int index) {
    938         Box b = mBoxes.get(index);
    939         b.mImageW = mViewW;
    940         b.mImageH = mViewH;
    941         b.mUseViewSize = true;
    942         b.mScaleMin = getMinimalScale(b);
    943         b.mScaleMax = getMaximalScale(b);
    944         b.mCurrentY = 0;
    945         b.mCurrentScale = b.mScaleMin;
    946         b.mAnimationStartTime = NO_ANIMATION;
    947         b.mAnimationKind = ANIM_KIND_NONE;
    948     }
    949 
    950     // Initialize a box to a given size.
    951     private void initBox(int index, Size size) {
    952         if (size.width == 0 || size.height == 0) {
    953             initBox(index);
    954             return;
    955         }
    956         Box b = mBoxes.get(index);
    957         b.mImageW = size.width;
    958         b.mImageH = size.height;
    959         b.mUseViewSize = false;
    960         b.mScaleMin = getMinimalScale(b);
    961         b.mScaleMax = getMaximalScale(b);
    962         b.mCurrentY = 0;
    963         b.mCurrentScale = b.mScaleMin;
    964         b.mAnimationStartTime = NO_ANIMATION;
    965         b.mAnimationKind = ANIM_KIND_NONE;
    966     }
    967 
    968     // Initialize a gap. This can only be called after the boxes around the gap
    969     // has been initialized.
    970     private void initGap(int index) {
    971         Gap g = mGaps.get(index);
    972         g.mDefaultSize = getDefaultGapSize(index);
    973         g.mCurrentGap = g.mDefaultSize;
    974         g.mAnimationStartTime = NO_ANIMATION;
    975     }
    976 
    977     private void initGap(int index, int size) {
    978         Gap g = mGaps.get(index);
    979         g.mDefaultSize = getDefaultGapSize(index);
    980         g.mCurrentGap = size;
    981         g.mAnimationStartTime = NO_ANIMATION;
    982     }
    983 
    984     @SuppressWarnings("unused")
    985     private void debugMoveBox(int fromIndex[]) {
    986         StringBuilder s = new StringBuilder("moveBox:");
    987         for (int i = 0; i < fromIndex.length; i++) {
    988             int j = fromIndex[i];
    989             if (j == Integer.MAX_VALUE) {
    990                 s.append(" N");
    991             } else {
    992                 s.append(" ");
    993                 s.append(fromIndex[i]);
    994             }
    995         }
    996         Log.d(TAG, s.toString());
    997     }
    998 
    999     // Move the boxes: it may indicate focus change, box deleted, box appearing,
   1000     // box reordered, etc.
   1001     //
   1002     // Each element in the fromIndex array indicates where each box was in the
   1003     // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
   1004     // means the box is new.
   1005     //
   1006     // For example:
   1007     // N N N N N N N -- all new boxes
   1008     // -3 -2 -1 0 1 2 3 -- nothing changed
   1009     // -2 -1 0 1 2 3 N -- focus goes to the next box
   1010     // N -3 -2 -1 0 1 2 -- focus goes to the previous box
   1011     // -3 -2 -1 1 2 3 N -- the focused box was deleted.
   1012     //
   1013     // hasPrev/hasNext indicates if there are previous/next boxes for the
   1014     // focused box. constrained indicates whether the focused box should be put
   1015     // into the constrained frame.
   1016     public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext,
   1017             boolean constrained, Size[] sizes) {
   1018         //debugMoveBox(fromIndex);
   1019         mHasPrev = hasPrev;
   1020         mHasNext = hasNext;
   1021 
   1022         RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
   1023 
   1024         // 1. Get the absolute X coordinates for the boxes.
   1025         layoutAndSetPosition();
   1026         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
   1027             Box b = mBoxes.get(i);
   1028             Rect r = mRects.get(i);
   1029             b.mAbsoluteX = r.centerX() - mViewW / 2;
   1030         }
   1031 
   1032         // 2. copy boxes and gaps to temporary storage.
   1033         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
   1034             mTempBoxes.put(i, mBoxes.get(i));
   1035             mBoxes.put(i, null);
   1036         }
   1037         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
   1038             mTempGaps.put(i, mGaps.get(i));
   1039             mGaps.put(i, null);
   1040         }
   1041 
   1042         // 3. move back boxes that are used in the new array.
   1043         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
   1044             int j = from.get(i);
   1045             if (j == Integer.MAX_VALUE) continue;
   1046             mBoxes.put(i, mTempBoxes.get(j));
   1047             mTempBoxes.put(j, null);
   1048         }
   1049 
   1050         // 4. move back gaps if both boxes around it are kept together.
   1051         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
   1052             int j = from.get(i);
   1053             if (j == Integer.MAX_VALUE) continue;
   1054             int k = from.get(i + 1);
   1055             if (k == Integer.MAX_VALUE) continue;
   1056             if (j + 1 == k) {
   1057                 mGaps.put(i, mTempGaps.get(j));
   1058                 mTempGaps.put(j, null);
   1059             }
   1060         }
   1061 
   1062         // 5. recycle the boxes that are not used in the new array.
   1063         int k = -BOX_MAX;
   1064         for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
   1065             if (mBoxes.get(i) != null) continue;
   1066             while (mTempBoxes.get(k) == null) {
   1067                 k++;
   1068             }
   1069             mBoxes.put(i, mTempBoxes.get(k++));
   1070             initBox(i, sizes[i + BOX_MAX]);
   1071         }
   1072 
   1073         // 6. Now give the recycled box a reasonable absolute X position.
   1074         //
   1075         // First try to find the first and the last box which the absolute X
   1076         // position is known.
   1077         int first, last;
   1078         for (first = -BOX_MAX; first <= BOX_MAX; first++) {
   1079             if (from.get(first) != Integer.MAX_VALUE) break;
   1080         }
   1081         for (last = BOX_MAX; last >= -BOX_MAX; last--) {
   1082             if (from.get(last) != Integer.MAX_VALUE) break;
   1083         }
   1084         // If there is no box has known X position at all, make the focused one
   1085         // as known.
   1086         if (first > BOX_MAX) {
   1087             mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
   1088             first = last = 0;
   1089         }
   1090         // Now for those boxes between first and last, assign their position to
   1091         // align to the previous box or the next box with known position. For
   1092         // the boxes before first or after last, we will use a new default gap
   1093         // size below.
   1094 
   1095         // Align to the previous box
   1096         for (int i = Math.max(0, first + 1); i < last; i++) {
   1097             if (from.get(i) != Integer.MAX_VALUE) continue;
   1098             Box a = mBoxes.get(i - 1);
   1099             Box b = mBoxes.get(i);
   1100             int wa = widthOf(a);
   1101             int wb = widthOf(b);
   1102             b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2
   1103                     + getDefaultGapSize(i);
   1104             if (mPopFromTop) {
   1105                 b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
   1106             } else {
   1107                 b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
   1108             }
   1109         }
   1110 
   1111         // Align to the next box
   1112         for (int i = Math.min(-1, last - 1); i > first; i--) {
   1113             if (from.get(i) != Integer.MAX_VALUE) continue;
   1114             Box a = mBoxes.get(i + 1);
   1115             Box b = mBoxes.get(i);
   1116             int wa = widthOf(a);
   1117             int wb = widthOf(b);
   1118             b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2)
   1119                     - getDefaultGapSize(i);
   1120             if (mPopFromTop) {
   1121                 b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
   1122             } else {
   1123                 b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
   1124             }
   1125         }
   1126 
   1127         // 7. recycle the gaps that are not used in the new array.
   1128         k = -BOX_MAX;
   1129         for (int i = -BOX_MAX; i < BOX_MAX; i++) {
   1130             if (mGaps.get(i) != null) continue;
   1131             while (mTempGaps.get(k) == null) {
   1132                 k++;
   1133             }
   1134             mGaps.put(i, mTempGaps.get(k++));
   1135             Box a = mBoxes.get(i);
   1136             Box b = mBoxes.get(i + 1);
   1137             int wa = widthOf(a);
   1138             int wb = widthOf(b);
   1139             if (i >= first && i < last) {
   1140                 int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
   1141                 initGap(i, g);
   1142             } else {
   1143                 initGap(i);
   1144             }
   1145         }
   1146 
   1147         // 8. calculate the new absolute X coordinates for those box before
   1148         // first or after last.
   1149         for (int i = first - 1; i >= -BOX_MAX; i--) {
   1150             Box a = mBoxes.get(i + 1);
   1151             Box b = mBoxes.get(i);
   1152             int wa = widthOf(a);
   1153             int wb = widthOf(b);
   1154             Gap g = mGaps.get(i);
   1155             b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap;
   1156         }
   1157 
   1158         for (int i = last + 1; i <= BOX_MAX; i++) {
   1159             Box a = mBoxes.get(i - 1);
   1160             Box b = mBoxes.get(i);
   1161             int wa = widthOf(a);
   1162             int wb = widthOf(b);
   1163             Gap g = mGaps.get(i - 1);
   1164             b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap;
   1165         }
   1166 
   1167         // 9. offset the Platform position
   1168         int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
   1169         mPlatform.mCurrentX += dx;
   1170         mPlatform.mFromX += dx;
   1171         mPlatform.mToX += dx;
   1172         mPlatform.mFlingOffset += dx;
   1173 
   1174         if (mConstrained != constrained) {
   1175             mConstrained = constrained;
   1176             mPlatform.updateDefaultXY();
   1177             updateScaleAndGapLimit();
   1178         }
   1179 
   1180         snapAndRedraw();
   1181     }
   1182 
   1183     ////////////////////////////////////////////////////////////////////////////
   1184     //  Public utilities
   1185     ////////////////////////////////////////////////////////////////////////////
   1186 
   1187     public boolean isAtMinimalScale() {
   1188         Box b = mBoxes.get(0);
   1189         return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
   1190     }
   1191 
   1192     public boolean isCenter() {
   1193         Box b = mBoxes.get(0);
   1194         return mPlatform.mCurrentX == mPlatform.mDefaultX
   1195             && b.mCurrentY == 0;
   1196     }
   1197 
   1198     public int getImageWidth() {
   1199         Box b = mBoxes.get(0);
   1200         return b.mImageW;
   1201     }
   1202 
   1203     public int getImageHeight() {
   1204         Box b = mBoxes.get(0);
   1205         return b.mImageH;
   1206     }
   1207 
   1208     public float getImageScale() {
   1209         Box b = mBoxes.get(0);
   1210         return b.mCurrentScale;
   1211     }
   1212 
   1213     public int getImageAtEdges() {
   1214         Box b = mBoxes.get(0);
   1215         Platform p = mPlatform;
   1216         calculateStableBound(b.mCurrentScale);
   1217         int edges = 0;
   1218         if (p.mCurrentX <= mBoundLeft) {
   1219             edges |= IMAGE_AT_RIGHT_EDGE;
   1220         }
   1221         if (p.mCurrentX >= mBoundRight) {
   1222             edges |= IMAGE_AT_LEFT_EDGE;
   1223         }
   1224         if (b.mCurrentY <= mBoundTop) {
   1225             edges |= IMAGE_AT_BOTTOM_EDGE;
   1226         }
   1227         if (b.mCurrentY >= mBoundBottom) {
   1228             edges |= IMAGE_AT_TOP_EDGE;
   1229         }
   1230         return edges;
   1231     }
   1232 
   1233     public boolean isScrolling() {
   1234         return mPlatform.mAnimationStartTime != NO_ANIMATION
   1235                 && mPlatform.mCurrentX != mPlatform.mToX;
   1236     }
   1237 
   1238     public void stopScrolling() {
   1239         if (mPlatform.mAnimationStartTime == NO_ANIMATION) return;
   1240         if (mFilmMode) mFilmScroller.forceFinished(true);
   1241         mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX;
   1242     }
   1243 
   1244     public float getFilmRatio() {
   1245         return mFilmRatio.mCurrentRatio;
   1246     }
   1247 
   1248     public void setPopFromTop(boolean top) {
   1249         mPopFromTop = top;
   1250     }
   1251 
   1252     public boolean hasDeletingBox() {
   1253         for(int i = -BOX_MAX; i <= BOX_MAX; i++) {
   1254             if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) {
   1255                 return true;
   1256             }
   1257         }
   1258         return false;
   1259     }
   1260 
   1261     ////////////////////////////////////////////////////////////////////////////
   1262     //  Private utilities
   1263     ////////////////////////////////////////////////////////////////////////////
   1264 
   1265     private float getMinimalScale(Box b) {
   1266         float wFactor = 1.0f;
   1267         float hFactor = 1.0f;
   1268         int viewW, viewH;
   1269 
   1270         if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty()
   1271                 && b == mBoxes.get(0)) {
   1272             viewW = mConstrainedFrame.width();
   1273             viewH = mConstrainedFrame.height();
   1274         } else {
   1275             viewW = mViewW;
   1276             viewH = mViewH;
   1277         }
   1278 
   1279         if (mFilmMode) {
   1280             if (mViewH > mViewW) {  // portrait
   1281                 wFactor = FILM_MODE_PORTRAIT_WIDTH;
   1282                 hFactor = FILM_MODE_PORTRAIT_HEIGHT;
   1283             } else {  // landscape
   1284                 wFactor = FILM_MODE_LANDSCAPE_WIDTH;
   1285                 hFactor = FILM_MODE_LANDSCAPE_HEIGHT;
   1286             }
   1287         }
   1288 
   1289         float s = Math.min(wFactor * viewW / b.mImageW,
   1290                 hFactor * viewH / b.mImageH);
   1291         return Math.min(SCALE_LIMIT, s);
   1292     }
   1293 
   1294     private float getMaximalScale(Box b) {
   1295         if (mFilmMode) return getMinimalScale(b);
   1296         if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b);
   1297         return SCALE_LIMIT;
   1298     }
   1299 
   1300     private static boolean isAlmostEqual(float a, float b) {
   1301         float diff = a - b;
   1302         return (diff < 0 ? -diff : diff) < 0.02f;
   1303     }
   1304 
   1305     // Calculates the stable region of mPlatform.mCurrentX and
   1306     // mBoxes.get(0).mCurrentY, where "stable" means
   1307     //
   1308     // (1) If the dimension of scaled image >= view dimension, we will not
   1309     // see black region outside the image (at that dimension).
   1310     // (2) If the dimension of scaled image < view dimension, we will center
   1311     // the scaled image.
   1312     //
   1313     // We might temporarily go out of this stable during user interaction,
   1314     // but will "snap back" after user stops interaction.
   1315     //
   1316     // The results are stored in mBound{Left/Right/Top/Bottom}.
   1317     //
   1318     // An extra parameter "horizontalSlack" (which has the value of 0 usually)
   1319     // is used to extend the stable region by some pixels on each side
   1320     // horizontally.
   1321     private void calculateStableBound(float scale, int horizontalSlack) {
   1322         Box b = mBoxes.get(0);
   1323 
   1324         // The width and height of the box in number of view pixels
   1325         int w = widthOf(b, scale);
   1326         int h = heightOf(b, scale);
   1327 
   1328         // When the edge of the view is aligned with the edge of the box
   1329         mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack;
   1330         mBoundRight = w / 2 - mViewW / 2 + horizontalSlack;
   1331         mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2;
   1332         mBoundBottom = h / 2 - mViewH / 2;
   1333 
   1334         // If the scaled height is smaller than the view height,
   1335         // force it to be in the center.
   1336         if (viewTallerThanScaledImage(scale)) {
   1337             mBoundTop = mBoundBottom = 0;
   1338         }
   1339 
   1340         // Same for width
   1341         if (viewWiderThanScaledImage(scale)) {
   1342             mBoundLeft = mBoundRight = mPlatform.mDefaultX;
   1343         }
   1344     }
   1345 
   1346     private void calculateStableBound(float scale) {
   1347         calculateStableBound(scale, 0);
   1348     }
   1349 
   1350     private boolean viewTallerThanScaledImage(float scale) {
   1351         return mViewH >= heightOf(mBoxes.get(0), scale);
   1352     }
   1353 
   1354     private boolean viewWiderThanScaledImage(float scale) {
   1355         return mViewW >= widthOf(mBoxes.get(0), scale);
   1356     }
   1357 
   1358     private float getTargetScale(Box b) {
   1359         return b.mAnimationStartTime == NO_ANIMATION
   1360                 ? b.mCurrentScale : b.mToScale;
   1361     }
   1362 
   1363     ////////////////////////////////////////////////////////////////////////////
   1364     //  Animatable: an thing which can do animation.
   1365     ////////////////////////////////////////////////////////////////////////////
   1366     private abstract static class Animatable {
   1367         public long mAnimationStartTime;
   1368         public int mAnimationKind;
   1369         public int mAnimationDuration;
   1370 
   1371         // This should be overridden in subclass to change the animation values
   1372         // give the progress value in [0, 1].
   1373         protected abstract boolean interpolate(float progress);
   1374         public abstract boolean startSnapback();
   1375 
   1376         // Returns true if the animation values changes, so things need to be
   1377         // redrawn.
   1378         public boolean advanceAnimation() {
   1379             if (mAnimationStartTime == NO_ANIMATION) {
   1380                 return false;
   1381             }
   1382             if (mAnimationStartTime == LAST_ANIMATION) {
   1383                 mAnimationStartTime = NO_ANIMATION;
   1384                 return startSnapback();
   1385             }
   1386 
   1387             float progress;
   1388             if (mAnimationDuration == 0) {
   1389                 progress = 1;
   1390             } else {
   1391                 long now = AnimationTime.get();
   1392                 progress =
   1393                     (float) (now - mAnimationStartTime) / mAnimationDuration;
   1394             }
   1395 
   1396             if (progress >= 1) {
   1397                 progress = 1;
   1398             } else {
   1399                 progress = applyInterpolationCurve(mAnimationKind, progress);
   1400             }
   1401 
   1402             boolean done = interpolate(progress);
   1403 
   1404             if (done) {
   1405                 mAnimationStartTime = LAST_ANIMATION;
   1406             }
   1407 
   1408             return true;
   1409         }
   1410 
   1411         private static float applyInterpolationCurve(int kind, float progress) {
   1412             float f = 1 - progress;
   1413             switch (kind) {
   1414                 case ANIM_KIND_SCROLL:
   1415                 case ANIM_KIND_FLING:
   1416                 case ANIM_KIND_FLING_X:
   1417                 case ANIM_KIND_DELETE:
   1418                 case ANIM_KIND_CAPTURE:
   1419                     progress = 1 - f;  // linear
   1420                     break;
   1421                 case ANIM_KIND_OPENING:
   1422                 case ANIM_KIND_SCALE:
   1423                     progress = 1 - f * f;  // quadratic
   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                 x = mDefaultX;
   1460             } else {
   1461                 calculateStableBound(scale, HORIZONTAL_SLACK);
   1462                 // If the picture is zoomed-in, we want to keep the focus point
   1463                 // stay in the same position on screen, so we need to adjust
   1464                 // target mCurrentX (which is the center of the focused
   1465                 // box). The position of the focus point on screen (relative the
   1466                 // the center of the view) is:
   1467                 //
   1468                 // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX
   1469                 // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX
   1470                 //
   1471                 if (!viewWiderThanScaledImage(scale)) {
   1472                     float scaleDiff = b.mCurrentScale - scale;
   1473                     x += (int) (mFocusX * scaleDiff + 0.5f);
   1474                 }
   1475                 x = Utils.clamp(x, mBoundLeft, mBoundRight);
   1476             }
   1477             if (mCurrentX != x || mCurrentY != y) {
   1478                 return doAnimation(x, y, ANIM_KIND_SNAPBACK);
   1479             }
   1480             return false;
   1481         }
   1482 
   1483         // The updateDefaultXY() should be called whenever these variables
   1484         // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4)
   1485         // mFilmMode
   1486         public void updateDefaultXY() {
   1487             // We don't check mFilmMode and return 0 for mDefaultX. Because
   1488             // otherwise if we decide to leave film mode because we are
   1489             // centered, we will immediately back into film mode because we find
   1490             // we are not centered.
   1491             if (mConstrained && !mConstrainedFrame.isEmpty()) {
   1492                 mDefaultX = mConstrainedFrame.centerX() - mViewW / 2;
   1493                 mDefaultY = mFilmMode ? 0 :
   1494                         mConstrainedFrame.centerY() - mViewH / 2;
   1495             } else {
   1496                 mDefaultX = 0;
   1497                 mDefaultY = 0;
   1498             }
   1499         }
   1500 
   1501         // Starts an animation for the platform.
   1502         private boolean doAnimation(int targetX, int targetY, int kind) {
   1503             if (mCurrentX == targetX && mCurrentY == targetY) return false;
   1504             mAnimationKind = kind;
   1505             mFromX = mCurrentX;
   1506             mFromY = mCurrentY;
   1507             mToX = targetX;
   1508             mToY = targetY;
   1509             mAnimationStartTime = AnimationTime.startTime();
   1510             mAnimationDuration = ANIM_TIME[kind];
   1511             mFlingOffset = 0;
   1512             advanceAnimation();
   1513             return true;
   1514         }
   1515 
   1516         @Override
   1517         protected boolean interpolate(float progress) {
   1518             if (mAnimationKind == ANIM_KIND_FLING) {
   1519                 return interpolateFlingPage(progress);
   1520             } else if (mAnimationKind == ANIM_KIND_FLING_X) {
   1521                 return interpolateFlingFilm(progress);
   1522             } else {
   1523                 return interpolateLinear(progress);
   1524             }
   1525         }
   1526 
   1527         private boolean interpolateFlingFilm(float progress) {
   1528             mFilmScroller.computeScrollOffset();
   1529             mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
   1530 
   1531             int dir = EdgeView.INVALID_DIRECTION;
   1532             if (mCurrentX < mDefaultX) {
   1533                 if (!mHasNext) {
   1534                     dir = EdgeView.RIGHT;
   1535                 }
   1536             } else if (mCurrentX > mDefaultX) {
   1537                 if (!mHasPrev) {
   1538                     dir = EdgeView.LEFT;
   1539                 }
   1540             }
   1541             if (dir != EdgeView.INVALID_DIRECTION) {
   1542                 // TODO: restore this onAbsorb call
   1543                 //int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
   1544                 //mListener.onAbsorb(v, dir);
   1545                 mFilmScroller.forceFinished(true);
   1546                 mCurrentX = mDefaultX;
   1547             }
   1548             return mFilmScroller.isFinished();
   1549         }
   1550 
   1551         private boolean interpolateFlingPage(float progress) {
   1552             mPageScroller.computeScrollOffset(progress);
   1553             Box b = mBoxes.get(0);
   1554             calculateStableBound(b.mCurrentScale);
   1555 
   1556             int oldX = mCurrentX;
   1557             mCurrentX = mPageScroller.getCurrX();
   1558 
   1559             // Check if we hit the edges; show edge effects if we do.
   1560             if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
   1561                 int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
   1562                 mListener.onAbsorb(v, EdgeView.RIGHT);
   1563             } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
   1564                 int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
   1565                 mListener.onAbsorb(v, EdgeView.LEFT);
   1566             }
   1567 
   1568             return progress >= 1;
   1569         }
   1570 
   1571         private boolean interpolateLinear(float progress) {
   1572             // Other animations
   1573             if (progress >= 1) {
   1574                 mCurrentX = mToX;
   1575                 mCurrentY = mToY;
   1576                 return true;
   1577             } else {
   1578                 if (mAnimationKind == ANIM_KIND_CAPTURE) {
   1579                     progress = CaptureAnimation.calculateSlide(progress);
   1580                 }
   1581                 mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
   1582                 mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
   1583                 if (mAnimationKind == ANIM_KIND_CAPTURE) {
   1584                     return false;
   1585                 } else {
   1586                     return (mCurrentX == mToX && mCurrentY == mToY);
   1587                 }
   1588             }
   1589         }
   1590     }
   1591 
   1592     ////////////////////////////////////////////////////////////////////////////
   1593     //  Box: represents a rectangular area which shows a picture.
   1594     ////////////////////////////////////////////////////////////////////////////
   1595     private class Box extends Animatable {
   1596         // Size of the bitmap
   1597         public int mImageW, mImageH;
   1598 
   1599         // This is true if we assume the image size is the same as view size
   1600         // until we know the actual size of image. This is also used to
   1601         // determine if there is an image ready to show.
   1602         public boolean mUseViewSize;
   1603 
   1604         // The minimum and maximum scale we allow for this box.
   1605         public float mScaleMin, mScaleMax;
   1606 
   1607         // The X/Y value indicates where the center of the box is on the view
   1608         // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
   1609         // actual values used currently. Note that the X values are implicitly
   1610         // defined by Platform and Gaps.
   1611         public int mCurrentY, mFromY, mToY;
   1612         public float mCurrentScale, mFromScale, mToScale;
   1613 
   1614         // The absolute X coordinate of the center of the box. This is only used
   1615         // during moveBox().
   1616         public int mAbsoluteX;
   1617 
   1618         @Override
   1619         public boolean startSnapback() {
   1620             if (mAnimationStartTime != NO_ANIMATION) return false;
   1621             if (mAnimationKind == ANIM_KIND_SCROLL
   1622                     && mListener.isHoldingDown()) return false;
   1623             if (mAnimationKind == ANIM_KIND_DELETE
   1624                     && mListener.isHoldingDelete()) return false;
   1625             if (mInScale && this == mBoxes.get(0)) return false;
   1626 
   1627             int y = mCurrentY;
   1628             float scale;
   1629 
   1630             if (this == mBoxes.get(0)) {
   1631                 float scaleMin = mExtraScalingRange ?
   1632                     mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
   1633                 float scaleMax = mExtraScalingRange ?
   1634                     mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
   1635                 scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
   1636                 if (mFilmMode) {
   1637                     y = 0;
   1638                 } else {
   1639                     calculateStableBound(scale, HORIZONTAL_SLACK);
   1640                     // If the picture is zoomed-in, we want to keep the focus
   1641                     // point stay in the same position on screen. See the
   1642                     // comment in Platform.startSnapback for details.
   1643                     if (!viewTallerThanScaledImage(scale)) {
   1644                         float scaleDiff = mCurrentScale - scale;
   1645                         y += (int) (mFocusY * scaleDiff + 0.5f);
   1646                     }
   1647                     y = Utils.clamp(y, mBoundTop, mBoundBottom);
   1648                 }
   1649             } else {
   1650                 y = 0;
   1651                 scale = mScaleMin;
   1652             }
   1653 
   1654             if (mCurrentY != y || mCurrentScale != scale) {
   1655                 return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
   1656             }
   1657             return false;
   1658         }
   1659 
   1660         private boolean doAnimation(int targetY, float targetScale, int kind) {
   1661             targetScale = clampScale(targetScale);
   1662 
   1663             if (mCurrentY == targetY && mCurrentScale == targetScale
   1664                     && kind != ANIM_KIND_CAPTURE) {
   1665                 return false;
   1666             }
   1667 
   1668             // Now starts an animation for the box.
   1669             mAnimationKind = kind;
   1670             mFromY = mCurrentY;
   1671             mFromScale = mCurrentScale;
   1672             mToY = targetY;
   1673             mToScale = targetScale;
   1674             mAnimationStartTime = AnimationTime.startTime();
   1675             mAnimationDuration = ANIM_TIME[kind];
   1676             advanceAnimation();
   1677             return true;
   1678         }
   1679 
   1680         // Clamps the input scale to the range that doAnimation() can reach.
   1681         public float clampScale(float s) {
   1682             return Utils.clamp(s,
   1683                     SCALE_MIN_EXTRA * mScaleMin,
   1684                     SCALE_MAX_EXTRA * mScaleMax);
   1685         }
   1686 
   1687         @Override
   1688         protected boolean interpolate(float progress) {
   1689             if (mAnimationKind == ANIM_KIND_FLING) {
   1690                 return interpolateFlingPage(progress);
   1691             } else {
   1692                 return interpolateLinear(progress);
   1693             }
   1694         }
   1695 
   1696         private boolean interpolateFlingPage(float progress) {
   1697             mPageScroller.computeScrollOffset(progress);
   1698             calculateStableBound(mCurrentScale);
   1699 
   1700             int oldY = mCurrentY;
   1701             mCurrentY = mPageScroller.getCurrY();
   1702 
   1703             // Check if we hit the edges; show edge effects if we do.
   1704             if (oldY > mBoundTop && mCurrentY == mBoundTop) {
   1705                 int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
   1706                 mListener.onAbsorb(v, EdgeView.BOTTOM);
   1707             } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
   1708                 int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
   1709                 mListener.onAbsorb(v, EdgeView.TOP);
   1710             }
   1711 
   1712             return progress >= 1;
   1713         }
   1714 
   1715         private boolean interpolateLinear(float progress) {
   1716             if (progress >= 1) {
   1717                 mCurrentY = mToY;
   1718                 mCurrentScale = mToScale;
   1719                 return true;
   1720             } else {
   1721                 mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
   1722                 mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
   1723                 if (mAnimationKind == ANIM_KIND_CAPTURE) {
   1724                     float f = CaptureAnimation.calculateScale(progress);
   1725                     mCurrentScale *= f;
   1726                     return false;
   1727                 } else {
   1728                     return (mCurrentY == mToY && mCurrentScale == mToScale);
   1729                 }
   1730             }
   1731         }
   1732     }
   1733 
   1734     ////////////////////////////////////////////////////////////////////////////
   1735     //  Gap: represents a rectangular area which is between two boxes.
   1736     ////////////////////////////////////////////////////////////////////////////
   1737     private class Gap extends Animatable {
   1738         // The default gap size between two boxes. The value may vary for
   1739         // different image size of the boxes and for different modes (page or
   1740         // film).
   1741         public int mDefaultSize;
   1742 
   1743         // The gap size between the two boxes.
   1744         public int mCurrentGap, mFromGap, mToGap;
   1745 
   1746         @Override
   1747         public boolean startSnapback() {
   1748             if (mAnimationStartTime != NO_ANIMATION) return false;
   1749             return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK);
   1750         }
   1751 
   1752         // Starts an animation for a gap.
   1753         public boolean doAnimation(int targetSize, int kind) {
   1754             if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) {
   1755                 return false;
   1756             }
   1757             mAnimationKind = kind;
   1758             mFromGap = mCurrentGap;
   1759             mToGap = targetSize;
   1760             mAnimationStartTime = AnimationTime.startTime();
   1761             mAnimationDuration = ANIM_TIME[mAnimationKind];
   1762             advanceAnimation();
   1763             return true;
   1764         }
   1765 
   1766         @Override
   1767         protected boolean interpolate(float progress) {
   1768             if (progress >= 1) {
   1769                 mCurrentGap = mToGap;
   1770                 return true;
   1771             } else {
   1772                 mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
   1773                 if (mAnimationKind == ANIM_KIND_CAPTURE) {
   1774                     float f = CaptureAnimation.calculateScale(progress);
   1775                     mCurrentGap = (int) (mCurrentGap * f);
   1776                     return false;
   1777                 } else {
   1778                     return (mCurrentGap == mToGap);
   1779                 }
   1780             }
   1781         }
   1782     }
   1783 
   1784     ////////////////////////////////////////////////////////////////////////////
   1785     //  FilmRatio: represents the progress of film mode change.
   1786     ////////////////////////////////////////////////////////////////////////////
   1787     private class FilmRatio extends Animatable {
   1788         // The film ratio: 1 means switching to film mode is complete, 0 means
   1789         // switching to page mode is complete.
   1790         public float mCurrentRatio, mFromRatio, mToRatio;
   1791 
   1792         @Override
   1793         public boolean startSnapback() {
   1794             float target = mFilmMode ? 1f : 0f;
   1795             if (target == mToRatio) return false;
   1796             return doAnimation(target, ANIM_KIND_SNAPBACK);
   1797         }
   1798 
   1799         // Starts an animation for the film ratio.
   1800         private boolean doAnimation(float targetRatio, int kind) {
   1801             mAnimationKind = kind;
   1802             mFromRatio = mCurrentRatio;
   1803             mToRatio = targetRatio;
   1804             mAnimationStartTime = AnimationTime.startTime();
   1805             mAnimationDuration = ANIM_TIME[mAnimationKind];
   1806             advanceAnimation();
   1807             return true;
   1808         }
   1809 
   1810         @Override
   1811         protected boolean interpolate(float progress) {
   1812             if (progress >= 1) {
   1813                 mCurrentRatio = mToRatio;
   1814                 return true;
   1815             } else {
   1816                 mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio);
   1817                 return (mCurrentRatio == mToRatio);
   1818             }
   1819         }
   1820     }
   1821 }
   1822