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