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