blob: d190e04ec5d09f4e7338d5ae467eec4b613bd804 [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;
75 private static final float FILM_MODE_SCALE_FACTOR = 0.7f;
Chih-Chung Changb3aab902011-10-03 21:11:39 +080076
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080077 // The scaling factor in current mode.
78 private float mScaleFactor = mFilmMode ? FILM_MODE_SCALE_FACTOR : 1.0f;
79
80 // In addition to the focused box (index == 0). We also keep information
81 // about this many boxes on each side.
82 private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX;
83
84 public static final int IMAGE_GAP = 96;
85 private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
86
87 private Listener mListener;
88 private volatile Rect mOpenAnimationRect;
89 private int mViewW = 640;
90 private int mViewH = 480;;
91
92 // A scaling guesture is in progress.
93 private boolean mInScale;
94 // The focus point of the scaling gesture, relative to the center of the
95 // picture in bitmap pixels.
96 private float mFocusX, mFocusY;
97
98 // This is used by the fling animation (page mode).
99 private FlingScroller mPageScroller;
100
101 // This is used by the fling animation (film mode).
102 private Scroller mFilmScroller;
103
104 // The bound of the stable region that the focused box can stay, see the
105 // comments above calculateStableBound() for details.
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800106 private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
107
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800108 //
109 // ___________________________________________________________
110 // | _____ _____ _____ _____ _____ |
111 // | | | | | | | | | | | |
112 // | | Box | | Box | | Box*| | Box | | Box | |
113 // | |_____|.....|_____|.....|_____|.....|_____|.....|_____| |
114 // | Gap Gap Gap Gap |
115 // |___________________________________________________________|
116 //
117 // <-- Platform -->
118 //
119 // The focused box (Box*) centers at mPlatform.mCurrentX
Chih-Chung Changec412542011-09-26 17:34:06 +0800120
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800121 private Platform mPlatform = new Platform();
122 private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
123 // The gap at the right of a Box i is at index i. The gap at the left of a
124 // Box i is at index i - 1.
125 private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
Chih-Chung Changec412542011-09-26 17:34:06 +0800126
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800127 // These are only used during moveBox().
128 private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
129 private RangeArray<Gap> mTempGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
130
131 // The output of the PositionController. Available throught getPosition().
132 private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX);
133
134 public interface Listener {
135 void invalidate();
136 boolean isDown();
137
138 // EdgeView
139 void onPull(int offset, int direction);
140 void onRelease();
141 void onAbsorb(int velocity, int direction);
Chih-Chung Changec412542011-09-26 17:34:06 +0800142 }
143
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800144 public PositionController(Context context, Listener listener) {
145 mListener = listener;
146 mPageScroller = new FlingScroller();
147 mFilmScroller = new Scroller(context);
Chih-Chung Changec412542011-09-26 17:34:06 +0800148
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800149 // Initialize the areas.
150 initPlatform();
151 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
152 mBoxes.put(i, new Box());
153 initBox(i);
154 mRects.put(i, new Rect());
155 }
156 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
157 mGaps.put(i, new Gap());
158 initGap(i);
159 }
160 }
161
162 public void setOpenAnimationRect(Rect r) {
163 mOpenAnimationRect = r;
164 }
165
166 public void setViewSize(int viewW, int viewH) {
167 if (viewW == mViewW && viewH == mViewH) return;
168
169 mViewW = viewW;
170 mViewH = viewH;
171 initPlatform();
172
173 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
174 setBoxSize(i, viewW, viewH, true);
175 }
176
177 updateScaleAndGapLimit();
178 snapAndRedraw();
179 }
180
181 public void setImageSize(int index, int width, int height) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800182 if (width == 0 || height == 0) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800183 initBox(index);
184 } else {
185 setBoxSize(index, width, height, false);
186 }
187
188 updateScaleAndGapLimit();
189 startOpeningAnimationIfNeeded();
190 snapAndRedraw();
191 }
192
193 private void setBoxSize(int i, int width, int height, boolean isViewSize) {
194 Box b = mBoxes.get(i);
195
196 // If we already have image size, we don't want to use the view size.
197 if (isViewSize && !b.mUseViewSize) return;
198 b.mUseViewSize = isViewSize;
199
200 if (width == b.mImageW && height == b.mImageH) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800201 return;
202 }
203
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800204 // The ratio of the old size and the new size.
Chih-Chung Changec412542011-09-26 17:34:06 +0800205 float ratio = Math.min(
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800206 (float) b.mImageW / width, (float) b.mImageH / height);
Chih-Chung Changec412542011-09-26 17:34:06 +0800207
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800208 b.mCurrentScale *= ratio;
209 b.mFromScale *= ratio;
210 b.mToScale *= ratio;
Chih-Chung Changec412542011-09-26 17:34:06 +0800211
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800212 b.mImageW = width;
213 b.mImageH = height;
Chih-Chung Changec412542011-09-26 17:34:06 +0800214
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800215 if (i == 0) {
216 mFocusX /= ratio;
217 mFocusY /= ratio;
Chih-Chung Changec412542011-09-26 17:34:06 +0800218 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800219 }
220
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800221 private void startOpeningAnimationIfNeeded() {
222 if (mOpenAnimationRect == null) return;
223 Box b = mBoxes.get(0);
224 if (b.mUseViewSize) return;
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800225
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800226 // Start animation from the saved rectangle if we have one.
227 Rect r = mOpenAnimationRect;
228 mOpenAnimationRect = null;
229 mPlatform.mCurrentX = r.centerX();
230 b.mCurrentY = r.centerY();
231 b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
232 r.height() / (float) b.mImageH);
233 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_OPENING);
234 }
235
236 public void setFilmMode(boolean enabled) {
237 if (enabled == mFilmMode) return;
238 mFilmMode = enabled;
239 mScaleFactor = enabled ? FILM_MODE_SCALE_FACTOR : 1.0f;
240
241 updateScaleAndGapLimit();
242 stopAnimation();
243 snapAndRedraw();
244 }
245
246 public void setExtraScalingRange(boolean enabled) {
247 if (mExtraScalingRange == enabled) return;
248 mExtraScalingRange = enabled;
249 if (!enabled) {
250 snapAndRedraw();
251 }
252 }
253
254 // This should be called whenever the scale range of boxes or the default
255 // gap size may change. Currently this can happen due to change of view
256 // size, image size, and mode.
257 private void updateScaleAndGapLimit() {
258 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
259 Box b = mBoxes.get(i);
260 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH);
261 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
262 }
263
264 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
265 Gap g = mGaps.get(i);
266 g.mDefaultSize = getDefaultGapSize(i);
267 }
268 }
269
270 // Returns the default gap size according the the size of the boxes around
271 // the gap and the current mode.
272 private int getDefaultGapSize(int i) {
273 if (mFilmMode) return IMAGE_GAP;
274 Box a = mBoxes.get(i);
275 Box b = mBoxes.get(i + 1);
276 return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b));
277 }
278
279 // Here is how we layout the boxes in the page mode.
280 //
281 // previous current next
282 // ___________ ________________ __________
283 // | _______ | | __________ | | ______ |
284 // | | | | | | right->| | | | | |
285 // | | |<-------->|<--left | | | | | |
286 // | |_______| | | | |__________| | | |______| |
287 // |___________| | |________________| |__________|
288 // | <--> gapToSide()
289 // |
290 // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current))
291 private int gapToSide(Box b) {
292 return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f);
293 }
294
295 // Stop all animations at where they are now.
296 public void stopAnimation() {
297 mPlatform.mAnimationStartTime = NO_ANIMATION;
298 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
299 mBoxes.get(i).mAnimationStartTime = NO_ANIMATION;
300 }
301 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
302 mGaps.get(i).mAnimationStartTime = NO_ANIMATION;
303 }
304 }
305
306 public void skipAnimation() {
307 if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
308 mPlatform.mCurrentX = mPlatform.mToX;
309 mPlatform.mAnimationStartTime = NO_ANIMATION;
310 }
311 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
312 Box b = mBoxes.get(i);
313 if (b.mAnimationStartTime == NO_ANIMATION) continue;
314 b.mCurrentY = b.mToY;
315 b.mCurrentScale = b.mToScale;
316 b.mAnimationStartTime = NO_ANIMATION;
317 }
318 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
319 Gap g = mGaps.get(i);
320 if (g.mAnimationStartTime == NO_ANIMATION) continue;
321 g.mCurrentGap = g.mToGap;
322 g.mAnimationStartTime = NO_ANIMATION;
323 }
324 redraw();
325 }
326
327 public void up() {
328 snapAndRedraw();
329 }
330
331 ////////////////////////////////////////////////////////////////////////////
332 // Start an animations for the focused box
333 ////////////////////////////////////////////////////////////////////////////
334
335 public void zoomIn(float tapX, float tapY, float targetScale) {
336 Box b = mBoxes.get(0);
337
338 // Convert the tap position to distance to center in bitmap coordinates
339 float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale;
340 float tempY = (tapY - b.mCurrentY) / b.mCurrentScale;
341
342 int x = (int) (mViewW / 2 - tempX * targetScale + 0.5f);
343 int y = (int) (mViewH / 2 - tempY * targetScale + 0.5f);
Chih-Chung Changec412542011-09-26 17:34:06 +0800344
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800345 calculateStableBound(targetScale);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800346 int targetX = Utils.clamp(x, mBoundLeft, mBoundRight);
347 int targetY = Utils.clamp(y, mBoundTop, mBoundBottom);
348 targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax);
Chih-Chung Changec412542011-09-26 17:34:06 +0800349
350 startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
351 }
352
353 public void resetToFullView() {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800354 Box b = mBoxes.get(0);
355 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_ZOOM);
Chih-Chung Changec412542011-09-26 17:34:06 +0800356 }
357
Chih-Chung Changec412542011-09-26 17:34:06 +0800358 public void beginScale(float focusX, float focusY) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800359 Box b = mBoxes.get(0);
360 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800361 mInScale = true;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800362 mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f);
363 mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f);
Chih-Chung Changec412542011-09-26 17:34:06 +0800364 }
365
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800366 // Scales the image by the given factor.
367 // Returns an out-of-range indicator:
368 // 1 if the intended scale is too large for the stable range.
369 // 0 if the intended scale is in the stable range.
370 // -1 if the intended scale is too small for the stable range.
371 public int scaleBy(float s, float focusX, float focusY) {
372 Box b = mBoxes.get(0);
373 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800374
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800375 // We want to keep the focus point (on the bitmap) the same as when we
376 // begin the scale guesture, that is,
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800377 //
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800378 // (focusX' - currentX') / scale' = (focusX - currentX) / scale
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800379 //
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800380 s *= getTargetScale(b);
381 int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f);
382 int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f);
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800383 startAnimation(x, y, s, ANIM_KIND_SCALE);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800384 if (s < b.mScaleMin) return -1;
385 if (s > b.mScaleMax) return 1;
386 return 0;
Chih-Chung Changec412542011-09-26 17:34:06 +0800387 }
388
389 public void endScale() {
390 mInScale = false;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800391 snapAndRedraw();
Chih-Chung Changec412542011-09-26 17:34:06 +0800392 }
393
394 public void startHorizontalSlide(int distance) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800395 Box b = mBoxes.get(0);
396 Platform p = mPlatform;
397 startAnimation(getTargetX(p) + distance, getTargetY(b),
398 b.mCurrentScale, ANIM_KIND_SLIDE);
Chih-Chung Changec412542011-09-26 17:34:06 +0800399 }
400
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800401 public void startScroll(float dx, float dy) {
402 boolean hasPrev = hasPrevImages();
403 boolean hasNext = hasNextImages();
404
405 Box b = mBoxes.get(0);
406 Platform p = mPlatform;
407
408 int x = getTargetX(p) + (int) (dx + 0.5f);
409 int y = getTargetY(b) + (int) (dy + 0.5f);
410
411 if (mFilmMode) {
412 scrollToFilm(x, y, hasPrev, hasNext);
413 } else {
414 scrollToPage(x, y, hasPrev, hasNext);
415 }
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800416 }
417
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800418 private void scrollToPage(int x, int y, boolean hasPrev, boolean hasNext) {
419 Box b = mBoxes.get(0);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800420
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800421 calculateStableBound(b.mCurrentScale);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800422
423 // Vertical direction: If we have space to move in the vertical
424 // direction, we show the edge effect when scrolling reaches the edge.
425 if (mBoundTop != mBoundBottom) {
426 if (y < mBoundTop) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800427 mListener.onPull(mBoundTop - y, EdgeView.BOTTOM);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800428 } else if (y > mBoundBottom) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800429 mListener.onPull(y - mBoundBottom, EdgeView.TOP);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800430 }
431 }
432
433 y = Utils.clamp(y, mBoundTop, mBoundBottom);
434
435 // Horizontal direction: we show the edge effect when the scrolling
436 // tries to go left of the first image or go right of the last image.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800437 if (!hasPrev && x > mBoundRight) {
438 int pixels = x - mBoundRight;
439 mListener.onPull(pixels, EdgeView.LEFT);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800440 x = mBoundRight;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800441 } else if (!hasNext && x < mBoundLeft) {
442 int pixels = mBoundLeft - x;
443 mListener.onPull(pixels, EdgeView.RIGHT);
444 x = mBoundLeft;
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800445 }
446
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800447 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
448 }
449
450 private void scrollToFilm(int x, int y, boolean hasPrev, boolean hasNext) {
451 Box b = mBoxes.get(0);
452
453 // Horizontal direction: we show the edge effect when the scrolling
454 // tries to go left of the first image or go right of the last image.
455 int cx = mViewW / 2;
456 if (!hasPrev && x > cx) {
457 int pixels = x - cx;
458 mListener.onPull(pixels, EdgeView.LEFT);
459 x = cx;
460 } else if (!hasNext && x < cx) {
461 int pixels = cx - x;
462 mListener.onPull(pixels, EdgeView.RIGHT);
463 x = cx;
464 }
465
466 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800467 }
468
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800469 public boolean fling(float velocityX, float velocityY) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800470 int vx = (int) (velocityX + 0.5f);
471 int vy = (int) (velocityY + 0.5f);
472 return mFilmMode ? flingFilm(vx, vy) : flingPage(vx, vy);
473 }
474
475 private boolean flingPage(int velocityX, int velocityY) {
476 Box b = mBoxes.get(0);
477 Platform p = mPlatform;
478
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800479 // We only want to do fling when the picture is zoomed-in.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800480 if (viewWiderThanScaledImage(b.mCurrentScale) &&
481 viewTallerThanScaledImage(b.mCurrentScale)) {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800482 return false;
483 }
484
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800485 // We only allow flinging in the directions where it won't go over the
486 // picture.
487 int edges = getImageAtEdges();
488 if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
489 (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
490 velocityX = 0;
491 }
492 if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
493 (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
494 velocityY = 0;
495 }
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800496
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800497 if (velocityX == 0 && velocityY == 0) return false;
498
499 mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY,
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800500 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800501 int targetX = mPageScroller.getFinalX();
502 int targetY = mPageScroller.getFinalY();
503 ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
504 startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800505 return true;
506 }
507
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800508 private boolean flingFilm(int velocityX, int velocityY) {
509 boolean hasPrev = hasPrevImages();
510 boolean hasNext = hasNextImages();
Chih-Chung Changec412542011-09-26 17:34:06 +0800511
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800512 Box b = mBoxes.get(0);
513 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800514
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800515 // If we are already at the edge, don't start the fling.
516 int cx = mViewW / 2;
517 if ((!hasPrev && p.mCurrentX >= cx) || (!hasNext && p.mCurrentX <= cx)) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800518 return false;
519 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800520
521 if (velocityX == 0) return false;
522
523 mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
524 Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
525 int targetX = mFilmScroller.getFinalX();
526 ANIM_TIME[ANIM_KIND_FLING] = mFilmScroller.getDuration();
527 startAnimation(targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING);
528 return true;
Chih-Chung Changec412542011-09-26 17:34:06 +0800529 }
530
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800531 ////////////////////////////////////////////////////////////////////////////
532 // Redraw
533 //
534 // If a method changes box positions directly, redraw()
535 // should be called.
536 //
537 // If a method may also cause a snapback to happen, snapAndRedraw() should
538 // be called.
539 //
540 // If a method starts an animation to change the position of focused box,
541 // startAnimation() should be called.
542 //
543 // If time advances to change the box position, advanceAnimation() should
544 // be called.
545 ////////////////////////////////////////////////////////////////////////////
546 private void redraw() {
547 layoutAndSetPosition();
548 mListener.invalidate();
549 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800550
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800551 private void snapAndRedraw() {
552 mPlatform.startSnapback();
553 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
554 mBoxes.get(i).startSnapback();
555 }
556 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
557 mGaps.get(i).startSnapback();
558 }
559 redraw();
560 }
Chih-Chung Chang534b12f2012-03-21 19:01:30 +0800561
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800562 private void startAnimation(int targetX, int targetY, float targetScale,
563 int kind) {
564 boolean changed = false;
565 changed |= mPlatform.doAnimation(targetX, kind);
566 changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
567 if (changed) redraw();
568 }
569
570 public boolean advanceAnimation() {
571 boolean changed = false;
572 changed |= mPlatform.advanceAnimation();
573 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
574 changed |= mBoxes.get(i).advanceAnimation();
575 }
576 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
577 changed |= mGaps.get(i).advanceAnimation();
578 }
579 if (changed) redraw();
580 return changed;
581 }
582
583 ////////////////////////////////////////////////////////////////////////////
584 // Layout
585 ////////////////////////////////////////////////////////////////////////////
586
587 // Returns the display width of this box.
588 private int widthOf(Box b) {
589 return (int) (b.mImageW * b.mCurrentScale + 0.5f);
590 }
591
592 // Returns the display height of this box.
593 private int heightOf(Box b) {
594 return (int) (b.mImageH * b.mCurrentScale + 0.5f);
595 }
596
597 // Returns the display width of this box, using the given scale.
598 private int widthOf(Box b, float scale) {
599 return (int) (b.mImageW * scale + 0.5f);
600 }
601
602 // Returns the display height of this box, using the given scale.
603 private int heightOf(Box b, float scale) {
604 return (int) (b.mImageH * scale + 0.5f);
605 }
606
607 // Convert the information in mPlatform and mBoxes to mRects, so the user
608 // can get the position of each box by getPosition().
609 //
610 // Note the loop index goes from inside-out because each box's X coordinate
611 // is relative to its anchor box (except the focused box).
612 private void layoutAndSetPosition() {
613 // layout box 0 (focused box)
614 convertBoxToRect(0);
615 for (int i = 1; i <= BOX_MAX; i++) {
616 // layout box i and -i
617 convertBoxToRect(i);
618 convertBoxToRect(-i);
619 }
620 //dumpState();
621 }
622
623 private void dumpState() {
624 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
625 Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
Chih-Chung Changec412542011-09-26 17:34:06 +0800626 }
627
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800628 dumpRect(0);
629 for (int i = 1; i <= BOX_MAX; i++) {
630 dumpRect(i);
631 dumpRect(-i);
Chih-Chung Changec412542011-09-26 17:34:06 +0800632 }
633
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800634 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
635 for (int j = i + 1; j <= BOX_MAX; j++) {
636 if (Rect.intersects(mRects.get(i), mRects.get(j))) {
637 Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
638 }
639 }
640 }
641 }
642
643 private void dumpRect(int i) {
644 StringBuilder sb = new StringBuilder();
645 Rect r = mRects.get(i);
646 sb.append("Rect " + i + ":");
647 sb.append("(");
648 sb.append(r.centerX());
649 sb.append(",");
650 sb.append(r.centerY());
651 sb.append(") [");
652 sb.append(r.width());
653 sb.append("x");
654 sb.append(r.height());
655 sb.append("]");
656 Log.d(TAG, sb.toString());
657 }
658
659 private void convertBoxToRect(int i) {
660 Box b = mBoxes.get(i);
661 Rect r = mRects.get(i);
662 int y = b.mCurrentY;
663 int w = widthOf(b);
664 int h = heightOf(b);
665 if (i == 0) {
666 int x = mPlatform.mCurrentX;
667 r.left = x - w / 2;
668 r.right = r.left + w;
669 } else if (i > 0) {
670 Rect a = mRects.get(i - 1);
671 Gap g = mGaps.get(i - 1);
672 r.left = a.right + g.mCurrentGap;
673 r.right = r.left + w;
674 } else { // i < 0
675 Rect a = mRects.get(i + 1);
676 Gap g = mGaps.get(i);
677 r.right = a.left - g.mCurrentGap;
678 r.left = r.right - w;
679 }
680 r.top = y - h / 2;
681 r.bottom = r.top + h;
682 }
683
684 // Returns the position of a box.
685 public Rect getPosition(int index) {
686 return mRects.get(index);
687 }
688
689 ////////////////////////////////////////////////////////////////////////////
690 // Box management
691 ////////////////////////////////////////////////////////////////////////////
692
693 // Initialize the platform to be at the view center.
694 private void initPlatform() {
695 mPlatform.mCurrentX = mViewW / 2;
696 mPlatform.mAnimationStartTime = NO_ANIMATION;
697 }
698
699 // Initialize a box to have the size of the view.
700 private void initBox(int index) {
701 Box b = mBoxes.get(index);
702 b.mImageW = mViewW;
703 b.mImageH = mViewH;
704 b.mUseViewSize = true;
705 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH);
706 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
707 b.mCurrentY = mViewH / 2;
708 b.mCurrentScale = b.mScaleMin;
709 b.mAnimationStartTime = NO_ANIMATION;
710 }
711
712 // Initialize a gap. This can only be called after the boxes around the gap
713 // has been initialized.
714 private void initGap(int index) {
715 Gap g = mGaps.get(index);
716 g.mDefaultSize = getDefaultGapSize(index);
717 g.mCurrentGap = g.mDefaultSize;
718 g.mAnimationStartTime = NO_ANIMATION;
719 }
720
721 private void initGap(int index, int size) {
722 Gap g = mGaps.get(index);
723 g.mDefaultSize = getDefaultGapSize(index);
724 g.mCurrentGap = size;
725 g.mAnimationStartTime = NO_ANIMATION;
726 }
727
728 private void debugMoveBox(int fromIndex[]) {
729 StringBuilder s = new StringBuilder("moveBox:");
730 for (int i = 0; i < fromIndex.length; i++) {
731 int j = fromIndex[i];
732 if (j == Integer.MAX_VALUE) {
733 s.append(" N");
734 } else {
735 s.append(" ");
736 s.append(fromIndex[i]);
737 }
738 }
739 Log.d(TAG, s.toString());
740 }
741
742 // Move the boxes: it may indicate focus change, box deleted, box appearing,
743 // box reordered, etc.
744 //
745 // Each element in the fromIndex array indicates where each box was in the
746 // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
747 // means the box is new.
748 //
749 // For example:
750 // N N N N N N N -- all new boxes
751 // -3 -2 -1 0 1 2 3 -- nothing changed
752 // -2 -1 0 1 2 3 N -- focus goes to the next box
753 // N-3 -2 -1 0 1 2 -- focuse goes to the previous box
754 // -3 -2 -1 1 2 3 N -- the focused box was deleted.
755 public void moveBox(int fromIndex[]) {
756 //debugMoveBox(fromIndex);
757 RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
758
759 // 1. Get the absolute X coordiates for the boxes.
760 layoutAndSetPosition();
761 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
762 Box b = mBoxes.get(i);
763 Rect r = mRects.get(i);
764 b.mAbsoluteX = r.centerX();
Chih-Chung Changec412542011-09-26 17:34:06 +0800765 }
766
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800767 // 2. copy boxes and gaps to temporary storage.
768 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
769 mTempBoxes.put(i, mBoxes.get(i));
770 mBoxes.put(i, null);
771 }
772 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
773 mTempGaps.put(i, mGaps.get(i));
774 mGaps.put(i, null);
775 }
776
777 // 3. move back boxes that are used in the new array.
778 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
779 int j = from.get(i);
780 if (j == Integer.MAX_VALUE) continue;
781 mBoxes.put(i, mTempBoxes.get(j));
782 mTempBoxes.put(j, null);
783 }
784
785 // 4. move back gaps if both boxes around it are kept together.
786 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
787 int j = from.get(i);
788 if (j == Integer.MAX_VALUE) continue;
789 int k = from.get(i + 1);
790 if (k == Integer.MAX_VALUE) continue;
791 if (j + 1 == k) {
792 mGaps.put(i, mTempGaps.get(j));
793 mTempGaps.put(j, null);
794 }
795 }
796
797 // 5. recycle the boxes that are not used in the new array.
798 int k = -BOX_MAX;
799 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
800 if (mBoxes.get(i) != null) continue;
801 while (mTempBoxes.get(k) == null) {
802 k++;
803 }
804 mBoxes.put(i, mTempBoxes.get(k++));
805 initBox(i);
806 }
807
808 // 6. Now give the recycled box a reasonable absolute X position.
809 //
810 // First try to find the first and the last box which the absolute X
811 // position is known.
812 int first, last;
813 for (first = -BOX_MAX; first <= BOX_MAX; first++) {
814 if (from.get(first) != Integer.MAX_VALUE) break;
815 }
816 for (last = BOX_MAX; last >= -BOX_MAX; last--) {
817 if (from.get(last) != Integer.MAX_VALUE) break;
818 }
819 // If there is no box has known X position at all, make the focused one
820 // as known.
821 if (first > BOX_MAX) {
822 mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
823 first = last = 0;
824 }
825 // Now for those boxes between first and last, just assign the same
826 // position as the previous box. (We can do better, but this should be
827 // rare). For the boxes before first or after last, we will use a new
828 // default gap size below.
829 for (int i = first + 1; i < last; i++) {
830 if (from.get(i) != Integer.MAX_VALUE) continue;
831 mBoxes.get(i).mAbsoluteX = mBoxes.get(i - 1).mAbsoluteX;
832 }
833
834 // 7. recycle the gaps that are not used in the new array.
835 k = -BOX_MAX;
836 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
837 if (mGaps.get(i) != null) continue;
838 while (mTempGaps.get(k) == null) {
839 k++;
840 }
841 mGaps.put(i, mTempGaps.get(k++));
842 Box a = mBoxes.get(i);
843 Box b = mBoxes.get(i + 1);
844 int wa = widthOf(a);
845 int wb = widthOf(b);
846 if (i >= first && i < last) {
847 int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
848 initGap(i, g);
849 } else {
850 initGap(i);
851 }
852 }
853
854 // 8. offset the Platform position
855 int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
856 mPlatform.mCurrentX += dx;
857 mPlatform.mFromX += dx;
858 mPlatform.mToX += dx;
859 mPlatform.mFlingOffset += dx;
860
861 snapAndRedraw();
862 }
863
864 ////////////////////////////////////////////////////////////////////////////
865 // Public utilities
866 ////////////////////////////////////////////////////////////////////////////
867
868 public float getMinimalScale(int imageW, int imageH) {
869 float s = Math.min(mScaleFactor * mViewW / imageW,
870 mScaleFactor * mViewH / imageH);
871 return Math.min(SCALE_LIMIT, s);
872 }
873
874 public float getMaximalScale(int imageW, int imageH) {
875 return mFilmMode ? getMinimalScale(imageW, imageH) : SCALE_LIMIT;
876 }
877
878 public boolean isAtMinimalScale() {
879 Box b = mBoxes.get(0);
880 return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
881 }
882
883 public int getImageWidth() {
884 Box b = mBoxes.get(0);
885 return b.mImageW;
886 }
887
888 public int getImageHeight() {
889 Box b = mBoxes.get(0);
890 return b.mImageH;
891 }
892
893 public float getImageScale() {
894 Box b = mBoxes.get(0);
895 return b.mCurrentScale;
896 }
897
898 public int getImageAtEdges() {
899 Box b = mBoxes.get(0);
900 Platform p = mPlatform;
901 calculateStableBound(b.mCurrentScale);
902 int edges = 0;
903 if (p.mCurrentX <= mBoundLeft) {
904 edges |= IMAGE_AT_RIGHT_EDGE;
905 }
906 if (p.mCurrentX >= mBoundRight) {
907 edges |= IMAGE_AT_LEFT_EDGE;
908 }
909 if (b.mCurrentY <= mBoundTop) {
910 edges |= IMAGE_AT_BOTTOM_EDGE;
911 }
912 if (b.mCurrentY >= mBoundBottom) {
913 edges |= IMAGE_AT_TOP_EDGE;
914 }
915 return edges;
916 }
917
918 ////////////////////////////////////////////////////////////////////////////
919 // Private utilities
920 ////////////////////////////////////////////////////////////////////////////
921
922 private float getMinimalScale(Box b) {
923 return getMinimalScale(b.mImageW, b.mImageH);
924 }
925
926 private float getMaxmimalScale(Box b) {
927 return getMaximalScale(b.mImageW, b.mImageH);
928 }
929
930 private static boolean isAlmostEqual(float a, float b) {
931 float diff = a - b;
932 return (diff < 0 ? -diff : diff) < 0.02f;
Chih-Chung Changec412542011-09-26 17:34:06 +0800933 }
934
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800935 // Calculates the stable region of mCurrent{X/Y}, where "stable" means
936 //
937 // (1) If the dimension of scaled image >= view dimension, we will not
938 // see black region outside the image (at that dimension).
939 // (2) If the dimension of scaled image < view dimension, we will center
940 // the scaled image.
941 //
942 // We might temporarily go out of this stable during user interaction,
943 // but will "snap back" after user stops interaction.
944 //
945 // The results are stored in mBound{Left/Right/Top/Bottom}.
946 //
Chih-Chung Chang8f568da2012-01-05 12:00:53 +0800947 // An extra parameter "horizontalSlack" (which has the value of 0 usually)
948 // is used to extend the stable region by some pixels on each side
949 // horizontally.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800950 private void calculateStableBound(float scale, int horizontalSlack) {
951 Box b = mBoxes.get(0);
Chih-Chung Chang8f568da2012-01-05 12:00:53 +0800952
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800953 // The width and height of the box in number of view pixels
954 int w = widthOf(b, scale);
955 int h = heightOf(b, scale);
956
957 // When the edge of the view is aligned with the edge of the box
958 mBoundLeft = (mViewW - horizontalSlack) - w / 2;
959 mBoundRight = mViewW - mBoundLeft;
960 mBoundTop = mViewH - h / 2;
961 mBoundBottom = mViewH - mBoundTop;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800962
963 // If the scaled height is smaller than the view height,
964 // force it to be in the center.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800965 if (viewTallerThanScaledImage(scale)) {
966 mBoundTop = mBoundBottom = mViewH / 2;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800967 }
968
969 // Same for width
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800970 if (viewWiderThanScaledImage(scale)) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800971 mBoundLeft = mBoundRight = mViewW / 2;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800972 }
973 }
974
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800975 private void calculateStableBound(float scale) {
976 calculateStableBound(scale, 0);
977 }
978
979 private boolean hasNextImages() {
980 for (int i = 1; i <= BOX_MAX; i++) {
981 if (!mBoxes.get(i).mUseViewSize) return true;
982 }
983 return false;
984 }
985
986 private boolean hasPrevImages() {
987 for (int i = -1; i >= -BOX_MAX; i--) {
988 if (!mBoxes.get(i).mUseViewSize) return true;
989 }
990 return false;
991 }
992
993 private boolean viewTallerThanScaledImage(float scale) {
994 return mViewH >= heightOf(mBoxes.get(0), scale);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800995 }
996
997 private boolean viewWiderThanScaledImage(float scale) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800998 return mViewW >= widthOf(mBoxes.get(0), scale);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800999 }
1000
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001001 private float getTargetScale(Box b) {
1002 return useCurrentValueAsTarget(b) ? b.mCurrentScale : b.mToScale;
Chih-Chung Changb3aab902011-10-03 21:11:39 +08001003 }
1004
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001005 private int getTargetX(Platform p) {
1006 return useCurrentValueAsTarget(p) ? p.mCurrentX : p.mToX;
Chih-Chung Changec412542011-09-26 17:34:06 +08001007 }
1008
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001009 private int getTargetY(Box b) {
1010 return useCurrentValueAsTarget(b) ? b.mCurrentY : b.mToY;
Chih-Chung Changec412542011-09-26 17:34:06 +08001011 }
1012
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001013 private boolean useCurrentValueAsTarget(Animatable a) {
1014 return a.mAnimationStartTime == NO_ANIMATION ||
1015 a.mAnimationKind == ANIM_KIND_SNAPBACK ||
1016 a.mAnimationKind == ANIM_KIND_FLING;
Chih-Chung Changec412542011-09-26 17:34:06 +08001017 }
1018
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001019 // Returns the index of the anchor box.
1020 private int anchorIndex(int i) {
1021 if (i > 0) return i - 1;
1022 if (i < 0) return i + 1;
1023 throw new IllegalArgumentException();
1024 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001025
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001026 ////////////////////////////////////////////////////////////////////////////
1027 // Animatable: an thing which can do animation.
1028 ////////////////////////////////////////////////////////////////////////////
1029 private abstract static class Animatable {
1030 public long mAnimationStartTime;
1031 public int mAnimationKind;
1032 public int mAnimationDuration;
Chih-Chung Changec412542011-09-26 17:34:06 +08001033
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001034 // This should be overidden in subclass to change the animation values
1035 // give the progress value in [0, 1].
1036 protected abstract boolean interpolate(float progress);
1037 public abstract boolean startSnapback();
Chih-Chung Changec412542011-09-26 17:34:06 +08001038
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001039 // Returns true if the animation values changes, so things need to be
1040 // redrawn.
1041 public boolean advanceAnimation() {
1042 if (mAnimationStartTime == NO_ANIMATION) {
1043 return false;
1044 }
1045 if (mAnimationStartTime == LAST_ANIMATION) {
1046 mAnimationStartTime = NO_ANIMATION;
1047 return startSnapback();
1048 }
1049
1050 float progress;
1051 if (mAnimationDuration == 0) {
1052 progress = 1;
1053 } else {
1054 long now = AnimationTime.get();
1055 progress =
1056 (float) (now - mAnimationStartTime) / mAnimationDuration;
1057 }
1058
1059 if (progress >= 1) {
1060 progress = 1;
1061 } else {
1062 progress = applyInterpolationCurve(mAnimationKind, progress);
1063 }
1064
1065 boolean done = interpolate(progress);
1066
1067 if (done) {
1068 mAnimationStartTime = LAST_ANIMATION;
1069 }
1070
1071 return true;
Chih-Chung Changec412542011-09-26 17:34:06 +08001072 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001073
1074 private static float applyInterpolationCurve(int kind, float progress) {
1075 float f = 1 - progress;
1076 switch (kind) {
1077 case ANIM_KIND_SCROLL:
1078 case ANIM_KIND_FLING:
1079 progress = 1 - f; // linear
1080 break;
1081 case ANIM_KIND_SCALE:
1082 progress = 1 - f * f; // quadratic
1083 break;
1084 case ANIM_KIND_SNAPBACK:
1085 case ANIM_KIND_ZOOM:
1086 case ANIM_KIND_SLIDE:
1087 case ANIM_KIND_OPENING:
1088 progress = 1 - f * f * f * f * f; // x^5
1089 break;
1090 }
1091 return progress;
1092 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001093 }
1094
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001095 ////////////////////////////////////////////////////////////////////////////
1096 // Platform: captures the global X movement.
1097 ////////////////////////////////////////////////////////////////////////////
1098 private class Platform extends Animatable {
1099 public int mCurrentX, mFromX, mToX;
1100 public int mFlingOffset;
1101
1102 @Override
1103 public boolean startSnapback() {
1104 if (mAnimationStartTime != NO_ANIMATION) return false;
1105 if (mAnimationKind == ANIM_KIND_SCROLL
1106 && mListener.isDown()) return false;
1107
1108 Box b = mBoxes.get(0);
1109 float scaleMin = mExtraScalingRange ?
1110 b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin;
1111 float scaleMax = mExtraScalingRange ?
1112 b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax;
1113 float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax);
1114 int x = mCurrentX;
1115 if (mFilmMode) {
1116 if (!hasNextImages()) x = Math.max(x, mViewW / 2);
1117 if (!hasPrevImages()) x = Math.min(x, mViewW / 2);
1118 } else {
1119 calculateStableBound(scale, HORIZONTAL_SLACK);
1120 x = Utils.clamp(x, mBoundLeft, mBoundRight);
1121 }
1122 if (mCurrentX != x) {
1123 return doAnimation(x, ANIM_KIND_SNAPBACK);
1124 }
1125 return false;
1126 }
1127
1128 // Starts an animation for the platform.
1129 public boolean doAnimation(int targetX, int kind) {
1130 if (mCurrentX == targetX) return false;
1131 mAnimationKind = kind;
1132 mFromX = mCurrentX;
1133 mToX = targetX;
1134 mAnimationStartTime = AnimationTime.startTime();
1135 mAnimationDuration = ANIM_TIME[kind];
1136 mFlingOffset = 0;
1137 advanceAnimation();
1138 return true;
1139 }
1140
1141 @Override
1142 protected boolean interpolate(float progress) {
1143 if (mAnimationKind == ANIM_KIND_FLING) {
1144 return mFilmMode
1145 ? interpolateFlingFilm(progress)
1146 : interpolateFlingPage(progress);
1147 } else {
1148 return interpolateLinear(progress);
1149 }
1150 }
1151
1152 private boolean interpolateFlingFilm(float progress) {
1153 mFilmScroller.computeScrollOffset();
1154 mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
1155
1156 int dir = EdgeView.INVALID_DIRECTION;
1157 if (mCurrentX < mViewW / 2) {
1158 if (!hasNextImages()) {
1159 dir = EdgeView.RIGHT;
1160 }
1161 } else if (mCurrentX > mViewW / 2) {
1162 if (!hasPrevImages()) {
1163 dir = EdgeView.LEFT;
1164 }
1165 }
1166 if (dir != EdgeView.INVALID_DIRECTION) {
1167 int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
1168 mListener.onAbsorb(v, dir);
1169 mFilmScroller.forceFinished(true);
1170 mCurrentX = mViewW / 2;
1171 }
1172 return mFilmScroller.isFinished();
1173 }
1174
1175 private boolean interpolateFlingPage(float progress) {
1176 mPageScroller.computeScrollOffset(progress);
1177 Box b = mBoxes.get(0);
1178 calculateStableBound(b.mCurrentScale);
1179
1180 int oldX = mCurrentX;
1181 mCurrentX = mPageScroller.getCurrX();
1182
1183 // Check if we hit the edges; show edge effects if we do.
1184 if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
1185 int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
1186 mListener.onAbsorb(v, EdgeView.RIGHT);
1187 } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
1188 int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
1189 mListener.onAbsorb(v, EdgeView.LEFT);
1190 }
1191
1192 return progress >= 1;
1193 }
1194
1195 private boolean interpolateLinear(float progress) {
1196 // Other animations
1197 if (progress >= 1) {
1198 mCurrentX = mToX;
1199 return true;
1200 } else {
1201 mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
1202 return (mCurrentX == mToX);
1203 }
1204 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001205 }
1206
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001207 ////////////////////////////////////////////////////////////////////////////
1208 // Box: represents a rectangular area which shows a picture.
1209 ////////////////////////////////////////////////////////////////////////////
1210 private class Box extends Animatable {
1211 // Size of the bitmap
1212 public int mImageW, mImageH;
1213
1214 // This is true if we assume the image size is the same as view size
1215 // until we know the actual size of image. This is also used to
1216 // determine if there is an image ready to show.
1217 public boolean mUseViewSize;
1218
1219 // The minimum and maximum scale we allow for this box.
1220 public float mScaleMin, mScaleMax;
1221
1222 // The X/Y value indicates where the center of the box is on the view
1223 // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
1224 // actual values used currently. Note that the X values are implicitly
1225 // defined by Platform and Gaps.
1226 public int mCurrentY, mFromY, mToY;
1227 public float mCurrentScale, mFromScale, mToScale;
1228
1229 // The absolute X coordinate of the center of the box. This is only used
1230 // during moveBox().
1231 public int mAbsoluteX;
1232
1233 @Override
1234 public boolean startSnapback() {
1235 if (mAnimationStartTime != NO_ANIMATION) return false;
1236 if (mAnimationKind == ANIM_KIND_SCROLL
1237 && mListener.isDown()) return false;
1238 if (mInScale && this == mBoxes.get(0)) return false;
1239
1240 int y;
1241 float scale;
1242
1243 if (this == mBoxes.get(0)) {
1244 float scaleMin = mExtraScalingRange ?
1245 mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
1246 float scaleMax = mExtraScalingRange ?
1247 mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
1248 scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
1249 if (mFilmMode) {
1250 y = mViewH / 2;
1251 } else {
1252 calculateStableBound(scale, HORIZONTAL_SLACK);
1253 y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
1254 }
1255 } else {
1256 y = mViewH / 2;
1257 scale = mScaleMin;
1258 }
1259
1260 if (mCurrentY != y || mCurrentScale != scale) {
1261 return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
1262 }
1263 return false;
1264 }
1265
1266 private boolean doAnimation(int targetY, float targetScale, int kind) {
1267 targetScale = Utils.clamp(targetScale,
1268 SCALE_MIN_EXTRA * mScaleMin,
1269 SCALE_MAX_EXTRA * mScaleMax);
1270
1271 // If the scaled height is smaller than the view height, force it to be
1272 // in the center. (We do this for height only, not width, because the
1273 // user may want to scroll to the previous/next image.)
1274 if (!mInScale && viewTallerThanScaledImage(targetScale)) {
1275 targetY = mViewH / 2;
1276 }
1277
1278 if (mCurrentY == targetY && mCurrentScale == targetScale) {
1279 return false;
1280 }
1281
1282 // Now starts an animation for the box.
1283 mAnimationKind = kind;
1284 mFromY = mCurrentY;
1285 mFromScale = mCurrentScale;
1286 mToY = targetY;
1287 mToScale = targetScale;
1288 mAnimationStartTime = AnimationTime.startTime();
1289 mAnimationDuration = ANIM_TIME[kind];
1290 advanceAnimation();
1291 return true;
1292 }
1293
1294 @Override
1295 protected boolean interpolate(float progress) {
1296 if (mAnimationKind == ANIM_KIND_FLING) {
1297 // Currently a Box can only be flung in page mode.
1298 return interpolateFlingPage(progress);
1299 } else {
1300 return interpolateLinear(progress);
1301 }
1302 }
1303
1304 private boolean interpolateFlingPage(float progress) {
1305 mPageScroller.computeScrollOffset(progress);
1306 calculateStableBound(mCurrentScale);
1307
1308 int oldY = mCurrentY;
1309 mCurrentY = mPageScroller.getCurrY();
1310
1311 // Check if we hit the edges; show edge effects if we do.
1312 if (oldY > mBoundTop && mCurrentY == mBoundTop) {
1313 int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
1314 mListener.onAbsorb(v, EdgeView.BOTTOM);
1315 } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
1316 int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
1317 mListener.onAbsorb(v, EdgeView.TOP);
1318 }
1319
1320 return progress >= 1;
1321 }
1322
1323 private boolean interpolateLinear(float progress) {
1324 if (progress >= 1) {
1325 mCurrentY = mToY;
1326 mCurrentScale = mToScale;
1327 return true;
1328 } else {
1329 mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
1330 mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
1331 return (mCurrentY == mToY && mCurrentScale == mToScale);
1332 }
1333 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001334 }
Chih-Chung Chang532d93c2011-10-12 17:10:33 +08001335
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001336 ////////////////////////////////////////////////////////////////////////////
1337 // Gap: represents a rectangular area which is between two boxes.
1338 ////////////////////////////////////////////////////////////////////////////
1339 private class Gap extends Animatable {
1340 // The default gap size between two boxes. The value may vary for
1341 // different image size of the boxes and for different modes (page or
1342 // film).
1343 public int mDefaultSize;
1344
1345 // The gap size between the two boxes.
1346 public int mCurrentGap, mFromGap, mToGap;
1347
1348 @Override
1349 public boolean startSnapback() {
1350 if (mAnimationStartTime != NO_ANIMATION) return false;
1351 return doAnimation(mDefaultSize);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001352 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001353
1354 // Starts an animation for a gap.
1355 public boolean doAnimation(int targetSize) {
1356 if (mCurrentGap == targetSize) return false;
1357 mAnimationKind = ANIM_KIND_SNAPBACK;
1358 mFromGap = mCurrentGap;
1359 mToGap = targetSize;
1360 mAnimationStartTime = AnimationTime.startTime();
1361 mAnimationDuration = ANIM_TIME[mAnimationKind];
1362 advanceAnimation();
1363 return true;
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001364 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001365
1366 @Override
1367 protected boolean interpolate(float progress) {
1368 if (progress >= 1) {
1369 mCurrentGap = mToGap;
1370 return true;
1371 } else {
1372 mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
1373 return (mCurrentGap == mToGap);
1374 }
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001375 }
Chih-Chung Chang532d93c2011-10-12 17:10:33 +08001376 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001377}