blob: 1c9aba83cbf1042492c73e1e083e59c04f231453 [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;
20import android.graphics.RectF;
21import android.util.FloatMath;
22
Chih-Chung Changec412542011-09-26 17:34:06 +080023import com.android.gallery3d.common.Utils;
Chih-Chung Changec412542011-09-26 17:34:06 +080024import com.android.gallery3d.ui.PositionRepository.Position;
Chih-Chung Chang8f568da2012-01-05 12:00:53 +080025import com.android.gallery3d.util.GalleryUtils;
Chih-Chung Changec412542011-09-26 17:34:06 +080026
Chih-Chung Changec412542011-09-26 17:34:06 +080027class PositionController {
Yuli Huang2ce3c3b2012-02-23 22:26:12 +080028 public static final int IMAGE_AT_LEFT_EDGE = 1;
29 public static final int IMAGE_AT_RIGHT_EDGE = 2;
30 public static final int IMAGE_AT_TOP_EDGE = 4;
31 public static final int IMAGE_AT_BOTTOM_EDGE = 8;
32
Chih-Chung Changec412542011-09-26 17:34:06 +080033 private long mAnimationStartTime = NO_ANIMATION;
34 private static final long NO_ANIMATION = -1;
35 private static final long LAST_ANIMATION = -2;
36
Chih-Chung Changec412542011-09-26 17:34:06 +080037 private int mAnimationKind;
Chih-Chung Changb3aab902011-10-03 21:11:39 +080038 private float mAnimationDuration;
Chih-Chung Changec412542011-09-26 17:34:06 +080039 private final static int ANIM_KIND_SCROLL = 0;
40 private final static int ANIM_KIND_SCALE = 1;
41 private final static int ANIM_KIND_SNAPBACK = 2;
42 private final static int ANIM_KIND_SLIDE = 3;
43 private final static int ANIM_KIND_ZOOM = 4;
Chih-Chung Changb3aab902011-10-03 21:11:39 +080044 private final static int ANIM_KIND_FLING = 5;
Chih-Chung Changec412542011-09-26 17:34:06 +080045
Chih-Chung Chang676170e2011-09-30 18:33:17 +080046 // Animation time in milliseconds. The order must match ANIM_KIND_* above.
47 private final static int ANIM_TIME[] = {
48 0, // ANIM_KIND_SCROLL
49 50, // ANIM_KIND_SCALE
50 600, // ANIM_KIND_SNAPBACK
51 400, // ANIM_KIND_SLIDE
52 300, // ANIM_KIND_ZOOM
Chih-Chung Changb3aab902011-10-03 21:11:39 +080053 0, // ANIM_KIND_FLING (the duration is calculated dynamically)
Chih-Chung Chang676170e2011-09-30 18:33:17 +080054 };
55
Chih-Chung Changec412542011-09-26 17:34:06 +080056 // We try to scale up the image to fill the screen. But in order not to
57 // scale too much for small icons, we limit the max up-scaling factor here.
58 private static final float SCALE_LIMIT = 4;
Chih-Chung Chang8f568da2012-01-05 12:00:53 +080059 private static final int sHorizontalSlack = GalleryUtils.dpToPixel(12);
Chih-Chung Changec412542011-09-26 17:34:06 +080060
61 private PhotoView mViewer;
Chih-Chung Chang532d93c2011-10-12 17:10:33 +080062 private EdgeView mEdgeView;
Chih-Chung Changec412542011-09-26 17:34:06 +080063 private int mImageW, mImageH;
64 private int mViewW, mViewH;
65
66 // The X, Y are the coordinate on bitmap which shows on the center of
Chih-Chung Chang676170e2011-09-30 18:33:17 +080067 // the view. We always keep the mCurrent{X,Y,Scale} sync with the actual
Chih-Chung Changec412542011-09-26 17:34:06 +080068 // values used currently.
69 private int mCurrentX, mFromX, mToX;
70 private int mCurrentY, mFromY, mToY;
71 private float mCurrentScale, mFromScale, mToScale;
72
Chih-Chung Chang676170e2011-09-30 18:33:17 +080073 // The focus point of the scaling gesture (in bitmap coordinates).
Chih-Chung Changb3aab902011-10-03 21:11:39 +080074 private int mFocusBitmapX;
75 private int mFocusBitmapY;
Chih-Chung Changec412542011-09-26 17:34:06 +080076 private boolean mInScale;
Chih-Chung Changec412542011-09-26 17:34:06 +080077
Chih-Chung Chang676170e2011-09-30 18:33:17 +080078 // The minimum and maximum scale we allow.
79 private float mScaleMin, mScaleMax = SCALE_LIMIT;
80
Chih-Chung Changb3aab902011-10-03 21:11:39 +080081 // This is used by the fling animation
82 private FlingScroller mScroller;
83
84 // The bound of the stable region, see the comments above
85 // calculateStableBound() for details.
86 private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
87
Chih-Chung Chang676170e2011-09-30 18:33:17 +080088 // Assume the image size is the same as view size before we know the actual
89 // size of image.
90 private boolean mUseViewSize = true;
Chih-Chung Changec412542011-09-26 17:34:06 +080091
92 private RectF mTempRect = new RectF();
93 private float[] mTempPoints = new float[8];
94
Chih-Chung Chang532d93c2011-10-12 17:10:33 +080095 public PositionController(PhotoView viewer, Context context,
96 EdgeView edgeView) {
Chih-Chung Changec412542011-09-26 17:34:06 +080097 mViewer = viewer;
Chih-Chung Chang532d93c2011-10-12 17:10:33 +080098 mEdgeView = edgeView;
Chih-Chung Changb3aab902011-10-03 21:11:39 +080099 mScroller = new FlingScroller();
Chih-Chung Changec412542011-09-26 17:34:06 +0800100 }
101
102 public void setImageSize(int width, int height) {
103
104 // If no image available, use view size.
105 if (width == 0 || height == 0) {
106 mUseViewSize = true;
107 mImageW = mViewW;
108 mImageH = mViewH;
109 mCurrentX = mImageW / 2;
110 mCurrentY = mImageH / 2;
111 mCurrentScale = 1;
112 mScaleMin = 1;
113 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
114 return;
115 }
116
117 mUseViewSize = false;
118
119 float ratio = Math.min(
120 (float) mImageW / width, (float) mImageH / height);
121
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800122 // See the comment above translate() for details.
Chih-Chung Changec412542011-09-26 17:34:06 +0800123 mCurrentX = translate(mCurrentX, mImageW, width, ratio);
124 mCurrentY = translate(mCurrentY, mImageH, height, ratio);
125 mCurrentScale = mCurrentScale * ratio;
126
127 mFromX = translate(mFromX, mImageW, width, ratio);
128 mFromY = translate(mFromY, mImageH, height, ratio);
129 mFromScale = mFromScale * ratio;
130
131 mToX = translate(mToX, mImageW, width, ratio);
132 mToY = translate(mToY, mImageH, height, ratio);
133 mToScale = mToScale * ratio;
134
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800135 mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio);
136 mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio);
137
Chih-Chung Changec412542011-09-26 17:34:06 +0800138 mImageW = width;
139 mImageH = height;
140
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800141 mScaleMin = getMinimalScale(mImageW, mImageH);
Chih-Chung Changec412542011-09-26 17:34:06 +0800142
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800143 // Start animation from the saved position if we have one.
144 Position position = mViewer.retrieveSavedPosition();
Chih-Chung Changec412542011-09-26 17:34:06 +0800145 if (position != null) {
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800146 // The animation starts from 240 pixels and centers at the image
147 // at the saved position.
Chih-Chung Changec412542011-09-26 17:34:06 +0800148 float scale = 240f / Math.min(width, height);
149 mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2;
150 mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2;
151 mCurrentScale = scale;
152 mViewer.openAnimationStarted();
153 startSnapback();
154 } else if (mAnimationStartTime == NO_ANIMATION) {
155 mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
156 }
157 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
158 }
159
160 public void zoomIn(float tapX, float tapY, float targetScale) {
161 if (targetScale > mScaleMax) targetScale = mScaleMax;
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800162
163 // Convert the tap position to image coordinate
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800164 int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX);
165 int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY);
Chih-Chung Changec412542011-09-26 17:34:06 +0800166
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800167 calculateStableBound(targetScale);
168 int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight);
169 int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom);
Chih-Chung Changec412542011-09-26 17:34:06 +0800170
171 startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
172 }
173
174 public void resetToFullView() {
175 startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM);
176 }
177
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800178 public float getMinimalScale(int w, int h) {
179 return Math.min(SCALE_LIMIT,
180 Math.min((float) mViewW / w, (float) mViewH / h));
Chih-Chung Changec412542011-09-26 17:34:06 +0800181 }
182
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800183 // Translate a coordinate on bitmap if the bitmap size changes.
184 // If the aspect ratio doesn't change, it's easy:
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800185 //
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800186 // r = w / w' (= h / h')
187 // x' = x / r
188 // y' = y / r
189 //
190 // However the aspect ratio may change. That happens when the user slides
191 // a image before it's loaded, we don't know the actual aspect ratio, so
192 // we will assume one. When we receive the actual bitmap size, we need to
193 // translate the coordinate from the old bitmap into the new bitmap.
194 //
195 // What we want to do is center the bitmap at the original position.
196 //
197 // ...+--+...
198 // . | | .
199 // . | | .
200 // ...+--+...
201 //
202 // First we scale down the new bitmap by a factor r = min(w/w', h/h').
203 // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps
204 // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of
205 // the old bitmap maps to (x', y') in the new bitmap, where
206 // x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r
207 // y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800208 private static int translate(int value, int size, int newSize, float ratio) {
209 return Math.round(newSize / 2f + (value - size / 2f) / ratio);
Chih-Chung Changec412542011-09-26 17:34:06 +0800210 }
211
212 public void setViewSize(int viewW, int viewH) {
213 boolean needLayout = mViewW == 0 || mViewH == 0;
214
215 mViewW = viewW;
216 mViewH = viewH;
217
218 if (mUseViewSize) {
219 mImageW = viewW;
220 mImageH = viewH;
221 mCurrentX = mImageW / 2;
222 mCurrentY = mImageH / 2;
223 mCurrentScale = 1;
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800224 mScaleMin = 1;
Chih-Chung Changec412542011-09-26 17:34:06 +0800225 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800226 return;
227 }
228
229 // In most cases we want to keep the scaling factor intact when the
230 // view size changes. The cases we want to reset the scaling factor
231 // (to fit the view if possible) are (1) the scaling factor is too
232 // small for the new view size (2) the scaling factor has not been
233 // changed by the user.
234 boolean wasMinScale = (mCurrentScale == mScaleMin);
235 mScaleMin = getMinimalScale(mImageW, mImageH);
236
237 if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
238 mCurrentX = mImageW / 2;
239 mCurrentY = mImageH / 2;
240 mCurrentScale = mScaleMin;
241 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
Chih-Chung Changec412542011-09-26 17:34:06 +0800242 }
243 }
244
245 public void stopAnimation() {
246 mAnimationStartTime = NO_ANIMATION;
247 }
248
249 public void skipAnimation() {
250 if (mAnimationStartTime == NO_ANIMATION) return;
251 mAnimationStartTime = NO_ANIMATION;
252 mCurrentX = mToX;
253 mCurrentY = mToY;
254 mCurrentScale = mToScale;
255 }
256
Chih-Chung Changec412542011-09-26 17:34:06 +0800257 public void beginScale(float focusX, float focusY) {
258 mInScale = true;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800259 mFocusBitmapX = Math.round(mCurrentX +
260 (focusX - mViewW / 2f) / mCurrentScale);
261 mFocusBitmapY = Math.round(mCurrentY +
262 (focusY - mViewH / 2f) / mCurrentScale);
Chih-Chung Changec412542011-09-26 17:34:06 +0800263 }
264
265 public void scaleBy(float s, float focusX, float focusY) {
266
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800267 // We want to keep the focus point (on the bitmap) the same as when
268 // we begin the scale guesture, that is,
269 //
270 // mCurrentX' + (focusX - mViewW / 2f) / scale = mFocusBitmapX
271 //
272 s *= getTargetScale();
273 int x = Math.round(mFocusBitmapX - (focusX - mViewW / 2f) / s);
274 int y = Math.round(mFocusBitmapY - (focusY - mViewH / 2f) / s);
Chih-Chung Changec412542011-09-26 17:34:06 +0800275
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800276 startAnimation(x, y, s, ANIM_KIND_SCALE);
Chih-Chung Changec412542011-09-26 17:34:06 +0800277 }
278
279 public void endScale() {
280 mInScale = false;
281 startSnapbackIfNeeded();
282 }
283
284 public float getCurrentScale() {
285 return mCurrentScale;
286 }
287
288 public boolean isAtMinimalScale() {
289 return isAlmostEquals(mCurrentScale, mScaleMin);
290 }
291
292 private static boolean isAlmostEquals(float a, float b) {
293 float diff = a - b;
294 return (diff < 0 ? -diff : diff) < 0.02f;
295 }
296
297 public void up() {
298 startSnapback();
299 }
300
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800301 // |<--| (1/2) * mImageW
302 // +-------+-------+-------+
303 // | | | |
304 // | | o | |
305 // | | | |
306 // +-------+-------+-------+
307 // |<----------| (3/2) * mImageW
308 // Slide in the image from left or right.
309 // Precondition: mCurrentScale = 1 (mView{W|H} == mImage{W|H}).
310 // Sliding from left: mCurrentX = (1/2) * mImageW
311 // right: mCurrentX = (3/2) * mImageW
Chih-Chung Changec412542011-09-26 17:34:06 +0800312 public void startSlideInAnimation(int direction) {
313 int fromX = (direction == PhotoView.TRANS_SLIDE_IN_LEFT) ?
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800314 mImageW / 2 : 3 * mImageW / 2;
315 mFromX = Math.round(fromX);
Chih-Chung Changec412542011-09-26 17:34:06 +0800316 mFromY = Math.round(mImageH / 2f);
317 mCurrentX = mFromX;
318 mCurrentY = mFromY;
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800319 startAnimation(
320 mImageW / 2, mImageH / 2, mCurrentScale, ANIM_KIND_SLIDE);
Chih-Chung Changec412542011-09-26 17:34:06 +0800321 }
322
323 public void startHorizontalSlide(int distance) {
324 scrollBy(distance, 0, ANIM_KIND_SLIDE);
325 }
326
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800327 private void scrollBy(float dx, float dy, int type) {
328 startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
329 getTargetY() + Math.round(dy / mCurrentScale),
330 mCurrentScale, type);
331 }
332
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800333 public void startScroll(float dx, float dy, boolean hasNext,
334 boolean hasPrev) {
335 int x = getTargetX() + Math.round(dx / mCurrentScale);
336 int y = getTargetY() + Math.round(dy / mCurrentScale);
337
338 calculateStableBound(mCurrentScale);
339
340 // Vertical direction: If we have space to move in the vertical
341 // direction, we show the edge effect when scrolling reaches the edge.
342 if (mBoundTop != mBoundBottom) {
343 if (y < mBoundTop) {
344 mEdgeView.onPull(mBoundTop - y, EdgeView.TOP);
345 } else if (y > mBoundBottom) {
346 mEdgeView.onPull(y - mBoundBottom, EdgeView.BOTTOM);
347 }
348 }
349
350 y = Utils.clamp(y, mBoundTop, mBoundBottom);
351
352 // Horizontal direction: we show the edge effect when the scrolling
353 // tries to go left of the first image or go right of the last image.
354 if (!hasPrev && x < mBoundLeft) {
355 int pixels = Math.round((mBoundLeft - x) * mCurrentScale);
356 mEdgeView.onPull(pixels, EdgeView.LEFT);
357 x = mBoundLeft;
358 } else if (!hasNext && x > mBoundRight) {
359 int pixels = Math.round((x - mBoundRight) * mCurrentScale);
360 mEdgeView.onPull(pixels, EdgeView.RIGHT);
361 x = mBoundRight;
362 }
363
364 startAnimation(x, y, mCurrentScale, ANIM_KIND_SCROLL);
365 }
366
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800367 public boolean fling(float velocityX, float velocityY) {
368 // We only want to do fling when the picture is zoomed-in.
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800369 if (viewWiderThanScaledImage(mCurrentScale) &&
370 viewHigherThanScaledImage(mCurrentScale)) {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800371 return false;
372 }
373
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800374 // We only allow flinging in the directions where it won't go over the
375 // picture.
376 int edges = getImageAtEdges();
377 if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
378 (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
379 velocityX = 0;
380 }
381 if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
382 (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
383 velocityY = 0;
384 }
385 if (isAlmostEquals(velocityX, 0) && isAlmostEquals(velocityY, 0)) {
386 return false;
387 }
388
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800389 mScroller.fling(mCurrentX, mCurrentY,
390 Math.round(-velocityX / mCurrentScale),
391 Math.round(-velocityY / mCurrentScale),
392 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
393 int targetX = mScroller.getFinalX();
394 int targetY = mScroller.getFinalY();
395 mAnimationDuration = mScroller.getDuration();
396 startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING);
397 return true;
398 }
399
Chih-Chung Changec412542011-09-26 17:34:06 +0800400 private void startAnimation(
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800401 int targetX, int targetY, float scale, int kind) {
Yuli Huang6068de22012-03-01 16:51:08 +0800402 mAnimationKind = kind;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800403 if (targetX == mCurrentX && targetY == mCurrentY
Yuli Huang6068de22012-03-01 16:51:08 +0800404 && scale == mCurrentScale) {
405 onAnimationComplete();
406 return;
407 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800408
409 mFromX = mCurrentX;
410 mFromY = mCurrentY;
411 mFromScale = mCurrentScale;
412
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800413 mToX = targetX;
414 mToY = targetY;
Chih-Chung Changec412542011-09-26 17:34:06 +0800415 mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax);
416
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800417 // If the scaled height is smaller than the view height,
Chih-Chung Changec412542011-09-26 17:34:06 +0800418 // force it to be in the center.
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800419 // (We do for height only, not width, because the user may
420 // want to scroll to the previous/next image.)
Yuli Huang8817fe82012-03-08 19:11:54 +0800421 if (!mInScale && viewHigherThanScaledImage(mToScale)) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800422 mToY = mImageH / 2;
423 }
424
Chih-Chung Changcb058342012-03-02 18:14:53 +0800425 mAnimationStartTime = AnimationTime.get();
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800426 if (mAnimationKind != ANIM_KIND_FLING) {
427 mAnimationDuration = ANIM_TIME[mAnimationKind];
428 }
Yuli Huang6068de22012-03-01 16:51:08 +0800429 advanceAnimation();
Chih-Chung Changec412542011-09-26 17:34:06 +0800430 }
431
Yuli Huang6068de22012-03-01 16:51:08 +0800432 public void advanceAnimation() {
Chih-Chung Changec412542011-09-26 17:34:06 +0800433 if (mAnimationStartTime == NO_ANIMATION) {
Yuli Huang6068de22012-03-01 16:51:08 +0800434 return;
Chih-Chung Changec412542011-09-26 17:34:06 +0800435 } else if (mAnimationStartTime == LAST_ANIMATION) {
Yuli Huang6068de22012-03-01 16:51:08 +0800436 onAnimationComplete();
437 return;
Chih-Chung Changec412542011-09-26 17:34:06 +0800438 }
439
Chih-Chung Changcb058342012-03-02 18:14:53 +0800440 long now = AnimationTime.get();
Chih-Chung Changec412542011-09-26 17:34:06 +0800441 float progress;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800442 if (mAnimationDuration == 0) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800443 progress = 1;
444 } else {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800445 progress = (now - mAnimationStartTime) / mAnimationDuration;
Chih-Chung Changec412542011-09-26 17:34:06 +0800446 }
447
448 if (progress >= 1) {
449 progress = 1;
450 mCurrentX = mToX;
451 mCurrentY = mToY;
452 mCurrentScale = mToScale;
453 mAnimationStartTime = LAST_ANIMATION;
454 } else {
455 float f = 1 - progress;
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800456 switch (mAnimationKind) {
457 case ANIM_KIND_SCROLL:
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800458 case ANIM_KIND_FLING:
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800459 progress = 1 - f; // linear
460 break;
461 case ANIM_KIND_SCALE:
462 progress = 1 - f * f; // quadratic
463 break;
464 case ANIM_KIND_SNAPBACK:
465 case ANIM_KIND_ZOOM:
466 case ANIM_KIND_SLIDE:
467 progress = 1 - f * f * f * f * f; // x^5
468 break;
Chih-Chung Changec412542011-09-26 17:34:06 +0800469 }
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800470 if (mAnimationKind == ANIM_KIND_FLING) {
471 flingInterpolate(progress);
472 } else {
473 linearInterpolate(progress);
474 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800475 }
476 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
Yuli Huang6068de22012-03-01 16:51:08 +0800477 mViewer.invalidate();
478 }
479
480 private void onAnimationComplete() {
481 mAnimationStartTime = NO_ANIMATION;
482 if (mViewer.isInTransition()) {
483 mViewer.notifyTransitionComplete();
484 } else {
485 if (startSnapbackIfNeeded()) mViewer.invalidate();
486 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800487 }
488
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800489 private void flingInterpolate(float progress) {
490 mScroller.computeScrollOffset(progress);
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800491 int oldX = mCurrentX;
492 int oldY = mCurrentY;
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800493 mCurrentX = mScroller.getCurrX();
494 mCurrentY = mScroller.getCurrY();
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800495
496 // Check if we hit the edges; show edge effects if we do.
497 if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
498 int v = Math.round(-mScroller.getCurrVelocityX() * mCurrentScale);
499 mEdgeView.onAbsorb(v, EdgeView.LEFT);
500 } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
501 int v = Math.round(mScroller.getCurrVelocityX() * mCurrentScale);
502 mEdgeView.onAbsorb(v, EdgeView.RIGHT);
503 }
504
505 if (oldY > mBoundTop && mCurrentY == mBoundTop) {
506 int v = Math.round(-mScroller.getCurrVelocityY() * mCurrentScale);
507 mEdgeView.onAbsorb(v, EdgeView.TOP);
508 } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
509 int v = Math.round(mScroller.getCurrVelocityY() * mCurrentScale);
510 mEdgeView.onAbsorb(v, EdgeView.BOTTOM);
511 }
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800512 }
513
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800514 // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1].
Chih-Chung Changec412542011-09-26 17:34:06 +0800515 private void linearInterpolate(float progress) {
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800516 // To linearly interpolate the position on view coordinates, we do the
517 // following steps:
518 // (1) convert a bitmap position (x, y) to view coordinates:
519 // from: (x - mFromX) * mFromScale + mViewW / 2
520 // to: (x - mToX) * mToScale + mViewW / 2
521 // (2) interpolate between the "from" and "to" coordinates:
522 // (x - mFromX) * mFromScale * (1 - p) + (x - mToX) * mToScale * p
523 // + mViewW / 2
524 // should be equal to
525 // (x - mCurrentX) * mCurrentScale + mViewW / 2
526 // (3) The x-related terms in the above equation can be removed because
527 // mFromScale * (1 - p) + ToScale * p = mCurrentScale
528 // (4) Solve for mCurrentX, we have mCurrentX =
529 // (mFromX * mFromScale * (1 - p) + mToX * mToScale * p) / mCurrentScale
530 float fromX = mFromX * mFromScale;
531 float toX = mToX * mToScale;
Chih-Chung Changec412542011-09-26 17:34:06 +0800532 float currentX = fromX + progress * (toX - fromX);
533
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800534 float fromY = mFromY * mFromScale;
535 float toY = mToY * mToScale;
Chih-Chung Changec412542011-09-26 17:34:06 +0800536 float currentY = fromY + progress * (toY - fromY);
537
538 mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
Chih-Chung Chang676170e2011-09-30 18:33:17 +0800539 mCurrentX = Math.round(currentX / mCurrentScale);
540 mCurrentY = Math.round(currentY / mCurrentScale);
Chih-Chung Changec412542011-09-26 17:34:06 +0800541 }
542
543 // Returns true if redraw is needed.
544 private boolean startSnapbackIfNeeded() {
545 if (mAnimationStartTime != NO_ANIMATION) return false;
546 if (mInScale) return false;
547 if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) {
548 return false;
549 }
550 return startSnapback();
551 }
552
Yuli Huang6068de22012-03-01 16:51:08 +0800553 private boolean startSnapback() {
Chih-Chung Changec412542011-09-26 17:34:06 +0800554 boolean needAnimation = false;
Chih-Chung Changec412542011-09-26 17:34:06 +0800555 float scale = mCurrentScale;
556
557 if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) {
558 needAnimation = true;
559 scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
560 }
561
Chih-Chung Chang8f568da2012-01-05 12:00:53 +0800562 calculateStableBound(scale, sHorizontalSlack);
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800563 int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight);
564 int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
Chih-Chung Changec412542011-09-26 17:34:06 +0800565
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800566 if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) {
Chih-Chung Changec412542011-09-26 17:34:06 +0800567 needAnimation = true;
Chih-Chung Changec412542011-09-26 17:34:06 +0800568 }
569
570 if (needAnimation) {
571 startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
572 }
573
574 return needAnimation;
575 }
576
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800577 // Calculates the stable region of mCurrent{X/Y}, where "stable" means
578 //
579 // (1) If the dimension of scaled image >= view dimension, we will not
580 // see black region outside the image (at that dimension).
581 // (2) If the dimension of scaled image < view dimension, we will center
582 // the scaled image.
583 //
584 // We might temporarily go out of this stable during user interaction,
585 // but will "snap back" after user stops interaction.
586 //
587 // The results are stored in mBound{Left/Right/Top/Bottom}.
588 //
Chih-Chung Chang8f568da2012-01-05 12:00:53 +0800589 // An extra parameter "horizontalSlack" (which has the value of 0 usually)
590 // is used to extend the stable region by some pixels on each side
591 // horizontally.
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800592 private void calculateStableBound(float scale) {
Chih-Chung Chang8f568da2012-01-05 12:00:53 +0800593 calculateStableBound(scale, 0f);
594 }
595
596 private void calculateStableBound(float scale, float horizontalSlack) {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800597 // The number of pixels between the center of the view
598 // and the edge when the edge is aligned.
Chih-Chung Changfd914132012-02-11 07:19:47 +0800599 mBoundLeft = (int) FloatMath.ceil((mViewW - horizontalSlack) / (2 * scale));
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800600 mBoundRight = mImageW - mBoundLeft;
Chih-Chung Changfd914132012-02-11 07:19:47 +0800601 mBoundTop = (int) FloatMath.ceil(mViewH / (2 * scale));
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800602 mBoundBottom = mImageH - mBoundTop;
603
604 // If the scaled height is smaller than the view height,
605 // force it to be in the center.
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800606 if (viewHigherThanScaledImage(scale)) {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800607 mBoundTop = mBoundBottom = mImageH / 2;
608 }
609
610 // Same for width
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800611 if (viewWiderThanScaledImage(scale)) {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800612 mBoundLeft = mBoundRight = mImageW / 2;
613 }
614 }
615
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800616 private boolean viewHigherThanScaledImage(float scale) {
617 return FloatMath.floor(mImageH * scale) <= mViewH;
618 }
619
620 private boolean viewWiderThanScaledImage(float scale) {
621 return FloatMath.floor(mImageW * scale) <= mViewW;
622 }
623
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800624 private boolean useCurrentValueAsTarget() {
625 return mAnimationStartTime == NO_ANIMATION ||
626 mAnimationKind == ANIM_KIND_SNAPBACK ||
627 mAnimationKind == ANIM_KIND_FLING;
628 }
629
Chih-Chung Changec412542011-09-26 17:34:06 +0800630 private float getTargetScale() {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800631 return useCurrentValueAsTarget() ? mCurrentScale : mToScale;
Chih-Chung Changec412542011-09-26 17:34:06 +0800632 }
633
634 private int getTargetX() {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800635 return useCurrentValueAsTarget() ? mCurrentX : mToX;
Chih-Chung Changec412542011-09-26 17:34:06 +0800636 }
637
638 private int getTargetY() {
Chih-Chung Changb3aab902011-10-03 21:11:39 +0800639 return useCurrentValueAsTarget() ? mCurrentY : mToY;
Chih-Chung Changec412542011-09-26 17:34:06 +0800640 }
641
642 public RectF getImageBounds() {
643 float points[] = mTempPoints;
644
645 /*
646 * (p0,p1)----------(p2,p3)
647 * | |
648 * | |
649 * (p4,p5)----------(p6,p7)
650 */
651 points[0] = points[4] = -mCurrentX;
652 points[1] = points[3] = -mCurrentY;
653 points[2] = points[6] = mImageW - mCurrentX;
654 points[5] = points[7] = mImageH - mCurrentY;
655
656 RectF rect = mTempRect;
657 rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
658 Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
659
660 float scale = mCurrentScale;
661 float offsetX = mViewW / 2;
662 float offsetY = mViewH / 2;
663 for (int i = 0; i < 4; ++i) {
664 float x = points[i + i] * scale + offsetX;
665 float y = points[i + i + 1] * scale + offsetY;
666 if (x < rect.left) rect.left = x;
667 if (x > rect.right) rect.right = x;
668 if (y < rect.top) rect.top = y;
669 if (y > rect.bottom) rect.bottom = y;
670 }
671 return rect;
672 }
673
674 public int getImageWidth() {
675 return mImageW;
676 }
677
678 public int getImageHeight() {
679 return mImageH;
680 }
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800681
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800682 public int getImageAtEdges() {
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800683 calculateStableBound(mCurrentScale);
Yuli Huang2ce3c3b2012-02-23 22:26:12 +0800684 int edges = 0;
685 if (mCurrentX <= mBoundLeft) {
686 edges |= IMAGE_AT_LEFT_EDGE;
687 }
688 if (mCurrentX >= mBoundRight) {
689 edges |= IMAGE_AT_RIGHT_EDGE;
690 }
691 if (mCurrentY <= mBoundTop) {
692 edges |= IMAGE_AT_TOP_EDGE;
693 }
694 if (mCurrentY >= mBoundBottom) {
695 edges |= IMAGE_AT_BOTTOM_EDGE;
696 }
697 return edges;
Chih-Chung Chang532d93c2011-10-12 17:10:33 +0800698 }
Chih-Chung Changec412542011-09-26 17:34:06 +0800699}