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