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