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 
17 package com.android.gallery3d.ui;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.util.Log;
22 import android.widget.Scroller;
23 
24 import com.android.gallery3d.app.PhotoPage;
25 import com.android.gallery3d.common.Utils;
26 import com.android.gallery3d.ui.PhotoView.Size;
27 import com.android.gallery3d.util.GalleryUtils;
28 import com.android.gallery3d.util.RangeArray;
29 import com.android.gallery3d.util.RangeIntArray;
30 
31 class 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 {
invalidate()190         void invalidate();
isHoldingDown()191         boolean isHoldingDown();
isHoldingDelete()192         boolean isHoldingDelete();
193 
194         // EdgeView
onPull(int offset, int direction)195         void onPull(int offset, int direction);
onRelease()196         void onRelease();
onAbsorb(int velocity, int direction)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 
PositionController(Context context, Listener listener)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 
setOpenAnimationRect(Rect r)229     public void setOpenAnimationRect(Rect r) {
230         mOpenAnimationRect = r;
231     }
232 
setViewSize(int viewW, int viewH)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 
setConstrainedFrame(Rect cFrame)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 
forceImageSize(int index, Size s)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 
setImageSize(int index, Size s, Rect cFrame)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.
setBoxSize(int i, int width, int height, boolean isViewSize)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 
startOpeningAnimationIfNeeded()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 
setFilmMode(boolean enabled)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 
setExtraScalingRange(boolean enabled)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.
updateScaleAndGapLimit()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.
getDefaultGapSize(int i)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))
gapToSide(Box b)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.
stopAnimation()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 
skipAnimation()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 
snapback()461     public void snapback() {
462         snapAndRedraw();
463     }
464 
skipToFinalPosition()465     public void skipToFinalPosition() {
466         stopAnimation();
467         snapAndRedraw();
468         skipAnimation();
469     }
470 
471     ////////////////////////////////////////////////////////////////////////////
472     //  Start an animations for the focused box
473     ////////////////////////////////////////////////////////////////////////////
474 
zoomIn(float tapX, float tapY, float targetScale)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 
resetToFullView()495     public void resetToFullView() {
496         Box b = mBoxes.get(0);
497         startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_ZOOM);
498     }
499 
beginScale(float focusX, float focusY)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.
scaleBy(float s, float focusX, float focusY)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 
endScale()535     public void endScale() {
536         mInScale = false;
537         snapAndRedraw();
538     }
539 
540     // Slide the focused box to the center of the view.
startHorizontalSlide()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.
startCaptureAnimationSlide(int offset)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.
canScroll()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 
scrollPage(int dx, int dy)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 
scrollFilmX(int dx)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 
scrollFilmY(int boxIndex, int dy)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 
flingPage(int velocityX, int velocityY)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 
flingFilmX(int velocityX)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.
flingFilmY(int boxIndex, int velocityY)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).
hitTest(int x, int y)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     ////////////////////////////////////////////////////////////////////////////
redraw()777     private void redraw() {
778         layoutAndSetPosition();
779         mListener.invalidate();
780     }
781 
snapAndRedraw()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 
startAnimation(int targetX, int targetY, float targetScale, int kind)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 
advanceAnimation()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 
inOpeningAnimation()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.
widthOf(Box b)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.
heightOf(Box b)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.
widthOf(Box b, float 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.
heightOf(Box b, float 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).
layoutAndSetPosition()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")
dumpState()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 
dumpRect(int i)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 
convertBoxToRect(int i)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.
getPosition(int index)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.
initPlatform()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.
initBox(int index)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.
initBox(int index, Size 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.
initGap(int index)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 
initGap(int index, int size)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")
debugMoveBox(int fromIndex[])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.
moveBox(int fromIndex[], boolean hasPrev, boolean hasNext, boolean constrained, Size[] sizes)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 
isAtMinimalScale()1187     public boolean isAtMinimalScale() {
1188         Box b = mBoxes.get(0);
1189         return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
1190     }
1191 
isCenter()1192     public boolean isCenter() {
1193         Box b = mBoxes.get(0);
1194         return mPlatform.mCurrentX == mPlatform.mDefaultX
1195             && b.mCurrentY == 0;
1196     }
1197 
getImageWidth()1198     public int getImageWidth() {
1199         Box b = mBoxes.get(0);
1200         return b.mImageW;
1201     }
1202 
getImageHeight()1203     public int getImageHeight() {
1204         Box b = mBoxes.get(0);
1205         return b.mImageH;
1206     }
1207 
getImageScale()1208     public float getImageScale() {
1209         Box b = mBoxes.get(0);
1210         return b.mCurrentScale;
1211     }
1212 
getImageAtEdges()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 
isScrolling()1233     public boolean isScrolling() {
1234         return mPlatform.mAnimationStartTime != NO_ANIMATION
1235                 && mPlatform.mCurrentX != mPlatform.mToX;
1236     }
1237 
stopScrolling()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 
getFilmRatio()1244     public float getFilmRatio() {
1245         return mFilmRatio.mCurrentRatio;
1246     }
1247 
setPopFromTop(boolean top)1248     public void setPopFromTop(boolean top) {
1249         mPopFromTop = top;
1250     }
1251 
hasDeletingBox()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 
getMinimalScale(Box b)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 
getMaximalScale(Box b)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 
isAlmostEqual(float a, float b)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.
calculateStableBound(float scale, int horizontalSlack)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 
calculateStableBound(float scale)1346     private void calculateStableBound(float scale) {
1347         calculateStableBound(scale, 0);
1348     }
1349 
viewTallerThanScaledImage(float scale)1350     private boolean viewTallerThanScaledImage(float scale) {
1351         return mViewH >= heightOf(mBoxes.get(0), scale);
1352     }
1353 
viewWiderThanScaledImage(float scale)1354     private boolean viewWiderThanScaledImage(float scale) {
1355         return mViewW >= widthOf(mBoxes.get(0), scale);
1356     }
1357 
getTargetScale(Box b)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].
interpolate(float progress)1373         protected abstract boolean interpolate(float progress);
startSnapback()1374         public abstract boolean startSnapback();
1375 
1376         // Returns true if the animation values changes, so things need to be
1377         // redrawn.
advanceAnimation()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 
applyInterpolationCurve(int kind, float progress)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
startSnapback()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
updateDefaultXY()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.
doAnimation(int targetX, int targetY, int kind)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
interpolate(float progress)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 
interpolateFlingFilm(float progress)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 
interpolateFlingPage(float progress)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 
interpolateLinear(float progress)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
startSnapback()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 
doAnimation(int targetY, float targetScale, int kind)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.
clampScale(float s)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
interpolate(float progress)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 
interpolateFlingPage(float progress)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 
interpolateLinear(float progress)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
startSnapback()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.
doAnimation(int targetSize, int kind)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
interpolate(float progress)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
startSnapback()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.
doAnimation(float targetRatio, int kind)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
interpolate(float progress)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