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