blob: a5eb22425b59564c880fa5ab9863f4b889a3b396 [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;
Chih-Chung Changd8488622012-04-17 12:56:08 +080022import android.widget.OverScroller;
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).
Chih-Chung Changd8488622012-04-17 12:56:08 +0800104 private OverScroller mFilmScroller;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800105
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();
Chih-Chung Changd8488622012-04-17 12:56:08 +0800149 mFilmScroller = new OverScroller(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);
Chih-Chung Changd8488622012-04-17 12:56:08 +0800197 boolean wasViewSize = b.mUseViewSize;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800198
Chih-Chung Changd8488622012-04-17 12:56:08 +0800199 // If we already have an image size, we don't want to use the view size.
200 if (!wasViewSize && isViewSize) return;
201
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800202 b.mUseViewSize = isViewSize;
203
204 if (width == b.mImageW && height == b.mImageH) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800205 return;
206 }
207
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800208 // The ratio of the old size and the new size.
Chih-Chung Changec412542011-09-26 17:34:06 +0800209 float ratio = Math.min(
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800210 (float) b.mImageW / width, (float) b.mImageH / height);
Chih-Chung Changec412542011-09-26 17:34:06 +0800211
Chih-Chung Changd8488622012-04-17 12:56:08 +0800212 // If this is the first time we receive an image size, we change the
213 // scale directly. Otherwise adjust the scales by a ratio, and snapback
214 // will animate the scale into the min/max bounds if necessary.
215 if (wasViewSize && !isViewSize) {
216 b.mCurrentScale = getMinimalScale(width, height);
217 b.mAnimationStartTime = NO_ANIMATION;
218 } else {
219 b.mCurrentScale *= ratio;
220 b.mFromScale *= ratio;
221 b.mToScale *= ratio;
222 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800223
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800224 b.mImageW = width;
225 b.mImageH = height;
Chih-Chung Changec412542011-09-26 17:34:06 +0800226
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800227 if (i == 0) {
228 mFocusX /= ratio;
229 mFocusY /= ratio;
Chih-Chung Changec412542011-09-26 17:34:06 +0800230 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800231 }
232
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800233 private void startOpeningAnimationIfNeeded() {
234 if (mOpenAnimationRect == null) return;
235 Box b = mBoxes.get(0);
236 if (b.mUseViewSize) return;
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800237
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800238 // Start animation from the saved rectangle if we have one.
239 Rect r = mOpenAnimationRect;
240 mOpenAnimationRect = null;
241 mPlatform.mCurrentX = r.centerX();
242 b.mCurrentY = r.centerY();
243 b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
244 r.height() / (float) b.mImageH);
245 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_OPENING);
246 }
247
248 public void setFilmMode(boolean enabled) {
249 if (enabled == mFilmMode) return;
250 mFilmMode = enabled;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800251
252 updateScaleAndGapLimit();
253 stopAnimation();
254 snapAndRedraw();
255 }
256
257 public void setExtraScalingRange(boolean enabled) {
258 if (mExtraScalingRange == enabled) return;
259 mExtraScalingRange = enabled;
260 if (!enabled) {
261 snapAndRedraw();
262 }
263 }
264
265 // This should be called whenever the scale range of boxes or the default
266 // gap size may change. Currently this can happen due to change of view
267 // size, image size, and mode.
268 private void updateScaleAndGapLimit() {
269 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
270 Box b = mBoxes.get(i);
271 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH);
272 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
273 }
274
275 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
276 Gap g = mGaps.get(i);
277 g.mDefaultSize = getDefaultGapSize(i);
278 }
279 }
280
281 // Returns the default gap size according the the size of the boxes around
282 // the gap and the current mode.
283 private int getDefaultGapSize(int i) {
284 if (mFilmMode) return IMAGE_GAP;
285 Box a = mBoxes.get(i);
286 Box b = mBoxes.get(i + 1);
287 return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b));
288 }
289
290 // Here is how we layout the boxes in the page mode.
291 //
292 // previous current next
293 // ___________ ________________ __________
294 // | _______ | | __________ | | ______ |
295 // | | | | | | right->| | | | | |
296 // | | |<-------->|<--left | | | | | |
297 // | |_______| | | | |__________| | | |______| |
298 // |___________| | |________________| |__________|
299 // | <--> gapToSide()
300 // |
301 // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current))
302 private int gapToSide(Box b) {
303 return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f);
304 }
305
306 // Stop all animations at where they are now.
307 public void stopAnimation() {
308 mPlatform.mAnimationStartTime = NO_ANIMATION;
309 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
310 mBoxes.get(i).mAnimationStartTime = NO_ANIMATION;
311 }
312 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
313 mGaps.get(i).mAnimationStartTime = NO_ANIMATION;
314 }
315 }
316
317 public void skipAnimation() {
318 if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
319 mPlatform.mCurrentX = mPlatform.mToX;
320 mPlatform.mAnimationStartTime = NO_ANIMATION;
321 }
322 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
323 Box b = mBoxes.get(i);
324 if (b.mAnimationStartTime == NO_ANIMATION) continue;
325 b.mCurrentY = b.mToY;
326 b.mCurrentScale = b.mToScale;
327 b.mAnimationStartTime = NO_ANIMATION;
328 }
329 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
330 Gap g = mGaps.get(i);
331 if (g.mAnimationStartTime == NO_ANIMATION) continue;
332 g.mCurrentGap = g.mToGap;
333 g.mAnimationStartTime = NO_ANIMATION;
334 }
335 redraw();
336 }
337
338 public void up() {
339 snapAndRedraw();
340 }
341
342 ////////////////////////////////////////////////////////////////////////////
343 // Start an animations for the focused box
344 ////////////////////////////////////////////////////////////////////////////
345
346 public void zoomIn(float tapX, float tapY, float targetScale) {
347 Box b = mBoxes.get(0);
348
349 // Convert the tap position to distance to center in bitmap coordinates
350 float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale;
351 float tempY = (tapY - b.mCurrentY) / b.mCurrentScale;
352
353 int x = (int) (mViewW / 2 - tempX * targetScale + 0.5f);
354 int y = (int) (mViewH / 2 - tempY * targetScale + 0.5f);
Chih-Chung Changec412542011-09-26 17:34:06 +0800355
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800356 calculateStableBound(targetScale);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800357 int targetX = Utils.clamp(x, mBoundLeft, mBoundRight);
358 int targetY = Utils.clamp(y, mBoundTop, mBoundBottom);
359 targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax);
Chih-Chung Changec412542011-09-26 17:34:06 +0800360
361 startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
362 }
363
364 public void resetToFullView() {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800365 Box b = mBoxes.get(0);
366 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_ZOOM);
Chih-Chung Changec412542011-09-26 17:34:06 +0800367 }
368
Chih-Chung Changec412542011-09-26 17:34:06 +0800369 public void beginScale(float focusX, float focusY) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800370 Box b = mBoxes.get(0);
371 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800372 mInScale = true;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800373 mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f);
374 mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f);
Chih-Chung Changec412542011-09-26 17:34:06 +0800375 }
376
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800377 // Scales the image by the given factor.
378 // Returns an out-of-range indicator:
379 // 1 if the intended scale is too large for the stable range.
380 // 0 if the intended scale is in the stable range.
381 // -1 if the intended scale is too small for the stable range.
382 public int scaleBy(float s, float focusX, float focusY) {
383 Box b = mBoxes.get(0);
384 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800385
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800386 // We want to keep the focus point (on the bitmap) the same as when we
387 // begin the scale guesture, that is,
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800388 //
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800389 // (focusX' - currentX') / scale' = (focusX - currentX) / scale
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800390 //
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800391 s *= getTargetScale(b);
392 int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f);
393 int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f);
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800394 startAnimation(x, y, s, ANIM_KIND_SCALE);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800395 if (s < b.mScaleMin) return -1;
396 if (s > b.mScaleMax) return 1;
397 return 0;
Chih-Chung Changec412542011-09-26 17:34:06 +0800398 }
399
400 public void endScale() {
401 mInScale = false;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800402 snapAndRedraw();
Chih-Chung Changec412542011-09-26 17:34:06 +0800403 }
404
405 public void startHorizontalSlide(int distance) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800406 Box b = mBoxes.get(0);
407 Platform p = mPlatform;
408 startAnimation(getTargetX(p) + distance, getTargetY(b),
409 b.mCurrentScale, ANIM_KIND_SLIDE);
Chih-Chung Changec412542011-09-26 17:34:06 +0800410 }
411
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800412 public void startScroll(float dx, float dy) {
413 boolean hasPrev = hasPrevImages();
414 boolean hasNext = hasNextImages();
415
416 Box b = mBoxes.get(0);
417 Platform p = mPlatform;
418
419 int x = getTargetX(p) + (int) (dx + 0.5f);
420 int y = getTargetY(b) + (int) (dy + 0.5f);
421
422 if (mFilmMode) {
423 scrollToFilm(x, y, hasPrev, hasNext);
424 } else {
425 scrollToPage(x, y, hasPrev, hasNext);
426 }
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800427 }
428
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800429 private void scrollToPage(int x, int y, boolean hasPrev, boolean hasNext) {
430 Box b = mBoxes.get(0);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800431
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800432 calculateStableBound(b.mCurrentScale);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800433
434 // Vertical direction: If we have space to move in the vertical
435 // direction, we show the edge effect when scrolling reaches the edge.
436 if (mBoundTop != mBoundBottom) {
437 if (y < mBoundTop) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800438 mListener.onPull(mBoundTop - y, EdgeView.BOTTOM);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800439 } else if (y > mBoundBottom) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800440 mListener.onPull(y - mBoundBottom, EdgeView.TOP);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800441 }
442 }
443
444 y = Utils.clamp(y, mBoundTop, mBoundBottom);
445
446 // Horizontal direction: we show the edge effect when the scrolling
447 // tries to go left of the first image or go right of the last image.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800448 if (!hasPrev && x > mBoundRight) {
449 int pixels = x - mBoundRight;
450 mListener.onPull(pixels, EdgeView.LEFT);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800451 x = mBoundRight;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800452 } else if (!hasNext && x < mBoundLeft) {
453 int pixels = mBoundLeft - x;
454 mListener.onPull(pixels, EdgeView.RIGHT);
455 x = mBoundLeft;
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800456 }
457
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800458 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
459 }
460
461 private void scrollToFilm(int x, int y, boolean hasPrev, boolean hasNext) {
462 Box b = mBoxes.get(0);
463
464 // Horizontal direction: we show the edge effect when the scrolling
465 // tries to go left of the first image or go right of the last image.
466 int cx = mViewW / 2;
467 if (!hasPrev && x > cx) {
468 int pixels = x - cx;
469 mListener.onPull(pixels, EdgeView.LEFT);
470 x = cx;
471 } else if (!hasNext && x < cx) {
472 int pixels = cx - x;
473 mListener.onPull(pixels, EdgeView.RIGHT);
474 x = cx;
475 }
476
477 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800478 }
479
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800480 public boolean fling(float velocityX, float velocityY) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800481 int vx = (int) (velocityX + 0.5f);
482 int vy = (int) (velocityY + 0.5f);
483 return mFilmMode ? flingFilm(vx, vy) : flingPage(vx, vy);
484 }
485
486 private boolean flingPage(int velocityX, int velocityY) {
487 Box b = mBoxes.get(0);
488 Platform p = mPlatform;
489
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800490 // We only want to do fling when the picture is zoomed-in.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800491 if (viewWiderThanScaledImage(b.mCurrentScale) &&
492 viewTallerThanScaledImage(b.mCurrentScale)) {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800493 return false;
494 }
495
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800496 // We only allow flinging in the directions where it won't go over the
497 // picture.
498 int edges = getImageAtEdges();
499 if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
500 (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
501 velocityX = 0;
502 }
503 if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
504 (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
505 velocityY = 0;
506 }
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800507
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800508 if (velocityX == 0 && velocityY == 0) return false;
509
510 mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY,
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800511 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800512 int targetX = mPageScroller.getFinalX();
513 int targetY = mPageScroller.getFinalY();
514 ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
515 startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800516 return true;
517 }
518
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800519 private boolean flingFilm(int velocityX, int velocityY) {
520 boolean hasPrev = hasPrevImages();
521 boolean hasNext = hasNextImages();
Chih-Chung Changec412542011-09-26 17:34:06 +0800522
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800523 Box b = mBoxes.get(0);
524 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800525
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800526 // If we are already at the edge, don't start the fling.
527 int cx = mViewW / 2;
528 if ((!hasPrev && p.mCurrentX >= cx) || (!hasNext && p.mCurrentX <= cx)) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800529 return false;
530 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800531
532 if (velocityX == 0) return false;
533
534 mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
535 Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
536 int targetX = mFilmScroller.getFinalX();
537 ANIM_TIME[ANIM_KIND_FLING] = mFilmScroller.getDuration();
538 startAnimation(targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING);
539 return true;
Chih-Chung Changec412542011-09-26 17:34:06 +0800540 }
541
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800542 ////////////////////////////////////////////////////////////////////////////
543 // Redraw
544 //
545 // If a method changes box positions directly, redraw()
546 // should be called.
547 //
548 // If a method may also cause a snapback to happen, snapAndRedraw() should
549 // be called.
550 //
551 // If a method starts an animation to change the position of focused box,
552 // startAnimation() should be called.
553 //
554 // If time advances to change the box position, advanceAnimation() should
555 // be called.
556 ////////////////////////////////////////////////////////////////////////////
557 private void redraw() {
558 layoutAndSetPosition();
559 mListener.invalidate();
560 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800561
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800562 private void snapAndRedraw() {
563 mPlatform.startSnapback();
564 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
565 mBoxes.get(i).startSnapback();
566 }
567 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
568 mGaps.get(i).startSnapback();
569 }
570 redraw();
571 }
Chih-Chung Chang534b12f2012-03-21 19:01:30 +0800572
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800573 private void startAnimation(int targetX, int targetY, float targetScale,
574 int kind) {
575 boolean changed = false;
576 changed |= mPlatform.doAnimation(targetX, kind);
577 changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
578 if (changed) redraw();
579 }
580
Chih-Chung Changb8be1e02012-04-17 20:35:14 +0800581 public void advanceAnimation() {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800582 boolean changed = false;
583 changed |= mPlatform.advanceAnimation();
584 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
585 changed |= mBoxes.get(i).advanceAnimation();
586 }
587 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
588 changed |= mGaps.get(i).advanceAnimation();
589 }
590 if (changed) redraw();
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800591 }
592
593 ////////////////////////////////////////////////////////////////////////////
594 // Layout
595 ////////////////////////////////////////////////////////////////////////////
596
597 // Returns the display width of this box.
598 private int widthOf(Box b) {
599 return (int) (b.mImageW * b.mCurrentScale + 0.5f);
600 }
601
602 // Returns the display height of this box.
603 private int heightOf(Box b) {
604 return (int) (b.mImageH * b.mCurrentScale + 0.5f);
605 }
606
607 // Returns the display width of this box, using the given scale.
608 private int widthOf(Box b, float scale) {
609 return (int) (b.mImageW * scale + 0.5f);
610 }
611
612 // Returns the display height of this box, using the given scale.
613 private int heightOf(Box b, float scale) {
614 return (int) (b.mImageH * scale + 0.5f);
615 }
616
617 // Convert the information in mPlatform and mBoxes to mRects, so the user
618 // can get the position of each box by getPosition().
619 //
620 // Note the loop index goes from inside-out because each box's X coordinate
621 // is relative to its anchor box (except the focused box).
622 private void layoutAndSetPosition() {
623 // layout box 0 (focused box)
624 convertBoxToRect(0);
625 for (int i = 1; i <= BOX_MAX; i++) {
626 // layout box i and -i
627 convertBoxToRect(i);
628 convertBoxToRect(-i);
629 }
630 //dumpState();
631 }
632
633 private void dumpState() {
634 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
635 Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
Chih-Chung Changec412542011-09-26 17:34:06 +0800636 }
637
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800638 dumpRect(0);
639 for (int i = 1; i <= BOX_MAX; i++) {
640 dumpRect(i);
641 dumpRect(-i);
Chih-Chung Changec412542011-09-26 17:34:06 +0800642 }
643
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800644 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
645 for (int j = i + 1; j <= BOX_MAX; j++) {
646 if (Rect.intersects(mRects.get(i), mRects.get(j))) {
647 Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
648 }
649 }
650 }
651 }
652
653 private void dumpRect(int i) {
654 StringBuilder sb = new StringBuilder();
655 Rect r = mRects.get(i);
656 sb.append("Rect " + i + ":");
657 sb.append("(");
658 sb.append(r.centerX());
659 sb.append(",");
660 sb.append(r.centerY());
661 sb.append(") [");
662 sb.append(r.width());
663 sb.append("x");
664 sb.append(r.height());
665 sb.append("]");
666 Log.d(TAG, sb.toString());
667 }
668
669 private void convertBoxToRect(int i) {
670 Box b = mBoxes.get(i);
671 Rect r = mRects.get(i);
672 int y = b.mCurrentY;
673 int w = widthOf(b);
674 int h = heightOf(b);
675 if (i == 0) {
676 int x = mPlatform.mCurrentX;
677 r.left = x - w / 2;
678 r.right = r.left + w;
679 } else if (i > 0) {
680 Rect a = mRects.get(i - 1);
681 Gap g = mGaps.get(i - 1);
682 r.left = a.right + g.mCurrentGap;
683 r.right = r.left + w;
684 } else { // i < 0
685 Rect a = mRects.get(i + 1);
686 Gap g = mGaps.get(i);
687 r.right = a.left - g.mCurrentGap;
688 r.left = r.right - w;
689 }
690 r.top = y - h / 2;
691 r.bottom = r.top + h;
692 }
693
694 // Returns the position of a box.
695 public Rect getPosition(int index) {
696 return mRects.get(index);
697 }
698
699 ////////////////////////////////////////////////////////////////////////////
700 // Box management
701 ////////////////////////////////////////////////////////////////////////////
702
703 // Initialize the platform to be at the view center.
704 private void initPlatform() {
705 mPlatform.mCurrentX = mViewW / 2;
706 mPlatform.mAnimationStartTime = NO_ANIMATION;
707 }
708
709 // Initialize a box to have the size of the view.
710 private void initBox(int index) {
711 Box b = mBoxes.get(index);
712 b.mImageW = mViewW;
713 b.mImageH = mViewH;
714 b.mUseViewSize = true;
715 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH);
716 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
717 b.mCurrentY = mViewH / 2;
718 b.mCurrentScale = b.mScaleMin;
719 b.mAnimationStartTime = NO_ANIMATION;
720 }
721
722 // Initialize a gap. This can only be called after the boxes around the gap
723 // has been initialized.
724 private void initGap(int index) {
725 Gap g = mGaps.get(index);
726 g.mDefaultSize = getDefaultGapSize(index);
727 g.mCurrentGap = g.mDefaultSize;
728 g.mAnimationStartTime = NO_ANIMATION;
729 }
730
731 private void initGap(int index, int size) {
732 Gap g = mGaps.get(index);
733 g.mDefaultSize = getDefaultGapSize(index);
734 g.mCurrentGap = size;
735 g.mAnimationStartTime = NO_ANIMATION;
736 }
737
738 private void debugMoveBox(int fromIndex[]) {
739 StringBuilder s = new StringBuilder("moveBox:");
740 for (int i = 0; i < fromIndex.length; i++) {
741 int j = fromIndex[i];
742 if (j == Integer.MAX_VALUE) {
743 s.append(" N");
744 } else {
745 s.append(" ");
746 s.append(fromIndex[i]);
747 }
748 }
749 Log.d(TAG, s.toString());
750 }
751
752 // Move the boxes: it may indicate focus change, box deleted, box appearing,
753 // box reordered, etc.
754 //
755 // Each element in the fromIndex array indicates where each box was in the
756 // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
757 // means the box is new.
758 //
759 // For example:
760 // N N N N N N N -- all new boxes
761 // -3 -2 -1 0 1 2 3 -- nothing changed
762 // -2 -1 0 1 2 3 N -- focus goes to the next box
763 // N-3 -2 -1 0 1 2 -- focuse goes to the previous box
764 // -3 -2 -1 1 2 3 N -- the focused box was deleted.
765 public void moveBox(int fromIndex[]) {
766 //debugMoveBox(fromIndex);
767 RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
768
769 // 1. Get the absolute X coordiates for the boxes.
770 layoutAndSetPosition();
771 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
772 Box b = mBoxes.get(i);
773 Rect r = mRects.get(i);
774 b.mAbsoluteX = r.centerX();
Chih-Chung Changec412542011-09-26 17:34:06 +0800775 }
776
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800777 // 2. copy boxes and gaps to temporary storage.
778 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
779 mTempBoxes.put(i, mBoxes.get(i));
780 mBoxes.put(i, null);
781 }
782 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
783 mTempGaps.put(i, mGaps.get(i));
784 mGaps.put(i, null);
785 }
786
787 // 3. move back boxes that are used in the new array.
788 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
789 int j = from.get(i);
790 if (j == Integer.MAX_VALUE) continue;
791 mBoxes.put(i, mTempBoxes.get(j));
792 mTempBoxes.put(j, null);
793 }
794
795 // 4. move back gaps if both boxes around it are kept together.
796 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
797 int j = from.get(i);
798 if (j == Integer.MAX_VALUE) continue;
799 int k = from.get(i + 1);
800 if (k == Integer.MAX_VALUE) continue;
801 if (j + 1 == k) {
802 mGaps.put(i, mTempGaps.get(j));
803 mTempGaps.put(j, null);
804 }
805 }
806
807 // 5. recycle the boxes that are not used in the new array.
808 int k = -BOX_MAX;
809 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
810 if (mBoxes.get(i) != null) continue;
811 while (mTempBoxes.get(k) == null) {
812 k++;
813 }
814 mBoxes.put(i, mTempBoxes.get(k++));
815 initBox(i);
816 }
817
818 // 6. Now give the recycled box a reasonable absolute X position.
819 //
820 // First try to find the first and the last box which the absolute X
821 // position is known.
822 int first, last;
823 for (first = -BOX_MAX; first <= BOX_MAX; first++) {
824 if (from.get(first) != Integer.MAX_VALUE) break;
825 }
826 for (last = BOX_MAX; last >= -BOX_MAX; last--) {
827 if (from.get(last) != Integer.MAX_VALUE) break;
828 }
829 // If there is no box has known X position at all, make the focused one
830 // as known.
831 if (first > BOX_MAX) {
832 mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
833 first = last = 0;
834 }
835 // Now for those boxes between first and last, just assign the same
836 // position as the previous box. (We can do better, but this should be
837 // rare). For the boxes before first or after last, we will use a new
838 // default gap size below.
839 for (int i = first + 1; i < last; i++) {
840 if (from.get(i) != Integer.MAX_VALUE) continue;
841 mBoxes.get(i).mAbsoluteX = mBoxes.get(i - 1).mAbsoluteX;
842 }
843
844 // 7. recycle the gaps that are not used in the new array.
845 k = -BOX_MAX;
846 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
847 if (mGaps.get(i) != null) continue;
848 while (mTempGaps.get(k) == null) {
849 k++;
850 }
851 mGaps.put(i, mTempGaps.get(k++));
852 Box a = mBoxes.get(i);
853 Box b = mBoxes.get(i + 1);
854 int wa = widthOf(a);
855 int wb = widthOf(b);
856 if (i >= first && i < last) {
857 int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
858 initGap(i, g);
859 } else {
860 initGap(i);
861 }
862 }
863
864 // 8. offset the Platform position
865 int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
866 mPlatform.mCurrentX += dx;
867 mPlatform.mFromX += dx;
868 mPlatform.mToX += dx;
869 mPlatform.mFlingOffset += dx;
870
871 snapAndRedraw();
872 }
873
874 ////////////////////////////////////////////////////////////////////////////
875 // Public utilities
876 ////////////////////////////////////////////////////////////////////////////
877
878 public float getMinimalScale(int imageW, int imageH) {
Chih-Chung Chang642561d2012-04-16 16:29:13 +0800879 float wFactor = 1.0f;
880 float hFactor = 1.0f;
881
882 if (mFilmMode) {
883 if (mViewH > mViewW) { // portrait
884 wFactor = FILM_MODE_PORTRAIT_WIDTH;
885 hFactor = FILM_MODE_PORTRAIT_HEIGHT;
886 } else { // landscape
887 wFactor = FILM_MODE_LANDSCAPE_WIDTH;
888 hFactor = FILM_MODE_LANDSCAPE_HEIGHT;
889 }
890 }
891
892 float s = Math.min(wFactor * mViewW / imageW,
893 hFactor * mViewH / imageH);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800894 return Math.min(SCALE_LIMIT, s);
895 }
896
897 public float getMaximalScale(int imageW, int imageH) {
898 return mFilmMode ? getMinimalScale(imageW, imageH) : SCALE_LIMIT;
899 }
900
901 public boolean isAtMinimalScale() {
902 Box b = mBoxes.get(0);
903 return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
904 }
905
906 public int getImageWidth() {
907 Box b = mBoxes.get(0);
908 return b.mImageW;
909 }
910
911 public int getImageHeight() {
912 Box b = mBoxes.get(0);
913 return b.mImageH;
914 }
915
916 public float getImageScale() {
917 Box b = mBoxes.get(0);
918 return b.mCurrentScale;
919 }
920
921 public int getImageAtEdges() {
922 Box b = mBoxes.get(0);
923 Platform p = mPlatform;
924 calculateStableBound(b.mCurrentScale);
925 int edges = 0;
926 if (p.mCurrentX <= mBoundLeft) {
927 edges |= IMAGE_AT_RIGHT_EDGE;
928 }
929 if (p.mCurrentX >= mBoundRight) {
930 edges |= IMAGE_AT_LEFT_EDGE;
931 }
932 if (b.mCurrentY <= mBoundTop) {
933 edges |= IMAGE_AT_BOTTOM_EDGE;
934 }
935 if (b.mCurrentY >= mBoundBottom) {
936 edges |= IMAGE_AT_TOP_EDGE;
937 }
938 return edges;
939 }
940
941 ////////////////////////////////////////////////////////////////////////////
942 // Private utilities
943 ////////////////////////////////////////////////////////////////////////////
944
945 private float getMinimalScale(Box b) {
946 return getMinimalScale(b.mImageW, b.mImageH);
947 }
948
949 private float getMaxmimalScale(Box b) {
950 return getMaximalScale(b.mImageW, b.mImageH);
951 }
952
953 private static boolean isAlmostEqual(float a, float b) {
954 float diff = a - b;
955 return (diff < 0 ? -diff : diff) < 0.02f;
Chih-Chung Changec412542011-09-26 17:34:06 +0800956 }
957
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800958 // Calculates the stable region of mCurrent{X/Y}, where "stable" means
959 //
960 // (1) If the dimension of scaled image >= view dimension, we will not
961 // see black region outside the image (at that dimension).
962 // (2) If the dimension of scaled image < view dimension, we will center
963 // the scaled image.
964 //
965 // We might temporarily go out of this stable during user interaction,
966 // but will "snap back" after user stops interaction.
967 //
968 // The results are stored in mBound{Left/Right/Top/Bottom}.
969 //
Chih-Chung Chang8f568da2012-01-05 12:00:53 +0800970 // An extra parameter "horizontalSlack" (which has the value of 0 usually)
971 // is used to extend the stable region by some pixels on each side
972 // horizontally.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800973 private void calculateStableBound(float scale, int horizontalSlack) {
974 Box b = mBoxes.get(0);
Chih-Chung Chang8f568da2012-01-05 12:00:53 +0800975
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800976 // The width and height of the box in number of view pixels
977 int w = widthOf(b, scale);
978 int h = heightOf(b, scale);
979
980 // When the edge of the view is aligned with the edge of the box
981 mBoundLeft = (mViewW - horizontalSlack) - w / 2;
982 mBoundRight = mViewW - mBoundLeft;
983 mBoundTop = mViewH - h / 2;
984 mBoundBottom = mViewH - mBoundTop;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800985
986 // If the scaled height is smaller than the view height,
987 // force it to be in the center.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800988 if (viewTallerThanScaledImage(scale)) {
989 mBoundTop = mBoundBottom = mViewH / 2;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800990 }
991
992 // Same for width
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800993 if (viewWiderThanScaledImage(scale)) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800994 mBoundLeft = mBoundRight = mViewW / 2;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800995 }
996 }
997
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800998 private void calculateStableBound(float scale) {
999 calculateStableBound(scale, 0);
1000 }
1001
1002 private boolean hasNextImages() {
1003 for (int i = 1; i <= BOX_MAX; i++) {
1004 if (!mBoxes.get(i).mUseViewSize) return true;
1005 }
1006 return false;
1007 }
1008
1009 private boolean hasPrevImages() {
1010 for (int i = -1; i >= -BOX_MAX; i--) {
1011 if (!mBoxes.get(i).mUseViewSize) return true;
1012 }
1013 return false;
1014 }
1015
1016 private boolean viewTallerThanScaledImage(float scale) {
1017 return mViewH >= heightOf(mBoxes.get(0), scale);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001018 }
1019
1020 private boolean viewWiderThanScaledImage(float scale) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001021 return mViewW >= widthOf(mBoxes.get(0), scale);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001022 }
1023
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001024 private float getTargetScale(Box b) {
1025 return useCurrentValueAsTarget(b) ? b.mCurrentScale : b.mToScale;
Chih-Chung Changb3aab902011-10-03 21:11:39 +08001026 }
1027
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001028 private int getTargetX(Platform p) {
1029 return useCurrentValueAsTarget(p) ? p.mCurrentX : p.mToX;
Chih-Chung Changec412542011-09-26 17:34:06 +08001030 }
1031
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001032 private int getTargetY(Box b) {
1033 return useCurrentValueAsTarget(b) ? b.mCurrentY : b.mToY;
Chih-Chung Changec412542011-09-26 17:34:06 +08001034 }
1035
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001036 private boolean useCurrentValueAsTarget(Animatable a) {
1037 return a.mAnimationStartTime == NO_ANIMATION ||
1038 a.mAnimationKind == ANIM_KIND_SNAPBACK ||
1039 a.mAnimationKind == ANIM_KIND_FLING;
Chih-Chung Changec412542011-09-26 17:34:06 +08001040 }
1041
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001042 // Returns the index of the anchor box.
1043 private int anchorIndex(int i) {
1044 if (i > 0) return i - 1;
1045 if (i < 0) return i + 1;
1046 throw new IllegalArgumentException();
1047 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001048
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001049 ////////////////////////////////////////////////////////////////////////////
1050 // Animatable: an thing which can do animation.
1051 ////////////////////////////////////////////////////////////////////////////
1052 private abstract static class Animatable {
1053 public long mAnimationStartTime;
1054 public int mAnimationKind;
1055 public int mAnimationDuration;
Chih-Chung Changec412542011-09-26 17:34:06 +08001056
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001057 // This should be overidden in subclass to change the animation values
1058 // give the progress value in [0, 1].
1059 protected abstract boolean interpolate(float progress);
1060 public abstract boolean startSnapback();
Chih-Chung Changec412542011-09-26 17:34:06 +08001061
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001062 // Returns true if the animation values changes, so things need to be
1063 // redrawn.
1064 public boolean advanceAnimation() {
1065 if (mAnimationStartTime == NO_ANIMATION) {
1066 return false;
1067 }
1068 if (mAnimationStartTime == LAST_ANIMATION) {
1069 mAnimationStartTime = NO_ANIMATION;
1070 return startSnapback();
1071 }
1072
1073 float progress;
1074 if (mAnimationDuration == 0) {
1075 progress = 1;
1076 } else {
1077 long now = AnimationTime.get();
1078 progress =
1079 (float) (now - mAnimationStartTime) / mAnimationDuration;
1080 }
1081
1082 if (progress >= 1) {
1083 progress = 1;
1084 } else {
1085 progress = applyInterpolationCurve(mAnimationKind, progress);
1086 }
1087
1088 boolean done = interpolate(progress);
1089
1090 if (done) {
1091 mAnimationStartTime = LAST_ANIMATION;
1092 }
1093
1094 return true;
Chih-Chung Changec412542011-09-26 17:34:06 +08001095 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001096
1097 private static float applyInterpolationCurve(int kind, float progress) {
1098 float f = 1 - progress;
1099 switch (kind) {
1100 case ANIM_KIND_SCROLL:
1101 case ANIM_KIND_FLING:
1102 progress = 1 - f; // linear
1103 break;
1104 case ANIM_KIND_SCALE:
1105 progress = 1 - f * f; // quadratic
1106 break;
1107 case ANIM_KIND_SNAPBACK:
1108 case ANIM_KIND_ZOOM:
1109 case ANIM_KIND_SLIDE:
1110 case ANIM_KIND_OPENING:
1111 progress = 1 - f * f * f * f * f; // x^5
1112 break;
1113 }
1114 return progress;
1115 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001116 }
1117
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001118 ////////////////////////////////////////////////////////////////////////////
1119 // Platform: captures the global X movement.
1120 ////////////////////////////////////////////////////////////////////////////
1121 private class Platform extends Animatable {
1122 public int mCurrentX, mFromX, mToX;
1123 public int mFlingOffset;
1124
1125 @Override
1126 public boolean startSnapback() {
1127 if (mAnimationStartTime != NO_ANIMATION) return false;
1128 if (mAnimationKind == ANIM_KIND_SCROLL
1129 && mListener.isDown()) return false;
1130
1131 Box b = mBoxes.get(0);
1132 float scaleMin = mExtraScalingRange ?
1133 b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin;
1134 float scaleMax = mExtraScalingRange ?
1135 b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax;
1136 float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax);
1137 int x = mCurrentX;
1138 if (mFilmMode) {
1139 if (!hasNextImages()) x = Math.max(x, mViewW / 2);
1140 if (!hasPrevImages()) x = Math.min(x, mViewW / 2);
1141 } else {
1142 calculateStableBound(scale, HORIZONTAL_SLACK);
1143 x = Utils.clamp(x, mBoundLeft, mBoundRight);
1144 }
1145 if (mCurrentX != x) {
1146 return doAnimation(x, ANIM_KIND_SNAPBACK);
1147 }
1148 return false;
1149 }
1150
1151 // Starts an animation for the platform.
1152 public boolean doAnimation(int targetX, int kind) {
1153 if (mCurrentX == targetX) return false;
1154 mAnimationKind = kind;
1155 mFromX = mCurrentX;
1156 mToX = targetX;
1157 mAnimationStartTime = AnimationTime.startTime();
1158 mAnimationDuration = ANIM_TIME[kind];
1159 mFlingOffset = 0;
1160 advanceAnimation();
1161 return true;
1162 }
1163
1164 @Override
1165 protected boolean interpolate(float progress) {
1166 if (mAnimationKind == ANIM_KIND_FLING) {
1167 return mFilmMode
1168 ? interpolateFlingFilm(progress)
1169 : interpolateFlingPage(progress);
1170 } else {
1171 return interpolateLinear(progress);
1172 }
1173 }
1174
1175 private boolean interpolateFlingFilm(float progress) {
1176 mFilmScroller.computeScrollOffset();
1177 mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
1178
1179 int dir = EdgeView.INVALID_DIRECTION;
1180 if (mCurrentX < mViewW / 2) {
1181 if (!hasNextImages()) {
1182 dir = EdgeView.RIGHT;
1183 }
1184 } else if (mCurrentX > mViewW / 2) {
1185 if (!hasPrevImages()) {
1186 dir = EdgeView.LEFT;
1187 }
1188 }
1189 if (dir != EdgeView.INVALID_DIRECTION) {
1190 int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
1191 mListener.onAbsorb(v, dir);
1192 mFilmScroller.forceFinished(true);
1193 mCurrentX = mViewW / 2;
1194 }
1195 return mFilmScroller.isFinished();
1196 }
1197
1198 private boolean interpolateFlingPage(float progress) {
1199 mPageScroller.computeScrollOffset(progress);
1200 Box b = mBoxes.get(0);
1201 calculateStableBound(b.mCurrentScale);
1202
1203 int oldX = mCurrentX;
1204 mCurrentX = mPageScroller.getCurrX();
1205
1206 // Check if we hit the edges; show edge effects if we do.
1207 if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
1208 int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
1209 mListener.onAbsorb(v, EdgeView.RIGHT);
1210 } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
1211 int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
1212 mListener.onAbsorb(v, EdgeView.LEFT);
1213 }
1214
1215 return progress >= 1;
1216 }
1217
1218 private boolean interpolateLinear(float progress) {
1219 // Other animations
1220 if (progress >= 1) {
1221 mCurrentX = mToX;
1222 return true;
1223 } else {
1224 mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
1225 return (mCurrentX == mToX);
1226 }
1227 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001228 }
1229
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001230 ////////////////////////////////////////////////////////////////////////////
1231 // Box: represents a rectangular area which shows a picture.
1232 ////////////////////////////////////////////////////////////////////////////
1233 private class Box extends Animatable {
1234 // Size of the bitmap
1235 public int mImageW, mImageH;
1236
1237 // This is true if we assume the image size is the same as view size
1238 // until we know the actual size of image. This is also used to
1239 // determine if there is an image ready to show.
1240 public boolean mUseViewSize;
1241
1242 // The minimum and maximum scale we allow for this box.
1243 public float mScaleMin, mScaleMax;
1244
1245 // The X/Y value indicates where the center of the box is on the view
1246 // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
1247 // actual values used currently. Note that the X values are implicitly
1248 // defined by Platform and Gaps.
1249 public int mCurrentY, mFromY, mToY;
1250 public float mCurrentScale, mFromScale, mToScale;
1251
1252 // The absolute X coordinate of the center of the box. This is only used
1253 // during moveBox().
1254 public int mAbsoluteX;
1255
1256 @Override
1257 public boolean startSnapback() {
1258 if (mAnimationStartTime != NO_ANIMATION) return false;
1259 if (mAnimationKind == ANIM_KIND_SCROLL
1260 && mListener.isDown()) return false;
1261 if (mInScale && this == mBoxes.get(0)) return false;
1262
1263 int y;
1264 float scale;
1265
1266 if (this == mBoxes.get(0)) {
1267 float scaleMin = mExtraScalingRange ?
1268 mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
1269 float scaleMax = mExtraScalingRange ?
1270 mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
1271 scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
1272 if (mFilmMode) {
1273 y = mViewH / 2;
1274 } else {
1275 calculateStableBound(scale, HORIZONTAL_SLACK);
1276 y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
1277 }
1278 } else {
1279 y = mViewH / 2;
1280 scale = mScaleMin;
1281 }
1282
1283 if (mCurrentY != y || mCurrentScale != scale) {
1284 return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
1285 }
1286 return false;
1287 }
1288
1289 private boolean doAnimation(int targetY, float targetScale, int kind) {
1290 targetScale = Utils.clamp(targetScale,
1291 SCALE_MIN_EXTRA * mScaleMin,
1292 SCALE_MAX_EXTRA * mScaleMax);
1293
1294 // If the scaled height is smaller than the view height, force it to be
1295 // in the center. (We do this for height only, not width, because the
1296 // user may want to scroll to the previous/next image.)
1297 if (!mInScale && viewTallerThanScaledImage(targetScale)) {
1298 targetY = mViewH / 2;
1299 }
1300
1301 if (mCurrentY == targetY && mCurrentScale == targetScale) {
1302 return false;
1303 }
1304
1305 // Now starts an animation for the box.
1306 mAnimationKind = kind;
1307 mFromY = mCurrentY;
1308 mFromScale = mCurrentScale;
1309 mToY = targetY;
1310 mToScale = targetScale;
1311 mAnimationStartTime = AnimationTime.startTime();
1312 mAnimationDuration = ANIM_TIME[kind];
1313 advanceAnimation();
1314 return true;
1315 }
1316
1317 @Override
1318 protected boolean interpolate(float progress) {
1319 if (mAnimationKind == ANIM_KIND_FLING) {
1320 // Currently a Box can only be flung in page mode.
1321 return interpolateFlingPage(progress);
1322 } else {
1323 return interpolateLinear(progress);
1324 }
1325 }
1326
1327 private boolean interpolateFlingPage(float progress) {
1328 mPageScroller.computeScrollOffset(progress);
1329 calculateStableBound(mCurrentScale);
1330
1331 int oldY = mCurrentY;
1332 mCurrentY = mPageScroller.getCurrY();
1333
1334 // Check if we hit the edges; show edge effects if we do.
1335 if (oldY > mBoundTop && mCurrentY == mBoundTop) {
1336 int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
1337 mListener.onAbsorb(v, EdgeView.BOTTOM);
1338 } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
1339 int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
1340 mListener.onAbsorb(v, EdgeView.TOP);
1341 }
1342
1343 return progress >= 1;
1344 }
1345
1346 private boolean interpolateLinear(float progress) {
1347 if (progress >= 1) {
1348 mCurrentY = mToY;
1349 mCurrentScale = mToScale;
1350 return true;
1351 } else {
1352 mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
1353 mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
1354 return (mCurrentY == mToY && mCurrentScale == mToScale);
1355 }
1356 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001357 }
Chih-Chung Chang532d93c2011-10-12 17:10:33 +08001358
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001359 ////////////////////////////////////////////////////////////////////////////
1360 // Gap: represents a rectangular area which is between two boxes.
1361 ////////////////////////////////////////////////////////////////////////////
1362 private class Gap extends Animatable {
1363 // The default gap size between two boxes. The value may vary for
1364 // different image size of the boxes and for different modes (page or
1365 // film).
1366 public int mDefaultSize;
1367
1368 // The gap size between the two boxes.
1369 public int mCurrentGap, mFromGap, mToGap;
1370
1371 @Override
1372 public boolean startSnapback() {
1373 if (mAnimationStartTime != NO_ANIMATION) return false;
1374 return doAnimation(mDefaultSize);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001375 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001376
1377 // Starts an animation for a gap.
1378 public boolean doAnimation(int targetSize) {
1379 if (mCurrentGap == targetSize) return false;
1380 mAnimationKind = ANIM_KIND_SNAPBACK;
1381 mFromGap = mCurrentGap;
1382 mToGap = targetSize;
1383 mAnimationStartTime = AnimationTime.startTime();
1384 mAnimationDuration = ANIM_TIME[mAnimationKind];
1385 advanceAnimation();
1386 return true;
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001387 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001388
1389 @Override
1390 protected boolean interpolate(float progress) {
1391 if (progress >= 1) {
1392 mCurrentGap = mToGap;
1393 return true;
1394 } else {
1395 mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
1396 return (mCurrentGap == mToGap);
1397 }
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001398 }
Chih-Chung Chang532d93c2011-10-12 17:10:33 +08001399 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001400}