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