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