blob: 625505f49a480b2559fdaf8b43103fa59ac1c7e1 [file] [log] [blame]
Chih-Chung Chang07e6fca2011-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 Lin73a04ff2012-03-14 17:27:24 +080019import android.content.Context;
Yuli Huang54fe02f2012-03-20 16:37:05 +080020import android.graphics.Rect;
Owen Lin73a04ff2012-03-14 17:27:24 +080021import android.graphics.RectF;
22import android.util.FloatMath;
23
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080024import com.android.gallery3d.common.Utils;
Yuli Huang54fe02f2012-03-20 16:37:05 +080025import com.android.gallery3d.data.MediaItem;
Chih-Chung Change9ca81a2012-01-05 12:00:53 +080026import com.android.gallery3d.util.GalleryUtils;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080027
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080028class PositionController {
Yuli Huang75e11d52012-02-23 22:26:12 +080029 public static final int IMAGE_AT_LEFT_EDGE = 1;
30 public static final int IMAGE_AT_RIGHT_EDGE = 2;
31 public static final int IMAGE_AT_TOP_EDGE = 4;
32 public static final int IMAGE_AT_BOTTOM_EDGE = 8;
33
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080034 private long mAnimationStartTime = NO_ANIMATION;
35 private static final long NO_ANIMATION = -1;
36 private static final long LAST_ANIMATION = -2;
37
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080038 private int mAnimationKind;
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +080039 private float mAnimationDuration;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080040 private final static int ANIM_KIND_SCROLL = 0;
41 private final static int ANIM_KIND_SCALE = 1;
42 private final static int ANIM_KIND_SNAPBACK = 2;
43 private final static int ANIM_KIND_SLIDE = 3;
44 private final static int ANIM_KIND_ZOOM = 4;
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +080045 private final static int ANIM_KIND_FLING = 5;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080046
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +080047 // Animation time in milliseconds. The order must match ANIM_KIND_* above.
48 private final static int ANIM_TIME[] = {
49 0, // ANIM_KIND_SCROLL
50 50, // ANIM_KIND_SCALE
51 600, // ANIM_KIND_SNAPBACK
52 400, // ANIM_KIND_SLIDE
53 300, // ANIM_KIND_ZOOM
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +080054 0, // ANIM_KIND_FLING (the duration is calculated dynamically)
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +080055 };
56
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080057 // We try to scale up the image to fill the screen. But in order not to
58 // scale too much for small icons, we limit the max up-scaling factor here.
59 private static final float SCALE_LIMIT = 4;
Chih-Chung Change9ca81a2012-01-05 12:00:53 +080060 private static final int sHorizontalSlack = GalleryUtils.dpToPixel(12);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080061
Chih-Chung Chang95860d22012-03-21 19:01:30 +080062 private static final float SCALE_MIN_EXTRA = 0.6f;
63 private static final float SCALE_MAX_EXTRA = 1.4f;
64
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080065 private PhotoView mViewer;
Chih-Chung Changbe074852011-10-12 17:10:33 +080066 private EdgeView mEdgeView;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080067 private int mImageW, mImageH;
68 private int mViewW, mViewH;
69
70 // The X, Y are the coordinate on bitmap which shows on the center of
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +080071 // the view. We always keep the mCurrent{X,Y,Scale} sync with the actual
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080072 // values used currently.
73 private int mCurrentX, mFromX, mToX;
74 private int mCurrentY, mFromY, mToY;
75 private float mCurrentScale, mFromScale, mToScale;
76
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +080077 // The focus point of the scaling gesture (in bitmap coordinates).
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +080078 private int mFocusBitmapX;
79 private int mFocusBitmapY;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080080 private boolean mInScale;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080081
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +080082 // The minimum and maximum scale we allow.
83 private float mScaleMin, mScaleMax = SCALE_LIMIT;
Chih-Chung Chang95860d22012-03-21 19:01:30 +080084 private boolean mExtraScalingRange = false;
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +080085
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +080086 // This is used by the fling animation
87 private FlingScroller mScroller;
88
89 // The bound of the stable region, see the comments above
90 // calculateStableBound() for details.
91 private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
92
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +080093 // Assume the image size is the same as view size before we know the actual
94 // size of image.
95 private boolean mUseViewSize = true;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +080096
97 private RectF mTempRect = new RectF();
98 private float[] mTempPoints = new float[8];
99
Chih-Chung Changbe074852011-10-12 17:10:33 +0800100 public PositionController(PhotoView viewer, Context context,
101 EdgeView edgeView) {
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800102 mViewer = viewer;
Chih-Chung Changbe074852011-10-12 17:10:33 +0800103 mEdgeView = edgeView;
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800104 mScroller = new FlingScroller();
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800105 }
106
107 public void setImageSize(int width, int height) {
108
109 // If no image available, use view size.
110 if (width == 0 || height == 0) {
111 mUseViewSize = true;
112 mImageW = mViewW;
113 mImageH = mViewH;
114 mCurrentX = mImageW / 2;
115 mCurrentY = mImageH / 2;
116 mCurrentScale = 1;
117 mScaleMin = 1;
118 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
119 return;
120 }
121
122 mUseViewSize = false;
123
124 float ratio = Math.min(
125 (float) mImageW / width, (float) mImageH / height);
126
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800127 // See the comment above translate() for details.
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800128 mCurrentX = translate(mCurrentX, mImageW, width, ratio);
129 mCurrentY = translate(mCurrentY, mImageH, height, ratio);
130 mCurrentScale = mCurrentScale * ratio;
131
132 mFromX = translate(mFromX, mImageW, width, ratio);
133 mFromY = translate(mFromY, mImageH, height, ratio);
134 mFromScale = mFromScale * ratio;
135
136 mToX = translate(mToX, mImageW, width, ratio);
137 mToY = translate(mToY, mImageH, height, ratio);
138 mToScale = mToScale * ratio;
139
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800140 mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio);
141 mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio);
142
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800143 mImageW = width;
144 mImageH = height;
145
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800146 mScaleMin = getMinimalScale(mImageW, mImageH);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800147
Yuli Huang54fe02f2012-03-20 16:37:05 +0800148 // Start animation from the saved rectangle if we have one.
149 Rect r = mViewer.retrieveOpenAnimationRect();
150 if (r != null) {
151 // The animation starts from the specified rectangle; the image
152 // should be scaled and centered as the thumbnail shown in the
153 // rectangle to minimize janky opening animation. Note: The below
154 // implementation depends on how thumbnails are drawn and placed.
155 float size = MediaItem.getTargetSize(
156 MediaItem.TYPE_MICROTHUMBNAIL);
157 float scale = (size / Math.min(width, height)) * Math.min(
158 r.width() / size, r.height() / size);
159
160 mCurrentX = Math.round((mViewW / 2f - r.centerX()) / scale) + mImageW / 2;
161 mCurrentY = Math.round((mViewH / 2f - r.centerY()) / scale) + mImageH / 2;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800162 mCurrentScale = scale;
163 mViewer.openAnimationStarted();
164 startSnapback();
165 } else if (mAnimationStartTime == NO_ANIMATION) {
166 mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
167 }
168 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
169 }
170
171 public void zoomIn(float tapX, float tapY, float targetScale) {
172 if (targetScale > mScaleMax) targetScale = mScaleMax;
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800173
174 // Convert the tap position to image coordinate
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800175 int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX);
176 int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800177
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800178 calculateStableBound(targetScale);
179 int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight);
180 int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800181
182 startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
183 }
184
185 public void resetToFullView() {
186 startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM);
187 }
188
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800189 public float getMinimalScale(int w, int h) {
190 return Math.min(SCALE_LIMIT,
191 Math.min((float) mViewW / w, (float) mViewH / h));
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800192 }
193
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800194 // Translate a coordinate on bitmap if the bitmap size changes.
195 // If the aspect ratio doesn't change, it's easy:
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800196 //
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800197 // r = w / w' (= h / h')
198 // x' = x / r
199 // y' = y / r
200 //
201 // However the aspect ratio may change. That happens when the user slides
202 // a image before it's loaded, we don't know the actual aspect ratio, so
203 // we will assume one. When we receive the actual bitmap size, we need to
204 // translate the coordinate from the old bitmap into the new bitmap.
205 //
206 // What we want to do is center the bitmap at the original position.
207 //
208 // ...+--+...
209 // . | | .
210 // . | | .
211 // ...+--+...
212 //
213 // First we scale down the new bitmap by a factor r = min(w/w', h/h').
214 // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps
215 // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of
216 // the old bitmap maps to (x', y') in the new bitmap, where
217 // x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r
218 // y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800219 private static int translate(int value, int size, int newSize, float ratio) {
220 return Math.round(newSize / 2f + (value - size / 2f) / ratio);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800221 }
222
223 public void setViewSize(int viewW, int viewH) {
224 boolean needLayout = mViewW == 0 || mViewH == 0;
225
226 mViewW = viewW;
227 mViewH = viewH;
228
229 if (mUseViewSize) {
230 mImageW = viewW;
231 mImageH = viewH;
232 mCurrentX = mImageW / 2;
233 mCurrentY = mImageH / 2;
234 mCurrentScale = 1;
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800235 mScaleMin = 1;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800236 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800237 return;
238 }
239
240 // In most cases we want to keep the scaling factor intact when the
241 // view size changes. The cases we want to reset the scaling factor
242 // (to fit the view if possible) are (1) the scaling factor is too
243 // small for the new view size (2) the scaling factor has not been
244 // changed by the user.
245 boolean wasMinScale = (mCurrentScale == mScaleMin);
246 mScaleMin = getMinimalScale(mImageW, mImageH);
247
248 if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
249 mCurrentX = mImageW / 2;
250 mCurrentY = mImageH / 2;
251 mCurrentScale = mScaleMin;
252 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800253 }
254 }
255
256 public void stopAnimation() {
257 mAnimationStartTime = NO_ANIMATION;
258 }
259
260 public void skipAnimation() {
261 if (mAnimationStartTime == NO_ANIMATION) return;
262 mAnimationStartTime = NO_ANIMATION;
263 mCurrentX = mToX;
264 mCurrentY = mToY;
265 mCurrentScale = mToScale;
266 }
267
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800268 public void beginScale(float focusX, float focusY) {
269 mInScale = true;
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800270 mFocusBitmapX = Math.round(mCurrentX +
271 (focusX - mViewW / 2f) / mCurrentScale);
272 mFocusBitmapY = Math.round(mCurrentY +
273 (focusY - mViewH / 2f) / mCurrentScale);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800274 }
275
Chih-Chung Chang95860d22012-03-21 19:01:30 +0800276 // Returns true if the result scale is outside the stable range.
277 public boolean scaleBy(float s, float focusX, float focusY) {
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800278
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800279 // We want to keep the focus point (on the bitmap) the same as when
280 // we begin the scale guesture, that is,
281 //
282 // mCurrentX' + (focusX - mViewW / 2f) / scale = mFocusBitmapX
283 //
284 s *= getTargetScale();
285 int x = Math.round(mFocusBitmapX - (focusX - mViewW / 2f) / s);
286 int y = Math.round(mFocusBitmapY - (focusY - mViewH / 2f) / s);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800287
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800288 startAnimation(x, y, s, ANIM_KIND_SCALE);
Chih-Chung Chang95860d22012-03-21 19:01:30 +0800289 return (s < mScaleMin || s > mScaleMax);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800290 }
291
292 public void endScale() {
293 mInScale = false;
294 startSnapbackIfNeeded();
295 }
296
Chih-Chung Chang95860d22012-03-21 19:01:30 +0800297 public void setExtraScalingRange(boolean enabled) {
298 mExtraScalingRange = enabled;
299 if (!enabled) {
300 startSnapbackIfNeeded();
301 }
302 }
303
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800304 public float getCurrentScale() {
305 return mCurrentScale;
306 }
307
308 public boolean isAtMinimalScale() {
309 return isAlmostEquals(mCurrentScale, mScaleMin);
310 }
311
312 private static boolean isAlmostEquals(float a, float b) {
313 float diff = a - b;
314 return (diff < 0 ? -diff : diff) < 0.02f;
315 }
316
317 public void up() {
318 startSnapback();
319 }
320
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800321 // |<--| (1/2) * mImageW
322 // +-------+-------+-------+
323 // | | | |
324 // | | o | |
325 // | | | |
326 // +-------+-------+-------+
327 // |<----------| (3/2) * mImageW
328 // Slide in the image from left or right.
329 // Precondition: mCurrentScale = 1 (mView{W|H} == mImage{W|H}).
330 // Sliding from left: mCurrentX = (1/2) * mImageW
331 // right: mCurrentX = (3/2) * mImageW
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800332 public void startSlideInAnimation(int direction) {
333 int fromX = (direction == PhotoView.TRANS_SLIDE_IN_LEFT) ?
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800334 mImageW / 2 : 3 * mImageW / 2;
335 mFromX = Math.round(fromX);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800336 mFromY = Math.round(mImageH / 2f);
337 mCurrentX = mFromX;
338 mCurrentY = mFromY;
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800339 startAnimation(
340 mImageW / 2, mImageH / 2, mCurrentScale, ANIM_KIND_SLIDE);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800341 }
342
343 public void startHorizontalSlide(int distance) {
344 scrollBy(distance, 0, ANIM_KIND_SLIDE);
345 }
346
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800347 private void scrollBy(float dx, float dy, int type) {
348 startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
349 getTargetY() + Math.round(dy / mCurrentScale),
350 mCurrentScale, type);
351 }
352
Chih-Chung Changbe074852011-10-12 17:10:33 +0800353 public void startScroll(float dx, float dy, boolean hasNext,
354 boolean hasPrev) {
355 int x = getTargetX() + Math.round(dx / mCurrentScale);
356 int y = getTargetY() + Math.round(dy / mCurrentScale);
357
358 calculateStableBound(mCurrentScale);
359
360 // Vertical direction: If we have space to move in the vertical
361 // direction, we show the edge effect when scrolling reaches the edge.
362 if (mBoundTop != mBoundBottom) {
363 if (y < mBoundTop) {
364 mEdgeView.onPull(mBoundTop - y, EdgeView.TOP);
365 } else if (y > mBoundBottom) {
366 mEdgeView.onPull(y - mBoundBottom, EdgeView.BOTTOM);
367 }
368 }
369
370 y = Utils.clamp(y, mBoundTop, mBoundBottom);
371
372 // Horizontal direction: we show the edge effect when the scrolling
373 // tries to go left of the first image or go right of the last image.
374 if (!hasPrev && x < mBoundLeft) {
375 int pixels = Math.round((mBoundLeft - x) * mCurrentScale);
376 mEdgeView.onPull(pixels, EdgeView.LEFT);
377 x = mBoundLeft;
378 } else if (!hasNext && x > mBoundRight) {
379 int pixels = Math.round((x - mBoundRight) * mCurrentScale);
380 mEdgeView.onPull(pixels, EdgeView.RIGHT);
381 x = mBoundRight;
382 }
383
384 startAnimation(x, y, mCurrentScale, ANIM_KIND_SCROLL);
385 }
386
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800387 public boolean fling(float velocityX, float velocityY) {
388 // We only want to do fling when the picture is zoomed-in.
Yuli Huang75e11d52012-02-23 22:26:12 +0800389 if (viewWiderThanScaledImage(mCurrentScale) &&
390 viewHigherThanScaledImage(mCurrentScale)) {
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800391 return false;
392 }
393
Yuli Huang75e11d52012-02-23 22:26:12 +0800394 // We only allow flinging in the directions where it won't go over the
395 // picture.
396 int edges = getImageAtEdges();
397 if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
398 (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
399 velocityX = 0;
400 }
401 if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
402 (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
403 velocityY = 0;
404 }
405 if (isAlmostEquals(velocityX, 0) && isAlmostEquals(velocityY, 0)) {
406 return false;
407 }
408
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800409 mScroller.fling(mCurrentX, mCurrentY,
410 Math.round(-velocityX / mCurrentScale),
411 Math.round(-velocityY / mCurrentScale),
412 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
413 int targetX = mScroller.getFinalX();
414 int targetY = mScroller.getFinalY();
415 mAnimationDuration = mScroller.getDuration();
416 startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING);
417 return true;
418 }
419
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800420 private void startAnimation(
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800421 int targetX, int targetY, float scale, int kind) {
Yuli Huang3976dea2012-03-01 16:51:08 +0800422 mAnimationKind = kind;
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800423 if (targetX == mCurrentX && targetY == mCurrentY
Yuli Huang3976dea2012-03-01 16:51:08 +0800424 && scale == mCurrentScale) {
425 onAnimationComplete();
426 return;
427 }
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800428
429 mFromX = mCurrentX;
430 mFromY = mCurrentY;
431 mFromScale = mCurrentScale;
432
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800433 mToX = targetX;
434 mToY = targetY;
Chih-Chung Chang95860d22012-03-21 19:01:30 +0800435 mToScale = Utils.clamp(scale, SCALE_MIN_EXTRA * mScaleMin,
436 SCALE_MAX_EXTRA * mScaleMax);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800437
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800438 // If the scaled height is smaller than the view height,
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800439 // force it to be in the center.
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800440 // (We do for height only, not width, because the user may
441 // want to scroll to the previous/next image.)
Yuli Huang67201752012-03-08 19:11:54 +0800442 if (!mInScale && viewHigherThanScaledImage(mToScale)) {
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800443 mToY = mImageH / 2;
444 }
445
Chih-Chung Chang66960432012-03-02 18:14:53 +0800446 mAnimationStartTime = AnimationTime.get();
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800447 if (mAnimationKind != ANIM_KIND_FLING) {
448 mAnimationDuration = ANIM_TIME[mAnimationKind];
449 }
Yuli Huang3976dea2012-03-01 16:51:08 +0800450 advanceAnimation();
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800451 }
452
Yuli Huang3976dea2012-03-01 16:51:08 +0800453 public void advanceAnimation() {
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800454 if (mAnimationStartTime == NO_ANIMATION) {
Yuli Huang3976dea2012-03-01 16:51:08 +0800455 return;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800456 } else if (mAnimationStartTime == LAST_ANIMATION) {
Yuli Huang3976dea2012-03-01 16:51:08 +0800457 onAnimationComplete();
458 return;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800459 }
460
Chih-Chung Chang66960432012-03-02 18:14:53 +0800461 long now = AnimationTime.get();
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800462 float progress;
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800463 if (mAnimationDuration == 0) {
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800464 progress = 1;
465 } else {
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800466 progress = (now - mAnimationStartTime) / mAnimationDuration;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800467 }
468
469 if (progress >= 1) {
470 progress = 1;
471 mCurrentX = mToX;
472 mCurrentY = mToY;
473 mCurrentScale = mToScale;
474 mAnimationStartTime = LAST_ANIMATION;
475 } else {
476 float f = 1 - progress;
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800477 switch (mAnimationKind) {
478 case ANIM_KIND_SCROLL:
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800479 case ANIM_KIND_FLING:
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800480 progress = 1 - f; // linear
481 break;
482 case ANIM_KIND_SCALE:
483 progress = 1 - f * f; // quadratic
484 break;
485 case ANIM_KIND_SNAPBACK:
486 case ANIM_KIND_ZOOM:
487 case ANIM_KIND_SLIDE:
488 progress = 1 - f * f * f * f * f; // x^5
489 break;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800490 }
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800491 if (mAnimationKind == ANIM_KIND_FLING) {
492 flingInterpolate(progress);
493 } else {
494 linearInterpolate(progress);
495 }
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800496 }
497 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
Yuli Huang3976dea2012-03-01 16:51:08 +0800498 mViewer.invalidate();
499 }
500
501 private void onAnimationComplete() {
502 mAnimationStartTime = NO_ANIMATION;
503 if (mViewer.isInTransition()) {
504 mViewer.notifyTransitionComplete();
505 } else {
506 if (startSnapbackIfNeeded()) mViewer.invalidate();
507 }
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800508 }
509
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800510 private void flingInterpolate(float progress) {
511 mScroller.computeScrollOffset(progress);
Chih-Chung Changbe074852011-10-12 17:10:33 +0800512 int oldX = mCurrentX;
513 int oldY = mCurrentY;
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800514 mCurrentX = mScroller.getCurrX();
515 mCurrentY = mScroller.getCurrY();
Chih-Chung Changbe074852011-10-12 17:10:33 +0800516
517 // Check if we hit the edges; show edge effects if we do.
518 if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
519 int v = Math.round(-mScroller.getCurrVelocityX() * mCurrentScale);
520 mEdgeView.onAbsorb(v, EdgeView.LEFT);
521 } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
522 int v = Math.round(mScroller.getCurrVelocityX() * mCurrentScale);
523 mEdgeView.onAbsorb(v, EdgeView.RIGHT);
524 }
525
526 if (oldY > mBoundTop && mCurrentY == mBoundTop) {
527 int v = Math.round(-mScroller.getCurrVelocityY() * mCurrentScale);
528 mEdgeView.onAbsorb(v, EdgeView.TOP);
529 } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
530 int v = Math.round(mScroller.getCurrVelocityY() * mCurrentScale);
531 mEdgeView.onAbsorb(v, EdgeView.BOTTOM);
532 }
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800533 }
534
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800535 // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1].
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800536 private void linearInterpolate(float progress) {
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800537 // To linearly interpolate the position on view coordinates, we do the
538 // following steps:
539 // (1) convert a bitmap position (x, y) to view coordinates:
540 // from: (x - mFromX) * mFromScale + mViewW / 2
541 // to: (x - mToX) * mToScale + mViewW / 2
542 // (2) interpolate between the "from" and "to" coordinates:
543 // (x - mFromX) * mFromScale * (1 - p) + (x - mToX) * mToScale * p
544 // + mViewW / 2
545 // should be equal to
546 // (x - mCurrentX) * mCurrentScale + mViewW / 2
547 // (3) The x-related terms in the above equation can be removed because
548 // mFromScale * (1 - p) + ToScale * p = mCurrentScale
549 // (4) Solve for mCurrentX, we have mCurrentX =
550 // (mFromX * mFromScale * (1 - p) + mToX * mToScale * p) / mCurrentScale
551 float fromX = mFromX * mFromScale;
552 float toX = mToX * mToScale;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800553 float currentX = fromX + progress * (toX - fromX);
554
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800555 float fromY = mFromY * mFromScale;
556 float toY = mToY * mToScale;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800557 float currentY = fromY + progress * (toY - fromY);
558
559 mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800560 mCurrentX = Math.round(currentX / mCurrentScale);
561 mCurrentY = Math.round(currentY / mCurrentScale);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800562 }
563
564 // Returns true if redraw is needed.
565 private boolean startSnapbackIfNeeded() {
566 if (mAnimationStartTime != NO_ANIMATION) return false;
567 if (mInScale) return false;
568 if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) {
569 return false;
570 }
571 return startSnapback();
572 }
573
Yuli Huang3976dea2012-03-01 16:51:08 +0800574 private boolean startSnapback() {
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800575 boolean needAnimation = false;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800576 float scale = mCurrentScale;
577
Chih-Chung Chang95860d22012-03-21 19:01:30 +0800578 float scaleMin = mExtraScalingRange ?
579 mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
580 float scaleMax = mExtraScalingRange ?
581 mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
582
583 if (mCurrentScale < scaleMin || mCurrentScale > scaleMax) {
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800584 needAnimation = true;
Chih-Chung Chang95860d22012-03-21 19:01:30 +0800585 scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800586 }
587
Chih-Chung Change9ca81a2012-01-05 12:00:53 +0800588 calculateStableBound(scale, sHorizontalSlack);
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800589 int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight);
590 int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800591
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800592 if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) {
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800593 needAnimation = true;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800594 }
595
596 if (needAnimation) {
597 startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
598 }
599
600 return needAnimation;
601 }
602
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800603 // Calculates the stable region of mCurrent{X/Y}, where "stable" means
604 //
605 // (1) If the dimension of scaled image >= view dimension, we will not
606 // see black region outside the image (at that dimension).
607 // (2) If the dimension of scaled image < view dimension, we will center
608 // the scaled image.
609 //
610 // We might temporarily go out of this stable during user interaction,
611 // but will "snap back" after user stops interaction.
612 //
613 // The results are stored in mBound{Left/Right/Top/Bottom}.
614 //
Chih-Chung Change9ca81a2012-01-05 12:00:53 +0800615 // An extra parameter "horizontalSlack" (which has the value of 0 usually)
616 // is used to extend the stable region by some pixels on each side
617 // horizontally.
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800618 private void calculateStableBound(float scale) {
Chih-Chung Change9ca81a2012-01-05 12:00:53 +0800619 calculateStableBound(scale, 0f);
620 }
621
622 private void calculateStableBound(float scale, float horizontalSlack) {
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800623 // The number of pixels between the center of the view
624 // and the edge when the edge is aligned.
Chih-Chung Chang4e051902012-02-11 07:19:47 +0800625 mBoundLeft = (int) FloatMath.ceil((mViewW - horizontalSlack) / (2 * scale));
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800626 mBoundRight = mImageW - mBoundLeft;
Chih-Chung Chang4e051902012-02-11 07:19:47 +0800627 mBoundTop = (int) FloatMath.ceil(mViewH / (2 * scale));
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800628 mBoundBottom = mImageH - mBoundTop;
629
630 // If the scaled height is smaller than the view height,
631 // force it to be in the center.
Yuli Huang75e11d52012-02-23 22:26:12 +0800632 if (viewHigherThanScaledImage(scale)) {
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800633 mBoundTop = mBoundBottom = mImageH / 2;
634 }
635
636 // Same for width
Yuli Huang75e11d52012-02-23 22:26:12 +0800637 if (viewWiderThanScaledImage(scale)) {
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800638 mBoundLeft = mBoundRight = mImageW / 2;
639 }
640 }
641
Yuli Huang75e11d52012-02-23 22:26:12 +0800642 private boolean viewHigherThanScaledImage(float scale) {
643 return FloatMath.floor(mImageH * scale) <= mViewH;
644 }
645
646 private boolean viewWiderThanScaledImage(float scale) {
647 return FloatMath.floor(mImageW * scale) <= mViewW;
648 }
649
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800650 private boolean useCurrentValueAsTarget() {
651 return mAnimationStartTime == NO_ANIMATION ||
652 mAnimationKind == ANIM_KIND_SNAPBACK ||
653 mAnimationKind == ANIM_KIND_FLING;
654 }
655
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800656 private float getTargetScale() {
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800657 return useCurrentValueAsTarget() ? mCurrentScale : mToScale;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800658 }
659
660 private int getTargetX() {
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800661 return useCurrentValueAsTarget() ? mCurrentX : mToX;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800662 }
663
664 private int getTargetY() {
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800665 return useCurrentValueAsTarget() ? mCurrentY : mToY;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800666 }
667
668 public RectF getImageBounds() {
669 float points[] = mTempPoints;
670
671 /*
672 * (p0,p1)----------(p2,p3)
673 * | |
674 * | |
675 * (p4,p5)----------(p6,p7)
676 */
677 points[0] = points[4] = -mCurrentX;
678 points[1] = points[3] = -mCurrentY;
679 points[2] = points[6] = mImageW - mCurrentX;
680 points[5] = points[7] = mImageH - mCurrentY;
681
682 RectF rect = mTempRect;
683 rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
684 Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
685
686 float scale = mCurrentScale;
687 float offsetX = mViewW / 2;
688 float offsetY = mViewH / 2;
689 for (int i = 0; i < 4; ++i) {
690 float x = points[i + i] * scale + offsetX;
691 float y = points[i + i + 1] * scale + offsetY;
692 if (x < rect.left) rect.left = x;
693 if (x > rect.right) rect.right = x;
694 if (y < rect.top) rect.top = y;
695 if (y > rect.bottom) rect.bottom = y;
696 }
697 return rect;
698 }
699
700 public int getImageWidth() {
701 return mImageW;
702 }
703
704 public int getImageHeight() {
705 return mImageH;
706 }
Chih-Chung Changbe074852011-10-12 17:10:33 +0800707
Yuli Huang75e11d52012-02-23 22:26:12 +0800708 public int getImageAtEdges() {
Chih-Chung Changbe074852011-10-12 17:10:33 +0800709 calculateStableBound(mCurrentScale);
Yuli Huang75e11d52012-02-23 22:26:12 +0800710 int edges = 0;
711 if (mCurrentX <= mBoundLeft) {
712 edges |= IMAGE_AT_LEFT_EDGE;
713 }
714 if (mCurrentX >= mBoundRight) {
715 edges |= IMAGE_AT_RIGHT_EDGE;
716 }
717 if (mCurrentY <= mBoundTop) {
718 edges |= IMAGE_AT_TOP_EDGE;
719 }
720 if (mCurrentY >= mBoundBottom) {
721 edges |= IMAGE_AT_BOTTOM_EDGE;
722 }
723 return edges;
Chih-Chung Changbe074852011-10-12 17:10:33 +0800724 }
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800725}