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