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.Scroller;
23
24import com.android.gallery3d.app.PhotoPage;
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        mFilmScroller = new Scroller(context, null, false);
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 or we are in fullscreen,
323        // we change the scale directly. Otherwise adjust the scales by a ratio,
324        // and snapback will animate the scale into the min/max bounds if necessary.
325        if ((wasViewSize && !isViewSize) || !mFilmMode) {
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    @SuppressWarnings("unused")
860    private void dumpState() {
861        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
862            Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
863        }
864
865        for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
866            dumpRect(CENTER_OUT_INDEX[i]);
867        }
868
869        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
870            for (int j = i + 1; j <= BOX_MAX; j++) {
871                if (Rect.intersects(mRects.get(i), mRects.get(j))) {
872                    Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
873                }
874            }
875        }
876    }
877
878    private void dumpRect(int i) {
879        StringBuilder sb = new StringBuilder();
880        Rect r = mRects.get(i);
881        sb.append("Rect " + i + ":");
882        sb.append("(");
883        sb.append(r.centerX());
884        sb.append(",");
885        sb.append(r.centerY());
886        sb.append(") [");
887        sb.append(r.width());
888        sb.append("x");
889        sb.append(r.height());
890        sb.append("]");
891        Log.d(TAG, sb.toString());
892    }
893
894    private void convertBoxToRect(int i) {
895        Box b = mBoxes.get(i);
896        Rect r = mRects.get(i);
897        int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2;
898        int w = widthOf(b);
899        int h = heightOf(b);
900        if (i == 0) {
901            int x = mPlatform.mCurrentX + mViewW / 2;
902            r.left = x - w / 2;
903            r.right = r.left + w;
904        } else if (i > 0) {
905            Rect a = mRects.get(i - 1);
906            Gap g = mGaps.get(i - 1);
907            r.left = a.right + g.mCurrentGap;
908            r.right = r.left + w;
909        } else {  // i < 0
910            Rect a = mRects.get(i + 1);
911            Gap g = mGaps.get(i);
912            r.right = a.left - g.mCurrentGap;
913            r.left = r.right - w;
914        }
915        r.top = y - h / 2;
916        r.bottom = r.top + h;
917    }
918
919    // Returns the position of a box.
920    public Rect getPosition(int index) {
921        return mRects.get(index);
922    }
923
924    ////////////////////////////////////////////////////////////////////////////
925    //  Box management
926    ////////////////////////////////////////////////////////////////////////////
927
928    // Initialize the platform to be at the view center.
929    private void initPlatform() {
930        mPlatform.updateDefaultXY();
931        mPlatform.mCurrentX = mPlatform.mDefaultX;
932        mPlatform.mCurrentY = mPlatform.mDefaultY;
933        mPlatform.mAnimationStartTime = NO_ANIMATION;
934    }
935
936    // Initialize a box to have the size of the view.
937    private void initBox(int index) {
938        Box b = mBoxes.get(index);
939        b.mImageW = mViewW;
940        b.mImageH = mViewH;
941        b.mUseViewSize = true;
942        b.mScaleMin = getMinimalScale(b);
943        b.mScaleMax = getMaximalScale(b);
944        b.mCurrentY = 0;
945        b.mCurrentScale = b.mScaleMin;
946        b.mAnimationStartTime = NO_ANIMATION;
947        b.mAnimationKind = ANIM_KIND_NONE;
948    }
949
950    // Initialize a box to a given size.
951    private void initBox(int index, Size size) {
952        if (size.width == 0 || size.height == 0) {
953            initBox(index);
954            return;
955        }
956        Box b = mBoxes.get(index);
957        b.mImageW = size.width;
958        b.mImageH = size.height;
959        b.mUseViewSize = false;
960        b.mScaleMin = getMinimalScale(b);
961        b.mScaleMax = getMaximalScale(b);
962        b.mCurrentY = 0;
963        b.mCurrentScale = b.mScaleMin;
964        b.mAnimationStartTime = NO_ANIMATION;
965        b.mAnimationKind = ANIM_KIND_NONE;
966    }
967
968    // Initialize a gap. This can only be called after the boxes around the gap
969    // has been initialized.
970    private void initGap(int index) {
971        Gap g = mGaps.get(index);
972        g.mDefaultSize = getDefaultGapSize(index);
973        g.mCurrentGap = g.mDefaultSize;
974        g.mAnimationStartTime = NO_ANIMATION;
975    }
976
977    private void initGap(int index, int size) {
978        Gap g = mGaps.get(index);
979        g.mDefaultSize = getDefaultGapSize(index);
980        g.mCurrentGap = size;
981        g.mAnimationStartTime = NO_ANIMATION;
982    }
983
984    @SuppressWarnings("unused")
985    private void debugMoveBox(int fromIndex[]) {
986        StringBuilder s = new StringBuilder("moveBox:");
987        for (int i = 0; i < fromIndex.length; i++) {
988            int j = fromIndex[i];
989            if (j == Integer.MAX_VALUE) {
990                s.append(" N");
991            } else {
992                s.append(" ");
993                s.append(fromIndex[i]);
994            }
995        }
996        Log.d(TAG, s.toString());
997    }
998
999    // Move the boxes: it may indicate focus change, box deleted, box appearing,
1000    // box reordered, etc.
1001    //
1002    // Each element in the fromIndex array indicates where each box was in the
1003    // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
1004    // means the box is new.
1005    //
1006    // For example:
1007    // N N N N N N N -- all new boxes
1008    // -3 -2 -1 0 1 2 3 -- nothing changed
1009    // -2 -1 0 1 2 3 N -- focus goes to the next box
1010    // N -3 -2 -1 0 1 2 -- focus goes to the previous box
1011    // -3 -2 -1 1 2 3 N -- the focused box was deleted.
1012    //
1013    // hasPrev/hasNext indicates if there are previous/next boxes for the
1014    // focused box. constrained indicates whether the focused box should be put
1015    // into the constrained frame.
1016    public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext,
1017            boolean constrained, Size[] sizes) {
1018        //debugMoveBox(fromIndex);
1019        mHasPrev = hasPrev;
1020        mHasNext = hasNext;
1021
1022        RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
1023
1024        // 1. Get the absolute X coordinates for the boxes.
1025        layoutAndSetPosition();
1026        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
1027            Box b = mBoxes.get(i);
1028            Rect r = mRects.get(i);
1029            b.mAbsoluteX = r.centerX() - mViewW / 2;
1030        }
1031
1032        // 2. copy boxes and gaps to temporary storage.
1033        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
1034            mTempBoxes.put(i, mBoxes.get(i));
1035            mBoxes.put(i, null);
1036        }
1037        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
1038            mTempGaps.put(i, mGaps.get(i));
1039            mGaps.put(i, null);
1040        }
1041
1042        // 3. move back boxes that are used in the new array.
1043        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
1044            int j = from.get(i);
1045            if (j == Integer.MAX_VALUE) continue;
1046            mBoxes.put(i, mTempBoxes.get(j));
1047            mTempBoxes.put(j, null);
1048        }
1049
1050        // 4. move back gaps if both boxes around it are kept together.
1051        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
1052            int j = from.get(i);
1053            if (j == Integer.MAX_VALUE) continue;
1054            int k = from.get(i + 1);
1055            if (k == Integer.MAX_VALUE) continue;
1056            if (j + 1 == k) {
1057                mGaps.put(i, mTempGaps.get(j));
1058                mTempGaps.put(j, null);
1059            }
1060        }
1061
1062        // 5. recycle the boxes that are not used in the new array.
1063        int k = -BOX_MAX;
1064        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
1065            if (mBoxes.get(i) != null) continue;
1066            while (mTempBoxes.get(k) == null) {
1067                k++;
1068            }
1069            mBoxes.put(i, mTempBoxes.get(k++));
1070            initBox(i, sizes[i + BOX_MAX]);
1071        }
1072
1073        // 6. Now give the recycled box a reasonable absolute X position.
1074        //
1075        // First try to find the first and the last box which the absolute X
1076        // position is known.
1077        int first, last;
1078        for (first = -BOX_MAX; first <= BOX_MAX; first++) {
1079            if (from.get(first) != Integer.MAX_VALUE) break;
1080        }
1081        for (last = BOX_MAX; last >= -BOX_MAX; last--) {
1082            if (from.get(last) != Integer.MAX_VALUE) break;
1083        }
1084        // If there is no box has known X position at all, make the focused one
1085        // as known.
1086        if (first > BOX_MAX) {
1087            mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
1088            first = last = 0;
1089        }
1090        // Now for those boxes between first and last, assign their position to
1091        // align to the previous box or the next box with known position. For
1092        // the boxes before first or after last, we will use a new default gap
1093        // size below.
1094
1095        // Align to the previous box
1096        for (int i = Math.max(0, first + 1); i < last; i++) {
1097            if (from.get(i) != Integer.MAX_VALUE) continue;
1098            Box a = mBoxes.get(i - 1);
1099            Box b = mBoxes.get(i);
1100            int wa = widthOf(a);
1101            int wb = widthOf(b);
1102            b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2
1103                    + getDefaultGapSize(i);
1104            if (mPopFromTop) {
1105                b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
1106            } else {
1107                b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
1108            }
1109        }
1110
1111        // Align to the next box
1112        for (int i = Math.min(-1, last - 1); i > first; i--) {
1113            if (from.get(i) != Integer.MAX_VALUE) continue;
1114            Box a = mBoxes.get(i + 1);
1115            Box b = mBoxes.get(i);
1116            int wa = widthOf(a);
1117            int wb = widthOf(b);
1118            b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2)
1119                    - getDefaultGapSize(i);
1120            if (mPopFromTop) {
1121                b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
1122            } else {
1123                b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
1124            }
1125        }
1126
1127        // 7. recycle the gaps that are not used in the new array.
1128        k = -BOX_MAX;
1129        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
1130            if (mGaps.get(i) != null) continue;
1131            while (mTempGaps.get(k) == null) {
1132                k++;
1133            }
1134            mGaps.put(i, mTempGaps.get(k++));
1135            Box a = mBoxes.get(i);
1136            Box b = mBoxes.get(i + 1);
1137            int wa = widthOf(a);
1138            int wb = widthOf(b);
1139            if (i >= first && i < last) {
1140                int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
1141                initGap(i, g);
1142            } else {
1143                initGap(i);
1144            }
1145        }
1146
1147        // 8. calculate the new absolute X coordinates for those box before
1148        // first or after last.
1149        for (int i = first - 1; i >= -BOX_MAX; i--) {
1150            Box a = mBoxes.get(i + 1);
1151            Box b = mBoxes.get(i);
1152            int wa = widthOf(a);
1153            int wb = widthOf(b);
1154            Gap g = mGaps.get(i);
1155            b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap;
1156        }
1157
1158        for (int i = last + 1; i <= BOX_MAX; i++) {
1159            Box a = mBoxes.get(i - 1);
1160            Box b = mBoxes.get(i);
1161            int wa = widthOf(a);
1162            int wb = widthOf(b);
1163            Gap g = mGaps.get(i - 1);
1164            b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap;
1165        }
1166
1167        // 9. offset the Platform position
1168        int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
1169        mPlatform.mCurrentX += dx;
1170        mPlatform.mFromX += dx;
1171        mPlatform.mToX += dx;
1172        mPlatform.mFlingOffset += dx;
1173
1174        if (mConstrained != constrained) {
1175            mConstrained = constrained;
1176            mPlatform.updateDefaultXY();
1177            updateScaleAndGapLimit();
1178        }
1179
1180        snapAndRedraw();
1181    }
1182
1183    ////////////////////////////////////////////////////////////////////////////
1184    //  Public utilities
1185    ////////////////////////////////////////////////////////////////////////////
1186
1187    public boolean isAtMinimalScale() {
1188        Box b = mBoxes.get(0);
1189        return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
1190    }
1191
1192    public boolean isCenter() {
1193        Box b = mBoxes.get(0);
1194        return mPlatform.mCurrentX == mPlatform.mDefaultX
1195            && b.mCurrentY == 0;
1196    }
1197
1198    public int getImageWidth() {
1199        Box b = mBoxes.get(0);
1200        return b.mImageW;
1201    }
1202
1203    public int getImageHeight() {
1204        Box b = mBoxes.get(0);
1205        return b.mImageH;
1206    }
1207
1208    public float getImageScale() {
1209        Box b = mBoxes.get(0);
1210        return b.mCurrentScale;
1211    }
1212
1213    public int getImageAtEdges() {
1214        Box b = mBoxes.get(0);
1215        Platform p = mPlatform;
1216        calculateStableBound(b.mCurrentScale);
1217        int edges = 0;
1218        if (p.mCurrentX <= mBoundLeft) {
1219            edges |= IMAGE_AT_RIGHT_EDGE;
1220        }
1221        if (p.mCurrentX >= mBoundRight) {
1222            edges |= IMAGE_AT_LEFT_EDGE;
1223        }
1224        if (b.mCurrentY <= mBoundTop) {
1225            edges |= IMAGE_AT_BOTTOM_EDGE;
1226        }
1227        if (b.mCurrentY >= mBoundBottom) {
1228            edges |= IMAGE_AT_TOP_EDGE;
1229        }
1230        return edges;
1231    }
1232
1233    public boolean isScrolling() {
1234        return mPlatform.mAnimationStartTime != NO_ANIMATION
1235                && mPlatform.mCurrentX != mPlatform.mToX;
1236    }
1237
1238    public void stopScrolling() {
1239        if (mPlatform.mAnimationStartTime == NO_ANIMATION) return;
1240        if (mFilmMode) mFilmScroller.forceFinished(true);
1241        mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX;
1242    }
1243
1244    public float getFilmRatio() {
1245        return mFilmRatio.mCurrentRatio;
1246    }
1247
1248    public void setPopFromTop(boolean top) {
1249        mPopFromTop = top;
1250    }
1251
1252    public boolean hasDeletingBox() {
1253        for(int i = -BOX_MAX; i <= BOX_MAX; i++) {
1254            if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) {
1255                return true;
1256            }
1257        }
1258        return false;
1259    }
1260
1261    ////////////////////////////////////////////////////////////////////////////
1262    //  Private utilities
1263    ////////////////////////////////////////////////////////////////////////////
1264
1265    private float getMinimalScale(Box b) {
1266        float wFactor = 1.0f;
1267        float hFactor = 1.0f;
1268        int viewW, viewH;
1269
1270        if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty()
1271                && b == mBoxes.get(0)) {
1272            viewW = mConstrainedFrame.width();
1273            viewH = mConstrainedFrame.height();
1274        } else {
1275            viewW = mViewW;
1276            viewH = mViewH;
1277        }
1278
1279        if (mFilmMode) {
1280            if (mViewH > mViewW) {  // portrait
1281                wFactor = FILM_MODE_PORTRAIT_WIDTH;
1282                hFactor = FILM_MODE_PORTRAIT_HEIGHT;
1283            } else {  // landscape
1284                wFactor = FILM_MODE_LANDSCAPE_WIDTH;
1285                hFactor = FILM_MODE_LANDSCAPE_HEIGHT;
1286            }
1287        }
1288
1289        float s = Math.min(wFactor * viewW / b.mImageW,
1290                hFactor * viewH / b.mImageH);
1291        return Math.min(SCALE_LIMIT, s);
1292    }
1293
1294    private float getMaximalScale(Box b) {
1295        if (mFilmMode) return getMinimalScale(b);
1296        if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b);
1297        return SCALE_LIMIT;
1298    }
1299
1300    private static boolean isAlmostEqual(float a, float b) {
1301        float diff = a - b;
1302        return (diff < 0 ? -diff : diff) < 0.02f;
1303    }
1304
1305    // Calculates the stable region of mPlatform.mCurrentX and
1306    // mBoxes.get(0).mCurrentY, where "stable" means
1307    //
1308    // (1) If the dimension of scaled image >= view dimension, we will not
1309    // see black region outside the image (at that dimension).
1310    // (2) If the dimension of scaled image < view dimension, we will center
1311    // the scaled image.
1312    //
1313    // We might temporarily go out of this stable during user interaction,
1314    // but will "snap back" after user stops interaction.
1315    //
1316    // The results are stored in mBound{Left/Right/Top/Bottom}.
1317    //
1318    // An extra parameter "horizontalSlack" (which has the value of 0 usually)
1319    // is used to extend the stable region by some pixels on each side
1320    // horizontally.
1321    private void calculateStableBound(float scale, int horizontalSlack) {
1322        Box b = mBoxes.get(0);
1323
1324        // The width and height of the box in number of view pixels
1325        int w = widthOf(b, scale);
1326        int h = heightOf(b, scale);
1327
1328        // When the edge of the view is aligned with the edge of the box
1329        mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack;
1330        mBoundRight = w / 2 - mViewW / 2 + horizontalSlack;
1331        mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2;
1332        mBoundBottom = h / 2 - mViewH / 2;
1333
1334        // If the scaled height is smaller than the view height,
1335        // force it to be in the center.
1336        if (viewTallerThanScaledImage(scale)) {
1337            mBoundTop = mBoundBottom = 0;
1338        }
1339
1340        // Same for width
1341        if (viewWiderThanScaledImage(scale)) {
1342            mBoundLeft = mBoundRight = mPlatform.mDefaultX;
1343        }
1344    }
1345
1346    private void calculateStableBound(float scale) {
1347        calculateStableBound(scale, 0);
1348    }
1349
1350    private boolean viewTallerThanScaledImage(float scale) {
1351        return mViewH >= heightOf(mBoxes.get(0), scale);
1352    }
1353
1354    private boolean viewWiderThanScaledImage(float scale) {
1355        return mViewW >= widthOf(mBoxes.get(0), scale);
1356    }
1357
1358    private float getTargetScale(Box b) {
1359        return b.mAnimationStartTime == NO_ANIMATION
1360                ? b.mCurrentScale : b.mToScale;
1361    }
1362
1363    ////////////////////////////////////////////////////////////////////////////
1364    //  Animatable: an thing which can do animation.
1365    ////////////////////////////////////////////////////////////////////////////
1366    private abstract static class Animatable {
1367        public long mAnimationStartTime;
1368        public int mAnimationKind;
1369        public int mAnimationDuration;
1370
1371        // This should be overridden in subclass to change the animation values
1372        // give the progress value in [0, 1].
1373        protected abstract boolean interpolate(float progress);
1374        public abstract boolean startSnapback();
1375
1376        // Returns true if the animation values changes, so things need to be
1377        // redrawn.
1378        public boolean advanceAnimation() {
1379            if (mAnimationStartTime == NO_ANIMATION) {
1380                return false;
1381            }
1382            if (mAnimationStartTime == LAST_ANIMATION) {
1383                mAnimationStartTime = NO_ANIMATION;
1384                return startSnapback();
1385            }
1386
1387            float progress;
1388            if (mAnimationDuration == 0) {
1389                progress = 1;
1390            } else {
1391                long now = AnimationTime.get();
1392                progress =
1393                    (float) (now - mAnimationStartTime) / mAnimationDuration;
1394            }
1395
1396            if (progress >= 1) {
1397                progress = 1;
1398            } else {
1399                progress = applyInterpolationCurve(mAnimationKind, progress);
1400            }
1401
1402            boolean done = interpolate(progress);
1403
1404            if (done) {
1405                mAnimationStartTime = LAST_ANIMATION;
1406            }
1407
1408            return true;
1409        }
1410
1411        private static float applyInterpolationCurve(int kind, float progress) {
1412            float f = 1 - progress;
1413            switch (kind) {
1414                case ANIM_KIND_SCROLL:
1415                case ANIM_KIND_FLING:
1416                case ANIM_KIND_FLING_X:
1417                case ANIM_KIND_DELETE:
1418                case ANIM_KIND_CAPTURE:
1419                    progress = 1 - f;  // linear
1420                    break;
1421                case ANIM_KIND_OPENING:
1422                case ANIM_KIND_SCALE:
1423                    progress = 1 - f * f;  // quadratic
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                x = mDefaultX;
1460            } else {
1461                calculateStableBound(scale, HORIZONTAL_SLACK);
1462                // If the picture is zoomed-in, we want to keep the focus point
1463                // stay in the same position on screen, so we need to adjust
1464                // target mCurrentX (which is the center of the focused
1465                // box). The position of the focus point on screen (relative the
1466                // the center of the view) is:
1467                //
1468                // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX
1469                // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX
1470                //
1471                if (!viewWiderThanScaledImage(scale)) {
1472                    float scaleDiff = b.mCurrentScale - scale;
1473                    x += (int) (mFocusX * scaleDiff + 0.5f);
1474                }
1475                x = Utils.clamp(x, mBoundLeft, mBoundRight);
1476            }
1477            if (mCurrentX != x || mCurrentY != y) {
1478                return doAnimation(x, y, ANIM_KIND_SNAPBACK);
1479            }
1480            return false;
1481        }
1482
1483        // The updateDefaultXY() should be called whenever these variables
1484        // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4)
1485        // mFilmMode
1486        public void updateDefaultXY() {
1487            // We don't check mFilmMode and return 0 for mDefaultX. Because
1488            // otherwise if we decide to leave film mode because we are
1489            // centered, we will immediately back into film mode because we find
1490            // we are not centered.
1491            if (mConstrained && !mConstrainedFrame.isEmpty()) {
1492                mDefaultX = mConstrainedFrame.centerX() - mViewW / 2;
1493                mDefaultY = mFilmMode ? 0 :
1494                        mConstrainedFrame.centerY() - mViewH / 2;
1495            } else {
1496                mDefaultX = 0;
1497                mDefaultY = 0;
1498            }
1499        }
1500
1501        // Starts an animation for the platform.
1502        private boolean doAnimation(int targetX, int targetY, int kind) {
1503            if (mCurrentX == targetX && mCurrentY == targetY) return false;
1504            mAnimationKind = kind;
1505            mFromX = mCurrentX;
1506            mFromY = mCurrentY;
1507            mToX = targetX;
1508            mToY = targetY;
1509            mAnimationStartTime = AnimationTime.startTime();
1510            mAnimationDuration = ANIM_TIME[kind];
1511            mFlingOffset = 0;
1512            advanceAnimation();
1513            return true;
1514        }
1515
1516        @Override
1517        protected boolean interpolate(float progress) {
1518            if (mAnimationKind == ANIM_KIND_FLING) {
1519                return interpolateFlingPage(progress);
1520            } else if (mAnimationKind == ANIM_KIND_FLING_X) {
1521                return interpolateFlingFilm(progress);
1522            } else {
1523                return interpolateLinear(progress);
1524            }
1525        }
1526
1527        private boolean interpolateFlingFilm(float progress) {
1528            mFilmScroller.computeScrollOffset();
1529            mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
1530
1531            int dir = EdgeView.INVALID_DIRECTION;
1532            if (mCurrentX < mDefaultX) {
1533                if (!mHasNext) {
1534                    dir = EdgeView.RIGHT;
1535                }
1536            } else if (mCurrentX > mDefaultX) {
1537                if (!mHasPrev) {
1538                    dir = EdgeView.LEFT;
1539                }
1540            }
1541            if (dir != EdgeView.INVALID_DIRECTION) {
1542                // TODO: restore this onAbsorb call
1543                //int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
1544                //mListener.onAbsorb(v, dir);
1545                mFilmScroller.forceFinished(true);
1546                mCurrentX = mDefaultX;
1547            }
1548            return mFilmScroller.isFinished();
1549        }
1550
1551        private boolean interpolateFlingPage(float progress) {
1552            mPageScroller.computeScrollOffset(progress);
1553            Box b = mBoxes.get(0);
1554            calculateStableBound(b.mCurrentScale);
1555
1556            int oldX = mCurrentX;
1557            mCurrentX = mPageScroller.getCurrX();
1558
1559            // Check if we hit the edges; show edge effects if we do.
1560            if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
1561                int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
1562                mListener.onAbsorb(v, EdgeView.RIGHT);
1563            } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
1564                int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
1565                mListener.onAbsorb(v, EdgeView.LEFT);
1566            }
1567
1568            return progress >= 1;
1569        }
1570
1571        private boolean interpolateLinear(float progress) {
1572            // Other animations
1573            if (progress >= 1) {
1574                mCurrentX = mToX;
1575                mCurrentY = mToY;
1576                return true;
1577            } else {
1578                if (mAnimationKind == ANIM_KIND_CAPTURE) {
1579                    progress = CaptureAnimation.calculateSlide(progress);
1580                }
1581                mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
1582                mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
1583                if (mAnimationKind == ANIM_KIND_CAPTURE) {
1584                    return false;
1585                } else {
1586                    return (mCurrentX == mToX && mCurrentY == mToY);
1587                }
1588            }
1589        }
1590    }
1591
1592    ////////////////////////////////////////////////////////////////////////////
1593    //  Box: represents a rectangular area which shows a picture.
1594    ////////////////////////////////////////////////////////////////////////////
1595    private class Box extends Animatable {
1596        // Size of the bitmap
1597        public int mImageW, mImageH;
1598
1599        // This is true if we assume the image size is the same as view size
1600        // until we know the actual size of image. This is also used to
1601        // determine if there is an image ready to show.
1602        public boolean mUseViewSize;
1603
1604        // The minimum and maximum scale we allow for this box.
1605        public float mScaleMin, mScaleMax;
1606
1607        // The X/Y value indicates where the center of the box is on the view
1608        // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
1609        // actual values used currently. Note that the X values are implicitly
1610        // defined by Platform and Gaps.
1611        public int mCurrentY, mFromY, mToY;
1612        public float mCurrentScale, mFromScale, mToScale;
1613
1614        // The absolute X coordinate of the center of the box. This is only used
1615        // during moveBox().
1616        public int mAbsoluteX;
1617
1618        @Override
1619        public boolean startSnapback() {
1620            if (mAnimationStartTime != NO_ANIMATION) return false;
1621            if (mAnimationKind == ANIM_KIND_SCROLL
1622                    && mListener.isHoldingDown()) return false;
1623            if (mAnimationKind == ANIM_KIND_DELETE
1624                    && mListener.isHoldingDelete()) return false;
1625            if (mInScale && this == mBoxes.get(0)) return false;
1626
1627            int y = mCurrentY;
1628            float scale;
1629
1630            if (this == mBoxes.get(0)) {
1631                float scaleMin = mExtraScalingRange ?
1632                    mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
1633                float scaleMax = mExtraScalingRange ?
1634                    mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
1635                scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
1636                if (mFilmMode) {
1637                    y = 0;
1638                } else {
1639                    calculateStableBound(scale, HORIZONTAL_SLACK);
1640                    // If the picture is zoomed-in, we want to keep the focus
1641                    // point stay in the same position on screen. See the
1642                    // comment in Platform.startSnapback for details.
1643                    if (!viewTallerThanScaledImage(scale)) {
1644                        float scaleDiff = mCurrentScale - scale;
1645                        y += (int) (mFocusY * scaleDiff + 0.5f);
1646                    }
1647                    y = Utils.clamp(y, mBoundTop, mBoundBottom);
1648                }
1649            } else {
1650                y = 0;
1651                scale = mScaleMin;
1652            }
1653
1654            if (mCurrentY != y || mCurrentScale != scale) {
1655                return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
1656            }
1657            return false;
1658        }
1659
1660        private boolean doAnimation(int targetY, float targetScale, int kind) {
1661            targetScale = clampScale(targetScale);
1662
1663            if (mCurrentY == targetY && mCurrentScale == targetScale
1664                    && kind != ANIM_KIND_CAPTURE) {
1665                return false;
1666            }
1667
1668            // Now starts an animation for the box.
1669            mAnimationKind = kind;
1670            mFromY = mCurrentY;
1671            mFromScale = mCurrentScale;
1672            mToY = targetY;
1673            mToScale = targetScale;
1674            mAnimationStartTime = AnimationTime.startTime();
1675            mAnimationDuration = ANIM_TIME[kind];
1676            advanceAnimation();
1677            return true;
1678        }
1679
1680        // Clamps the input scale to the range that doAnimation() can reach.
1681        public float clampScale(float s) {
1682            return Utils.clamp(s,
1683                    SCALE_MIN_EXTRA * mScaleMin,
1684                    SCALE_MAX_EXTRA * mScaleMax);
1685        }
1686
1687        @Override
1688        protected boolean interpolate(float progress) {
1689            if (mAnimationKind == ANIM_KIND_FLING) {
1690                return interpolateFlingPage(progress);
1691            } else {
1692                return interpolateLinear(progress);
1693            }
1694        }
1695
1696        private boolean interpolateFlingPage(float progress) {
1697            mPageScroller.computeScrollOffset(progress);
1698            calculateStableBound(mCurrentScale);
1699
1700            int oldY = mCurrentY;
1701            mCurrentY = mPageScroller.getCurrY();
1702
1703            // Check if we hit the edges; show edge effects if we do.
1704            if (oldY > mBoundTop && mCurrentY == mBoundTop) {
1705                int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
1706                mListener.onAbsorb(v, EdgeView.BOTTOM);
1707            } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
1708                int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
1709                mListener.onAbsorb(v, EdgeView.TOP);
1710            }
1711
1712            return progress >= 1;
1713        }
1714
1715        private boolean interpolateLinear(float progress) {
1716            if (progress >= 1) {
1717                mCurrentY = mToY;
1718                mCurrentScale = mToScale;
1719                return true;
1720            } else {
1721                mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
1722                mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
1723                if (mAnimationKind == ANIM_KIND_CAPTURE) {
1724                    float f = CaptureAnimation.calculateScale(progress);
1725                    mCurrentScale *= f;
1726                    return false;
1727                } else {
1728                    return (mCurrentY == mToY && mCurrentScale == mToScale);
1729                }
1730            }
1731        }
1732    }
1733
1734    ////////////////////////////////////////////////////////////////////////////
1735    //  Gap: represents a rectangular area which is between two boxes.
1736    ////////////////////////////////////////////////////////////////////////////
1737    private class Gap extends Animatable {
1738        // The default gap size between two boxes. The value may vary for
1739        // different image size of the boxes and for different modes (page or
1740        // film).
1741        public int mDefaultSize;
1742
1743        // The gap size between the two boxes.
1744        public int mCurrentGap, mFromGap, mToGap;
1745
1746        @Override
1747        public boolean startSnapback() {
1748            if (mAnimationStartTime != NO_ANIMATION) return false;
1749            return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK);
1750        }
1751
1752        // Starts an animation for a gap.
1753        public boolean doAnimation(int targetSize, int kind) {
1754            if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) {
1755                return false;
1756            }
1757            mAnimationKind = kind;
1758            mFromGap = mCurrentGap;
1759            mToGap = targetSize;
1760            mAnimationStartTime = AnimationTime.startTime();
1761            mAnimationDuration = ANIM_TIME[mAnimationKind];
1762            advanceAnimation();
1763            return true;
1764        }
1765
1766        @Override
1767        protected boolean interpolate(float progress) {
1768            if (progress >= 1) {
1769                mCurrentGap = mToGap;
1770                return true;
1771            } else {
1772                mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
1773                if (mAnimationKind == ANIM_KIND_CAPTURE) {
1774                    float f = CaptureAnimation.calculateScale(progress);
1775                    mCurrentGap = (int) (mCurrentGap * f);
1776                    return false;
1777                } else {
1778                    return (mCurrentGap == mToGap);
1779                }
1780            }
1781        }
1782    }
1783
1784    ////////////////////////////////////////////////////////////////////////////
1785    //  FilmRatio: represents the progress of film mode change.
1786    ////////////////////////////////////////////////////////////////////////////
1787    private class FilmRatio extends Animatable {
1788        // The film ratio: 1 means switching to film mode is complete, 0 means
1789        // switching to page mode is complete.
1790        public float mCurrentRatio, mFromRatio, mToRatio;
1791
1792        @Override
1793        public boolean startSnapback() {
1794            float target = mFilmMode ? 1f : 0f;
1795            if (target == mToRatio) return false;
1796            return doAnimation(target, ANIM_KIND_SNAPBACK);
1797        }
1798
1799        // Starts an animation for the film ratio.
1800        private boolean doAnimation(float targetRatio, int kind) {
1801            mAnimationKind = kind;
1802            mFromRatio = mCurrentRatio;
1803            mToRatio = targetRatio;
1804            mAnimationStartTime = AnimationTime.startTime();
1805            mAnimationDuration = ANIM_TIME[mAnimationKind];
1806            advanceAnimation();
1807            return true;
1808        }
1809
1810        @Override
1811        protected boolean interpolate(float progress) {
1812            if (progress >= 1) {
1813                mCurrentRatio = mToRatio;
1814                return true;
1815            } else {
1816                mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio);
1817                return (mCurrentRatio == mToRatio);
1818            }
1819        }
1820    }
1821}
1822