blob: e6c132b855adb0eb296bce0210dadb404225ee74 [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 Chang2c617382012-04-20 20:06:19 +080048 private static final int ANIM_KIND_CAPTURE = 7;
Chih-Chung Changec412542011-09-26 17:34:06 +080049
Chih-Chung Chang676170e2011-09-30 18:33:17 +080050 // Animation time in milliseconds. The order must match ANIM_KIND_* above.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080051 private static final int ANIM_TIME[] = {
Chih-Chung Chang676170e2011-09-30 18:33:17 +080052 0, // ANIM_KIND_SCROLL
53 50, // ANIM_KIND_SCALE
54 600, // ANIM_KIND_SNAPBACK
55 400, // ANIM_KIND_SLIDE
56 300, // ANIM_KIND_ZOOM
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080057 600, // ANIM_KIND_OPENING
Chih-Chung Changb3aab902011-10-03 21:11:39 +080058 0, // ANIM_KIND_FLING (the duration is calculated dynamically)
Chih-Chung Chang2c617382012-04-20 20:06:19 +080059 800, // ANIM_KIND_CAPTURE
Chih-Chung Chang676170e2011-09-30 18:33:17 +080060 };
61
Chih-Chung Changec412542011-09-26 17:34:06 +080062 // We try to scale up the image to fill the screen. But in order not to
63 // scale too much for small icons, we limit the max up-scaling factor here.
64 private static final float SCALE_LIMIT = 4;
65
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080066 // For user's gestures, we give a temporary extra scaling range which goes
67 // above or below the usual scaling limits.
68 private static final float SCALE_MIN_EXTRA = 0.7f;
Chih-Chung Chang534b12f2012-03-21 19:01:30 +080069 private static final float SCALE_MAX_EXTRA = 1.4f;
70
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080071 // Setting this true makes the extra scaling range permanent (until this is
72 // set to false again).
Chih-Chung Chang534b12f2012-03-21 19:01:30 +080073 private boolean mExtraScalingRange = false;
Chih-Chung Chang676170e2011-09-30 18:33:17 +080074
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080075 // Film Mode v.s. Page Mode: in film mode we show smaller pictures.
76 private boolean mFilmMode = false;
Chih-Chung Changb3aab902011-10-03 21:11:39 +080077
Chih-Chung Chang642561d2012-04-16 16:29:13 +080078 // These are the limits for width / height of the picture in film mode.
79 private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f;
80 private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f;
81 private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f;
82 private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080083
84 // In addition to the focused box (index == 0). We also keep information
85 // about this many boxes on each side.
86 private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX;
87
Chih-Chung Changfb1a1552012-04-19 13:34:48 +080088 private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +080089 private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
90
91 private Listener mListener;
92 private volatile Rect mOpenAnimationRect;
93 private int mViewW = 640;
94 private int mViewH = 480;;
95
96 // A scaling guesture is in progress.
97 private boolean mInScale;
98 // The focus point of the scaling gesture, relative to the center of the
99 // picture in bitmap pixels.
100 private float mFocusX, mFocusY;
101
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800102 // whether there is a previous/next picture.
103 private boolean mHasPrev, mHasNext;
104
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800105 // This is used by the fling animation (page mode).
106 private FlingScroller mPageScroller;
107
108 // This is used by the fling animation (film mode).
Chih-Chung Changd8488622012-04-17 12:56:08 +0800109 private OverScroller mFilmScroller;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800110
111 // The bound of the stable region that the focused box can stay, see the
112 // comments above calculateStableBound() for details.
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800113 private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
114
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800115 //
116 // ___________________________________________________________
117 // | _____ _____ _____ _____ _____ |
118 // | | | | | | | | | | | |
119 // | | Box | | Box | | Box*| | Box | | Box | |
120 // | |_____|.....|_____|.....|_____|.....|_____|.....|_____| |
121 // | Gap Gap Gap Gap |
122 // |___________________________________________________________|
123 //
124 // <-- Platform -->
125 //
126 // The focused box (Box*) centers at mPlatform.mCurrentX
Chih-Chung Changec412542011-09-26 17:34:06 +0800127
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800128 private Platform mPlatform = new Platform();
129 private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
130 // The gap at the right of a Box i is at index i. The gap at the left of a
131 // Box i is at index i - 1.
132 private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
Chih-Chung Changec412542011-09-26 17:34:06 +0800133
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800134 // These are only used during moveBox().
135 private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800136 private RangeArray<Gap> mTempGaps =
137 new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800138
139 // The output of the PositionController. Available throught getPosition().
140 private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX);
141
142 public interface Listener {
143 void invalidate();
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800144 boolean isHolding();
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800145
146 // EdgeView
147 void onPull(int offset, int direction);
148 void onRelease();
149 void onAbsorb(int velocity, int direction);
Chih-Chung Changec412542011-09-26 17:34:06 +0800150 }
151
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800152 public PositionController(Context context, Listener listener) {
153 mListener = listener;
154 mPageScroller = new FlingScroller();
Chih-Chung Changd8488622012-04-17 12:56:08 +0800155 mFilmScroller = new OverScroller(context);
Chih-Chung Changec412542011-09-26 17:34:06 +0800156
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800157 // Initialize the areas.
158 initPlatform();
159 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
160 mBoxes.put(i, new Box());
161 initBox(i);
162 mRects.put(i, new Rect());
163 }
164 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
165 mGaps.put(i, new Gap());
166 initGap(i);
167 }
168 }
169
170 public void setOpenAnimationRect(Rect r) {
171 mOpenAnimationRect = r;
172 }
173
174 public void setViewSize(int viewW, int viewH) {
175 if (viewW == mViewW && viewH == mViewH) return;
176
177 mViewW = viewW;
178 mViewH = viewH;
179 initPlatform();
180
181 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
182 setBoxSize(i, viewW, viewH, true);
183 }
184
185 updateScaleAndGapLimit();
186 snapAndRedraw();
187 }
188
189 public void setImageSize(int index, int width, int height) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800190 if (width == 0 || height == 0) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800191 initBox(index);
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800192 } else if (!setBoxSize(index, width, height, false)) {
193 return;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800194 }
195
196 updateScaleAndGapLimit();
197 startOpeningAnimationIfNeeded();
198 snapAndRedraw();
199 }
200
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800201 // Returns false if the box size doesn't change.
202 private boolean setBoxSize(int i, int width, int height, boolean isViewSize) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800203 Box b = mBoxes.get(i);
Chih-Chung Changd8488622012-04-17 12:56:08 +0800204 boolean wasViewSize = b.mUseViewSize;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800205
Chih-Chung Changd8488622012-04-17 12:56:08 +0800206 // If we already have an image size, we don't want to use the view size.
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800207 if (!wasViewSize && isViewSize) return false;
Chih-Chung Changd8488622012-04-17 12:56:08 +0800208
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800209 b.mUseViewSize = isViewSize;
210
211 if (width == b.mImageW && height == b.mImageH) {
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800212 return false;
Chih-Chung Changec412542011-09-26 17:34:06 +0800213 }
214
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800215 // The ratio of the old size and the new size.
Chih-Chung Changec412542011-09-26 17:34:06 +0800216 float ratio = Math.min(
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800217 (float) b.mImageW / width, (float) b.mImageH / height);
Chih-Chung Changec412542011-09-26 17:34:06 +0800218
Chih-Chung Changd8488622012-04-17 12:56:08 +0800219 // If this is the first time we receive an image size, we change the
220 // scale directly. Otherwise adjust the scales by a ratio, and snapback
221 // will animate the scale into the min/max bounds if necessary.
222 if (wasViewSize && !isViewSize) {
223 b.mCurrentScale = getMinimalScale(width, height);
224 b.mAnimationStartTime = NO_ANIMATION;
225 } else {
226 b.mCurrentScale *= ratio;
227 b.mFromScale *= ratio;
228 b.mToScale *= ratio;
229 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800230
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800231 b.mImageW = width;
232 b.mImageH = height;
Chih-Chung Changec412542011-09-26 17:34:06 +0800233
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800234 if (i == 0) {
235 mFocusX /= ratio;
236 mFocusY /= ratio;
Chih-Chung Changec412542011-09-26 17:34:06 +0800237 }
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800238
239 return true;
Chih-Chung Changec412542011-09-26 17:34:06 +0800240 }
241
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800242 private void startOpeningAnimationIfNeeded() {
243 if (mOpenAnimationRect == null) return;
244 Box b = mBoxes.get(0);
245 if (b.mUseViewSize) return;
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800246
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800247 // Start animation from the saved rectangle if we have one.
248 Rect r = mOpenAnimationRect;
249 mOpenAnimationRect = null;
250 mPlatform.mCurrentX = r.centerX();
251 b.mCurrentY = r.centerY();
252 b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
253 r.height() / (float) b.mImageH);
254 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_OPENING);
255 }
256
257 public void setFilmMode(boolean enabled) {
258 if (enabled == mFilmMode) return;
259 mFilmMode = enabled;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800260
261 updateScaleAndGapLimit();
262 stopAnimation();
263 snapAndRedraw();
264 }
265
266 public void setExtraScalingRange(boolean enabled) {
267 if (mExtraScalingRange == enabled) return;
268 mExtraScalingRange = enabled;
269 if (!enabled) {
270 snapAndRedraw();
271 }
272 }
273
274 // This should be called whenever the scale range of boxes or the default
275 // gap size may change. Currently this can happen due to change of view
276 // size, image size, and mode.
277 private void updateScaleAndGapLimit() {
278 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
279 Box b = mBoxes.get(i);
280 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH);
281 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
282 }
283
284 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
285 Gap g = mGaps.get(i);
286 g.mDefaultSize = getDefaultGapSize(i);
287 }
288 }
289
290 // Returns the default gap size according the the size of the boxes around
291 // the gap and the current mode.
292 private int getDefaultGapSize(int i) {
293 if (mFilmMode) return IMAGE_GAP;
294 Box a = mBoxes.get(i);
295 Box b = mBoxes.get(i + 1);
296 return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b));
297 }
298
299 // Here is how we layout the boxes in the page mode.
300 //
301 // previous current next
302 // ___________ ________________ __________
303 // | _______ | | __________ | | ______ |
304 // | | | | | | right->| | | | | |
305 // | | |<-------->|<--left | | | | | |
306 // | |_______| | | | |__________| | | |______| |
307 // |___________| | |________________| |__________|
308 // | <--> gapToSide()
309 // |
310 // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current))
311 private int gapToSide(Box b) {
312 return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f);
313 }
314
315 // Stop all animations at where they are now.
316 public void stopAnimation() {
317 mPlatform.mAnimationStartTime = NO_ANIMATION;
318 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
319 mBoxes.get(i).mAnimationStartTime = NO_ANIMATION;
320 }
321 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
322 mGaps.get(i).mAnimationStartTime = NO_ANIMATION;
323 }
324 }
325
326 public void skipAnimation() {
327 if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
328 mPlatform.mCurrentX = mPlatform.mToX;
329 mPlatform.mAnimationStartTime = NO_ANIMATION;
330 }
331 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
332 Box b = mBoxes.get(i);
333 if (b.mAnimationStartTime == NO_ANIMATION) continue;
334 b.mCurrentY = b.mToY;
335 b.mCurrentScale = b.mToScale;
336 b.mAnimationStartTime = NO_ANIMATION;
337 }
338 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
339 Gap g = mGaps.get(i);
340 if (g.mAnimationStartTime == NO_ANIMATION) continue;
341 g.mCurrentGap = g.mToGap;
342 g.mAnimationStartTime = NO_ANIMATION;
343 }
344 redraw();
345 }
346
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800347 public void snapback() {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800348 snapAndRedraw();
349 }
350
351 ////////////////////////////////////////////////////////////////////////////
352 // Start an animations for the focused box
353 ////////////////////////////////////////////////////////////////////////////
354
355 public void zoomIn(float tapX, float tapY, float targetScale) {
356 Box b = mBoxes.get(0);
357
358 // Convert the tap position to distance to center in bitmap coordinates
359 float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale;
360 float tempY = (tapY - b.mCurrentY) / b.mCurrentScale;
361
362 int x = (int) (mViewW / 2 - tempX * targetScale + 0.5f);
363 int y = (int) (mViewH / 2 - tempY * targetScale + 0.5f);
Chih-Chung Changec412542011-09-26 17:34:06 +0800364
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800365 calculateStableBound(targetScale);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800366 int targetX = Utils.clamp(x, mBoundLeft, mBoundRight);
367 int targetY = Utils.clamp(y, mBoundTop, mBoundBottom);
368 targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax);
Chih-Chung Changec412542011-09-26 17:34:06 +0800369
370 startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
371 }
372
373 public void resetToFullView() {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800374 Box b = mBoxes.get(0);
375 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_ZOOM);
Chih-Chung Changec412542011-09-26 17:34:06 +0800376 }
377
Chih-Chung Changec412542011-09-26 17:34:06 +0800378 public void beginScale(float focusX, float focusY) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800379 Box b = mBoxes.get(0);
380 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800381 mInScale = true;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800382 mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f);
383 mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f);
Chih-Chung Changec412542011-09-26 17:34:06 +0800384 }
385
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800386 // Scales the image by the given factor.
387 // Returns an out-of-range indicator:
388 // 1 if the intended scale is too large for the stable range.
389 // 0 if the intended scale is in the stable range.
390 // -1 if the intended scale is too small for the stable range.
391 public int scaleBy(float s, float focusX, float focusY) {
392 Box b = mBoxes.get(0);
393 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800394
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800395 // We want to keep the focus point (on the bitmap) the same as when we
396 // begin the scale guesture, that is,
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800397 //
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800398 // (focusX' - currentX') / scale' = (focusX - currentX) / scale
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800399 //
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800400 s *= getTargetScale(b);
401 int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f);
402 int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f);
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800403 startAnimation(x, y, s, ANIM_KIND_SCALE);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800404 if (s < b.mScaleMin) return -1;
405 if (s > b.mScaleMax) return 1;
406 return 0;
Chih-Chung Changec412542011-09-26 17:34:06 +0800407 }
408
409 public void endScale() {
410 mInScale = false;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800411 snapAndRedraw();
Chih-Chung Changec412542011-09-26 17:34:06 +0800412 }
413
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800414 // Slide the focused box to the center of the view.
415 public void startHorizontalSlide() {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800416 Box b = mBoxes.get(0);
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800417 startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_SLIDE);
418 }
419
420 // Slide the focused box to the center of the view with the capture
421 // animation. In addition to the sliding, the animation will also scale the
422 // the focused box, the specified neighbor box, and the gap between the
423 // two. The specified offset should be 1 or -1.
424 public void startCaptureAnimationSlide(int offset) {
425 Box b = mBoxes.get(0);
426 Box n = mBoxes.get(offset); // the neighbor box
427 Gap g = mGaps.get(offset); // the gap between the two boxes
428
429 mPlatform.doAnimation(mViewW / 2, ANIM_KIND_CAPTURE);
430 b.doAnimation(mViewH / 2, b.mScaleMin, ANIM_KIND_CAPTURE);
431 n.doAnimation(mViewH / 2, n.mScaleMin, ANIM_KIND_CAPTURE);
432 g.doAnimation(g.mDefaultSize, ANIM_KIND_CAPTURE);
433 redraw();
Chih-Chung Changec412542011-09-26 17:34:06 +0800434 }
435
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800436 public void startScroll(float dx, float dy) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800437 Box b = mBoxes.get(0);
438 Platform p = mPlatform;
439
440 int x = getTargetX(p) + (int) (dx + 0.5f);
441 int y = getTargetY(b) + (int) (dy + 0.5f);
442
443 if (mFilmMode) {
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800444 scrollToFilm(x, y);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800445 } else {
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800446 scrollToPage(x, y);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800447 }
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800448 }
449
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800450 private void scrollToPage(int x, int y) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800451 Box b = mBoxes.get(0);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800452
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800453 calculateStableBound(b.mCurrentScale);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800454
455 // Vertical direction: If we have space to move in the vertical
456 // direction, we show the edge effect when scrolling reaches the edge.
457 if (mBoundTop != mBoundBottom) {
458 if (y < mBoundTop) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800459 mListener.onPull(mBoundTop - y, EdgeView.BOTTOM);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800460 } else if (y > mBoundBottom) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800461 mListener.onPull(y - mBoundBottom, EdgeView.TOP);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800462 }
463 }
464
465 y = Utils.clamp(y, mBoundTop, mBoundBottom);
466
467 // Horizontal direction: we show the edge effect when the scrolling
468 // tries to go left of the first image or go right of the last image.
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800469 if (!mHasPrev && x > mBoundRight) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800470 int pixels = x - mBoundRight;
471 mListener.onPull(pixels, EdgeView.LEFT);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800472 x = mBoundRight;
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800473 } else if (!mHasNext && x < mBoundLeft) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800474 int pixels = mBoundLeft - x;
475 mListener.onPull(pixels, EdgeView.RIGHT);
476 x = mBoundLeft;
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800477 }
478
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800479 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
480 }
481
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800482 private void scrollToFilm(int x, int y) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800483 Box b = mBoxes.get(0);
484
485 // Horizontal direction: we show the edge effect when the scrolling
486 // tries to go left of the first image or go right of the last image.
487 int cx = mViewW / 2;
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800488 if (!mHasPrev && x > cx) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800489 int pixels = x - cx;
490 mListener.onPull(pixels, EdgeView.LEFT);
491 x = cx;
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800492 } else if (!mHasNext && x < cx) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800493 int pixels = cx - x;
494 mListener.onPull(pixels, EdgeView.RIGHT);
495 x = cx;
496 }
497
498 startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800499 }
500
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800501 public boolean fling(float velocityX, float velocityY) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800502 int vx = (int) (velocityX + 0.5f);
503 int vy = (int) (velocityY + 0.5f);
504 return mFilmMode ? flingFilm(vx, vy) : flingPage(vx, vy);
505 }
506
507 private boolean flingPage(int velocityX, int velocityY) {
508 Box b = mBoxes.get(0);
509 Platform p = mPlatform;
510
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800511 // We only want to do fling when the picture is zoomed-in.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800512 if (viewWiderThanScaledImage(b.mCurrentScale) &&
513 viewTallerThanScaledImage(b.mCurrentScale)) {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800514 return false;
515 }
516
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800517 // We only allow flinging in the directions where it won't go over the
518 // picture.
519 int edges = getImageAtEdges();
520 if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
521 (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
522 velocityX = 0;
523 }
524 if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
525 (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
526 velocityY = 0;
527 }
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800528
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800529 if (velocityX == 0 && velocityY == 0) return false;
530
531 mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY,
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800532 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800533 int targetX = mPageScroller.getFinalX();
534 int targetY = mPageScroller.getFinalY();
535 ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
536 startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800537 return true;
538 }
539
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800540 private boolean flingFilm(int velocityX, int velocityY) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800541 Box b = mBoxes.get(0);
542 Platform p = mPlatform;
Chih-Chung Changec412542011-09-26 17:34:06 +0800543
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800544 // If we are already at the edge, don't start the fling.
545 int cx = mViewW / 2;
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800546 if ((!mHasPrev && p.mCurrentX >= cx)
547 || (!mHasNext && p.mCurrentX <= cx)) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800548 return false;
549 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800550
551 if (velocityX == 0) return false;
552
553 mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
554 Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
555 int targetX = mFilmScroller.getFinalX();
Chih-Chung Changc3b2d472012-04-19 20:14:11 +0800556 // This value doesn't matter because we use mFilmScroller.isFinished()
557 // to decide when to stop. We set this to 0 so it's faster for
558 // Animatable.advanceAnimation() to calculate the progress (always 1).
559 ANIM_TIME[ANIM_KIND_FLING] = 0;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800560 startAnimation(targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING);
561 return true;
Chih-Chung Changec412542011-09-26 17:34:06 +0800562 }
563
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800564 ////////////////////////////////////////////////////////////////////////////
565 // Redraw
566 //
567 // If a method changes box positions directly, redraw()
568 // should be called.
569 //
570 // If a method may also cause a snapback to happen, snapAndRedraw() should
571 // be called.
572 //
573 // If a method starts an animation to change the position of focused box,
574 // startAnimation() should be called.
575 //
576 // If time advances to change the box position, advanceAnimation() should
577 // be called.
578 ////////////////////////////////////////////////////////////////////////////
579 private void redraw() {
580 layoutAndSetPosition();
581 mListener.invalidate();
582 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800583
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800584 private void snapAndRedraw() {
585 mPlatform.startSnapback();
586 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
587 mBoxes.get(i).startSnapback();
588 }
589 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
590 mGaps.get(i).startSnapback();
591 }
592 redraw();
593 }
Chih-Chung Chang534b12f2012-03-21 19:01:30 +0800594
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800595 private void startAnimation(int targetX, int targetY, float targetScale,
596 int kind) {
597 boolean changed = false;
598 changed |= mPlatform.doAnimation(targetX, kind);
599 changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
600 if (changed) redraw();
601 }
602
Chih-Chung Changb8be1e02012-04-17 20:35:14 +0800603 public void advanceAnimation() {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800604 boolean changed = false;
605 changed |= mPlatform.advanceAnimation();
606 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
607 changed |= mBoxes.get(i).advanceAnimation();
608 }
609 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
610 changed |= mGaps.get(i).advanceAnimation();
611 }
612 if (changed) redraw();
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800613 }
614
615 ////////////////////////////////////////////////////////////////////////////
616 // Layout
617 ////////////////////////////////////////////////////////////////////////////
618
619 // Returns the display width of this box.
620 private int widthOf(Box b) {
621 return (int) (b.mImageW * b.mCurrentScale + 0.5f);
622 }
623
624 // Returns the display height of this box.
625 private int heightOf(Box b) {
626 return (int) (b.mImageH * b.mCurrentScale + 0.5f);
627 }
628
629 // Returns the display width of this box, using the given scale.
630 private int widthOf(Box b, float scale) {
631 return (int) (b.mImageW * scale + 0.5f);
632 }
633
634 // Returns the display height of this box, using the given scale.
635 private int heightOf(Box b, float scale) {
636 return (int) (b.mImageH * scale + 0.5f);
637 }
638
639 // Convert the information in mPlatform and mBoxes to mRects, so the user
640 // can get the position of each box by getPosition().
641 //
642 // Note the loop index goes from inside-out because each box's X coordinate
643 // is relative to its anchor box (except the focused box).
644 private void layoutAndSetPosition() {
645 // layout box 0 (focused box)
646 convertBoxToRect(0);
647 for (int i = 1; i <= BOX_MAX; i++) {
648 // layout box i and -i
649 convertBoxToRect(i);
650 convertBoxToRect(-i);
651 }
652 //dumpState();
653 }
654
655 private void dumpState() {
656 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
657 Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
Chih-Chung Changec412542011-09-26 17:34:06 +0800658 }
659
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800660 dumpRect(0);
661 for (int i = 1; i <= BOX_MAX; i++) {
662 dumpRect(i);
663 dumpRect(-i);
Chih-Chung Changec412542011-09-26 17:34:06 +0800664 }
665
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800666 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
667 for (int j = i + 1; j <= BOX_MAX; j++) {
668 if (Rect.intersects(mRects.get(i), mRects.get(j))) {
669 Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
670 }
671 }
672 }
673 }
674
675 private void dumpRect(int i) {
676 StringBuilder sb = new StringBuilder();
677 Rect r = mRects.get(i);
678 sb.append("Rect " + i + ":");
679 sb.append("(");
680 sb.append(r.centerX());
681 sb.append(",");
682 sb.append(r.centerY());
683 sb.append(") [");
684 sb.append(r.width());
685 sb.append("x");
686 sb.append(r.height());
687 sb.append("]");
688 Log.d(TAG, sb.toString());
689 }
690
691 private void convertBoxToRect(int i) {
692 Box b = mBoxes.get(i);
693 Rect r = mRects.get(i);
694 int y = b.mCurrentY;
695 int w = widthOf(b);
696 int h = heightOf(b);
697 if (i == 0) {
698 int x = mPlatform.mCurrentX;
699 r.left = x - w / 2;
700 r.right = r.left + w;
701 } else if (i > 0) {
702 Rect a = mRects.get(i - 1);
703 Gap g = mGaps.get(i - 1);
704 r.left = a.right + g.mCurrentGap;
705 r.right = r.left + w;
706 } else { // i < 0
707 Rect a = mRects.get(i + 1);
708 Gap g = mGaps.get(i);
709 r.right = a.left - g.mCurrentGap;
710 r.left = r.right - w;
711 }
712 r.top = y - h / 2;
713 r.bottom = r.top + h;
714 }
715
716 // Returns the position of a box.
717 public Rect getPosition(int index) {
718 return mRects.get(index);
719 }
720
721 ////////////////////////////////////////////////////////////////////////////
722 // Box management
723 ////////////////////////////////////////////////////////////////////////////
724
725 // Initialize the platform to be at the view center.
726 private void initPlatform() {
727 mPlatform.mCurrentX = mViewW / 2;
728 mPlatform.mAnimationStartTime = NO_ANIMATION;
729 }
730
731 // Initialize a box to have the size of the view.
732 private void initBox(int index) {
733 Box b = mBoxes.get(index);
734 b.mImageW = mViewW;
735 b.mImageH = mViewH;
736 b.mUseViewSize = true;
737 b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH);
738 b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
739 b.mCurrentY = mViewH / 2;
740 b.mCurrentScale = b.mScaleMin;
741 b.mAnimationStartTime = NO_ANIMATION;
742 }
743
744 // Initialize a gap. This can only be called after the boxes around the gap
745 // has been initialized.
746 private void initGap(int index) {
747 Gap g = mGaps.get(index);
748 g.mDefaultSize = getDefaultGapSize(index);
749 g.mCurrentGap = g.mDefaultSize;
750 g.mAnimationStartTime = NO_ANIMATION;
751 }
752
753 private void initGap(int index, int size) {
754 Gap g = mGaps.get(index);
755 g.mDefaultSize = getDefaultGapSize(index);
756 g.mCurrentGap = size;
757 g.mAnimationStartTime = NO_ANIMATION;
758 }
759
760 private void debugMoveBox(int fromIndex[]) {
761 StringBuilder s = new StringBuilder("moveBox:");
762 for (int i = 0; i < fromIndex.length; i++) {
763 int j = fromIndex[i];
764 if (j == Integer.MAX_VALUE) {
765 s.append(" N");
766 } else {
767 s.append(" ");
768 s.append(fromIndex[i]);
769 }
770 }
771 Log.d(TAG, s.toString());
772 }
773
774 // Move the boxes: it may indicate focus change, box deleted, box appearing,
775 // box reordered, etc.
776 //
777 // Each element in the fromIndex array indicates where each box was in the
778 // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
779 // means the box is new.
780 //
781 // For example:
782 // N N N N N N N -- all new boxes
783 // -3 -2 -1 0 1 2 3 -- nothing changed
784 // -2 -1 0 1 2 3 N -- focus goes to the next box
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800785 // N -3 -2 -1 0 1 2 -- focuse goes to the previous box
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800786 // -3 -2 -1 1 2 3 N -- the focused box was deleted.
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800787 public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800788 //debugMoveBox(fromIndex);
Chih-Chung Changfb1a1552012-04-19 13:34:48 +0800789 mHasPrev = hasPrev;
790 mHasNext = hasNext;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800791 RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
792
793 // 1. Get the absolute X coordiates for the boxes.
794 layoutAndSetPosition();
795 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
796 Box b = mBoxes.get(i);
797 Rect r = mRects.get(i);
798 b.mAbsoluteX = r.centerX();
Chih-Chung Changec412542011-09-26 17:34:06 +0800799 }
800
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800801 // 2. copy boxes and gaps to temporary storage.
802 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
803 mTempBoxes.put(i, mBoxes.get(i));
804 mBoxes.put(i, null);
805 }
806 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
807 mTempGaps.put(i, mGaps.get(i));
808 mGaps.put(i, null);
809 }
810
811 // 3. move back boxes that are used in the new array.
812 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
813 int j = from.get(i);
814 if (j == Integer.MAX_VALUE) continue;
815 mBoxes.put(i, mTempBoxes.get(j));
816 mTempBoxes.put(j, null);
817 }
818
819 // 4. move back gaps if both boxes around it are kept together.
820 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
821 int j = from.get(i);
822 if (j == Integer.MAX_VALUE) continue;
823 int k = from.get(i + 1);
824 if (k == Integer.MAX_VALUE) continue;
825 if (j + 1 == k) {
826 mGaps.put(i, mTempGaps.get(j));
827 mTempGaps.put(j, null);
828 }
829 }
830
831 // 5. recycle the boxes that are not used in the new array.
832 int k = -BOX_MAX;
833 for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
834 if (mBoxes.get(i) != null) continue;
835 while (mTempBoxes.get(k) == null) {
836 k++;
837 }
838 mBoxes.put(i, mTempBoxes.get(k++));
839 initBox(i);
840 }
841
842 // 6. Now give the recycled box a reasonable absolute X position.
843 //
844 // First try to find the first and the last box which the absolute X
845 // position is known.
846 int first, last;
847 for (first = -BOX_MAX; first <= BOX_MAX; first++) {
848 if (from.get(first) != Integer.MAX_VALUE) break;
849 }
850 for (last = BOX_MAX; last >= -BOX_MAX; last--) {
851 if (from.get(last) != Integer.MAX_VALUE) break;
852 }
853 // If there is no box has known X position at all, make the focused one
854 // as known.
855 if (first > BOX_MAX) {
856 mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
857 first = last = 0;
858 }
859 // Now for those boxes between first and last, just assign the same
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800860 // position as the next box. (We can do better, but this should be
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800861 // rare). For the boxes before first or after last, we will use a new
862 // default gap size below.
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800863 for (int i = last - 1; i > first; i--) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800864 if (from.get(i) != Integer.MAX_VALUE) continue;
Chih-Chung Chang2c617382012-04-20 20:06:19 +0800865 mBoxes.get(i).mAbsoluteX = mBoxes.get(i + 1).mAbsoluteX;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800866 }
867
868 // 7. recycle the gaps that are not used in the new array.
869 k = -BOX_MAX;
870 for (int i = -BOX_MAX; i < BOX_MAX; i++) {
871 if (mGaps.get(i) != null) continue;
872 while (mTempGaps.get(k) == null) {
873 k++;
874 }
875 mGaps.put(i, mTempGaps.get(k++));
876 Box a = mBoxes.get(i);
877 Box b = mBoxes.get(i + 1);
878 int wa = widthOf(a);
879 int wb = widthOf(b);
880 if (i >= first && i < last) {
881 int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
882 initGap(i, g);
883 } else {
884 initGap(i);
885 }
886 }
887
888 // 8. offset the Platform position
889 int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
890 mPlatform.mCurrentX += dx;
891 mPlatform.mFromX += dx;
892 mPlatform.mToX += dx;
893 mPlatform.mFlingOffset += dx;
894
895 snapAndRedraw();
896 }
897
898 ////////////////////////////////////////////////////////////////////////////
899 // Public utilities
900 ////////////////////////////////////////////////////////////////////////////
901
902 public float getMinimalScale(int imageW, int imageH) {
Chih-Chung Chang642561d2012-04-16 16:29:13 +0800903 float wFactor = 1.0f;
904 float hFactor = 1.0f;
905
906 if (mFilmMode) {
907 if (mViewH > mViewW) { // portrait
908 wFactor = FILM_MODE_PORTRAIT_WIDTH;
909 hFactor = FILM_MODE_PORTRAIT_HEIGHT;
910 } else { // landscape
911 wFactor = FILM_MODE_LANDSCAPE_WIDTH;
912 hFactor = FILM_MODE_LANDSCAPE_HEIGHT;
913 }
914 }
915
916 float s = Math.min(wFactor * mViewW / imageW,
917 hFactor * mViewH / imageH);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800918 return Math.min(SCALE_LIMIT, s);
919 }
920
921 public float getMaximalScale(int imageW, int imageH) {
922 return mFilmMode ? getMinimalScale(imageW, imageH) : SCALE_LIMIT;
923 }
924
925 public boolean isAtMinimalScale() {
926 Box b = mBoxes.get(0);
927 return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
928 }
929
930 public int getImageWidth() {
931 Box b = mBoxes.get(0);
932 return b.mImageW;
933 }
934
935 public int getImageHeight() {
936 Box b = mBoxes.get(0);
937 return b.mImageH;
938 }
939
940 public float getImageScale() {
941 Box b = mBoxes.get(0);
942 return b.mCurrentScale;
943 }
944
945 public int getImageAtEdges() {
946 Box b = mBoxes.get(0);
947 Platform p = mPlatform;
948 calculateStableBound(b.mCurrentScale);
949 int edges = 0;
950 if (p.mCurrentX <= mBoundLeft) {
951 edges |= IMAGE_AT_RIGHT_EDGE;
952 }
953 if (p.mCurrentX >= mBoundRight) {
954 edges |= IMAGE_AT_LEFT_EDGE;
955 }
956 if (b.mCurrentY <= mBoundTop) {
957 edges |= IMAGE_AT_BOTTOM_EDGE;
958 }
959 if (b.mCurrentY >= mBoundBottom) {
960 edges |= IMAGE_AT_TOP_EDGE;
961 }
962 return edges;
963 }
964
965 ////////////////////////////////////////////////////////////////////////////
966 // Private utilities
967 ////////////////////////////////////////////////////////////////////////////
968
969 private float getMinimalScale(Box b) {
970 return getMinimalScale(b.mImageW, b.mImageH);
971 }
972
973 private float getMaxmimalScale(Box b) {
974 return getMaximalScale(b.mImageW, b.mImageH);
975 }
976
977 private static boolean isAlmostEqual(float a, float b) {
978 float diff = a - b;
979 return (diff < 0 ? -diff : diff) < 0.02f;
Chih-Chung Changec412542011-09-26 17:34:06 +0800980 }
981
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800982 // Calculates the stable region of mCurrent{X/Y}, where "stable" means
983 //
984 // (1) If the dimension of scaled image >= view dimension, we will not
985 // see black region outside the image (at that dimension).
986 // (2) If the dimension of scaled image < view dimension, we will center
987 // the scaled image.
988 //
989 // We might temporarily go out of this stable during user interaction,
990 // but will "snap back" after user stops interaction.
991 //
992 // The results are stored in mBound{Left/Right/Top/Bottom}.
993 //
Chih-Chung Chang8f568da2012-01-05 12:00:53 +0800994 // An extra parameter "horizontalSlack" (which has the value of 0 usually)
995 // is used to extend the stable region by some pixels on each side
996 // horizontally.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +0800997 private void calculateStableBound(float scale, int horizontalSlack) {
998 Box b = mBoxes.get(0);
Chih-Chung Chang8f568da2012-01-05 12:00:53 +0800999
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001000 // The width and height of the box in number of view pixels
1001 int w = widthOf(b, scale);
1002 int h = heightOf(b, scale);
1003
1004 // When the edge of the view is aligned with the edge of the box
1005 mBoundLeft = (mViewW - horizontalSlack) - w / 2;
1006 mBoundRight = mViewW - mBoundLeft;
1007 mBoundTop = mViewH - h / 2;
1008 mBoundBottom = mViewH - mBoundTop;
Chih-Chung Changb3aab902011-10-03 21:11:39 +08001009
1010 // If the scaled height is smaller than the view height,
1011 // force it to be in the center.
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001012 if (viewTallerThanScaledImage(scale)) {
1013 mBoundTop = mBoundBottom = mViewH / 2;
Chih-Chung Changb3aab902011-10-03 21:11:39 +08001014 }
1015
1016 // Same for width
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001017 if (viewWiderThanScaledImage(scale)) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001018 mBoundLeft = mBoundRight = mViewW / 2;
Chih-Chung Changb3aab902011-10-03 21:11:39 +08001019 }
1020 }
1021
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001022 private void calculateStableBound(float scale) {
1023 calculateStableBound(scale, 0);
1024 }
1025
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001026 private boolean viewTallerThanScaledImage(float scale) {
1027 return mViewH >= heightOf(mBoxes.get(0), scale);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001028 }
1029
1030 private boolean viewWiderThanScaledImage(float scale) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001031 return mViewW >= widthOf(mBoxes.get(0), scale);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001032 }
1033
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001034 private float getTargetScale(Box b) {
1035 return useCurrentValueAsTarget(b) ? b.mCurrentScale : b.mToScale;
Chih-Chung Changb3aab902011-10-03 21:11:39 +08001036 }
1037
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001038 private int getTargetX(Platform p) {
1039 return useCurrentValueAsTarget(p) ? p.mCurrentX : p.mToX;
Chih-Chung Changec412542011-09-26 17:34:06 +08001040 }
1041
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001042 private int getTargetY(Box b) {
1043 return useCurrentValueAsTarget(b) ? b.mCurrentY : b.mToY;
Chih-Chung Changec412542011-09-26 17:34:06 +08001044 }
1045
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001046 private boolean useCurrentValueAsTarget(Animatable a) {
1047 return a.mAnimationStartTime == NO_ANIMATION ||
1048 a.mAnimationKind == ANIM_KIND_SNAPBACK ||
1049 a.mAnimationKind == ANIM_KIND_FLING;
Chih-Chung Changec412542011-09-26 17:34:06 +08001050 }
1051
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001052 // Returns the index of the anchor box.
1053 private int anchorIndex(int i) {
1054 if (i > 0) return i - 1;
1055 if (i < 0) return i + 1;
1056 throw new IllegalArgumentException();
1057 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001058
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001059 ////////////////////////////////////////////////////////////////////////////
1060 // Animatable: an thing which can do animation.
1061 ////////////////////////////////////////////////////////////////////////////
1062 private abstract static class Animatable {
1063 public long mAnimationStartTime;
1064 public int mAnimationKind;
1065 public int mAnimationDuration;
Chih-Chung Changec412542011-09-26 17:34:06 +08001066
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001067 // This should be overidden in subclass to change the animation values
1068 // give the progress value in [0, 1].
1069 protected abstract boolean interpolate(float progress);
1070 public abstract boolean startSnapback();
Chih-Chung Changec412542011-09-26 17:34:06 +08001071
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001072 // Returns true if the animation values changes, so things need to be
1073 // redrawn.
1074 public boolean advanceAnimation() {
1075 if (mAnimationStartTime == NO_ANIMATION) {
1076 return false;
1077 }
1078 if (mAnimationStartTime == LAST_ANIMATION) {
1079 mAnimationStartTime = NO_ANIMATION;
1080 return startSnapback();
1081 }
1082
1083 float progress;
1084 if (mAnimationDuration == 0) {
1085 progress = 1;
1086 } else {
1087 long now = AnimationTime.get();
1088 progress =
1089 (float) (now - mAnimationStartTime) / mAnimationDuration;
1090 }
1091
1092 if (progress >= 1) {
1093 progress = 1;
1094 } else {
1095 progress = applyInterpolationCurve(mAnimationKind, progress);
1096 }
1097
1098 boolean done = interpolate(progress);
1099
1100 if (done) {
1101 mAnimationStartTime = LAST_ANIMATION;
1102 }
1103
1104 return true;
Chih-Chung Changec412542011-09-26 17:34:06 +08001105 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001106
1107 private static float applyInterpolationCurve(int kind, float progress) {
1108 float f = 1 - progress;
1109 switch (kind) {
1110 case ANIM_KIND_SCROLL:
1111 case ANIM_KIND_FLING:
Chih-Chung Chang2c617382012-04-20 20:06:19 +08001112 case ANIM_KIND_CAPTURE:
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001113 progress = 1 - f; // linear
1114 break;
1115 case ANIM_KIND_SCALE:
1116 progress = 1 - f * f; // quadratic
1117 break;
1118 case ANIM_KIND_SNAPBACK:
1119 case ANIM_KIND_ZOOM:
1120 case ANIM_KIND_SLIDE:
1121 case ANIM_KIND_OPENING:
1122 progress = 1 - f * f * f * f * f; // x^5
1123 break;
1124 }
1125 return progress;
1126 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001127 }
1128
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001129 ////////////////////////////////////////////////////////////////////////////
1130 // Platform: captures the global X movement.
1131 ////////////////////////////////////////////////////////////////////////////
1132 private class Platform extends Animatable {
1133 public int mCurrentX, mFromX, mToX;
1134 public int mFlingOffset;
1135
1136 @Override
1137 public boolean startSnapback() {
1138 if (mAnimationStartTime != NO_ANIMATION) return false;
1139 if (mAnimationKind == ANIM_KIND_SCROLL
Chih-Chung Chang2c617382012-04-20 20:06:19 +08001140 && mListener.isHolding()) return false;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001141
1142 Box b = mBoxes.get(0);
1143 float scaleMin = mExtraScalingRange ?
1144 b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin;
1145 float scaleMax = mExtraScalingRange ?
1146 b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax;
1147 float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax);
1148 int x = mCurrentX;
1149 if (mFilmMode) {
Chih-Chung Changc3b2d472012-04-19 20:14:11 +08001150 if (!mHasNext) x = Math.max(x, mViewW / 2);
1151 if (!mHasPrev) x = Math.min(x, mViewW / 2);
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001152 } else {
1153 calculateStableBound(scale, HORIZONTAL_SLACK);
1154 x = Utils.clamp(x, mBoundLeft, mBoundRight);
1155 }
1156 if (mCurrentX != x) {
1157 return doAnimation(x, ANIM_KIND_SNAPBACK);
1158 }
1159 return false;
1160 }
1161
1162 // Starts an animation for the platform.
1163 public boolean doAnimation(int targetX, int kind) {
1164 if (mCurrentX == targetX) return false;
1165 mAnimationKind = kind;
1166 mFromX = mCurrentX;
1167 mToX = targetX;
1168 mAnimationStartTime = AnimationTime.startTime();
1169 mAnimationDuration = ANIM_TIME[kind];
1170 mFlingOffset = 0;
1171 advanceAnimation();
1172 return true;
1173 }
1174
1175 @Override
1176 protected boolean interpolate(float progress) {
1177 if (mAnimationKind == ANIM_KIND_FLING) {
1178 return mFilmMode
1179 ? interpolateFlingFilm(progress)
1180 : interpolateFlingPage(progress);
1181 } else {
1182 return interpolateLinear(progress);
1183 }
1184 }
1185
1186 private boolean interpolateFlingFilm(float progress) {
1187 mFilmScroller.computeScrollOffset();
1188 mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
1189
1190 int dir = EdgeView.INVALID_DIRECTION;
1191 if (mCurrentX < mViewW / 2) {
Chih-Chung Changfb1a1552012-04-19 13:34:48 +08001192 if (!mHasNext) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001193 dir = EdgeView.RIGHT;
1194 }
1195 } else if (mCurrentX > mViewW / 2) {
Chih-Chung Changfb1a1552012-04-19 13:34:48 +08001196 if (!mHasPrev) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001197 dir = EdgeView.LEFT;
1198 }
1199 }
1200 if (dir != EdgeView.INVALID_DIRECTION) {
1201 int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
1202 mListener.onAbsorb(v, dir);
1203 mFilmScroller.forceFinished(true);
1204 mCurrentX = mViewW / 2;
1205 }
1206 return mFilmScroller.isFinished();
1207 }
1208
1209 private boolean interpolateFlingPage(float progress) {
1210 mPageScroller.computeScrollOffset(progress);
1211 Box b = mBoxes.get(0);
1212 calculateStableBound(b.mCurrentScale);
1213
1214 int oldX = mCurrentX;
1215 mCurrentX = mPageScroller.getCurrX();
1216
1217 // Check if we hit the edges; show edge effects if we do.
1218 if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
1219 int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
1220 mListener.onAbsorb(v, EdgeView.RIGHT);
1221 } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
1222 int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
1223 mListener.onAbsorb(v, EdgeView.LEFT);
1224 }
1225
1226 return progress >= 1;
1227 }
1228
1229 private boolean interpolateLinear(float progress) {
1230 // Other animations
1231 if (progress >= 1) {
1232 mCurrentX = mToX;
1233 return true;
1234 } else {
Chih-Chung Chang2c617382012-04-20 20:06:19 +08001235 if (mAnimationKind == ANIM_KIND_CAPTURE) {
1236 progress = CaptureAnimation.calculateSlide(progress);
1237 mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
1238 return false;
1239 } else {
1240 mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
1241 return (mCurrentX == mToX);
1242 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001243 }
1244 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001245 }
1246
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001247 ////////////////////////////////////////////////////////////////////////////
1248 // Box: represents a rectangular area which shows a picture.
1249 ////////////////////////////////////////////////////////////////////////////
1250 private class Box extends Animatable {
1251 // Size of the bitmap
1252 public int mImageW, mImageH;
1253
1254 // This is true if we assume the image size is the same as view size
1255 // until we know the actual size of image. This is also used to
1256 // determine if there is an image ready to show.
1257 public boolean mUseViewSize;
1258
1259 // The minimum and maximum scale we allow for this box.
1260 public float mScaleMin, mScaleMax;
1261
1262 // The X/Y value indicates where the center of the box is on the view
1263 // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
1264 // actual values used currently. Note that the X values are implicitly
1265 // defined by Platform and Gaps.
1266 public int mCurrentY, mFromY, mToY;
1267 public float mCurrentScale, mFromScale, mToScale;
1268
1269 // The absolute X coordinate of the center of the box. This is only used
1270 // during moveBox().
1271 public int mAbsoluteX;
1272
1273 @Override
1274 public boolean startSnapback() {
1275 if (mAnimationStartTime != NO_ANIMATION) return false;
1276 if (mAnimationKind == ANIM_KIND_SCROLL
Chih-Chung Chang2c617382012-04-20 20:06:19 +08001277 && mListener.isHolding()) return false;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001278 if (mInScale && this == mBoxes.get(0)) return false;
1279
1280 int y;
1281 float scale;
1282
1283 if (this == mBoxes.get(0)) {
1284 float scaleMin = mExtraScalingRange ?
1285 mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
1286 float scaleMax = mExtraScalingRange ?
1287 mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
1288 scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
1289 if (mFilmMode) {
1290 y = mViewH / 2;
1291 } else {
1292 calculateStableBound(scale, HORIZONTAL_SLACK);
1293 y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
1294 }
1295 } else {
1296 y = mViewH / 2;
1297 scale = mScaleMin;
1298 }
1299
1300 if (mCurrentY != y || mCurrentScale != scale) {
1301 return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
1302 }
1303 return false;
1304 }
1305
1306 private boolean doAnimation(int targetY, float targetScale, int kind) {
1307 targetScale = Utils.clamp(targetScale,
1308 SCALE_MIN_EXTRA * mScaleMin,
1309 SCALE_MAX_EXTRA * mScaleMax);
1310
1311 // If the scaled height is smaller than the view height, force it to be
1312 // in the center. (We do this for height only, not width, because the
1313 // user may want to scroll to the previous/next image.)
1314 if (!mInScale && viewTallerThanScaledImage(targetScale)) {
1315 targetY = mViewH / 2;
1316 }
1317
Chih-Chung Chang2c617382012-04-20 20:06:19 +08001318 if (mCurrentY == targetY && mCurrentScale == targetScale
1319 && kind != ANIM_KIND_CAPTURE) {
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001320 return false;
1321 }
1322
1323 // Now starts an animation for the box.
1324 mAnimationKind = kind;
1325 mFromY = mCurrentY;
1326 mFromScale = mCurrentScale;
1327 mToY = targetY;
1328 mToScale = targetScale;
1329 mAnimationStartTime = AnimationTime.startTime();
1330 mAnimationDuration = ANIM_TIME[kind];
1331 advanceAnimation();
1332 return true;
1333 }
1334
1335 @Override
1336 protected boolean interpolate(float progress) {
1337 if (mAnimationKind == ANIM_KIND_FLING) {
1338 // Currently a Box can only be flung in page mode.
1339 return interpolateFlingPage(progress);
1340 } else {
1341 return interpolateLinear(progress);
1342 }
1343 }
1344
1345 private boolean interpolateFlingPage(float progress) {
1346 mPageScroller.computeScrollOffset(progress);
1347 calculateStableBound(mCurrentScale);
1348
1349 int oldY = mCurrentY;
1350 mCurrentY = mPageScroller.getCurrY();
1351
1352 // Check if we hit the edges; show edge effects if we do.
1353 if (oldY > mBoundTop && mCurrentY == mBoundTop) {
1354 int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
1355 mListener.onAbsorb(v, EdgeView.BOTTOM);
1356 } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
1357 int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
1358 mListener.onAbsorb(v, EdgeView.TOP);
1359 }
1360
1361 return progress >= 1;
1362 }
1363
1364 private boolean interpolateLinear(float progress) {
1365 if (progress >= 1) {
1366 mCurrentY = mToY;
1367 mCurrentScale = mToScale;
1368 return true;
1369 } else {
1370 mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
1371 mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
Chih-Chung Chang2c617382012-04-20 20:06:19 +08001372 if (mAnimationKind == ANIM_KIND_CAPTURE) {
1373 float f = CaptureAnimation.calculateScale(progress);
1374 mCurrentScale *= f;
1375 return false;
1376 } else {
1377 return (mCurrentY == mToY && mCurrentScale == mToScale);
1378 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001379 }
1380 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001381 }
Chih-Chung Chang532d93c2011-10-12 17:10:33 +08001382
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001383 ////////////////////////////////////////////////////////////////////////////
1384 // Gap: represents a rectangular area which is between two boxes.
1385 ////////////////////////////////////////////////////////////////////////////
1386 private class Gap extends Animatable {
1387 // The default gap size between two boxes. The value may vary for
1388 // different image size of the boxes and for different modes (page or
1389 // film).
1390 public int mDefaultSize;
1391
1392 // The gap size between the two boxes.
1393 public int mCurrentGap, mFromGap, mToGap;
1394
1395 @Override
1396 public boolean startSnapback() {
1397 if (mAnimationStartTime != NO_ANIMATION) return false;
Chih-Chung Chang2c617382012-04-20 20:06:19 +08001398 return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001399 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001400
1401 // Starts an animation for a gap.
Chih-Chung Chang2c617382012-04-20 20:06:19 +08001402 public boolean doAnimation(int targetSize, int kind) {
1403 if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) {
1404 return false;
1405 }
1406 mAnimationKind = kind;
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001407 mFromGap = mCurrentGap;
1408 mToGap = targetSize;
1409 mAnimationStartTime = AnimationTime.startTime();
1410 mAnimationDuration = ANIM_TIME[mAnimationKind];
1411 advanceAnimation();
1412 return true;
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001413 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001414
1415 @Override
1416 protected boolean interpolate(float progress) {
1417 if (progress >= 1) {
1418 mCurrentGap = mToGap;
1419 return true;
1420 } else {
1421 mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
Chih-Chung Chang2c617382012-04-20 20:06:19 +08001422 if (mAnimationKind == ANIM_KIND_CAPTURE) {
1423 float f = CaptureAnimation.calculateScale(progress);
1424 mCurrentGap = (int) (mCurrentGap * f);
1425 return false;
1426 } else {
1427 return (mCurrentGap == mToGap);
1428 }
Chih-Chung Changb7ec5532012-04-03 12:21:16 +08001429 }
Yuli Huang2ce3c3b2012-02-23 22:26:12 +08001430 }
Chih-Chung Chang532d93c2011-10-12 17:10:33 +08001431 }
Chih-Chung Changec412542011-09-26 17:34:06 +08001432}