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