blob: 8d5563e9432892b5dc31e3b1b24769e0184c1b3a [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
19import com.android.gallery3d.R;
20import com.android.gallery3d.app.GalleryActivity;
21import com.android.gallery3d.common.Utils;
22import com.android.gallery3d.data.Path;
23import com.android.gallery3d.ui.PositionRepository.Position;
24
25import android.content.Context;
26import android.graphics.Bitmap;
27import android.graphics.Color;
28import android.graphics.RectF;
29import android.os.Message;
30import android.os.SystemClock;
31import android.view.GestureDetector;
32import android.view.MotionEvent;
33import android.view.ScaleGestureDetector;
34
35class PositionController {
36 private long mAnimationStartTime = NO_ANIMATION;
37 private static final long NO_ANIMATION = -1;
38 private static final long LAST_ANIMATION = -2;
39
40 // Animation time in milliseconds.
41 private static final float ANIM_TIME_SCROLL = 0;
42 private static final float ANIM_TIME_SCALE = 50;
43 private static final float ANIM_TIME_SNAPBACK = 600;
44 private static final float ANIM_TIME_SLIDE = 400;
45 private static final float ANIM_TIME_ZOOM = 300;
46
47 private int mAnimationKind;
48 private final static int ANIM_KIND_SCROLL = 0;
49 private final static int ANIM_KIND_SCALE = 1;
50 private final static int ANIM_KIND_SNAPBACK = 2;
51 private final static int ANIM_KIND_SLIDE = 3;
52 private final static int ANIM_KIND_ZOOM = 4;
53
54 // We try to scale up the image to fill the screen. But in order not to
55 // scale too much for small icons, we limit the max up-scaling factor here.
56 private static final float SCALE_LIMIT = 4;
57
58 private PhotoView mViewer;
59 private int mImageW, mImageH;
60 private int mViewW, mViewH;
61
62 // The X, Y are the coordinate on bitmap which shows on the center of
63 // the view. We always keep the mCurrent{X,Y,SCALE} sync with the actual
64 // values used currently.
65 private int mCurrentX, mFromX, mToX;
66 private int mCurrentY, mFromY, mToY;
67 private float mCurrentScale, mFromScale, mToScale;
68
69 // The offsets from the center of the view to the user's focus point,
70 // converted to the bitmap domain.
71 private float mPrevOffsetX;
72 private float mPrevOffsetY;
73 private boolean mInScale;
74 private boolean mUseViewSize = true;
75
76 // The limits for position and scale.
77 private float mScaleMin, mScaleMax = 4f;
78
79 private RectF mTempRect = new RectF();
80 private float[] mTempPoints = new float[8];
81
82 PositionController(PhotoView viewer) {
83 mViewer = viewer;
84 }
85
86 public void setImageSize(int width, int height) {
87
88 // If no image available, use view size.
89 if (width == 0 || height == 0) {
90 mUseViewSize = true;
91 mImageW = mViewW;
92 mImageH = mViewH;
93 mCurrentX = mImageW / 2;
94 mCurrentY = mImageH / 2;
95 mCurrentScale = 1;
96 mScaleMin = 1;
97 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
98 return;
99 }
100
101 mUseViewSize = false;
102
103 float ratio = Math.min(
104 (float) mImageW / width, (float) mImageH / height);
105
106 mCurrentX = translate(mCurrentX, mImageW, width, ratio);
107 mCurrentY = translate(mCurrentY, mImageH, height, ratio);
108 mCurrentScale = mCurrentScale * ratio;
109
110 mFromX = translate(mFromX, mImageW, width, ratio);
111 mFromY = translate(mFromY, mImageH, height, ratio);
112 mFromScale = mFromScale * ratio;
113
114 mToX = translate(mToX, mImageW, width, ratio);
115 mToY = translate(mToY, mImageH, height, ratio);
116 mToScale = mToScale * ratio;
117
118 mImageW = width;
119 mImageH = height;
120
121 mScaleMin = getMinimalScale(width, height, 0);
122
123 // Scale the new image to fit into the old one
124 Position position = mViewer.retrieveOldPosition();
125 if (position != null) {
126 float scale = 240f / Math.min(width, height);
127 mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2;
128 mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2;
129 mCurrentScale = scale;
130 mViewer.openAnimationStarted();
131 startSnapback();
132 } else if (mAnimationStartTime == NO_ANIMATION) {
133 mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
134 }
135 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
136 }
137
138 public void zoomIn(float tapX, float tapY, float targetScale) {
139 if (targetScale > mScaleMax) targetScale = mScaleMax;
140 float scale = mCurrentScale;
141 float tempX = (tapX - mViewW / 2) / mCurrentScale + mCurrentX;
142 float tempY = (tapY - mViewH / 2) / mCurrentScale + mCurrentY;
143
144 // mCurrentX + (mViewW / 2) * (1 / targetScale) < mImageW
145 // mCurrentX - (mViewW / 2) * (1 / targetScale) > 0
146 float min = mViewW / 2.0f / targetScale;
147 float max = mImageW - mViewW / 2.0f / targetScale;
148 int targetX = (int) Utils.clamp(tempX, min, max);
149
150 min = mViewH / 2.0f / targetScale;
151 max = mImageH - mViewH / 2.0f / targetScale;
152 int targetY = (int) Utils.clamp(tempY, min, max);
153
154 // If the width of the image is less then the view, center the image
155 if (mImageW * targetScale < mViewW) targetX = mImageW / 2;
156 if (mImageH * targetScale < mViewH) targetY = mImageH / 2;
157
158 startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
159 }
160
161 public void resetToFullView() {
162 startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM);
163 }
164
165 public float getMinimalScale(int w, int h, int rotation) {
166 return Math.min(SCALE_LIMIT, ((rotation / 90) & 0x01) == 0
167 ? Math.min((float) mViewW / w, (float) mViewH / h)
168 : Math.min((float) mViewW / h, (float) mViewH / w));
169 }
170
171 private static int translate(int value, int size, int updateSize, float ratio) {
172 return Math.round(
173 (value + (updateSize * ratio - size) / 2f) / ratio);
174 }
175
176 public void setViewSize(int viewW, int viewH) {
177 boolean needLayout = mViewW == 0 || mViewH == 0;
178
179 mViewW = viewW;
180 mViewH = viewH;
181
182 if (mUseViewSize) {
183 mImageW = viewW;
184 mImageH = viewH;
185 mCurrentX = mImageW / 2;
186 mCurrentY = mImageH / 2;
187 mCurrentScale = 1;
188 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
189 } else {
190 boolean wasMinScale = (mCurrentScale == mScaleMin);
191 mScaleMin = Math.min(SCALE_LIMIT, Math.min(
192 (float) viewW / mImageW, (float) viewH / mImageH));
193 if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
194 mCurrentX = mImageW / 2;
195 mCurrentY = mImageH / 2;
196 mCurrentScale = mScaleMin;
197 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
198 }
199 }
200 }
201
202 public void stopAnimation() {
203 mAnimationStartTime = NO_ANIMATION;
204 }
205
206 public void skipAnimation() {
207 if (mAnimationStartTime == NO_ANIMATION) return;
208 mAnimationStartTime = NO_ANIMATION;
209 mCurrentX = mToX;
210 mCurrentY = mToY;
211 mCurrentScale = mToScale;
212 }
213
214 public void scrollBy(float dx, float dy, int type) {
215 startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
216 getTargetY() + Math.round(dy / mCurrentScale),
217 mCurrentScale, type);
218 }
219
220 public void beginScale(float focusX, float focusY) {
221 mInScale = true;
222 mPrevOffsetX = (focusX - mViewW / 2f) / mCurrentScale;
223 mPrevOffsetY = (focusY - mViewH / 2f) / mCurrentScale;
224 }
225
226 public void scaleBy(float s, float focusX, float focusY) {
227
228 // The focus point should keep this position on the ImageView.
229 // So, mCurrentX + mPrevOffsetX = mCurrentX' + offsetX.
230 // mCurrentY + mPrevOffsetY = mCurrentY' + offsetY.
231 float offsetX = (focusX - mViewW / 2f) / mCurrentScale;
232 float offsetY = (focusY - mViewH / 2f) / mCurrentScale;
233
234 startAnimation(getTargetX() - Math.round(offsetX - mPrevOffsetX),
235 getTargetY() - Math.round(offsetY - mPrevOffsetY),
236 getTargetScale() * s, ANIM_KIND_SCALE);
237 mPrevOffsetX = offsetX;
238 mPrevOffsetY = offsetY;
239 }
240
241 public void endScale() {
242 mInScale = false;
243 startSnapbackIfNeeded();
244 }
245
246 public float getCurrentScale() {
247 return mCurrentScale;
248 }
249
250 public boolean isAtMinimalScale() {
251 return isAlmostEquals(mCurrentScale, mScaleMin);
252 }
253
254 private static boolean isAlmostEquals(float a, float b) {
255 float diff = a - b;
256 return (diff < 0 ? -diff : diff) < 0.02f;
257 }
258
259 public void up() {
260 startSnapback();
261 }
262
263 public void startSlideInAnimation(int direction) {
264 int fromX = (direction == PhotoView.TRANS_SLIDE_IN_LEFT) ?
265 mViewW : -mViewW;
266 mFromX = Math.round(fromX + (mImageW - mViewW) / 2f);
267 mFromY = Math.round(mImageH / 2f);
268 mCurrentX = mFromX;
269 mCurrentY = mFromY;
270 startAnimation(mImageW / 2, mImageH / 2, mCurrentScale,
271 ANIM_KIND_SLIDE);
272 }
273
274 public void startHorizontalSlide(int distance) {
275 scrollBy(distance, 0, ANIM_KIND_SLIDE);
276 }
277
278 public void startScroll(float dx, float dy) {
279 scrollBy(dx, dy, ANIM_KIND_SCROLL);
280 }
281
282 private void startAnimation(
283 int centerX, int centerY, float scale, int kind) {
284 if (centerX == mCurrentX && centerY == mCurrentY
285 && scale == mCurrentScale) return;
286
287 mFromX = mCurrentX;
288 mFromY = mCurrentY;
289 mFromScale = mCurrentScale;
290
291 mToX = centerX;
292 mToY = centerY;
293 mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax);
294
295 // If the scaled dimension is smaller than the view,
296 // force it to be in the center.
297 if (Math.floor(mImageH * mToScale) <= mViewH) {
298 mToY = mImageH / 2;
299 }
300
301 mAnimationStartTime = SystemClock.uptimeMillis();
302 mAnimationKind = kind;
303 if (advanceAnimation()) mViewer.invalidate();
304 }
305
306 // Returns true if redraw is needed.
307 public boolean advanceAnimation() {
308 if (mAnimationStartTime == NO_ANIMATION) {
309 return false;
310 } else if (mAnimationStartTime == LAST_ANIMATION) {
311 mAnimationStartTime = NO_ANIMATION;
312 if (mViewer.isInTransition()) {
313 mViewer.notifyTransitionComplete();
314 return false;
315 } else {
316 return startSnapbackIfNeeded();
317 }
318 }
319
320 float animationTime;
321 if (mAnimationKind == ANIM_KIND_SCROLL) {
322 animationTime = ANIM_TIME_SCROLL;
323 } else if (mAnimationKind == ANIM_KIND_SCALE) {
324 animationTime = ANIM_TIME_SCALE;
325 } else if (mAnimationKind == ANIM_KIND_SLIDE) {
326 animationTime = ANIM_TIME_SLIDE;
327 } else if (mAnimationKind == ANIM_KIND_ZOOM) {
328 animationTime = ANIM_TIME_ZOOM;
329 } else /* if (mAnimationKind == ANIM_KIND_SNAPBACK) */ {
330 animationTime = ANIM_TIME_SNAPBACK;
331 }
332
333 float progress;
334 if (animationTime == 0) {
335 progress = 1;
336 } else {
337 long now = SystemClock.uptimeMillis();
338 progress = (now - mAnimationStartTime) / animationTime;
339 }
340
341 if (progress >= 1) {
342 progress = 1;
343 mCurrentX = mToX;
344 mCurrentY = mToY;
345 mCurrentScale = mToScale;
346 mAnimationStartTime = LAST_ANIMATION;
347 } else {
348 float f = 1 - progress;
349 if (mAnimationKind == ANIM_KIND_SCROLL) {
350 progress = 1 - f; // linear
351 } else if (mAnimationKind == ANIM_KIND_SCALE) {
352 progress = 1 - f * f; // quadratic
353 } else /* if mAnimationKind is ANIM_KIND_SNAPBACK,
354 ANIM_KIND_ZOOM or ANIM_KIND_SLIDE */ {
355 progress = 1 - f * f * f * f * f; // x^5
356 }
357 linearInterpolate(progress);
358 }
359 mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
360 return true;
361 }
362
363 private void linearInterpolate(float progress) {
364 // To linearly interpolate the position, we have to translate the
365 // coordinates. The meaning of the translated point (x, y) is the
366 // coordinates of the center of the bitmap on the view component.
367 float fromX = mViewW / 2f + (mImageW / 2f - mFromX) * mFromScale;
368 float toX = mViewW / 2f + (mImageW / 2f - mToX) * mToScale;
369 float currentX = fromX + progress * (toX - fromX);
370
371 float fromY = mViewH / 2f + (mImageH / 2f - mFromY) * mFromScale;
372 float toY = mViewH / 2f + (mImageH / 2f - mToY) * mToScale;
373 float currentY = fromY + progress * (toY - fromY);
374
375 mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
376 mCurrentX = Math.round(
377 mImageW / 2f + (mViewW / 2f - currentX) / mCurrentScale);
378 mCurrentY = Math.round(
379 mImageH / 2f + (mViewH / 2f - currentY) / mCurrentScale);
380 }
381
382 // Returns true if redraw is needed.
383 private boolean startSnapbackIfNeeded() {
384 if (mAnimationStartTime != NO_ANIMATION) return false;
385 if (mInScale) return false;
386 if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) {
387 return false;
388 }
389 return startSnapback();
390 }
391
392 public boolean startSnapback() {
393 boolean needAnimation = false;
394 int x = mCurrentX;
395 int y = mCurrentY;
396 float scale = mCurrentScale;
397
398 if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) {
399 needAnimation = true;
400 scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
401 }
402
403 // The number of pixels when the edge is aligned.
404 int left = (int) Math.ceil(mViewW / (2 * scale));
405 int right = mImageW - left;
406 int top = (int) Math.ceil(mViewH / (2 * scale));
407 int bottom = mImageH - top;
408
409 if (mImageW * scale > mViewW) {
410 if (mCurrentX < left) {
411 needAnimation = true;
412 x = left;
413 } else if (mCurrentX > right) {
414 needAnimation = true;
415 x = right;
416 }
417 } else if (mCurrentX != mImageW / 2) {
418 needAnimation = true;
419 x = mImageW / 2;
420 }
421
422 if (mImageH * scale > mViewH) {
423 if (mCurrentY < top) {
424 needAnimation = true;
425 y = top;
426 } else if (mCurrentY > bottom) {
427 needAnimation = true;
428 y = bottom;
429 }
430 } else if (mCurrentY != mImageH / 2) {
431 needAnimation = true;
432 y = mImageH / 2;
433 }
434
435 if (needAnimation) {
436 startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
437 }
438
439 return needAnimation;
440 }
441
442 private float getTargetScale() {
443 if (mAnimationStartTime == NO_ANIMATION
444 || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentScale;
445 return mToScale;
446 }
447
448 private int getTargetX() {
449 if (mAnimationStartTime == NO_ANIMATION
450 || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentX;
451 return mToX;
452 }
453
454 private int getTargetY() {
455 if (mAnimationStartTime == NO_ANIMATION
456 || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentY;
457 return mToY;
458 }
459
460 public RectF getImageBounds() {
461 float points[] = mTempPoints;
462
463 /*
464 * (p0,p1)----------(p2,p3)
465 * | |
466 * | |
467 * (p4,p5)----------(p6,p7)
468 */
469 points[0] = points[4] = -mCurrentX;
470 points[1] = points[3] = -mCurrentY;
471 points[2] = points[6] = mImageW - mCurrentX;
472 points[5] = points[7] = mImageH - mCurrentY;
473
474 RectF rect = mTempRect;
475 rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
476 Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
477
478 float scale = mCurrentScale;
479 float offsetX = mViewW / 2;
480 float offsetY = mViewH / 2;
481 for (int i = 0; i < 4; ++i) {
482 float x = points[i + i] * scale + offsetX;
483 float y = points[i + i + 1] * scale + offsetY;
484 if (x < rect.left) rect.left = x;
485 if (x > rect.right) rect.right = x;
486 if (y < rect.top) rect.top = y;
487 if (y > rect.bottom) rect.bottom = y;
488 }
489 return rect;
490 }
491
492 public int getImageWidth() {
493 return mImageW;
494 }
495
496 public int getImageHeight() {
497 return mImageH;
498 }
499}