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