blob: 252f3484d6838ad1d26e1f477b96239f43bd803b [file] [log] [blame]
Owen Lina2fba682011-08-17 22:07:43 +08001/*
2 * Copyright (C) 2010 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 Lina2fba682011-08-17 22:07:43 +080019import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.Color;
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +080022import android.graphics.Point;
Yuli Huang54fe02f2012-03-20 16:37:05 +080023import android.graphics.Rect;
Owen Lina2fba682011-08-17 22:07:43 +080024import android.graphics.RectF;
25import android.os.Message;
Owen Lina2fba682011-08-17 22:07:43 +080026import android.view.MotionEvent;
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +080027import android.view.animation.AccelerateInterpolator;
Owen Lina2fba682011-08-17 22:07:43 +080028
Owen Lin73a04ff2012-03-14 17:27:24 +080029import com.android.gallery3d.R;
30import com.android.gallery3d.app.GalleryActivity;
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +080031import com.android.gallery3d.common.Utils;
Owen Lin73a04ff2012-03-14 17:27:24 +080032
Owen Lina2fba682011-08-17 22:07:43 +080033public class PhotoView extends GLView {
34 @SuppressWarnings("unused")
35 private static final String TAG = "PhotoView";
36
37 public static final int INVALID_SIZE = -1;
38
39 private static final int MSG_TRANSITION_COMPLETE = 1;
40 private static final int MSG_SHOW_LOADING = 2;
Chih-Chung Chang95860d22012-03-21 19:01:30 +080041 private static final int MSG_CANCEL_EXTRA_SCALING = 3;
Owen Lina2fba682011-08-17 22:07:43 +080042
43 private static final long DELAY_SHOW_LOADING = 250; // 250ms;
44
45 private static final int TRANS_NONE = 0;
46 private static final int TRANS_SWITCH_NEXT = 3;
47 private static final int TRANS_SWITCH_PREVIOUS = 4;
48
49 public static final int TRANS_SLIDE_IN_RIGHT = 1;
50 public static final int TRANS_SLIDE_IN_LEFT = 2;
51 public static final int TRANS_OPEN_ANIMATION = 5;
52
53 private static final int LOADING_INIT = 0;
54 private static final int LOADING_TIMEOUT = 1;
55 private static final int LOADING_COMPLETE = 2;
56 private static final int LOADING_FAIL = 3;
57
58 private static final int ENTRY_PREVIOUS = 0;
59 private static final int ENTRY_NEXT = 1;
60
61 private static final int IMAGE_GAP = 96;
62 private static final int SWITCH_THRESHOLD = 256;
63 private static final float SWIPE_THRESHOLD = 300f;
64
65 private static final float DEFAULT_TEXT_SIZE = 20;
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +080066 private static float TRANSITION_SCALE_FACTOR = 0.74f;
67
68 // Used to calculate the scaling factor for the fading animation.
69 private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f);
70
71 // Used to calculate the alpha factor for the fading animation.
72 private AccelerateInterpolator mAlphaInterpolator =
73 new AccelerateInterpolator(0.9f);
Owen Lina2fba682011-08-17 22:07:43 +080074
Owen Lina2fba682011-08-17 22:07:43 +080075 public interface PhotoTapListener {
76 public void onSingleTapUp(int x, int y);
77 }
78
79 // the previous/next image entries
80 private final ScreenNailEntry mScreenNails[] = new ScreenNailEntry[2];
81
Chih-Chung Chang762f8e22012-03-14 17:39:42 +080082 private final GestureRecognizer mGestureRecognizer;
Owen Lina2fba682011-08-17 22:07:43 +080083
84 private PhotoTapListener mPhotoTapListener;
85
86 private final PositionController mPositionController;
87
88 private Model mModel;
89 private StringTexture mLoadingText;
90 private StringTexture mNoThumbnailText;
91 private int mTransitionMode = TRANS_NONE;
92 private final TileImageView mTileView;
Chih-Chung Changbe074852011-10-12 17:10:33 +080093 private EdgeView mEdgeView;
Owen Lina2fba682011-08-17 22:07:43 +080094 private Texture mVideoPlayIcon;
95
96 private boolean mShowVideoPlayIcon;
97 private ProgressSpinner mLoadingSpinner;
98
99 private SynchronizedHandler mHandler;
100
101 private int mLoadingState = LOADING_COMPLETE;
102
Owen Lina2fba682011-08-17 22:07:43 +0800103 private int mImageRotation;
104
Yuli Huang54fe02f2012-03-20 16:37:05 +0800105 private Rect mOpenAnimationRect;
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800106 private Point mImageCenter = new Point();
Chih-Chung Chang95860d22012-03-21 19:01:30 +0800107 private boolean mCancelExtraScalingPending;
Owen Lina2fba682011-08-17 22:07:43 +0800108
109 public PhotoView(GalleryActivity activity) {
Owen Lina2fba682011-08-17 22:07:43 +0800110 mTileView = new TileImageView(activity);
111 addComponent(mTileView);
112 Context context = activity.getAndroidContext();
Chih-Chung Changbe074852011-10-12 17:10:33 +0800113 mEdgeView = new EdgeView(context);
114 addComponent(mEdgeView);
Owen Lina2fba682011-08-17 22:07:43 +0800115 mLoadingSpinner = new ProgressSpinner(context);
116 mLoadingText = StringTexture.newInstance(
117 context.getString(R.string.loading),
118 DEFAULT_TEXT_SIZE, Color.WHITE);
119 mNoThumbnailText = StringTexture.newInstance(
120 context.getString(R.string.no_thumbnail),
121 DEFAULT_TEXT_SIZE, Color.WHITE);
122
123 mHandler = new SynchronizedHandler(activity.getGLRoot()) {
124 @Override
125 public void handleMessage(Message message) {
126 switch (message.what) {
127 case MSG_TRANSITION_COMPLETE: {
128 onTransitionComplete();
129 break;
130 }
131 case MSG_SHOW_LOADING: {
132 if (mLoadingState == LOADING_INIT) {
133 // We don't need the opening animation
Yuli Huang54fe02f2012-03-20 16:37:05 +0800134 mOpenAnimationRect = null;
Owen Lina2fba682011-08-17 22:07:43 +0800135
136 mLoadingSpinner.startAnimation();
137 mLoadingState = LOADING_TIMEOUT;
138 invalidate();
139 }
140 break;
141 }
Chih-Chung Chang95860d22012-03-21 19:01:30 +0800142 case MSG_CANCEL_EXTRA_SCALING: {
143 mGestureRecognizer.cancelScale();
144 mPositionController.setExtraScalingRange(false);
145 mCancelExtraScalingPending = false;
146 break;
147 }
Owen Lina2fba682011-08-17 22:07:43 +0800148 default: throw new AssertionError(message.what);
149 }
150 }
151 };
152
Chih-Chung Chang762f8e22012-03-14 17:39:42 +0800153 mGestureRecognizer = new GestureRecognizer(
154 context, new MyGestureListener());
Owen Lina2fba682011-08-17 22:07:43 +0800155
156 for (int i = 0, n = mScreenNails.length; i < n; ++i) {
157 mScreenNails[i] = new ScreenNailEntry();
158 }
159
Chih-Chung Changbe074852011-10-12 17:10:33 +0800160 mPositionController = new PositionController(this, context, mEdgeView);
Owen Lina2fba682011-08-17 22:07:43 +0800161 mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
162 }
163
164
165 public void setModel(Model model) {
166 if (mModel == model) return;
167 mModel = model;
168 mTileView.setModel(model);
169 if (model != null) notifyOnNewImage();
170 }
171
172 public void setPhotoTapListener(PhotoTapListener listener) {
173 mPhotoTapListener = listener;
174 }
175
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800176 private void setTileViewPosition(int centerX, int centerY, float scale) {
177 TileImageView t = mTileView;
178
179 // Calculate the move-out progress value.
180 RectF bounds = mPositionController.getImageBounds();
181 int left = Math.round(bounds.left);
182 int right = Math.round(bounds.right);
183 int width = getWidth();
184 float progress = calculateMoveOutProgress(left, right, width);
185 progress = Utils.clamp(progress, -1f, 1f);
186
187 // We only want to apply the fading animation if the scrolling movement
188 // is to the right.
189 if (progress < 0) {
190 if (right - left < width) {
191 // If the picture is narrower than the view, keep it at the center
192 // of the view.
193 centerX = mPositionController.getImageWidth() / 2;
194 } else {
195 // If the picture is wider than the view (it's zoomed-in), keep
196 // the left edge of the object align the the left edge of the view.
197 centerX = Math.round(width / 2f / scale);
198 }
199 scale *= getScrollScale(progress);
200 t.setAlpha(getScrollAlpha(progress));
201 }
202
203 // set the position of the tile view
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800204 int inverseX = mPositionController.getImageWidth() - centerX;
205 int inverseY = mPositionController.getImageHeight() - centerY;
Owen Lina2fba682011-08-17 22:07:43 +0800206 int rotation = mImageRotation;
207 switch (rotation) {
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800208 case 0: t.setPosition(centerX, centerY, scale, 0); break;
209 case 90: t.setPosition(centerY, inverseX, scale, 90); break;
210 case 180: t.setPosition(inverseX, inverseY, scale, 180); break;
211 case 270: t.setPosition(inverseY, centerX, scale, 270); break;
Owen Lina2fba682011-08-17 22:07:43 +0800212 default: throw new IllegalArgumentException(String.valueOf(rotation));
213 }
214 }
215
216 public void setPosition(int centerX, int centerY, float scale) {
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800217 setTileViewPosition(centerX, centerY, scale);
218 layoutScreenNails();
Owen Lina2fba682011-08-17 22:07:43 +0800219 }
220
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800221 private void updateScreenNailEntry(int which, ScreenNail screenNail) {
Owen Lina2fba682011-08-17 22:07:43 +0800222 if (mTransitionMode == TRANS_SWITCH_NEXT
223 || mTransitionMode == TRANS_SWITCH_PREVIOUS) {
224 // ignore screen nail updating during switching
225 return;
226 }
227 ScreenNailEntry entry = mScreenNails[which];
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800228 entry.set(screenNail);
Owen Lina2fba682011-08-17 22:07:43 +0800229 }
230
231 // -1 previous, 0 current, 1 next
232 public void notifyImageInvalidated(int which) {
233 switch (which) {
234 case -1: {
235 updateScreenNailEntry(
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800236 ENTRY_PREVIOUS, mModel.getPrevScreenNail());
Owen Lina2fba682011-08-17 22:07:43 +0800237 layoutScreenNails();
238 invalidate();
239 break;
240 }
241 case 1: {
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800242 updateScreenNailEntry(ENTRY_NEXT, mModel.getNextScreenNail());
Owen Lina2fba682011-08-17 22:07:43 +0800243 layoutScreenNails();
244 invalidate();
245 break;
246 }
247 case 0: {
248 // mImageWidth and mImageHeight will get updated
249 mTileView.notifyModelInvalidated();
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800250 mTileView.setAlpha(1.0f);
Owen Lina2fba682011-08-17 22:07:43 +0800251
252 mImageRotation = mModel.getImageRotation();
253 if (((mImageRotation / 90) & 1) == 0) {
254 mPositionController.setImageSize(
255 mTileView.mImageWidth, mTileView.mImageHeight);
256 } else {
257 mPositionController.setImageSize(
258 mTileView.mImageHeight, mTileView.mImageWidth);
259 }
260 updateLoadingState();
261 break;
262 }
263 }
264 }
265
266 private void updateLoadingState() {
267 // Possible transitions of mLoadingState:
268 // INIT --> TIMEOUT, COMPLETE, FAIL
269 // TIMEOUT --> COMPLETE, FAIL, INIT
270 // COMPLETE --> INIT
271 // FAIL --> INIT
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800272 if (mModel.getLevelCount() != 0 || mModel.getScreenNail() != null) {
Owen Lina2fba682011-08-17 22:07:43 +0800273 mHandler.removeMessages(MSG_SHOW_LOADING);
274 mLoadingState = LOADING_COMPLETE;
275 } else if (mModel.isFailedToLoad()) {
276 mHandler.removeMessages(MSG_SHOW_LOADING);
277 mLoadingState = LOADING_FAIL;
Yuli Huanga565ca22012-02-07 15:51:24 +0800278 // We don't want the opening animation after loading failure
Yuli Huang54fe02f2012-03-20 16:37:05 +0800279 mOpenAnimationRect = null;
Owen Lina2fba682011-08-17 22:07:43 +0800280 } else if (mLoadingState != LOADING_INIT) {
281 mLoadingState = LOADING_INIT;
282 mHandler.removeMessages(MSG_SHOW_LOADING);
283 mHandler.sendEmptyMessageDelayed(
284 MSG_SHOW_LOADING, DELAY_SHOW_LOADING);
285 }
286 }
287
288 public void notifyModelInvalidated() {
289 if (mModel == null) {
290 updateScreenNailEntry(ENTRY_PREVIOUS, null);
291 updateScreenNailEntry(ENTRY_NEXT, null);
292 } else {
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800293 updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPrevScreenNail());
294 updateScreenNailEntry(ENTRY_NEXT, mModel.getNextScreenNail());
Owen Lina2fba682011-08-17 22:07:43 +0800295 }
296 layoutScreenNails();
297
298 if (mModel == null) {
299 mTileView.notifyModelInvalidated();
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800300 mTileView.setAlpha(1.0f);
Owen Lina2fba682011-08-17 22:07:43 +0800301 mImageRotation = 0;
302 mPositionController.setImageSize(0, 0);
303 updateLoadingState();
304 } else {
305 notifyImageInvalidated(0);
306 }
307 }
308
309 @Override
310 protected boolean onTouch(MotionEvent event) {
Chih-Chung Chang762f8e22012-03-14 17:39:42 +0800311 mGestureRecognizer.onTouchEvent(event);
Owen Lina2fba682011-08-17 22:07:43 +0800312 return true;
313 }
314
315 @Override
316 protected void onLayout(
317 boolean changeSize, int left, int top, int right, int bottom) {
318 mTileView.layout(left, top, right, bottom);
Chih-Chung Changbe074852011-10-12 17:10:33 +0800319 mEdgeView.layout(left, top, right, bottom);
Owen Lina2fba682011-08-17 22:07:43 +0800320 if (changeSize) {
321 mPositionController.setViewSize(getWidth(), getHeight());
322 for (ScreenNailEntry entry : mScreenNails) {
323 entry.updateDrawingSize();
324 }
325 }
326 }
327
328 private static int gapToSide(int imageWidth, int viewWidth) {
329 return Math.max(0, (viewWidth - imageWidth) / 2);
330 }
331
Owen Lina2fba682011-08-17 22:07:43 +0800332 /*
333 * Here is how we layout the screen nails
334 *
335 * previous current next
336 * ___________ ________________ __________
337 * | _______ | | __________ | | ______ |
338 * | | | | | | right->| | | | | |
339 * | | |<-------->|<--left | | | | | |
340 * | |_______| | | | |__________| | | |______| |
341 * |___________| | |________________| |__________|
342 * | <--> gapToSide()
343 * |
344 * IMAGE_GAP + Max(previous.gapToSide(), current.gapToSide)
345 */
346 private void layoutScreenNails() {
347 int width = getWidth();
348 int height = getHeight();
349
350 // Use the image width in AC, since we may fake the size if the
351 // image is unavailable
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800352 RectF bounds = mPositionController.getImageBounds();
Owen Lina2fba682011-08-17 22:07:43 +0800353 int left = Math.round(bounds.left);
354 int right = Math.round(bounds.right);
355 int gap = gapToSide(right - left, width);
356
357 // layout the previous image
358 ScreenNailEntry entry = mScreenNails[ENTRY_PREVIOUS];
359
360 if (entry.isEnabled()) {
361 entry.layoutRightEdgeAt(left - (
362 IMAGE_GAP + Math.max(gap, entry.gapToSide())));
363 }
364
365 // layout the next image
366 entry = mScreenNails[ENTRY_NEXT];
367 if (entry.isEnabled()) {
368 entry.layoutLeftEdgeAt(right + (
369 IMAGE_GAP + Math.max(gap, entry.gapToSide())));
370 }
371 }
372
Owen Lina2fba682011-08-17 22:07:43 +0800373 @Override
374 protected void render(GLCanvas canvas) {
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800375 boolean drawScreenNail = (mTransitionMode != TRANS_SLIDE_IN_LEFT
376 && mTransitionMode != TRANS_SLIDE_IN_RIGHT
377 && mTransitionMode != TRANS_OPEN_ANIMATION);
378
379 // Draw the next photo
380 if (drawScreenNail) {
381 ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
382 nextNail.draw(canvas, true);
383 }
384
Owen Lina2fba682011-08-17 22:07:43 +0800385 // Draw the current photo
386 if (mLoadingState == LOADING_COMPLETE) {
387 super.render(canvas);
388 }
389
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800390 // If the photo is loaded, draw the message/icon at the center of it,
391 // otherwise draw the message/icon at the center of the view.
392 if (mLoadingState == LOADING_COMPLETE) {
393 mTileView.getImageCenter(mImageCenter);
394 renderMessage(canvas, mImageCenter.x, mImageCenter.y);
395 } else {
396 renderMessage(canvas, getWidth() / 2, getHeight() / 2);
Owen Lina2fba682011-08-17 22:07:43 +0800397 }
398
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800399 // Draw the previous photo
400 if (drawScreenNail) {
401 ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
402 prevNail.draw(canvas, false);
403 }
404 }
405
406 private void renderMessage(GLCanvas canvas, int x, int y) {
Owen Lina2fba682011-08-17 22:07:43 +0800407 // Draw the progress spinner and the text below it
408 //
409 // (x, y) is where we put the center of the spinner.
410 // s is the size of the video play icon, and we use s to layout text
411 // because we want to keep the text at the same place when the video
412 // play icon is shown instead of the spinner.
413 int w = getWidth();
414 int h = getHeight();
Owen Lina2fba682011-08-17 22:07:43 +0800415 int s = Math.min(getWidth(), getHeight()) / 6;
416
417 if (mLoadingState == LOADING_TIMEOUT) {
418 StringTexture m = mLoadingText;
419 ProgressSpinner r = mLoadingSpinner;
420 r.draw(canvas, x - r.getWidth() / 2, y - r.getHeight() / 2);
421 m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
422 invalidate(); // we need to keep the spinner rotating
423 } else if (mLoadingState == LOADING_FAIL) {
424 StringTexture m = mNoThumbnailText;
425 m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
426 }
427
428 // Draw the video play icon (in the place where the spinner was)
429 if (mShowVideoPlayIcon
430 && mLoadingState != LOADING_INIT
431 && mLoadingState != LOADING_TIMEOUT) {
432 mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s);
433 }
434
Yuli Huang3976dea2012-03-01 16:51:08 +0800435 mPositionController.advanceAnimation();
Owen Lina2fba682011-08-17 22:07:43 +0800436 }
437
438 private void stopCurrentSwipingIfNeeded() {
Chih-Chung Chang762f8e22012-03-14 17:39:42 +0800439 // Enable fast swiping
Owen Lina2fba682011-08-17 22:07:43 +0800440 if (mTransitionMode == TRANS_SWITCH_NEXT) {
441 mTransitionMode = TRANS_NONE;
442 mPositionController.stopAnimation();
443 switchToNextImage();
444 } else if (mTransitionMode == TRANS_SWITCH_PREVIOUS) {
445 mTransitionMode = TRANS_NONE;
446 mPositionController.stopAnimation();
447 switchToPreviousImage();
448 }
449 }
450
Yuli Huang75e11d52012-02-23 22:26:12 +0800451 private boolean swipeImages(float velocityX, float velocityY) {
Owen Lina2fba682011-08-17 22:07:43 +0800452 if (mTransitionMode != TRANS_NONE
453 && mTransitionMode != TRANS_SWITCH_NEXT
454 && mTransitionMode != TRANS_SWITCH_PREVIOUS) return false;
455
Yuli Huang75e11d52012-02-23 22:26:12 +0800456 // Avoid swiping images if we're possibly flinging to view the
457 // zoomed in picture vertically.
458 PositionController controller = mPositionController;
459 boolean isMinimal = controller.isAtMinimalScale();
460 int edges = controller.getImageAtEdges();
461 if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX))
462 if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0
463 || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0)
464 return false;
Owen Lina2fba682011-08-17 22:07:43 +0800465
Chih-Chung Changbe074852011-10-12 17:10:33 +0800466 // If we are at the edge of the current photo and the sweeping velocity
467 // exceeds the threshold, switch to next / previous image.
Yuli Huang75e11d52012-02-23 22:26:12 +0800468 int halfWidth = getWidth() / 2;
469 if (velocityX < -SWIPE_THRESHOLD && (isMinimal
470 || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) {
Chih-Chung Changbe074852011-10-12 17:10:33 +0800471 stopCurrentSwipingIfNeeded();
Yuli Huang75e11d52012-02-23 22:26:12 +0800472 ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
Chih-Chung Changbe074852011-10-12 17:10:33 +0800473 if (next.isEnabled()) {
474 mTransitionMode = TRANS_SWITCH_NEXT;
Yuli Huang75e11d52012-02-23 22:26:12 +0800475 controller.startHorizontalSlide(next.mOffsetX - halfWidth);
Chih-Chung Changbe074852011-10-12 17:10:33 +0800476 return true;
Owen Lina2fba682011-08-17 22:07:43 +0800477 }
Yuli Huang75e11d52012-02-23 22:26:12 +0800478 } else if (velocityX > SWIPE_THRESHOLD && (isMinimal
479 || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) {
Chih-Chung Changbe074852011-10-12 17:10:33 +0800480 stopCurrentSwipingIfNeeded();
Yuli Huang75e11d52012-02-23 22:26:12 +0800481 ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
Chih-Chung Changbe074852011-10-12 17:10:33 +0800482 if (prev.isEnabled()) {
483 mTransitionMode = TRANS_SWITCH_PREVIOUS;
Yuli Huang75e11d52012-02-23 22:26:12 +0800484 controller.startHorizontalSlide(prev.mOffsetX - halfWidth);
Chih-Chung Changbe074852011-10-12 17:10:33 +0800485 return true;
Owen Lina2fba682011-08-17 22:07:43 +0800486 }
487 }
488
Chih-Chung Changbe074852011-10-12 17:10:33 +0800489 return false;
490 }
491
Yuli Huang75e11d52012-02-23 22:26:12 +0800492 private boolean snapToNeighborImage() {
Owen Lina2fba682011-08-17 22:07:43 +0800493 if (mTransitionMode != TRANS_NONE) return false;
494
Chih-Chung Changbe074852011-10-12 17:10:33 +0800495 PositionController controller = mPositionController;
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800496 RectF bounds = controller.getImageBounds();
Owen Lina2fba682011-08-17 22:07:43 +0800497 int left = Math.round(bounds.left);
498 int right = Math.round(bounds.right);
Yuli Huang75e11d52012-02-23 22:26:12 +0800499 int width = getWidth();
Owen Lina2fba682011-08-17 22:07:43 +0800500 int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width);
501
502 // If we have moved the picture a lot, switching.
Yuli Huang75e11d52012-02-23 22:26:12 +0800503 ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
Owen Lina2fba682011-08-17 22:07:43 +0800504 if (next.isEnabled() && threshold < width - right) {
505 mTransitionMode = TRANS_SWITCH_NEXT;
506 controller.startHorizontalSlide(next.mOffsetX - width / 2);
507 return true;
508 }
Yuli Huang75e11d52012-02-23 22:26:12 +0800509 ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
Owen Lina2fba682011-08-17 22:07:43 +0800510 if (prev.isEnabled() && threshold < left) {
511 mTransitionMode = TRANS_SWITCH_PREVIOUS;
512 controller.startHorizontalSlide(prev.mOffsetX - width / 2);
513 return true;
514 }
515
516 return false;
517 }
518
Chih-Chung Chang762f8e22012-03-14 17:39:42 +0800519 private class MyGestureListener implements GestureRecognizer.Listener {
520 private boolean mIgnoreUpEvent = false;
Owen Lina2fba682011-08-17 22:07:43 +0800521
Owen Lina2fba682011-08-17 22:07:43 +0800522 @Override
Chih-Chung Chang762f8e22012-03-14 17:39:42 +0800523 public boolean onSingleTapUp(float x, float y) {
524 if (mPhotoTapListener != null) {
525 mPhotoTapListener.onSingleTapUp((int) x, (int) y);
526 }
527 return true;
528 }
529
530 @Override
531 public boolean onDoubleTap(float x, float y) {
532 if (mTransitionMode != TRANS_NONE) return true;
533 PositionController controller = mPositionController;
534 float scale = controller.getCurrentScale();
535 // onDoubleTap happened on the second ACTION_DOWN.
536 // We need to ignore the next UP event.
537 mIgnoreUpEvent = true;
538 if (scale <= 1.0f || controller.isAtMinimalScale()) {
539 controller.zoomIn(x, y, Math.max(1.5f, scale * 1.5f));
540 } else {
541 controller.resetToFullView();
542 }
543 return true;
544 }
545
546 @Override
547 public boolean onScroll(float dx, float dy) {
Owen Lina2fba682011-08-17 22:07:43 +0800548 if (mTransitionMode != TRANS_NONE) return true;
Chih-Chung Changbe074852011-10-12 17:10:33 +0800549
550 ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
551 ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
552
553 mPositionController.startScroll(dx, dy, next.isEnabled(),
554 prev.isEnabled());
Owen Lina2fba682011-08-17 22:07:43 +0800555 return true;
556 }
557
558 @Override
Chih-Chung Chang762f8e22012-03-14 17:39:42 +0800559 public boolean onFling(float velocityX, float velocityY) {
Yuli Huang75e11d52012-02-23 22:26:12 +0800560 if (swipeImages(velocityX, velocityY)) {
Chih-Chung Chang4fdf38f2011-10-03 21:11:39 +0800561 mIgnoreUpEvent = true;
562 } else if (mTransitionMode != TRANS_NONE) {
563 // do nothing
564 } else if (mPositionController.fling(velocityX, velocityY)) {
565 mIgnoreUpEvent = true;
Owen Lina2fba682011-08-17 22:07:43 +0800566 }
567 return true;
568 }
569
570 @Override
Chih-Chung Chang762f8e22012-03-14 17:39:42 +0800571 public boolean onScaleBegin(float focusX, float focusY) {
572 if (mTransitionMode != TRANS_NONE) return false;
573 mPositionController.beginScale(focusX, focusY);
Owen Lina2fba682011-08-17 22:07:43 +0800574 return true;
575 }
Owen Lina2fba682011-08-17 22:07:43 +0800576
577 @Override
Chih-Chung Chang762f8e22012-03-14 17:39:42 +0800578 public boolean onScale(float focusX, float focusY, float scale) {
Owen Lina2fba682011-08-17 22:07:43 +0800579 if (Float.isNaN(scale) || Float.isInfinite(scale)
580 || mTransitionMode != TRANS_NONE) return true;
Chih-Chung Chang95860d22012-03-21 19:01:30 +0800581 boolean outOfRange = mPositionController.scaleBy(
582 scale, focusX, focusY);
583 if (outOfRange) {
584 if (!mCancelExtraScalingPending) {
585 mHandler.sendEmptyMessageDelayed(
586 MSG_CANCEL_EXTRA_SCALING, 700);
587 mPositionController.setExtraScalingRange(true);
588 mCancelExtraScalingPending = true;
589 }
590 } else {
591 if (mCancelExtraScalingPending) {
592 mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
593 mPositionController.setExtraScalingRange(false);
594 mCancelExtraScalingPending = false;
595 }
596 }
Owen Lina2fba682011-08-17 22:07:43 +0800597 return true;
598 }
599
600 @Override
Chih-Chung Chang762f8e22012-03-14 17:39:42 +0800601 public void onScaleEnd() {
Owen Lina2fba682011-08-17 22:07:43 +0800602 mPositionController.endScale();
Chih-Chung Changbe074852011-10-12 17:10:33 +0800603 snapToNeighborImage();
Owen Lina2fba682011-08-17 22:07:43 +0800604 }
Chih-Chung Chang762f8e22012-03-14 17:39:42 +0800605
606 @Override
607 public void onDown() {
608 }
609
610 @Override
611 public void onUp() {
612 mEdgeView.onRelease();
613
614 if (mIgnoreUpEvent) {
615 mIgnoreUpEvent = false;
616 return;
617 }
618 if (!snapToNeighborImage() && mTransitionMode == TRANS_NONE) {
619 mPositionController.up();
620 }
621 }
Owen Lina2fba682011-08-17 22:07:43 +0800622 }
623
Chih-Chung Chang36064d12011-09-14 12:07:17 +0800624 public boolean jumpTo(int index) {
625 if (mTransitionMode != TRANS_NONE) return false;
626 mModel.jumpTo(index);
627 return true;
628 }
629
Owen Lina2fba682011-08-17 22:07:43 +0800630 public void notifyOnNewImage() {
631 mPositionController.setImageSize(0, 0);
632 }
633
634 public void startSlideInAnimation(int direction) {
635 PositionController a = mPositionController;
636 a.stopAnimation();
637 switch (direction) {
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800638 case TRANS_SLIDE_IN_LEFT:
Owen Lina2fba682011-08-17 22:07:43 +0800639 case TRANS_SLIDE_IN_RIGHT: {
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800640 mTransitionMode = direction;
641 a.startSlideInAnimation(direction);
Owen Lina2fba682011-08-17 22:07:43 +0800642 break;
643 }
644 default: throw new IllegalArgumentException(String.valueOf(direction));
645 }
646 }
647
Owen Lina2fba682011-08-17 22:07:43 +0800648 private void switchToNextImage() {
649 // We update the texture here directly to prevent texture uploading.
650 ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
651 ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
652 mTileView.invalidateTiles();
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800653 if (prevNail.mScreenNail != null) prevNail.mScreenNail.recycle();
654 prevNail.set(mTileView.mScreenNail);
655 mTileView.updateScreenNail(nextNail.mScreenNail);
656 nextNail.set(null);
Owen Lina2fba682011-08-17 22:07:43 +0800657 mModel.next();
658 }
659
660 private void switchToPreviousImage() {
661 // We update the texture here directly to prevent texture uploading.
662 ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
663 ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
664 mTileView.invalidateTiles();
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800665 if (nextNail.mScreenNail != null) nextNail.mScreenNail.recycle();
666 nextNail.set(mTileView.mScreenNail);
667 mTileView.updateScreenNail(prevNail.mScreenNail);
668 nextNail.set(null);
Owen Lina2fba682011-08-17 22:07:43 +0800669 mModel.previous();
670 }
671
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800672 public void notifyTransitionComplete() {
673 mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE);
674 }
675
Owen Lina2fba682011-08-17 22:07:43 +0800676 private void onTransitionComplete() {
677 int mode = mTransitionMode;
678 mTransitionMode = TRANS_NONE;
679
680 if (mModel == null) return;
681 if (mode == TRANS_SWITCH_NEXT) {
682 switchToNextImage();
683 } else if (mode == TRANS_SWITCH_PREVIOUS) {
684 switchToPreviousImage();
685 }
686 }
687
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800688 public boolean isDown() {
Chih-Chung Chang762f8e22012-03-14 17:39:42 +0800689 return mGestureRecognizer.isDown();
Owen Lina2fba682011-08-17 22:07:43 +0800690 }
691
692 public static interface Model extends TileImageView.Model {
693 public void next();
694 public void previous();
Chih-Chung Chang36064d12011-09-14 12:07:17 +0800695 public void jumpTo(int index);
Owen Lina2fba682011-08-17 22:07:43 +0800696 public int getImageRotation();
697
698 // Return null if the specified image is unavailable.
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800699 public ScreenNail getNextScreenNail();
700 public ScreenNail getPrevScreenNail();
Owen Lina2fba682011-08-17 22:07:43 +0800701 }
702
703 private static int getRotated(int degree, int original, int theother) {
704 return ((degree / 90) & 1) == 0 ? original : theother;
705 }
706
707 private class ScreenNailEntry {
708 private boolean mVisible;
709 private boolean mEnabled;
710
Owen Lina2fba682011-08-17 22:07:43 +0800711 private int mDrawWidth;
712 private int mDrawHeight;
713 private int mOffsetX;
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800714 private int mRotation;
Owen Lina2fba682011-08-17 22:07:43 +0800715
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800716 private ScreenNail mScreenNail;
Owen Lina2fba682011-08-17 22:07:43 +0800717
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800718 public void set(ScreenNail screenNail) {
719 mEnabled = (screenNail != null);
720 if (mScreenNail == screenNail) return;
721 if (mScreenNail != null) mScreenNail.recycle();
722 mScreenNail = screenNail;
723 if (mScreenNail != null) {
724 mRotation = mScreenNail.getRotation();
Owen Lina2fba682011-08-17 22:07:43 +0800725 updateDrawingSize();
726 }
727 }
728
729 public void layoutRightEdgeAt(int x) {
730 mVisible = x > 0;
731 mOffsetX = x - getRotated(
732 mRotation, mDrawWidth, mDrawHeight) / 2;
733 }
734
735 public void layoutLeftEdgeAt(int x) {
736 mVisible = x < getWidth();
737 mOffsetX = x + getRotated(
738 mRotation, mDrawWidth, mDrawHeight) / 2;
739 }
740
741 public int gapToSide() {
742 return ((mRotation / 90) & 1) != 0
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800743 ? PhotoView.gapToSide(mDrawHeight, getWidth())
744 : PhotoView.gapToSide(mDrawWidth, getWidth());
Owen Lina2fba682011-08-17 22:07:43 +0800745 }
746
747 public void updateDrawingSize() {
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800748 if (mScreenNail == null) return;
Owen Lina2fba682011-08-17 22:07:43 +0800749
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800750 int width = mScreenNail.getWidth();
751 int height = mScreenNail.getHeight();
Chih-Chung Changf3c77ac2011-09-30 18:33:17 +0800752
753 // Calculate the initial scale that will used by PositionController
754 // (usually fit-to-screen)
755 float s = ((mRotation / 90) & 0x01) == 0
756 ? mPositionController.getMinimalScale(width, height)
757 : mPositionController.getMinimalScale(height, width);
758
Owen Lina2fba682011-08-17 22:07:43 +0800759 mDrawWidth = Math.round(width * s);
760 mDrawHeight = Math.round(height * s);
761 }
762
763 public boolean isEnabled() {
764 return mEnabled;
765 }
766
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800767 public void draw(GLCanvas canvas, boolean applyFadingAnimation) {
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800768 if (mScreenNail == null) return;
769 if (!mVisible) {
770 mScreenNail.disableDraw();
771 return;
772 }
773
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800774 int w = getWidth();
775 int x = applyFadingAnimation ? w / 2 : mOffsetX;
Owen Lina2fba682011-08-17 22:07:43 +0800776 int y = getHeight() / 2;
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800777 int flags = GLCanvas.SAVE_FLAG_MATRIX;
Owen Lina2fba682011-08-17 22:07:43 +0800778
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800779 if (applyFadingAnimation) flags |= GLCanvas.SAVE_FLAG_ALPHA;
780 canvas.save(flags);
781 canvas.translate(x, y);
782 if (applyFadingAnimation) {
783 float progress = (float) (x - mOffsetX) / w;
784 float alpha = getScrollAlpha(progress);
785 float scale = getScrollScale(progress);
786 canvas.multiplyAlpha(alpha);
787 canvas.scale(scale, scale, 1);
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800788 }
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800789 if (mRotation != 0) {
790 canvas.rotate(mRotation, 0, 0, 1);
791 }
792 canvas.translate(-x, -y);
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800793 mScreenNail.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2,
794 mDrawWidth, mDrawHeight);
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800795 canvas.restore();
796 }
797 }
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800798
Chih-Chung Chang4a01c6e2012-03-21 16:19:21 +0800799 // Returns the scrolling progress value for an object moving out of a
800 // view. The progress value measures how much the object has moving out of
801 // the view. The object currently displays in [left, right), and the view is
802 // at [0, viewWidth].
803 //
804 // The returned value is negative when the object is moving right, and
805 // positive when the object is moving left. The value goes to -1 or 1 when
806 // the object just moves out of the view completely. The value is 0 if the
807 // object currently fills the view.
808 private static float calculateMoveOutProgress(int left, int right,
809 int viewWidth) {
810 // w = object width
811 // viewWidth = view width
812 int w = right - left;
813
814 // If the object width is smaller than the view width,
815 // |....view....|
816 // |<-->| progress = -1 when left = viewWidth
817 // |<-->| progress = 1 when left = -w
818 // So progress = 1 - 2 * (left + w) / (viewWidth + w)
819 if (w < viewWidth) {
820 return 1f - 2f * (left + w) / (viewWidth + w);
821 }
822
823 // If the object width is larger than the view width,
824 // |..view..|
825 // |<--------->| progress = -1 when left = viewWidth
826 // |<--------->| progress = 0 between left = 0
827 // |<--------->| and right = viewWidth
828 // |<--------->| progress = 1 when right = 0
829 if (left > 0) {
830 return -left / (float) viewWidth;
831 }
832
833 if (right < viewWidth) {
834 return (viewWidth - right) / (float) viewWidth;
835 }
836
837 return 0;
838 }
839
840 // Maps a scrolling progress value to the alpha factor in the fading
841 // animation.
842 private float getScrollAlpha(float scrollProgress) {
843 return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation(
844 1 - Math.abs(scrollProgress)) : 1.0f;
845 }
846
847 // Maps a scrolling progress value to the scaling factor in the fading
848 // animation.
849 private float getScrollScale(float scrollProgress) {
850 float interpolatedProgress = mScaleInterpolator.getInterpolation(
851 Math.abs(scrollProgress));
852 float scale = (1 - interpolatedProgress) +
853 interpolatedProgress * TRANSITION_SCALE_FACTOR;
854 return scale;
855 }
856
857
858 // This interpolator emulates the rate at which the perceived scale of an
859 // object changes as its distance from a camera increases. When this
860 // interpolator is applied to a scale animation on a view, it evokes the
861 // sense that the object is shrinking due to moving away from the camera.
862 private static class ZInterpolator {
863 private float focalLength;
864
865 public ZInterpolator(float foc) {
866 focalLength = foc;
867 }
868
869 public float getInterpolation(float input) {
870 return (1.0f - focalLength / (focalLength + input)) /
871 (1.0f - focalLength / (focalLength + 1.0f));
Owen Lina2fba682011-08-17 22:07:43 +0800872 }
873 }
874
875 public void pause() {
876 mPositionController.skipAnimation();
877 mTransitionMode = TRANS_NONE;
878 mTileView.freeTextures();
879 for (ScreenNailEntry entry : mScreenNails) {
Chih-Chung Chang706a7cf2012-03-15 16:38:45 +0800880 entry.set(null);
Owen Lina2fba682011-08-17 22:07:43 +0800881 }
882 }
883
884 public void resume() {
885 mTileView.prepareTextures();
886 }
887
Yuli Huang54fe02f2012-03-20 16:37:05 +0800888 public void setOpenAnimationRect(Rect rect) {
889 mOpenAnimationRect = rect;
Owen Lina2fba682011-08-17 22:07:43 +0800890 }
891
892 public void showVideoPlayIcon(boolean show) {
893 mShowVideoPlayIcon = show;
894 }
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800895
Yuli Huang54fe02f2012-03-20 16:37:05 +0800896 // Returns the opening animation rectangle saved by the previous page.
897 public Rect retrieveOpenAnimationRect() {
898 Rect r = mOpenAnimationRect;
899 mOpenAnimationRect = null;
900 return r;
Chih-Chung Chang07e6fca2011-09-26 17:34:06 +0800901 }
902
903 public void openAnimationStarted() {
904 mTransitionMode = TRANS_OPEN_ANIMATION;
905 }
906
907 public boolean isInTransition() {
908 return mTransitionMode != TRANS_NONE;
909 }
Owen Lina2fba682011-08-17 22:07:43 +0800910}