blob: 3774cce1c89e0dbceba5d93ca54b9b46cc598d7e [file] [log] [blame]
Chih-Chung Changec412542011-09-26 17:34:06 +08001/*
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
Owen Lin2b3ee0e2012-03-14 17:27:24 +080019import android.content.Context;
Yuli Huang04ac0452012-03-20 16:37:05 +080020import android.graphics.Rect;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080021import android.util.Log;
22import android.widget.Scroller;
Owen Lin2b3ee0e2012-03-14 17:27:24 +080023
Chih-Chung Changec412542011-09-26 17:34:06 +080024import com.android.gallery3d.common.Utils;
Chih-Chung Chang8f568da2012-01-05 12:00:53 +080025import com.android.gallery3d.util.GalleryUtils;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080026import com.android.gallery3d.util.RangeArray;
27import com.android.gallery3d.util.RangeIntArray;
Chih-Chung Changec412542011-09-26 17:34:06 +080028
Chih-Chung Changec412542011-09-26 17:34:06 +080029class PositionController {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080030 private static final String TAG = "PositionController";
31
Yuli Huang2ce3c3b2012-02-23 22:26:12 +080032 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
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080037 // Special values for animation time.
Chih-Chung Changec412542011-09-26 17:34:06 +080038 private static final long NO_ANIMATION = -1;
39 private static final long LAST_ANIMATION = -2;
40
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080041 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;
Chih-Chung Changec412542011-09-26 17:34:06 +080048
Chih-Chung Chang676170e2011-09-30 18:33:17 +080049 // Animation time in milliseconds. The order must match ANIM_KIND_* above.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080050 private static final int ANIM_TIME[] = {
Chih-Chung Chang676170e2011-09-30 18:33:17 +080051 0, // ANIM_KIND_SCROLL
52 50, // ANIM_KIND_SCALE
53 600, // ANIM_KIND_SNAPBACK
54 400, // ANIM_KIND_SLIDE
55 300, // ANIM_KIND_ZOOM
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080056 600, // ANIM_KIND_OPENING
Chih-Chung Changb3aab902011-10-03 21:11:39 +080057 0, // ANIM_KIND_FLING (the duration is calculated dynamically)
Chih-Chung Chang676170e2011-09-30 18:33:17 +080058 };
59
Chih-Chung Changec412542011-09-26 17:34:06 +080060 // 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
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080064 // 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;
Chih-Chung Chang534b12f2012-03-21 19:01:30 +080067 private static final float SCALE_MAX_EXTRA = 1.4f;
68
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080069 // Setting this true makes the extra scaling range permanent (until this is
70 // set to false again).
Chih-Chung Chang534b12f2012-03-21 19:01:30 +080071 private boolean mExtraScalingRange = false;
Chih-Chung Chang676170e2011-09-30 18:33:17 +080072
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080073 // Film Mode v.s. Page Mode: in film mode we show smaller pictures.
74 private boolean mFilmMode = false;
Chih-Chung Changb3aab902011-10-03 21:11:39 +080075
Chih-Chung Chang642561d2012-04-16 16:29:13 +080076 // 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;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080081
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
Chih-Chung Changaced34c2012-04-16 14:34:46 +080086 public static final int IMAGE_GAP = GalleryUtils.dpToPixel(16);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080087 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 Scroller mFilmScroller;
105
106 // The bound of the stable region that the focused box can stay, see the
107 // comments above calculateStableBound() for details.
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800108 private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
109
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800110 //
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
Chih-Chung Changec412542011-09-26 17:34:06 +0800122
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800123 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);
Chih-Chung Changec412542011-09-26 17:34:06 +0800128
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800129 // 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);
Chih-Chung Changec412542011-09-26 17:34:06 +0800144 }
145
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800146 public PositionController(Context context, Listener listener) {
147 mListener = listener;
148 mPageScroller = new FlingScroller();
149 mFilmScroller = new Scroller(context);
Chih-Chung Changec412542011-09-26 17:34:06 +0800150
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800151 // 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) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800184 if (width == 0 || height == 0) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800185 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
198 // If we already have image size, we don't want to use the view size.
199 if (isViewSize && !b.mUseViewSize) return;
200 b.mUseViewSize = isViewSize;
201
202 if (width == b.mImageW && height == b.mImageH) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800203 return;
204 }
205
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800206 // The ratio of the old size and the new size.
Chih-Chung Changec412542011-09-26 17:34:06 +0800207 float ratio = Math.min(
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800208 (float) b.mImageW / width, (float) b.mImageH / height);
Chih-Chung Changec412542011-09-26 17:34:06 +0800209
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800210 b.mCurrentScale *= ratio;
211 b.mFromScale *= ratio;
212 b.mToScale *= ratio;
Chih-Chung Changec412542011-09-26 17:34:06 +0800213
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800214 b.mImageW = width;
215 b.mImageH = height;
Chih-Chung Changec412542011-09-26 17:34:06 +0800216
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800217 if (i == 0) {
218 mFocusX /= ratio;
219 mFocusY /= ratio;
Chih-Chung Changec412542011-09-26 17:34:06 +0800220 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800221 }
222
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800223 private void startOpeningAnimationIfNeeded() {
224 if (mOpenAnimationRect == null) return;
225 Box b = mBoxes.get(0);
226 if (b.mUseViewSize) return;
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800227
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800228 // Start animation from the saved rectangle if we have one.
229 Rect r = mOpenAnimationRect;
230 mOpenAnimationRect = null;
231 mPlatform.mCurrentX = r.centerX();
232 b.mCurrentY = r.centerY();
233 b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
234 r.height() / (float) b.mImageH);
235 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_OPENING);
236 }
237
238 public void setFilmMode(boolean enabled) {
239 if (enabled == mFilmMode) return;
240 mFilmMode = enabled;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800241
242 updateScaleAndGapLimit();
243 stopAnimation();
244 snapAndRedraw();
245 }
246
247 public void setExtraScalingRange(boolean enabled) {
248 if (mExtraScalingRange == enabled) return;
249 mExtraScalingRange = enabled;
250 if (!enabled) {
251 snapAndRedraw();
252 }
253 }
254
255 // This should be called whenever the scale range of boxes or the default
256 // gap size may change. Currently this can happen due to change of view
257 // size, image size, and mode.
258 private void updateScaleAndGapLimit() {
259 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
260 Box b = mBoxes.get(i);
261 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH);
262 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
263 }
264
265 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
266 Gap g = mGaps.get(i);
267 g.mDefaultSize = getDefaultGapSize(i);
268 }
269 }
270
271 // Returns the default gap size according the the size of the boxes around
272 // the gap and the current mode.
273 private int getDefaultGapSize(int i) {
274 if (mFilmMode) return IMAGE_GAP;
275 Box a = mBoxes.get(i);
276 Box b = mBoxes.get(i + 1);
277 return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b));
278 }
279
280 // Here is how we layout the boxes in the page mode.
281 //
282 // previous current next
283 // ___________ ________________ __________
284 // | _______ | | __________ | | ______ |
285 // | | | | | | right->| | | | | |
286 // | | |<-------->|<--left | | | | | |
287 // | |_______| | | | |__________| | | |______| |
288 // |___________| | |________________| |__________|
289 // | <--> gapToSide()
290 // |
291 // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current))
292 private int gapToSide(Box b) {
293 return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f);
294 }
295
296 // Stop all animations at where they are now.
297 public void stopAnimation() {
298 mPlatform.mAnimationStartTime = NO_ANIMATION;
299 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
300 mBoxes.get(i).mAnimationStartTime = NO_ANIMATION;
301 }
302 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
303 mGaps.get(i).mAnimationStartTime = NO_ANIMATION;
304 }
305 }
306
307 public void skipAnimation() {
308 if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
309 mPlatform.mCurrentX = mPlatform.mToX;
310 mPlatform.mAnimationStartTime = NO_ANIMATION;
311 }
312 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
313 Box b = mBoxes.get(i);
314 if (b.mAnimationStartTime == NO_ANIMATION) continue;
315 b.mCurrentY = b.mToY;
316 b.mCurrentScale = b.mToScale;
317 b.mAnimationStartTime = NO_ANIMATION;
318 }
319 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
320 Gap g = mGaps.get(i);
321 if (g.mAnimationStartTime == NO_ANIMATION) continue;
322 g.mCurrentGap = g.mToGap;
323 g.mAnimationStartTime = NO_ANIMATION;
324 }
325 redraw();
326 }
327
328 public void up() {
329 snapAndRedraw();
330 }
331
332 ////////////////////////////////////////////////////////////////////////////
333 // Start an animations for the focused box
334 ////////////////////////////////////////////////////////////////////////////
335
336 public void zoomIn(float tapX, float tapY, float targetScale) {
337 Box b = mBoxes.get(0);
338
339 // Convert the tap position to distance to center in bitmap coordinates
340 float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale;
341 float tempY = (tapY - b.mCurrentY) / b.mCurrentScale;
342
343 int x = (int) (mViewW / 2 - tempX * targetScale + 0.5f);
344 int y = (int) (mViewH / 2 - tempY * targetScale + 0.5f);
Chih-Chung Changec412542011-09-26 17:34:06 +0800345
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800346 calculateStableBound(targetScale);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800347 int targetX = Utils.clamp(x, mBoundLeft, mBoundRight);
348 int targetY = Utils.clamp(y, mBoundTop, mBoundBottom);
349 targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax);
Chih-Chung Changec412542011-09-26 17:34:06 +0800350
351 startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
352 }
353
354 public void resetToFullView() {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800355 Box b = mBoxes.get(0);
356 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_ZOOM);
Chih-Chung Changec412542011-09-26 17:34:06 +0800357 }
358
Chih-Chung Changec412542011-09-26 17:34:06 +0800359 public void beginScale(float focusX, float focusY) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800360 Box b = mBoxes.get(0);
361 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800362 mInScale = true;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800363 mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f);
364 mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f);
Chih-Chung Changec412542011-09-26 17:34:06 +0800365 }
366
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800367 // Scales the image by the given factor.
368 // Returns an out-of-range indicator:
369 // 1 if the intended scale is too large for the stable range.
370 // 0 if the intended scale is in the stable range.
371 // -1 if the intended scale is too small for the stable range.
372 public int scaleBy(float s, float focusX, float focusY) {
373 Box b = mBoxes.get(0);
374 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800375
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800376 // We want to keep the focus point (on the bitmap) the same as when we
377 // begin the scale guesture, that is,
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800378 //
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800379 // (focusX' - currentX') / scale' = (focusX - currentX) / scale
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800380 //
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800381 s *= getTargetScale(b);
382 int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f);
383 int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f);
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800384 startAnimation(x, y, s, ANIM_KIND_SCALE);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800385 if (s < b.mScaleMin) return -1;
386 if (s > b.mScaleMax) return 1;
387 return 0;
Chih-Chung Changec412542011-09-26 17:34:06 +0800388 }
389
390 public void endScale() {
391 mInScale = false;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800392 snapAndRedraw();
Chih-Chung Changec412542011-09-26 17:34:06 +0800393 }
394
395 public void startHorizontalSlide(int distance) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800396 Box b = mBoxes.get(0);
397 Platform p = mPlatform;
398 startAnimation(getTargetX(p) + distance, getTargetY(b),
399 b.mCurrentScale, ANIM_KIND_SLIDE);
Chih-Chung Changec412542011-09-26 17:34:06 +0800400 }
401
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800402 public void startScroll(float dx, float dy) {
403 boolean hasPrev = hasPrevImages();
404 boolean hasNext = hasNextImages();
405
406 Box b = mBoxes.get(0);
407 Platform p = mPlatform;
408
409 int x = getTargetX(p) + (int) (dx + 0.5f);
410 int y = getTargetY(b) + (int) (dy + 0.5f);
411
412 if (mFilmMode) {
413 scrollToFilm(x, y, hasPrev, hasNext);
414 } else {
415 scrollToPage(x, y, hasPrev, hasNext);
416 }
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800417 }
418
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800419 private void scrollToPage(int x, int y, boolean hasPrev, boolean hasNext) {
420 Box b = mBoxes.get(0);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800421
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800422 calculateStableBound(b.mCurrentScale);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800423
424 // Vertical direction: If we have space to move in the vertical
425 // direction, we show the edge effect when scrolling reaches the edge.
426 if (mBoundTop != mBoundBottom) {
427 if (y < mBoundTop) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800428 mListener.onPull(mBoundTop - y, EdgeView.BOTTOM);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800429 } else if (y > mBoundBottom) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800430 mListener.onPull(y - mBoundBottom, EdgeView.TOP);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800431 }
432 }
433
434 y = Utils.clamp(y, mBoundTop, mBoundBottom);
435
436 // Horizontal direction: we show the edge effect when the scrolling
437 // tries to go left of the first image or go right of the last image.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800438 if (!hasPrev && x > mBoundRight) {
439 int pixels = x - mBoundRight;
440 mListener.onPull(pixels, EdgeView.LEFT);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800441 x = mBoundRight;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800442 } else if (!hasNext && x < mBoundLeft) {
443 int pixels = mBoundLeft - x;
444 mListener.onPull(pixels, EdgeView.RIGHT);
445 x = mBoundLeft;
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800446 }
447
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800448 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
449 }
450
451 private void scrollToFilm(int x, int y, boolean hasPrev, boolean hasNext) {
452 Box b = mBoxes.get(0);
453
454 // Horizontal direction: we show the edge effect when the scrolling
455 // tries to go left of the first image or go right of the last image.
456 int cx = mViewW / 2;
457 if (!hasPrev && x > cx) {
458 int pixels = x - cx;
459 mListener.onPull(pixels, EdgeView.LEFT);
460 x = cx;
461 } else if (!hasNext && x < cx) {
462 int pixels = cx - x;
463 mListener.onPull(pixels, EdgeView.RIGHT);
464 x = cx;
465 }
466
467 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800468 }
469
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800470 public boolean fling(float velocityX, float velocityY) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800471 int vx = (int) (velocityX + 0.5f);
472 int vy = (int) (velocityY + 0.5f);
473 return mFilmMode ? flingFilm(vx, vy) : flingPage(vx, vy);
474 }
475
476 private boolean flingPage(int velocityX, int velocityY) {
477 Box b = mBoxes.get(0);
478 Platform p = mPlatform;
479
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800480 // We only want to do fling when the picture is zoomed-in.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800481 if (viewWiderThanScaledImage(b.mCurrentScale) &&
482 viewTallerThanScaledImage(b.mCurrentScale)) {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800483 return false;
484 }
485
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800486 // We only allow flinging in the directions where it won't go over the
487 // picture.
488 int edges = getImageAtEdges();
489 if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
490 (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
491 velocityX = 0;
492 }
493 if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
494 (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
495 velocityY = 0;
496 }
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800497
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800498 if (velocityX == 0 && velocityY == 0) return false;
499
500 mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY,
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800501 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800502 int targetX = mPageScroller.getFinalX();
503 int targetY = mPageScroller.getFinalY();
504 ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
505 startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800506 return true;
507 }
508
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800509 private boolean flingFilm(int velocityX, int velocityY) {
510 boolean hasPrev = hasPrevImages();
511 boolean hasNext = hasNextImages();
Chih-Chung Changec412542011-09-26 17:34:06 +0800512
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800513 Box b = mBoxes.get(0);
514 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800515
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800516 // If we are already at the edge, don't start the fling.
517 int cx = mViewW / 2;
518 if ((!hasPrev && p.mCurrentX >= cx) || (!hasNext && p.mCurrentX <= cx)) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800519 return false;
520 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800521
522 if (velocityX == 0) return false;
523
524 mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
525 Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
526 int targetX = mFilmScroller.getFinalX();
527 ANIM_TIME[ANIM_KIND_FLING] = mFilmScroller.getDuration();
528 startAnimation(targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING);
529 return true;
Chih-Chung Changec412542011-09-26 17:34:06 +0800530 }
531
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800532 ////////////////////////////////////////////////////////////////////////////
533 // Redraw
534 //
535 // If a method changes box positions directly, redraw()
536 // should be called.
537 //
538 // If a method may also cause a snapback to happen, snapAndRedraw() should
539 // be called.
540 //
541 // If a method starts an animation to change the position of focused box,
542 // startAnimation() should be called.
543 //
544 // If time advances to change the box position, advanceAnimation() should
545 // be called.
546 ////////////////////////////////////////////////////////////////////////////
547 private void redraw() {
548 layoutAndSetPosition();
549 mListener.invalidate();
550 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800551
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800552 private void snapAndRedraw() {
553 mPlatform.startSnapback();
554 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
555 mBoxes.get(i).startSnapback();
556 }
557 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
558 mGaps.get(i).startSnapback();
559 }
560 redraw();
561 }
Chih-Chung Chang534b12f2012-03-21 19:01:30 +0800562
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800563 private void startAnimation(int targetX, int targetY, float targetScale,
564 int kind) {
565 boolean changed = false;
566 changed |= mPlatform.doAnimation(targetX, kind);
567 changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
568 if (changed) redraw();
569 }
570
571 public boolean advanceAnimation() {
572 boolean changed = false;
573 changed |= mPlatform.advanceAnimation();
574 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
575 changed |= mBoxes.get(i).advanceAnimation();
576 }
577 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
578 changed |= mGaps.get(i).advanceAnimation();
579 }
580 if (changed) redraw();
581 return changed;
582 }
583
584 ////////////////////////////////////////////////////////////////////////////
585 // Layout
586 ////////////////////////////////////////////////////////////////////////////
587
588 // Returns the display width of this box.
589 private int widthOf(Box b) {
590 return (int) (b.mImageW * b.mCurrentScale + 0.5f);
591 }
592
593 // Returns the display height of this box.
594 private int heightOf(Box b) {
595 return (int) (b.mImageH * b.mCurrentScale + 0.5f);
596 }
597
598 // Returns the display width of this box, using the given scale.
599 private int widthOf(Box b, float scale) {
600 return (int) (b.mImageW * scale + 0.5f);
601 }
602
603 // Returns the display height of this box, using the given scale.
604 private int heightOf(Box b, float scale) {
605 return (int) (b.mImageH * scale + 0.5f);
606 }
607
608 // Convert the information in mPlatform and mBoxes to mRects, so the user
609 // can get the position of each box by getPosition().
610 //
611 // Note the loop index goes from inside-out because each box's X coordinate
612 // is relative to its anchor box (except the focused box).
613 private void layoutAndSetPosition() {
614 // layout box 0 (focused box)
615 convertBoxToRect(0);
616 for (int i = 1; i <= BOX_MAX; i++) {
617 // layout box i and -i
618 convertBoxToRect(i);
619 convertBoxToRect(-i);
620 }
621 //dumpState();
622 }
623
624 private void dumpState() {
625 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
626 Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
Chih-Chung Changec412542011-09-26 17:34:06 +0800627 }
628
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800629 dumpRect(0);
630 for (int i = 1; i <= BOX_MAX; i++) {
631 dumpRect(i);
632 dumpRect(-i);
Chih-Chung Changec412542011-09-26 17:34:06 +0800633 }
634
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800635 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
636 for (int j = i + 1; j <= BOX_MAX; j++) {
637 if (Rect.intersects(mRects.get(i), mRects.get(j))) {
638 Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
639 }
640 }
641 }
642 }
643
644 private void dumpRect(int i) {
645 StringBuilder sb = new StringBuilder();
646 Rect r = mRects.get(i);
647 sb.append("Rect " + i + ":");
648 sb.append("(");
649 sb.append(r.centerX());
650 sb.append(",");
651 sb.append(r.centerY());
652 sb.append(") [");
653 sb.append(r.width());
654 sb.append("x");
655 sb.append(r.height());
656 sb.append("]");
657 Log.d(TAG, sb.toString());
658 }
659
660 private void convertBoxToRect(int i) {
661 Box b = mBoxes.get(i);
662 Rect r = mRects.get(i);
663 int y = b.mCurrentY;
664 int w = widthOf(b);
665 int h = heightOf(b);
666 if (i == 0) {
667 int x = mPlatform.mCurrentX;
668 r.left = x - w / 2;
669 r.right = r.left + w;
670 } else if (i > 0) {
671 Rect a = mRects.get(i - 1);
672 Gap g = mGaps.get(i - 1);
673 r.left = a.right + g.mCurrentGap;
674 r.right = r.left + w;
675 } else { // i < 0
676 Rect a = mRects.get(i + 1);
677 Gap g = mGaps.get(i);
678 r.right = a.left - g.mCurrentGap;
679 r.left = r.right - w;
680 }
681 r.top = y - h / 2;
682 r.bottom = r.top + h;
683 }
684
685 // Returns the position of a box.
686 public Rect getPosition(int index) {
687 return mRects.get(index);
688 }
689
690 ////////////////////////////////////////////////////////////////////////////
691 // Box management
692 ////////////////////////////////////////////////////////////////////////////
693
694 // Initialize the platform to be at the view center.
695 private void initPlatform() {
696 mPlatform.mCurrentX = mViewW / 2;
697 mPlatform.mAnimationStartTime = NO_ANIMATION;
698 }
699
700 // Initialize a box to have the size of the view.
701 private void initBox(int index) {
702 Box b = mBoxes.get(index);
703 b.mImageW = mViewW;
704 b.mImageH = mViewH;
705 b.mUseViewSize = true;
706 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH);
707 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
708 b.mCurrentY = mViewH / 2;
709 b.mCurrentScale = b.mScaleMin;
710 b.mAnimationStartTime = NO_ANIMATION;
711 }
712
713 // Initialize a gap. This can only be called after the boxes around the gap
714 // has been initialized.
715 private void initGap(int index) {
716 Gap g = mGaps.get(index);
717 g.mDefaultSize = getDefaultGapSize(index);
718 g.mCurrentGap = g.mDefaultSize;
719 g.mAnimationStartTime = NO_ANIMATION;
720 }
721
722 private void initGap(int index, int size) {
723 Gap g = mGaps.get(index);
724 g.mDefaultSize = getDefaultGapSize(index);
725 g.mCurrentGap = size;
726 g.mAnimationStartTime = NO_ANIMATION;
727 }
728
729 private void debugMoveBox(int fromIndex[]) {
730 StringBuilder s = new StringBuilder("moveBox:");
731 for (int i = 0; i < fromIndex.length; i++) {
732 int j = fromIndex[i];
733 if (j == Integer.MAX_VALUE) {
734 s.append(" N");
735 } else {
736 s.append(" ");
737 s.append(fromIndex[i]);
738 }
739 }
740 Log.d(TAG, s.toString());
741 }
742
743 // Move the boxes: it may indicate focus change, box deleted, box appearing,
744 // box reordered, etc.
745 //
746 // Each element in the fromIndex array indicates where each box was in the
747 // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
748 // means the box is new.
749 //
750 // For example:
751 // N N N N N N N -- all new boxes
752 // -3 -2 -1 0 1 2 3 -- nothing changed
753 // -2 -1 0 1 2 3 N -- focus goes to the next box
754 // N-3 -2 -1 0 1 2 -- focuse goes to the previous box
755 // -3 -2 -1 1 2 3 N -- the focused box was deleted.
756 public void moveBox(int fromIndex[]) {
757 //debugMoveBox(fromIndex);
758 RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
759
760 // 1. Get the absolute X coordiates for the boxes.
761 layoutAndSetPosition();
762 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
763 Box b = mBoxes.get(i);
764 Rect r = mRects.get(i);
765 b.mAbsoluteX = r.centerX();
Chih-Chung Changec412542011-09-26 17:34:06 +0800766 }
767
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800768 // 2. copy boxes and gaps to temporary storage.
769 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
770 mTempBoxes.put(i, mBoxes.get(i));
771 mBoxes.put(i, null);
772 }
773 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
774 mTempGaps.put(i, mGaps.get(i));
775 mGaps.put(i, null);
776 }
777
778 // 3. move back boxes that are used in the new array.
779 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
780 int j = from.get(i);
781 if (j == Integer.MAX_VALUE) continue;
782 mBoxes.put(i, mTempBoxes.get(j));
783 mTempBoxes.put(j, null);
784 }
785
786 // 4. move back gaps if both boxes around it are kept together.
787 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
788 int j = from.get(i);
789 if (j == Integer.MAX_VALUE) continue;
790 int k = from.get(i + 1);
791 if (k == Integer.MAX_VALUE) continue;
792 if (j + 1 == k) {
793 mGaps.put(i, mTempGaps.get(j));
794 mTempGaps.put(j, null);
795 }
796 }
797
798 // 5. recycle the boxes that are not used in the new array.
799 int k = -BOX_MAX;
800 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
801 if (mBoxes.get(i) != null) continue;
802 while (mTempBoxes.get(k) == null) {
803 k++;
804 }
805 mBoxes.put(i, mTempBoxes.get(k++));
806 initBox(i);
807 }
808
809 // 6. Now give the recycled box a reasonable absolute X position.
810 //
811 // First try to find the first and the last box which the absolute X
812 // position is known.
813 int first, last;
814 for (first = -BOX_MAX; first <= BOX_MAX; first++) {
815 if (from.get(first) != Integer.MAX_VALUE) break;
816 }
817 for (last = BOX_MAX; last >= -BOX_MAX; last--) {
818 if (from.get(last) != Integer.MAX_VALUE) break;
819 }
820 // If there is no box has known X position at all, make the focused one
821 // as known.
822 if (first > BOX_MAX) {
823 mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
824 first = last = 0;
825 }
826 // Now for those boxes between first and last, just assign the same
827 // position as the previous box. (We can do better, but this should be
828 // rare). For the boxes before first or after last, we will use a new
829 // default gap size below.
830 for (int i = first + 1; i < last; i++) {
831 if (from.get(i) != Integer.MAX_VALUE) continue;
832 mBoxes.get(i).mAbsoluteX = mBoxes.get(i - 1).mAbsoluteX;
833 }
834
835 // 7. recycle the gaps that are not used in the new array.
836 k = -BOX_MAX;
837 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
838 if (mGaps.get(i) != null) continue;
839 while (mTempGaps.get(k) == null) {
840 k++;
841 }
842 mGaps.put(i, mTempGaps.get(k++));
843 Box a = mBoxes.get(i);
844 Box b = mBoxes.get(i + 1);
845 int wa = widthOf(a);
846 int wb = widthOf(b);
847 if (i >= first && i < last) {
848 int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
849 initGap(i, g);
850 } else {
851 initGap(i);
852 }
853 }
854
855 // 8. offset the Platform position
856 int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
857 mPlatform.mCurrentX += dx;
858 mPlatform.mFromX += dx;
859 mPlatform.mToX += dx;
860 mPlatform.mFlingOffset += dx;
861
862 snapAndRedraw();
863 }
864
865 ////////////////////////////////////////////////////////////////////////////
866 // Public utilities
867 ////////////////////////////////////////////////////////////////////////////
868
869 public float getMinimalScale(int imageW, int imageH) {
Chih-Chung Chang642561d2012-04-16 16:29:13 +0800870 float wFactor = 1.0f;
871 float hFactor = 1.0f;
872
873 if (mFilmMode) {
874 if (mViewH > mViewW) { // portrait
875 wFactor = FILM_MODE_PORTRAIT_WIDTH;
876 hFactor = FILM_MODE_PORTRAIT_HEIGHT;
877 } else { // landscape
878 wFactor = FILM_MODE_LANDSCAPE_WIDTH;
879 hFactor = FILM_MODE_LANDSCAPE_HEIGHT;
880 }
881 }
882
883 float s = Math.min(wFactor * mViewW / imageW,
884 hFactor * mViewH / imageH);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800885 return Math.min(SCALE_LIMIT, s);
886 }
887
888 public float getMaximalScale(int imageW, int imageH) {
889 return mFilmMode ? getMinimalScale(imageW, imageH) : SCALE_LIMIT;
890 }
891
892 public boolean isAtMinimalScale() {
893 Box b = mBoxes.get(0);
894 return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
895 }
896
897 public int getImageWidth() {
898 Box b = mBoxes.get(0);
899 return b.mImageW;
900 }
901
902 public int getImageHeight() {
903 Box b = mBoxes.get(0);
904 return b.mImageH;
905 }
906
907 public float getImageScale() {
908 Box b = mBoxes.get(0);
909 return b.mCurrentScale;
910 }
911
912 public int getImageAtEdges() {
913 Box b = mBoxes.get(0);
914 Platform p = mPlatform;
915 calculateStableBound(b.mCurrentScale);
916 int edges = 0;
917 if (p.mCurrentX <= mBoundLeft) {
918 edges |= IMAGE_AT_RIGHT_EDGE;
919 }
920 if (p.mCurrentX >= mBoundRight) {
921 edges |= IMAGE_AT_LEFT_EDGE;
922 }
923 if (b.mCurrentY <= mBoundTop) {
924 edges |= IMAGE_AT_BOTTOM_EDGE;
925 }
926 if (b.mCurrentY >= mBoundBottom) {
927 edges |= IMAGE_AT_TOP_EDGE;
928 }
929 return edges;
930 }
931
932 ////////////////////////////////////////////////////////////////////////////
933 // Private utilities
934 ////////////////////////////////////////////////////////////////////////////
935
936 private float getMinimalScale(Box b) {
937 return getMinimalScale(b.mImageW, b.mImageH);
938 }
939
940 private float getMaxmimalScale(Box b) {
941 return getMaximalScale(b.mImageW, b.mImageH);
942 }
943
944 private static boolean isAlmostEqual(float a, float b) {
945 float diff = a - b;
946 return (diff < 0 ? -diff : diff) < 0.02f;
Chih-Chung Changec412542011-09-26 17:34:06 +0800947 }
948
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800949 // Calculates the stable region of mCurrent{X/Y}, where "stable" means
950 //
951 // (1) If the dimension of scaled image >= view dimension, we will not
952 // see black region outside the image (at that dimension).
953 // (2) If the dimension of scaled image < view dimension, we will center
954 // the scaled image.
955 //
956 // We might temporarily go out of this stable during user interaction,
957 // but will "snap back" after user stops interaction.
958 //
959 // The results are stored in mBound{Left/Right/Top/Bottom}.
960 //
Chih-Chung Chang8f568da2012-01-05 12:00:53 +0800961 // An extra parameter "horizontalSlack" (which has the value of 0 usually)
962 // is used to extend the stable region by some pixels on each side
963 // horizontally.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800964 private void calculateStableBound(float scale, int horizontalSlack) {
965 Box b = mBoxes.get(0);
Chih-Chung Chang8f568da2012-01-05 12:00:53 +0800966
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800967 // The width and height of the box in number of view pixels
968 int w = widthOf(b, scale);
969 int h = heightOf(b, scale);
970
971 // When the edge of the view is aligned with the edge of the box
972 mBoundLeft = (mViewW - horizontalSlack) - w / 2;
973 mBoundRight = mViewW - mBoundLeft;
974 mBoundTop = mViewH - h / 2;
975 mBoundBottom = mViewH - mBoundTop;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800976
977 // If the scaled height is smaller than the view height,
978 // force it to be in the center.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800979 if (viewTallerThanScaledImage(scale)) {
980 mBoundTop = mBoundBottom = mViewH / 2;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800981 }
982
983 // Same for width
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800984 if (viewWiderThanScaledImage(scale)) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800985 mBoundLeft = mBoundRight = mViewW / 2;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800986 }
987 }
988
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800989 private void calculateStableBound(float scale) {
990 calculateStableBound(scale, 0);
991 }
992
993 private boolean hasNextImages() {
994 for (int i = 1; i <= BOX_MAX; i++) {
995 if (!mBoxes.get(i).mUseViewSize) return true;
996 }
997 return false;
998 }
999
1000 private boolean hasPrevImages() {
1001 for (int i = -1; i >= -BOX_MAX; i--) {
1002 if (!mBoxes.get(i).mUseViewSize) return true;
1003 }
1004 return false;
1005 }
1006
1007 private boolean viewTallerThanScaledImage(float scale) {
1008 return mViewH >= heightOf(mBoxes.get(0), scale);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001009 }
1010
1011 private boolean viewWiderThanScaledImage(float scale) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001012 return mViewW >= widthOf(mBoxes.get(0), scale);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001013 }
1014
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001015 private float getTargetScale(Box b) {
1016 return useCurrentValueAsTarget(b) ? b.mCurrentScale : b.mToScale;
Chih-Chung Changb3aab902011-10-03 21:11:39 +08001017 }
1018
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001019 private int getTargetX(Platform p) {
1020 return useCurrentValueAsTarget(p) ? p.mCurrentX : p.mToX;
Chih-Chung Changec412542011-09-26 17:34:06 +08001021 }
1022
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001023 private int getTargetY(Box b) {
1024 return useCurrentValueAsTarget(b) ? b.mCurrentY : b.mToY;
Chih-Chung Changec412542011-09-26 17:34:06 +08001025 }
1026
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001027 private boolean useCurrentValueAsTarget(Animatable a) {
1028 return a.mAnimationStartTime == NO_ANIMATION ||
1029 a.mAnimationKind == ANIM_KIND_SNAPBACK ||
1030 a.mAnimationKind == ANIM_KIND_FLING;
Chih-Chung Changec412542011-09-26 17:34:06 +08001031 }
1032
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001033 // Returns the index of the anchor box.
1034 private int anchorIndex(int i) {
1035 if (i > 0) return i - 1;
1036 if (i < 0) return i + 1;
1037 throw new IllegalArgumentException();
1038 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001039
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001040 ////////////////////////////////////////////////////////////////////////////
1041 // Animatable: an thing which can do animation.
1042 ////////////////////////////////////////////////////////////////////////////
1043 private abstract static class Animatable {
1044 public long mAnimationStartTime;
1045 public int mAnimationKind;
1046 public int mAnimationDuration;
Chih-Chung Changec412542011-09-26 17:34:06 +08001047
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001048 // This should be overidden in subclass to change the animation values
1049 // give the progress value in [0, 1].
1050 protected abstract boolean interpolate(float progress);
1051 public abstract boolean startSnapback();
Chih-Chung Changec412542011-09-26 17:34:06 +08001052
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001053 // Returns true if the animation values changes, so things need to be
1054 // redrawn.
1055 public boolean advanceAnimation() {
1056 if (mAnimationStartTime == NO_ANIMATION) {
1057 return false;
1058 }
1059 if (mAnimationStartTime == LAST_ANIMATION) {
1060 mAnimationStartTime = NO_ANIMATION;
1061 return startSnapback();
1062 }
1063
1064 float progress;
1065 if (mAnimationDuration == 0) {
1066 progress = 1;
1067 } else {
1068 long now = AnimationTime.get();
1069 progress =
1070 (float) (now - mAnimationStartTime) / mAnimationDuration;
1071 }
1072
1073 if (progress >= 1) {
1074 progress = 1;
1075 } else {
1076 progress = applyInterpolationCurve(mAnimationKind, progress);
1077 }
1078
1079 boolean done = interpolate(progress);
1080
1081 if (done) {
1082 mAnimationStartTime = LAST_ANIMATION;
1083 }
1084
1085 return true;
Chih-Chung Changec412542011-09-26 17:34:06 +08001086 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001087
1088 private static float applyInterpolationCurve(int kind, float progress) {
1089 float f = 1 - progress;
1090 switch (kind) {
1091 case ANIM_KIND_SCROLL:
1092 case ANIM_KIND_FLING:
1093 progress = 1 - f; // linear
1094 break;
1095 case ANIM_KIND_SCALE:
1096 progress = 1 - f * f; // quadratic
1097 break;
1098 case ANIM_KIND_SNAPBACK:
1099 case ANIM_KIND_ZOOM:
1100 case ANIM_KIND_SLIDE:
1101 case ANIM_KIND_OPENING:
1102 progress = 1 - f * f * f * f * f; // x^5
1103 break;
1104 }
1105 return progress;
1106 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001107 }
1108
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001109 ////////////////////////////////////////////////////////////////////////////
1110 // Platform: captures the global X movement.
1111 ////////////////////////////////////////////////////////////////////////////
1112 private class Platform extends Animatable {
1113 public int mCurrentX, mFromX, mToX;
1114 public int mFlingOffset;
1115
1116 @Override
1117 public boolean startSnapback() {
1118 if (mAnimationStartTime != NO_ANIMATION) return false;
1119 if (mAnimationKind == ANIM_KIND_SCROLL
1120 && mListener.isDown()) return false;
1121
1122 Box b = mBoxes.get(0);
1123 float scaleMin = mExtraScalingRange ?
1124 b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin;
1125 float scaleMax = mExtraScalingRange ?
1126 b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax;
1127 float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax);
1128 int x = mCurrentX;
1129 if (mFilmMode) {
1130 if (!hasNextImages()) x = Math.max(x, mViewW / 2);
1131 if (!hasPrevImages()) x = Math.min(x, mViewW / 2);
1132 } else {
1133 calculateStableBound(scale, HORIZONTAL_SLACK);
1134 x = Utils.clamp(x, mBoundLeft, mBoundRight);
1135 }
1136 if (mCurrentX != x) {
1137 return doAnimation(x, ANIM_KIND_SNAPBACK);
1138 }
1139 return false;
1140 }
1141
1142 // Starts an animation for the platform.
1143 public boolean doAnimation(int targetX, int kind) {
1144 if (mCurrentX == targetX) return false;
1145 mAnimationKind = kind;
1146 mFromX = mCurrentX;
1147 mToX = targetX;
1148 mAnimationStartTime = AnimationTime.startTime();
1149 mAnimationDuration = ANIM_TIME[kind];
1150 mFlingOffset = 0;
1151 advanceAnimation();
1152 return true;
1153 }
1154
1155 @Override
1156 protected boolean interpolate(float progress) {
1157 if (mAnimationKind == ANIM_KIND_FLING) {
1158 return mFilmMode
1159 ? interpolateFlingFilm(progress)
1160 : interpolateFlingPage(progress);
1161 } else {
1162 return interpolateLinear(progress);
1163 }
1164 }
1165
1166 private boolean interpolateFlingFilm(float progress) {
1167 mFilmScroller.computeScrollOffset();
1168 mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
1169
1170 int dir = EdgeView.INVALID_DIRECTION;
1171 if (mCurrentX < mViewW / 2) {
1172 if (!hasNextImages()) {
1173 dir = EdgeView.RIGHT;
1174 }
1175 } else if (mCurrentX > mViewW / 2) {
1176 if (!hasPrevImages()) {
1177 dir = EdgeView.LEFT;
1178 }
1179 }
1180 if (dir != EdgeView.INVALID_DIRECTION) {
1181 int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
1182 mListener.onAbsorb(v, dir);
1183 mFilmScroller.forceFinished(true);
1184 mCurrentX = mViewW / 2;
1185 }
1186 return mFilmScroller.isFinished();
1187 }
1188
1189 private boolean interpolateFlingPage(float progress) {
1190 mPageScroller.computeScrollOffset(progress);
1191 Box b = mBoxes.get(0);
1192 calculateStableBound(b.mCurrentScale);
1193
1194 int oldX = mCurrentX;
1195 mCurrentX = mPageScroller.getCurrX();
1196
1197 // Check if we hit the edges; show edge effects if we do.
1198 if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
1199 int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
1200 mListener.onAbsorb(v, EdgeView.RIGHT);
1201 } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
1202 int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
1203 mListener.onAbsorb(v, EdgeView.LEFT);
1204 }
1205
1206 return progress >= 1;
1207 }
1208
1209 private boolean interpolateLinear(float progress) {
1210 // Other animations
1211 if (progress >= 1) {
1212 mCurrentX = mToX;
1213 return true;
1214 } else {
1215 mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
1216 return (mCurrentX == mToX);
1217 }
1218 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001219 }
1220
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001221 ////////////////////////////////////////////////////////////////////////////
1222 // Box: represents a rectangular area which shows a picture.
1223 ////////////////////////////////////////////////////////////////////////////
1224 private class Box extends Animatable {
1225 // Size of the bitmap
1226 public int mImageW, mImageH;
1227
1228 // This is true if we assume the image size is the same as view size
1229 // until we know the actual size of image. This is also used to
1230 // determine if there is an image ready to show.
1231 public boolean mUseViewSize;
1232
1233 // The minimum and maximum scale we allow for this box.
1234 public float mScaleMin, mScaleMax;
1235
1236 // The X/Y value indicates where the center of the box is on the view
1237 // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
1238 // actual values used currently. Note that the X values are implicitly
1239 // defined by Platform and Gaps.
1240 public int mCurrentY, mFromY, mToY;
1241 public float mCurrentScale, mFromScale, mToScale;
1242
1243 // The absolute X coordinate of the center of the box. This is only used
1244 // during moveBox().
1245 public int mAbsoluteX;
1246
1247 @Override
1248 public boolean startSnapback() {
1249 if (mAnimationStartTime != NO_ANIMATION) return false;
1250 if (mAnimationKind == ANIM_KIND_SCROLL
1251 && mListener.isDown()) return false;
1252 if (mInScale && this == mBoxes.get(0)) return false;
1253
1254 int y;
1255 float scale;
1256
1257 if (this == mBoxes.get(0)) {
1258 float scaleMin = mExtraScalingRange ?
1259 mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
1260 float scaleMax = mExtraScalingRange ?
1261 mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
1262 scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
1263 if (mFilmMode) {
1264 y = mViewH / 2;
1265 } else {
1266 calculateStableBound(scale, HORIZONTAL_SLACK);
1267 y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
1268 }
1269 } else {
1270 y = mViewH / 2;
1271 scale = mScaleMin;
1272 }
1273
1274 if (mCurrentY != y || mCurrentScale != scale) {
1275 return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
1276 }
1277 return false;
1278 }
1279
1280 private boolean doAnimation(int targetY, float targetScale, int kind) {
1281 targetScale = Utils.clamp(targetScale,
1282 SCALE_MIN_EXTRA * mScaleMin,
1283 SCALE_MAX_EXTRA * mScaleMax);
1284
1285 // If the scaled height is smaller than the view height, force it to be
1286 // in the center. (We do this for height only, not width, because the
1287 // user may want to scroll to the previous/next image.)
1288 if (!mInScale && viewTallerThanScaledImage(targetScale)) {
1289 targetY = mViewH / 2;
1290 }
1291
1292 if (mCurrentY == targetY && mCurrentScale == targetScale) {
1293 return false;
1294 }
1295
1296 // Now starts an animation for the box.
1297 mAnimationKind = kind;
1298 mFromY = mCurrentY;
1299 mFromScale = mCurrentScale;
1300 mToY = targetY;
1301 mToScale = targetScale;
1302 mAnimationStartTime = AnimationTime.startTime();
1303 mAnimationDuration = ANIM_TIME[kind];
1304 advanceAnimation();
1305 return true;
1306 }
1307
1308 @Override
1309 protected boolean interpolate(float progress) {
1310 if (mAnimationKind == ANIM_KIND_FLING) {
1311 // Currently a Box can only be flung in page mode.
1312 return interpolateFlingPage(progress);
1313 } else {
1314 return interpolateLinear(progress);
1315 }
1316 }
1317
1318 private boolean interpolateFlingPage(float progress) {
1319 mPageScroller.computeScrollOffset(progress);
1320 calculateStableBound(mCurrentScale);
1321
1322 int oldY = mCurrentY;
1323 mCurrentY = mPageScroller.getCurrY();
1324
1325 // Check if we hit the edges; show edge effects if we do.
1326 if (oldY > mBoundTop && mCurrentY == mBoundTop) {
1327 int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
1328 mListener.onAbsorb(v, EdgeView.BOTTOM);
1329 } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
1330 int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
1331 mListener.onAbsorb(v, EdgeView.TOP);
1332 }
1333
1334 return progress >= 1;
1335 }
1336
1337 private boolean interpolateLinear(float progress) {
1338 if (progress >= 1) {
1339 mCurrentY = mToY;
1340 mCurrentScale = mToScale;
1341 return true;
1342 } else {
1343 mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
1344 mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
1345 return (mCurrentY == mToY && mCurrentScale == mToScale);
1346 }
1347 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001348 }
Chih-Chung Chang532d93c2011-10-12 17:10:33 +08001349
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001350 ////////////////////////////////////////////////////////////////////////////
1351 // Gap: represents a rectangular area which is between two boxes.
1352 ////////////////////////////////////////////////////////////////////////////
1353 private class Gap extends Animatable {
1354 // The default gap size between two boxes. The value may vary for
1355 // different image size of the boxes and for different modes (page or
1356 // film).
1357 public int mDefaultSize;
1358
1359 // The gap size between the two boxes.
1360 public int mCurrentGap, mFromGap, mToGap;
1361
1362 @Override
1363 public boolean startSnapback() {
1364 if (mAnimationStartTime != NO_ANIMATION) return false;
1365 return doAnimation(mDefaultSize);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001366 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001367
1368 // Starts an animation for a gap.
1369 public boolean doAnimation(int targetSize) {
1370 if (mCurrentGap == targetSize) return false;
1371 mAnimationKind = ANIM_KIND_SNAPBACK;
1372 mFromGap = mCurrentGap;
1373 mToGap = targetSize;
1374 mAnimationStartTime = AnimationTime.startTime();
1375 mAnimationDuration = ANIM_TIME[mAnimationKind];
1376 advanceAnimation();
1377 return true;
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001378 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001379
1380 @Override
1381 protected boolean interpolate(float progress) {
1382 if (progress >= 1) {
1383 mCurrentGap = mToGap;
1384 return true;
1385 } else {
1386 mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
1387 return (mCurrentGap == mToGap);
1388 }
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001389 }
Chih-Chung Chang532d93c2011-10-12 17:10:33 +08001390 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001391}