2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.gallery3d.ui;
19 import android.content.Context;
20 import android.graphics.Color;
21 import android.graphics.Matrix;
22 import android.graphics.Rect;
23 import android.os.Build;
24 import android.os.Message;
25 import android.util.FloatMath;
26 import android.view.MotionEvent;
27 import android.view.View.MeasureSpec;
28 import android.view.animation.AccelerateInterpolator;
30 import com.android.gallery3d.R;
31 import com.android.gallery3d.app.AbstractGalleryActivity;
32 import com.android.gallery3d.common.ApiHelper;
33 import com.android.gallery3d.common.Utils;
34 import com.android.gallery3d.data.MediaItem;
35 import com.android.gallery3d.data.MediaObject;
36 import com.android.gallery3d.data.Path;
37 import com.android.gallery3d.util.GalleryUtils;
38 import com.android.gallery3d.util.RangeArray;
40 public class PhotoView extends GLView {
41 @SuppressWarnings("unused")
42 private static final String TAG = "PhotoView";
43 private final int mPlaceholderColor;
45 public static final int INVALID_SIZE = -1;
46 public static final long INVALID_DATA_VERSION =
47 MediaObject.INVALID_DATA_VERSION;
49 public static class Size {
54 public interface Model extends TileImageView.Model {
55 public int getCurrentIndex();
56 public void moveTo(int index);
58 // Returns the size for the specified picture. If the size information is
59 // not avaiable, width = height = 0.
60 public void getImageSize(int offset, Size size);
62 // Returns the media item for the specified picture.
63 public MediaItem getMediaItem(int offset);
65 // Returns the rotation for the specified picture.
66 public int getImageRotation(int offset);
68 // This amends the getScreenNail() method of TileImageView.Model to get
69 // ScreenNail at previous (negative offset) or next (positive offset)
70 // positions. Returns null if the specified ScreenNail is unavailable.
71 public ScreenNail getScreenNail(int offset);
73 // Set this to true if we need the model to provide full images.
74 public void setNeedFullImage(boolean enabled);
76 // Returns true if the item is the Camera preview.
77 public boolean isCamera(int offset);
79 // Returns true if the item is the Panorama.
80 public boolean isPanorama(int offset);
82 // Returns true if the item is a static image that represents camera
84 public boolean isStaticCamera(int offset);
86 // Returns true if the item is a Video.
87 public boolean isVideo(int offset);
89 // Returns true if the item can be deleted.
90 public boolean isDeletable(int offset);
92 public static final int LOADING_INIT = 0;
93 public static final int LOADING_COMPLETE = 1;
94 public static final int LOADING_FAIL = 2;
96 public int getLoadingState(int offset);
98 // When data change happens, we need to decide which MediaItem to focus
101 // 1. If focus hint path != null, we try to focus on it if we can find
102 // it. This is used for undo a deletion, so we can focus on the
105 // 2. Otherwise try to focus on the MediaItem that is currently focused,
106 // if we can find it.
108 // 3. Otherwise try to focus on the previous MediaItem or the next
109 // MediaItem, depending on the value of focus hint direction.
110 public static final int FOCUS_HINT_NEXT = 0;
111 public static final int FOCUS_HINT_PREVIOUS = 1;
112 public void setFocusHintDirection(int direction);
113 public void setFocusHintPath(Path path);
116 public interface Listener {
117 public void onSingleTapUp(int x, int y);
118 public void lockOrientation();
119 public void unlockOrientation();
120 public void onFullScreenChanged(boolean full);
121 public void onActionBarAllowed(boolean allowed);
122 public void onActionBarWanted();
123 public void onCurrentImageUpdated();
124 public void onDeleteImage(Path path, int offset);
125 public void onUndoDeleteImage();
126 public void onCommitDeleteImage();
127 public void onFilmModeChanged(boolean enabled);
128 public void onCameraCenter();
131 // The rules about orientation locking:
133 // (1) We need to lock the orientation if we are in page mode camera
134 // preview, so there is no (unwanted) rotation animation when the user
135 // rotates the device.
137 // (2) We need to unlock the orientation if we want to show the action bar
138 // because the action bar follows the system orientation.
140 // The rules about action bar:
142 // (1) If we are in film mode, we don't show action bar.
144 // (2) If we go from camera to gallery with capture animation, we show
146 private static final int MSG_CANCEL_EXTRA_SCALING = 2;
147 private static final int MSG_SWITCH_FOCUS = 3;
148 private static final int MSG_CAPTURE_ANIMATION_DONE = 4;
149 private static final int MSG_DELETE_ANIMATION_DONE = 5;
150 private static final int MSG_DELETE_DONE = 6;
151 private static final int MSG_UNDO_BAR_TIMEOUT = 7;
152 private static final int MSG_UNDO_BAR_FULL_CAMERA = 8;
154 private static final float SWIPE_THRESHOLD = 300f;
156 private static final float DEFAULT_TEXT_SIZE = 20;
157 private static float TRANSITION_SCALE_FACTOR = 0.74f;
158 private static final int ICON_RATIO = 6;
160 // whether we want to apply card deck effect in page mode.
161 private static final boolean CARD_EFFECT = true;
163 // whether we want to apply offset effect in film mode.
164 private static final boolean OFFSET_EFFECT = true;
166 // Used to calculate the scaling factor for the card deck effect.
167 private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f);
169 // Used to calculate the alpha factor for the fading animation.
170 private AccelerateInterpolator mAlphaInterpolator =
171 new AccelerateInterpolator(0.9f);
173 // We keep this many previous ScreenNails. (also this many next ScreenNails)
174 public static final int SCREEN_NAIL_MAX = 3;
176 // These are constants for the delete gesture.
177 private static final int SWIPE_ESCAPE_VELOCITY = 500; // dp/sec
178 private static final int MAX_DISMISS_VELOCITY = 2000; // dp/sec
180 // The picture entries, the valid index is from -SCREEN_NAIL_MAX to
182 private final RangeArray<Picture> mPictures =
183 new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX);
184 private Size[] mSizes = new Size[2 * SCREEN_NAIL_MAX + 1];
186 private final MyGestureListener mGestureListener;
187 private final GestureRecognizer mGestureRecognizer;
188 private final PositionController mPositionController;
190 private Listener mListener;
191 private Model mModel;
192 private StringTexture mNoThumbnailText;
193 private TileImageView mTileView;
194 private EdgeView mEdgeView;
195 private UndoBarView mUndoBar;
196 private Texture mVideoPlayIcon;
198 private SynchronizedHandler mHandler;
200 private boolean mCancelExtraScalingPending;
201 private boolean mFilmMode = false;
202 private boolean mWantCameraCenterCallbacks = false;
203 private int mDisplayRotation = 0;
204 private int mCompensation = 0;
205 private boolean mFullScreenCamera;
206 private Rect mCameraRelativeFrame = new Rect();
207 private Rect mCameraRect = new Rect();
209 // [mPrevBound, mNextBound] is the range of index for all pictures in the
210 // model, if we assume the index of current focused picture is 0. So if
211 // there are some previous pictures, mPrevBound < 0, and if there are some
212 // next pictures, mNextBound > 0.
213 private int mPrevBound;
214 private int mNextBound;
216 // This variable prevents us doing snapback until its values goes to 0. This
217 // happens if the user gesture is still in progress or we are in a capture
219 private int mHolding;
220 private static final int HOLD_TOUCH_DOWN = 1;
221 private static final int HOLD_CAPTURE_ANIMATION = 2;
222 private static final int HOLD_DELETE = 4;
224 // mTouchBoxIndex is the index of the box that is touched by the down
225 // gesture in film mode. The value Integer.MAX_VALUE means no box was
227 private int mTouchBoxIndex = Integer.MAX_VALUE;
228 // Whether the box indicated by mTouchBoxIndex is deletable. Only meaningful
229 // if mTouchBoxIndex is not Integer.MAX_VALUE.
230 private boolean mTouchBoxDeletable;
231 // This is the index of the last deleted item. This is only used as a hint
232 // to hide the undo button when we are too far away from the deleted
233 // item. The value Integer.MAX_VALUE means there is no such hint.
234 private int mUndoIndexHint = Integer.MAX_VALUE;
236 public PhotoView(AbstractGalleryActivity activity) {
237 mTileView = new TileImageView(activity);
238 addComponent(mTileView);
239 Context context = activity.getAndroidContext();
240 mPlaceholderColor = context.getResources().getColor(
241 R.color.photo_placeholder);
242 mEdgeView = new EdgeView(context);
243 addComponent(mEdgeView);
244 mUndoBar = new UndoBarView(context);
245 addComponent(mUndoBar);
246 mUndoBar.setVisibility(GLView.INVISIBLE);
247 mUndoBar.setOnClickListener(new OnClickListener() {
249 public void onClick(GLView v) {
250 mListener.onUndoDeleteImage();
254 mNoThumbnailText = StringTexture.newInstance(
255 context.getString(R.string.no_thumbnail),
256 DEFAULT_TEXT_SIZE, Color.WHITE);
258 mHandler = new MyHandler(activity.getGLRoot());
260 mGestureListener = new MyGestureListener();
261 mGestureRecognizer = new GestureRecognizer(context, mGestureListener);
263 mPositionController = new PositionController(context,
264 new PositionController.Listener() {
267 public void invalidate() {
268 PhotoView.this.invalidate();
272 public boolean isHoldingDown() {
273 return (mHolding & HOLD_TOUCH_DOWN) != 0;
277 public boolean isHoldingDelete() {
278 return (mHolding & HOLD_DELETE) != 0;
282 public void onPull(int offset, int direction) {
283 mEdgeView.onPull(offset, direction);
287 public void onRelease() {
288 mEdgeView.onRelease();
292 public void onAbsorb(int velocity, int direction) {
293 mEdgeView.onAbsorb(velocity, direction);
296 mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
297 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
299 mPictures.put(i, new FullPicture());
301 mPictures.put(i, new ScreenNailPicture(i));
306 public void stopScrolling() {
307 mPositionController.stopScrolling();
310 public void setModel(Model model) {
312 mTileView.setModel(mModel);
315 class MyHandler extends SynchronizedHandler {
316 public MyHandler(GLRoot root) {
321 public void handleMessage(Message message) {
322 switch (message.what) {
323 case MSG_CANCEL_EXTRA_SCALING: {
324 mGestureRecognizer.cancelScale();
325 mPositionController.setExtraScalingRange(false);
326 mCancelExtraScalingPending = false;
329 case MSG_SWITCH_FOCUS: {
333 case MSG_CAPTURE_ANIMATION_DONE: {
334 // message.arg1 is the offset parameter passed to
335 // switchWithCaptureAnimation().
336 captureAnimationDone(message.arg1);
339 case MSG_DELETE_ANIMATION_DONE: {
340 // message.obj is the Path of the MediaItem which should be
341 // deleted. message.arg1 is the offset of the image.
342 mListener.onDeleteImage((Path) message.obj, message.arg1);
343 // Normally a box which finishes delete animation will hold
344 // position until the underlying MediaItem is actually
345 // deleted, and HOLD_DELETE will be cancelled that time. In
346 // case the MediaItem didn't actually get deleted in 2
347 // seconds, we will cancel HOLD_DELETE and make it bounce
350 // We make sure there is at most one MSG_DELETE_DONE
352 mHandler.removeMessages(MSG_DELETE_DONE);
353 Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
354 mHandler.sendMessageDelayed(m, 2000);
356 int numberOfPictures = mNextBound - mPrevBound + 1;
357 if (numberOfPictures == 2) {
358 if (mModel.isCamera(mNextBound)
359 || mModel.isCamera(mPrevBound)) {
363 showUndoBar(numberOfPictures <= 1);
366 case MSG_DELETE_DONE: {
367 if (!mHandler.hasMessages(MSG_DELETE_ANIMATION_DONE)) {
368 mHolding &= ~HOLD_DELETE;
373 case MSG_UNDO_BAR_TIMEOUT: {
374 checkHideUndoBar(UNDO_BAR_TIMEOUT);
377 case MSG_UNDO_BAR_FULL_CAMERA: {
378 checkHideUndoBar(UNDO_BAR_FULL_CAMERA);
381 default: throw new AssertionError(message.what);
386 public void setWantCameraCenterCallbacks(boolean wanted) {
387 mWantCameraCenterCallbacks = wanted;
390 ////////////////////////////////////////////////////////////////////////////
391 // Data/Image change notifications
392 ////////////////////////////////////////////////////////////////////////////
394 public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) {
395 mPrevBound = prevBound;
396 mNextBound = nextBound;
398 // Update mTouchBoxIndex
399 if (mTouchBoxIndex != Integer.MAX_VALUE) {
400 int k = mTouchBoxIndex;
401 mTouchBoxIndex = Integer.MAX_VALUE;
402 for (int i = 0; i < 2 * SCREEN_NAIL_MAX + 1; i++) {
403 if (fromIndex[i] == k) {
404 mTouchBoxIndex = i - SCREEN_NAIL_MAX;
410 // Hide undo button if we are too far away
411 if (mUndoIndexHint != Integer.MAX_VALUE) {
412 if (Math.abs(mUndoIndexHint - mModel.getCurrentIndex()) >= 3) {
417 // Update the ScreenNails.
418 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
419 Picture p = mPictures.get(i);
421 mSizes[i + SCREEN_NAIL_MAX] = p.getSize();
424 boolean wasDeleting = mPositionController.hasDeletingBox();
427 mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0,
428 mModel.isCamera(0), mSizes);
430 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
434 boolean isDeleting = mPositionController.hasDeletingBox();
436 // If the deletion is done, make HOLD_DELETE persist for only the time
437 // needed for a snapback animation.
438 if (wasDeleting && !isDeleting) {
439 mHandler.removeMessages(MSG_DELETE_DONE);
440 Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
441 mHandler.sendMessageDelayed(
442 m, PositionController.SNAPBACK_ANIMATION_TIME);
448 public boolean isDeleting() {
449 return (mHolding & HOLD_DELETE) != 0
450 && mPositionController.hasDeletingBox();
453 public void notifyImageChange(int index) {
455 mListener.onCurrentImageUpdated();
457 mPictures.get(index).reload();
458 setPictureSize(index);
462 private void setPictureSize(int index) {
463 Picture p = mPictures.get(index);
464 mPositionController.setImageSize(index, p.getSize(),
465 index == 0 && p.isCamera() ? mCameraRect : null);
469 protected void onLayout(
470 boolean changeSize, int left, int top, int right, int bottom) {
471 int w = right - left;
472 int h = bottom - top;
473 mTileView.layout(0, 0, w, h);
474 mEdgeView.layout(0, 0, w, h);
475 mUndoBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
476 mUndoBar.layout(0, h - mUndoBar.getMeasuredHeight(), w, h);
478 GLRoot root = getGLRoot();
479 int displayRotation = root.getDisplayRotation();
480 int compensation = root.getCompensation();
481 if (mDisplayRotation != displayRotation
482 || mCompensation != compensation) {
483 mDisplayRotation = displayRotation;
484 mCompensation = compensation;
486 // We need to change the size and rotation of the Camera ScreenNail,
487 // but we don't want it to animate because the size doen't actually
488 // change in the eye of the user.
489 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
490 Picture p = mPictures.get(i);
498 mPositionController.setConstrainedFrame(mCameraRect);
500 mPositionController.setViewSize(getWidth(), getHeight());
504 // Update the camera rectangle due to layout change or camera relative frame
506 private void updateCameraRect() {
507 // Get the width and height in framework orientation because the given
508 // mCameraRelativeFrame is in that coordinates.
511 if (mCompensation % 180 != 0) {
516 int l = mCameraRelativeFrame.left;
517 int t = mCameraRelativeFrame.top;
518 int r = mCameraRelativeFrame.right;
519 int b = mCameraRelativeFrame.bottom;
521 // Now convert it to the coordinates we are using.
522 switch (mCompensation) {
523 case 0: mCameraRect.set(l, t, r, b); break;
524 case 90: mCameraRect.set(h - b, l, h - t, r); break;
525 case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break;
526 case 270: mCameraRect.set(t, w - r, b, w - l); break;
529 Log.d(TAG, "compensation = " + mCompensation
530 + ", CameraRelativeFrame = " + mCameraRelativeFrame
531 + ", mCameraRect = " + mCameraRect);
534 public void setCameraRelativeFrame(Rect frame) {
535 mCameraRelativeFrame.set(frame);
538 // mPositionController.setConstrainedFrame(mCameraRect);
539 // here, but it is moved to a parameter of the setImageSize() call, so
540 // it can be updated atomically with the CameraScreenNail's size change.
543 // Returns the rotation we need to do to the camera texture before drawing
544 // it to the canvas, assuming the camera texture is correct when the device
545 // is in its natural orientation.
546 private int getCameraRotation() {
547 return (mCompensation - mDisplayRotation + 360) % 360;
550 private int getPanoramaRotation() {
551 return mCompensation;
554 ////////////////////////////////////////////////////////////////////////////
556 ////////////////////////////////////////////////////////////////////////////
558 private interface Picture {
560 void draw(GLCanvas canvas, Rect r);
561 void setScreenNail(ScreenNail s);
562 boolean isCamera(); // whether the picture is a camera preview
563 boolean isDeletable(); // whether the picture can be deleted
564 void forceSize(); // called when mCompensation changes
568 class FullPicture implements Picture {
569 private int mRotation;
570 private boolean mIsCamera;
571 private boolean mIsPanorama;
572 private boolean mIsStaticCamera;
573 private boolean mIsVideo;
574 private boolean mIsDeletable;
575 private int mLoadingState = Model.LOADING_INIT;
576 private Size mSize = new Size();
579 public void reload() {
580 // mImageWidth and mImageHeight will get updated
581 mTileView.notifyModelInvalidated();
583 mIsCamera = mModel.isCamera(0);
584 mIsPanorama = mModel.isPanorama(0);
585 mIsStaticCamera = mModel.isStaticCamera(0);
586 mIsVideo = mModel.isVideo(0);
587 mIsDeletable = mModel.isDeletable(0);
588 mLoadingState = mModel.getLoadingState(0);
589 setScreenNail(mModel.getScreenNail(0));
594 public Size getSize() {
599 public void forceSize() {
601 mPositionController.forceImageSize(0, mSize);
604 private void updateSize() {
606 mRotation = getPanoramaRotation();
607 } else if (mIsCamera && !mIsStaticCamera) {
608 mRotation = getCameraRotation();
610 mRotation = mModel.getImageRotation(0);
613 int w = mTileView.mImageWidth;
614 int h = mTileView.mImageHeight;
615 mSize.width = getRotated(mRotation, w, h);
616 mSize.height = getRotated(mRotation, h, w);
620 public void draw(GLCanvas canvas, Rect r) {
621 drawTileView(canvas, r);
623 // We want to have the following transitions:
624 // (1) Move camera preview out of its place: switch to film mode
625 // (2) Move camera preview into its place: switch to page mode
626 // The extra mWasCenter check makes sure (1) does not apply if in
627 // page mode, we move _to_ the camera preview from another picture.
629 // Holdings except touch-down prevent the transitions.
630 if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return;
632 boolean isCameraCenter = mIsCamera && mPositionController.isCenter() && !canUndoLastPicture();
634 if (isCameraCenter && mWantCameraCenterCallbacks) {
635 mListener.onCameraCenter();
640 public void setScreenNail(ScreenNail s) {
641 mTileView.setScreenNail(s);
645 public boolean isCamera() {
650 public boolean isDeletable() {
654 private void drawTileView(GLCanvas canvas, Rect r) {
655 float imageScale = mPositionController.getImageScale();
656 int viewW = getWidth();
657 int viewH = getHeight();
658 float cx = r.exactCenterX();
659 float cy = r.exactCenterY();
660 float scale = 1f; // the scaling factor due to card effect
662 canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
663 float filmRatio = mPositionController.getFilmRatio();
664 boolean wantsCardEffect = CARD_EFFECT && !mIsCamera
665 && filmRatio != 1f && !mPictures.get(-1).isCamera()
666 && !mPositionController.inOpeningAnimation();
667 boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
668 && filmRatio == 1f && r.centerY() != viewH / 2;
669 if (wantsCardEffect) {
670 // Calculate the move-out progress value.
673 float progress = calculateMoveOutProgress(left, right, viewW);
674 progress = Utils.clamp(progress, -1f, 1f);
676 // We only want to apply the fading animation if the scrolling
677 // movement is to the right.
679 scale = getScrollScale(progress);
680 float alpha = getScrollAlpha(progress);
681 scale = interpolate(filmRatio, scale, 1f);
682 alpha = interpolate(filmRatio, alpha, 1f);
685 canvas.multiplyAlpha(alpha);
687 float cxPage; // the cx value in page mode
688 if (right - left <= viewW) {
689 // If the picture is narrower than the view, keep it at
690 // the center of the view.
693 // If the picture is wider than the view (it's
694 // zoomed-in), keep the left edge of the object align
695 // the the left edge of the view.
696 cxPage = (right - left) * scale / 2f;
698 cx = interpolate(filmRatio, cxPage, cx);
700 } else if (wantsOffsetEffect) {
701 float offset = (float) (r.centerY() - viewH / 2) / viewH;
702 float alpha = getOffsetAlpha(offset);
703 canvas.multiplyAlpha(alpha);
706 // Draw the tile view.
707 setTileViewPosition(cx, cy, viewW, viewH, imageScale);
708 renderChild(canvas, mTileView);
710 // Draw the play video icon and the message.
711 canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f));
712 int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f);
713 if (mIsVideo) drawVideoPlayIcon(canvas, s);
714 if (mLoadingState == Model.LOADING_FAIL) {
715 drawLoadingFailMessage(canvas);
718 // Draw a debug indicator showing which picture has focus (index ==
720 //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF);
725 // Set the position of the tile view
726 private void setTileViewPosition(float cx, float cy,
727 int viewW, int viewH, float scale) {
728 // Find out the bitmap coordinates of the center of the view
729 int imageW = mPositionController.getImageWidth();
730 int imageH = mPositionController.getImageHeight();
731 int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f);
732 int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f);
734 int inverseX = imageW - centerX;
735 int inverseY = imageH - centerY;
738 case 0: x = centerX; y = centerY; break;
739 case 90: x = centerY; y = inverseX; break;
740 case 180: x = inverseX; y = inverseY; break;
741 case 270: x = inverseY; y = centerX; break;
743 throw new RuntimeException(String.valueOf(mRotation));
745 mTileView.setPosition(x, y, scale, mRotation);
749 private class ScreenNailPicture implements Picture {
751 private int mRotation;
752 private ScreenNail mScreenNail;
753 private boolean mIsCamera;
754 private boolean mIsPanorama;
755 private boolean mIsStaticCamera;
756 private boolean mIsVideo;
757 private boolean mIsDeletable;
758 private int mLoadingState = Model.LOADING_INIT;
759 private Size mSize = new Size();
761 public ScreenNailPicture(int index) {
766 public void reload() {
767 mIsCamera = mModel.isCamera(mIndex);
768 mIsPanorama = mModel.isPanorama(mIndex);
769 mIsStaticCamera = mModel.isStaticCamera(mIndex);
770 mIsVideo = mModel.isVideo(mIndex);
771 mIsDeletable = mModel.isDeletable(mIndex);
772 mLoadingState = mModel.getLoadingState(mIndex);
773 setScreenNail(mModel.getScreenNail(mIndex));
778 public Size getSize() {
783 public void draw(GLCanvas canvas, Rect r) {
784 if (mScreenNail == null) {
785 // Draw a placeholder rectange if there should be a picture in
786 // this position (but somehow there isn't).
787 if (mIndex >= mPrevBound && mIndex <= mNextBound) {
788 drawPlaceHolder(canvas, r);
794 if (r.left >= w || r.right <= 0 || r.top >= h || r.bottom <= 0) {
795 mScreenNail.noDraw();
799 float filmRatio = mPositionController.getFilmRatio();
800 boolean wantsCardEffect = CARD_EFFECT && mIndex > 0
801 && filmRatio != 1f && !mPictures.get(0).isCamera();
802 boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
803 && filmRatio == 1f && r.centerY() != h / 2;
804 int cx = wantsCardEffect
805 ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f)
807 int cy = r.centerY();
808 canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
809 canvas.translate(cx, cy);
810 if (wantsCardEffect) {
811 float progress = (float) (w / 2 - r.centerX()) / w;
812 progress = Utils.clamp(progress, -1, 1);
813 float alpha = getScrollAlpha(progress);
814 float scale = getScrollScale(progress);
815 alpha = interpolate(filmRatio, alpha, 1f);
816 scale = interpolate(filmRatio, scale, 1f);
817 canvas.multiplyAlpha(alpha);
818 canvas.scale(scale, scale, 1);
819 } else if (wantsOffsetEffect) {
820 float offset = (float) (r.centerY() - h / 2) / h;
821 float alpha = getOffsetAlpha(offset);
822 canvas.multiplyAlpha(alpha);
824 if (mRotation != 0) {
825 canvas.rotate(mRotation, 0, 0, 1);
827 int drawW = getRotated(mRotation, r.width(), r.height());
828 int drawH = getRotated(mRotation, r.height(), r.width());
829 mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH);
830 if (isScreenNailAnimating()) {
833 int s = Math.min(drawW, drawH);
834 if (mIsVideo) drawVideoPlayIcon(canvas, s);
835 if (mLoadingState == Model.LOADING_FAIL) {
836 drawLoadingFailMessage(canvas);
841 private boolean isScreenNailAnimating() {
842 return (mScreenNail instanceof BitmapScreenNail)
843 && ((BitmapScreenNail) mScreenNail).isAnimating();
847 public void setScreenNail(ScreenNail s) {
852 public void forceSize() {
854 mPositionController.forceImageSize(mIndex, mSize);
857 private void updateSize() {
859 mRotation = getPanoramaRotation();
860 } else if (mIsCamera && !mIsStaticCamera) {
861 mRotation = getCameraRotation();
863 mRotation = mModel.getImageRotation(mIndex);
866 if (mScreenNail != null) {
867 mSize.width = mScreenNail.getWidth();
868 mSize.height = mScreenNail.getHeight();
870 // If we don't have ScreenNail available, we can still try to
871 // get the size information of it.
872 mModel.getImageSize(mIndex, mSize);
876 int h = mSize.height;
877 mSize.width = getRotated(mRotation, w, h);
878 mSize.height = getRotated(mRotation, h, w);
882 public boolean isCamera() {
887 public boolean isDeletable() {
892 // Draw a gray placeholder in the specified rectangle.
893 private void drawPlaceHolder(GLCanvas canvas, Rect r) {
894 canvas.fillRect(r.left, r.top, r.width(), r.height(), mPlaceholderColor);
897 // Draw the video play icon (in the place where the spinner was)
898 private void drawVideoPlayIcon(GLCanvas canvas, int side) {
899 int s = side / ICON_RATIO;
900 // Draw the video play icon at the center
901 mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s);
904 // Draw the "no thumbnail" message
905 private void drawLoadingFailMessage(GLCanvas canvas) {
906 StringTexture m = mNoThumbnailText;
907 m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2);
910 private static int getRotated(int degree, int original, int theother) {
911 return (degree % 180 == 0) ? original : theother;
914 ////////////////////////////////////////////////////////////////////////////
916 ////////////////////////////////////////////////////////////////////////////
919 protected boolean onTouch(MotionEvent event) {
920 mGestureRecognizer.onTouchEvent(event);
924 private class MyGestureListener implements GestureRecognizer.Listener {
925 private boolean mIgnoreUpEvent = false;
926 // If we can change mode for this scale gesture.
927 private boolean mCanChangeMode;
928 // If we have changed the film mode in this scaling gesture.
929 private boolean mModeChanged;
930 // If this scaling gesture should be ignored.
931 private boolean mIgnoreScalingGesture;
932 // whether the down action happened while the view is scrolling.
933 private boolean mDownInScrolling;
934 // If we should ignore all gestures other than onSingleTapUp.
935 private boolean mIgnoreSwipingGesture;
936 // If a scrolling has happened after a down gesture.
937 private boolean mScrolledAfterDown;
938 // If the first scrolling move is in X direction. In the film mode, X
939 // direction scrolling is normal scrolling. but Y direction scrolling is
941 private boolean mFirstScrollX;
942 // The accumulated Y delta that has been sent to mPositionController.
944 // The accumulated scaling change from a scaling gesture.
945 private float mAccScale;
948 public boolean onSingleTapUp(float x, float y) {
949 // On crespo running Android 2.3.6 (gingerbread), a pinch out gesture results in the
950 // following call sequence: onDown(), onUp() and then onSingleTapUp(). The correct
951 // sequence for a single-tap-up gesture should be: onDown(), onSingleTapUp() and onUp().
952 // The call sequence for a pinch out gesture in JB is: onDown(), then onUp() and there's
953 // no onSingleTapUp(). Base on these observations, the following condition is added to
954 // filter out the false alarm where onSingleTapUp() is called within a pinch out
955 // gesture. The framework fix went into ICS. Refer to b/4588114.
956 if (Build.VERSION.SDK_INT < ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) {
957 if ((mHolding & HOLD_TOUCH_DOWN) == 0) {
962 // We do this in addition to onUp() because we want the snapback of
963 // setFilmMode to happen.
964 mHolding &= ~HOLD_TOUCH_DOWN;
966 if (mFilmMode && !mDownInScrolling) {
967 switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f));
969 // If this is a lock screen photo, let the listener handle the
970 // event. Tapping on lock screen photo should take the user
971 // directly to the lock screen.
972 MediaItem item = mModel.getMediaItem(0);
974 if (item != null) supported = item.getSupportedOperations();
975 if ((supported & MediaItem.SUPPORT_ACTION) == 0) {
977 mIgnoreUpEvent = true;
982 if (mListener != null) {
983 // Do the inverse transform of the touch coordinates.
984 Matrix m = getGLRoot().getCompensationMatrix();
985 Matrix inv = new Matrix();
987 float[] pts = new float[] {x, y};
989 mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f));
995 public boolean onDoubleTap(float x, float y) {
996 if (mIgnoreSwipingGesture) return true;
997 if (mPictures.get(0).isCamera()) return false;
998 PositionController controller = mPositionController;
999 float scale = controller.getImageScale();
1000 // onDoubleTap happened on the second ACTION_DOWN.
1001 // We need to ignore the next UP event.
1002 mIgnoreUpEvent = true;
1003 if (scale <= 1.0f || controller.isAtMinimalScale()) {
1004 controller.zoomIn(x, y, Math.max(1.5f, scale * 1.5f));
1006 controller.resetToFullView();
1012 public boolean onScroll(float dx, float dy, float totalX, float totalY) {
1013 if (mIgnoreSwipingGesture) return true;
1014 if (!mScrolledAfterDown) {
1015 mScrolledAfterDown = true;
1016 mFirstScrollX = (Math.abs(dx) > Math.abs(dy));
1019 int dxi = (int) (-dx + 0.5f);
1020 int dyi = (int) (-dy + 0.5f);
1022 if (mFirstScrollX) {
1023 mPositionController.scrollFilmX(dxi);
1025 if (mTouchBoxIndex == Integer.MAX_VALUE) return true;
1026 int newDeltaY = calculateDeltaY(totalY);
1027 int d = newDeltaY - mDeltaY;
1029 mPositionController.scrollFilmY(mTouchBoxIndex, d);
1030 mDeltaY = newDeltaY;
1034 mPositionController.scrollPage(dxi, dyi);
1039 private int calculateDeltaY(float delta) {
1040 if (mTouchBoxDeletable) return (int) (delta + 0.5f);
1042 // don't let items that can't be deleted be dragged more than
1043 // maxScrollDistance, and make it harder and harder to drag.
1044 int size = getHeight();
1045 float maxScrollDistance = 0.15f * size;
1046 if (Math.abs(delta) >= size) {
1047 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
1049 delta = maxScrollDistance *
1050 FloatMath.sin((delta / size) * (float) (Math.PI / 2));
1052 return (int) (delta + 0.5f);
1056 public boolean onFling(float velocityX, float velocityY) {
1057 if (mIgnoreSwipingGesture) return true;
1058 if (mModeChanged) return true;
1059 if (swipeImages(velocityX, velocityY)) {
1060 mIgnoreUpEvent = true;
1062 flingImages(velocityX, velocityY);
1067 private boolean flingImages(float velocityX, float velocityY) {
1068 int vx = (int) (velocityX + 0.5f);
1069 int vy = (int) (velocityY + 0.5f);
1071 return mPositionController.flingPage(vx, vy);
1073 if (Math.abs(velocityX) > Math.abs(velocityY)) {
1074 return mPositionController.flingFilmX(vx);
1076 // If we scrolled in Y direction fast enough, treat it as a delete
1078 if (!mFilmMode || mTouchBoxIndex == Integer.MAX_VALUE
1079 || !mTouchBoxDeletable) {
1082 int maxVelocity = GalleryUtils.dpToPixel(MAX_DISMISS_VELOCITY);
1083 int escapeVelocity = GalleryUtils.dpToPixel(SWIPE_ESCAPE_VELOCITY);
1084 int centerY = mPositionController.getPosition(mTouchBoxIndex)
1086 boolean fastEnough = (Math.abs(vy) > escapeVelocity)
1087 && (Math.abs(vy) > Math.abs(vx))
1088 && ((vy > 0) == (centerY > getHeight() / 2));
1090 vy = Math.min(vy, maxVelocity);
1091 int duration = mPositionController.flingFilmY(mTouchBoxIndex, vy);
1092 if (duration >= 0) {
1093 mPositionController.setPopFromTop(vy < 0);
1094 deleteAfterAnimation(duration);
1095 // We reset mTouchBoxIndex, so up() won't check if Y
1096 // scrolled far enough to be a delete gesture.
1097 mTouchBoxIndex = Integer.MAX_VALUE;
1104 private void deleteAfterAnimation(int duration) {
1105 MediaItem item = mModel.getMediaItem(mTouchBoxIndex);
1106 if (item == null) return;
1107 mListener.onCommitDeleteImage();
1108 mUndoIndexHint = mModel.getCurrentIndex() + mTouchBoxIndex;
1109 mHolding |= HOLD_DELETE;
1110 Message m = mHandler.obtainMessage(MSG_DELETE_ANIMATION_DONE);
1111 m.obj = item.getPath();
1112 m.arg1 = mTouchBoxIndex;
1113 mHandler.sendMessageDelayed(m, duration);
1117 public boolean onScaleBegin(float focusX, float focusY) {
1118 if (mIgnoreSwipingGesture) return true;
1119 // We ignore the scaling gesture if it is a camera preview.
1120 mIgnoreScalingGesture = mPictures.get(0).isCamera();
1121 if (mIgnoreScalingGesture) {
1124 mPositionController.beginScale(focusX, focusY);
1125 // We can change mode if we are in film mode, or we are in page
1126 // mode and at minimal scale.
1127 mCanChangeMode = mFilmMode
1128 || mPositionController.isAtMinimalScale();
1134 public boolean onScale(float focusX, float focusY, float scale) {
1135 if (mIgnoreSwipingGesture) return true;
1136 if (mIgnoreScalingGesture) return true;
1137 if (mModeChanged) return true;
1138 if (Float.isNaN(scale) || Float.isInfinite(scale)) return false;
1140 int outOfRange = mPositionController.scaleBy(scale, focusX, focusY);
1142 // We wait for a large enough scale change before changing mode.
1143 // Otherwise we may mistakenly treat a zoom-in gesture as zoom-out
1146 boolean largeEnough = (mAccScale < 0.97f || mAccScale > 1.03f);
1148 // If mode changes, we treat this scaling gesture has ended.
1149 if (mCanChangeMode && largeEnough) {
1150 if ((outOfRange < 0 && !mFilmMode) ||
1151 (outOfRange > 0 && mFilmMode)) {
1152 stopExtraScalingIfNeeded();
1154 // Removing the touch down flag allows snapback to happen
1155 // for film mode change.
1156 mHolding &= ~HOLD_TOUCH_DOWN;
1157 setFilmMode(!mFilmMode);
1159 // We need to call onScaleEnd() before setting mModeChanged
1162 mModeChanged = true;
1167 if (outOfRange != 0) {
1168 startExtraScalingIfNeeded();
1170 stopExtraScalingIfNeeded();
1176 public void onScaleEnd() {
1177 if (mIgnoreSwipingGesture) return;
1178 if (mIgnoreScalingGesture) return;
1179 if (mModeChanged) return;
1180 mPositionController.endScale();
1183 private void startExtraScalingIfNeeded() {
1184 if (!mCancelExtraScalingPending) {
1185 mHandler.sendEmptyMessageDelayed(
1186 MSG_CANCEL_EXTRA_SCALING, 700);
1187 mPositionController.setExtraScalingRange(true);
1188 mCancelExtraScalingPending = true;
1192 private void stopExtraScalingIfNeeded() {
1193 if (mCancelExtraScalingPending) {
1194 mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
1195 mPositionController.setExtraScalingRange(false);
1196 mCancelExtraScalingPending = false;
1201 public void onDown(float x, float y) {
1202 checkHideUndoBar(UNDO_BAR_TOUCHED);
1205 mModeChanged = false;
1207 if (mIgnoreSwipingGesture) return;
1209 mHolding |= HOLD_TOUCH_DOWN;
1211 if (mFilmMode && mPositionController.isScrolling()) {
1212 mDownInScrolling = true;
1213 mPositionController.stopScrolling();
1215 mDownInScrolling = false;
1218 mScrolledAfterDown = false;
1220 int xi = (int) (x + 0.5f);
1221 int yi = (int) (y + 0.5f);
1222 mTouchBoxIndex = mPositionController.hitTest(xi, yi);
1223 if (mTouchBoxIndex < mPrevBound || mTouchBoxIndex > mNextBound) {
1224 mTouchBoxIndex = Integer.MAX_VALUE;
1226 mTouchBoxDeletable =
1227 mPictures.get(mTouchBoxIndex).isDeletable();
1230 mTouchBoxIndex = Integer.MAX_VALUE;
1235 public void onUp() {
1236 if (mIgnoreSwipingGesture) return;
1238 mHolding &= ~HOLD_TOUCH_DOWN;
1239 mEdgeView.onRelease();
1241 // If we scrolled in Y direction far enough, treat it as a delete
1243 if (mFilmMode && mScrolledAfterDown && !mFirstScrollX
1244 && mTouchBoxIndex != Integer.MAX_VALUE) {
1245 Rect r = mPositionController.getPosition(mTouchBoxIndex);
1246 int h = getHeight();
1247 if (Math.abs(r.centerY() - h * 0.5f) > 0.4f * h) {
1248 int duration = mPositionController
1249 .flingFilmY(mTouchBoxIndex, 0);
1250 if (duration >= 0) {
1251 mPositionController.setPopFromTop(r.centerY() < h * 0.5f);
1252 deleteAfterAnimation(duration);
1257 if (mIgnoreUpEvent) {
1258 mIgnoreUpEvent = false;
1265 public void setSwipingEnabled(boolean enabled) {
1266 mIgnoreSwipingGesture = !enabled;
1270 public void setSwipingEnabled(boolean enabled) {
1271 mGestureListener.setSwipingEnabled(enabled);
1274 private void updateActionBar() {
1275 boolean isCamera = mPictures.get(0).isCamera();
1276 if (isCamera && !mFilmMode) {
1277 // Move into camera in page mode, lock
1278 mListener.lockOrientation();
1279 mListener.onActionBarAllowed(false);
1281 mListener.onActionBarAllowed(true);
1282 if (mFilmMode) mListener.onActionBarWanted();
1286 public void setFilmMode(boolean enabled) {
1287 if (mFilmMode == enabled) return;
1288 mFilmMode = enabled;
1289 mPositionController.setFilmMode(mFilmMode);
1290 mModel.setNeedFullImage(!enabled);
1291 mModel.setFocusHintDirection(
1292 mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT);
1294 mListener.onFilmModeChanged(enabled);
1297 public boolean getFilmMode() {
1301 ////////////////////////////////////////////////////////////////////////////
1303 ////////////////////////////////////////////////////////////////////////////
1305 public void pause() {
1306 mPositionController.skipAnimation();
1307 mTileView.freeTextures();
1308 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
1309 mPictures.get(i).setScreenNail(null);
1314 public void resume() {
1315 mTileView.prepareTextures();
1318 // move to the camera preview and show controls after resume
1319 public void resetToFirstPicture() {
1324 ////////////////////////////////////////////////////////////////////////////
1326 ////////////////////////////////////////////////////////////////////////////
1328 private int mUndoBarState;
1329 private static final int UNDO_BAR_SHOW = 1;
1330 private static final int UNDO_BAR_TIMEOUT = 2;
1331 private static final int UNDO_BAR_TOUCHED = 4;
1332 private static final int UNDO_BAR_FULL_CAMERA = 8;
1333 private static final int UNDO_BAR_DELETE_LAST = 16;
1335 // "deleteLast" means if the deletion is on the last remaining picture in
1337 private void showUndoBar(boolean deleteLast) {
1338 mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT);
1339 mUndoBarState = UNDO_BAR_SHOW;
1340 if(deleteLast) mUndoBarState |= UNDO_BAR_DELETE_LAST;
1341 mUndoBar.animateVisibility(GLView.VISIBLE);
1342 mHandler.sendEmptyMessageDelayed(MSG_UNDO_BAR_TIMEOUT, 3000);
1345 private void hideUndoBar() {
1346 mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT);
1347 mListener.onCommitDeleteImage();
1348 mUndoBar.animateVisibility(GLView.INVISIBLE);
1350 mUndoIndexHint = Integer.MAX_VALUE;
1353 // Check if the one of the conditions for hiding the undo bar has been
1354 // met. The conditions are:
1356 // 1. It has been three seconds since last showing, and (a) the user has
1357 // touched, or (b) the deleted picture is the last remaining picture in the
1360 // 2. The camera is shown in full screen.
1361 private void checkHideUndoBar(int addition) {
1362 mUndoBarState |= addition;
1363 if ((mUndoBarState & UNDO_BAR_SHOW) == 0) return;
1364 boolean timeout = (mUndoBarState & UNDO_BAR_TIMEOUT) != 0;
1365 boolean touched = (mUndoBarState & UNDO_BAR_TOUCHED) != 0;
1366 boolean fullCamera = (mUndoBarState & UNDO_BAR_FULL_CAMERA) != 0;
1367 boolean deleteLast = (mUndoBarState & UNDO_BAR_DELETE_LAST) != 0;
1368 if ((timeout && (touched || deleteLast)) || fullCamera) {
1373 // Returns true if the user can still undo the deletion of the last
1374 // remaining picture in the album. We need to check this and delay making
1375 // the camera preview full screen, otherwise the user won't have a chance to
1377 private boolean canUndoLastPicture() {
1378 if ((mUndoBarState & UNDO_BAR_SHOW) == 0) return false;
1379 return (mUndoBarState & UNDO_BAR_DELETE_LAST) != 0;
1382 ////////////////////////////////////////////////////////////////////////////
1384 ////////////////////////////////////////////////////////////////////////////
1387 protected void render(GLCanvas canvas) {
1388 // Check if the camera preview occupies the full screen.
1389 boolean full = !mFilmMode && mPictures.get(0).isCamera()
1390 && mPositionController.isCenter()
1391 && mPositionController.isAtMinimalScale();
1392 if (full != mFullScreenCamera) {
1393 mFullScreenCamera = full;
1394 mListener.onFullScreenChanged(full);
1395 if (full) mHandler.sendEmptyMessage(MSG_UNDO_BAR_FULL_CAMERA);
1398 // Determine how many photos we need to draw in addition to the center
1401 if (mFullScreenCamera) {
1404 // In page mode, we draw only one previous/next photo. But if we are
1405 // doing capture animation, we want to draw all photos.
1406 boolean inPageMode = (mPositionController.getFilmRatio() == 0f);
1407 boolean inCaptureAnimation =
1408 ((mHolding & HOLD_CAPTURE_ANIMATION) != 0);
1409 if (inPageMode && !inCaptureAnimation) {
1412 neighbors = SCREEN_NAIL_MAX;
1416 // Draw photos from back to front
1417 for (int i = neighbors; i >= -neighbors; i--) {
1418 Rect r = mPositionController.getPosition(i);
1419 mPictures.get(i).draw(canvas, r);
1422 renderChild(canvas, mEdgeView);
1423 renderChild(canvas, mUndoBar);
1425 mPositionController.advanceAnimation();
1426 checkFocusSwitching();
1429 ////////////////////////////////////////////////////////////////////////////
1430 // Film mode focus switching
1431 ////////////////////////////////////////////////////////////////////////////
1433 // Runs in GL thread.
1434 private void checkFocusSwitching() {
1435 if (!mFilmMode) return;
1436 if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return;
1437 if (switchPosition() != 0) {
1438 mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS);
1442 // Runs in main thread.
1443 private void switchFocus() {
1444 if (mHolding != 0) return;
1445 switch (switchPosition()) {
1447 switchToPrevImage();
1450 switchToNextImage();
1455 // Returns -1 if we should switch focus to the previous picture, +1 if we
1456 // should switch to the next, 0 otherwise.
1457 private int switchPosition() {
1458 Rect curr = mPositionController.getPosition(0);
1459 int center = getWidth() / 2;
1461 if (curr.left > center && mPrevBound < 0) {
1462 Rect prev = mPositionController.getPosition(-1);
1463 int currDist = curr.left - center;
1464 int prevDist = center - prev.right;
1465 if (prevDist < currDist) {
1468 } else if (curr.right < center && mNextBound > 0) {
1469 Rect next = mPositionController.getPosition(1);
1470 int currDist = center - curr.right;
1471 int nextDist = next.left - center;
1472 if (nextDist < currDist) {
1480 // Switch to the previous or next picture if the hit position is inside
1481 // one of their boxes. This runs in main thread.
1482 private void switchToHitPicture(int x, int y) {
1483 if (mPrevBound < 0) {
1484 Rect r = mPositionController.getPosition(-1);
1486 slideToPrevPicture();
1491 if (mNextBound > 0) {
1492 Rect r = mPositionController.getPosition(1);
1494 slideToNextPicture();
1500 ////////////////////////////////////////////////////////////////////////////
1501 // Page mode focus switching
1503 // We slide image to the next one or the previous one in two cases: 1: If
1504 // the user did a fling gesture with enough velocity. 2 If the user has
1505 // moved the picture a lot.
1506 ////////////////////////////////////////////////////////////////////////////
1508 private boolean swipeImages(float velocityX, float velocityY) {
1509 if (mFilmMode) return false;
1511 // Avoid swiping images if we're possibly flinging to view the
1512 // zoomed in picture vertically.
1513 PositionController controller = mPositionController;
1514 boolean isMinimal = controller.isAtMinimalScale();
1515 int edges = controller.getImageAtEdges();
1516 if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX))
1517 if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0
1518 || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0)
1521 // If we are at the edge of the current photo and the sweeping velocity
1522 // exceeds the threshold, slide to the next / previous image.
1523 if (velocityX < -SWIPE_THRESHOLD && (isMinimal
1524 || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) {
1525 return slideToNextPicture();
1526 } else if (velocityX > SWIPE_THRESHOLD && (isMinimal
1527 || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) {
1528 return slideToPrevPicture();
1534 private void snapback() {
1535 if ((mHolding & ~HOLD_DELETE) != 0) return;
1536 if (!snapToNeighborImage()) {
1537 mPositionController.snapback();
1541 private boolean snapToNeighborImage() {
1542 if (mFilmMode) return false;
1544 Rect r = mPositionController.getPosition(0);
1545 int viewW = getWidth();
1546 // Setting the move threshold proportional to the width of the view
1547 int moveThreshold = viewW / 5 ;
1548 int threshold = moveThreshold + gapToSide(r.width(), viewW);
1550 // If we have moved the picture a lot, switching.
1551 if (viewW - r.right > threshold) {
1552 return slideToNextPicture();
1553 } else if (r.left > threshold) {
1554 return slideToPrevPicture();
1560 private boolean slideToNextPicture() {
1561 if (mNextBound <= 0) return false;
1562 switchToNextImage();
1563 mPositionController.startHorizontalSlide();
1567 private boolean slideToPrevPicture() {
1568 if (mPrevBound >= 0) return false;
1569 switchToPrevImage();
1570 mPositionController.startHorizontalSlide();
1574 private static int gapToSide(int imageWidth, int viewWidth) {
1575 return Math.max(0, (viewWidth - imageWidth) / 2);
1578 ////////////////////////////////////////////////////////////////////////////
1580 ////////////////////////////////////////////////////////////////////////////
1582 public void switchToImage(int index) {
1583 mModel.moveTo(index);
1586 private void switchToNextImage() {
1587 mModel.moveTo(mModel.getCurrentIndex() + 1);
1590 private void switchToPrevImage() {
1591 mModel.moveTo(mModel.getCurrentIndex() - 1);
1594 private void switchToFirstImage() {
1598 ////////////////////////////////////////////////////////////////////////////
1599 // Opening Animation
1600 ////////////////////////////////////////////////////////////////////////////
1602 public void setOpenAnimationRect(Rect rect) {
1603 mPositionController.setOpenAnimationRect(rect);
1606 ////////////////////////////////////////////////////////////////////////////
1607 // Capture Animation
1608 ////////////////////////////////////////////////////////////////////////////
1610 public boolean switchWithCaptureAnimation(int offset) {
1611 GLRoot root = getGLRoot();
1612 if(root == null) return false;
1613 root.lockRenderThread();
1615 return switchWithCaptureAnimationLocked(offset);
1617 root.unlockRenderThread();
1621 private boolean switchWithCaptureAnimationLocked(int offset) {
1622 if (mHolding != 0) return true;
1624 if (mNextBound <= 0) return false;
1625 // Temporary disable action bar until the capture animation is done.
1626 if (!mFilmMode) mListener.onActionBarAllowed(false);
1627 switchToNextImage();
1628 mPositionController.startCaptureAnimationSlide(-1);
1629 } else if (offset == -1) {
1630 if (mPrevBound >= 0) return false;
1631 if (mFilmMode) setFilmMode(false);
1633 // If we are too far away from the first image (so that we don't
1634 // have all the ScreenNails in-between), we go directly without
1636 if (mModel.getCurrentIndex() > SCREEN_NAIL_MAX) {
1637 switchToFirstImage();
1638 mPositionController.skipToFinalPosition();
1642 switchToFirstImage();
1643 mPositionController.startCaptureAnimationSlide(1);
1647 mHolding |= HOLD_CAPTURE_ANIMATION;
1648 Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0);
1649 mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME);
1653 private void captureAnimationDone(int offset) {
1654 mHolding &= ~HOLD_CAPTURE_ANIMATION;
1655 if (offset == 1 && !mFilmMode) {
1656 // Now the capture animation is done, enable the action bar.
1657 mListener.onActionBarAllowed(true);
1658 mListener.onActionBarWanted();
1663 ////////////////////////////////////////////////////////////////////////////
1664 // Card deck effect calculation
1665 ////////////////////////////////////////////////////////////////////////////
1667 // Returns the scrolling progress value for an object moving out of a
1668 // view. The progress value measures how much the object has moving out of
1669 // the view. The object currently displays in [left, right), and the view is
1670 // at [0, viewWidth].
1672 // The returned value is negative when the object is moving right, and
1673 // positive when the object is moving left. The value goes to -1 or 1 when
1674 // the object just moves out of the view completely. The value is 0 if the
1675 // object currently fills the view.
1676 private static float calculateMoveOutProgress(int left, int right,
1679 // viewWidth = view width
1680 int w = right - left;
1682 // If the object width is smaller than the view width,
1684 // |<-->| progress = -1 when left = viewWidth
1685 // |<-->| progress = 0 when left = viewWidth / 2 - w / 2
1686 // |<-->| progress = 1 when left = -w
1687 if (w < viewWidth) {
1688 int zx = viewWidth / 2 - w / 2;
1690 return -(left - zx) / (float) (viewWidth - zx); // progress = (0, -1]
1692 return (left - zx) / (float) (-w - zx); // progress = [0, 1]
1696 // If the object width is larger than the view width,
1698 // |<--------->| progress = -1 when left = viewWidth
1699 // |<--------->| progress = 0 between left = 0
1700 // |<--------->| and right = viewWidth
1701 // |<--------->| progress = 1 when right = 0
1703 return -left / (float) viewWidth;
1706 if (right < viewWidth) {
1707 return (viewWidth - right) / (float) viewWidth;
1713 // Maps a scrolling progress value to the alpha factor in the fading
1715 private float getScrollAlpha(float scrollProgress) {
1716 return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation(
1717 1 - Math.abs(scrollProgress)) : 1.0f;
1720 // Maps a scrolling progress value to the scaling factor in the fading
1722 private float getScrollScale(float scrollProgress) {
1723 float interpolatedProgress = mScaleInterpolator.getInterpolation(
1724 Math.abs(scrollProgress));
1725 float scale = (1 - interpolatedProgress) +
1726 interpolatedProgress * TRANSITION_SCALE_FACTOR;
1731 // This interpolator emulates the rate at which the perceived scale of an
1732 // object changes as its distance from a camera increases. When this
1733 // interpolator is applied to a scale animation on a view, it evokes the
1734 // sense that the object is shrinking due to moving away from the camera.
1735 private static class ZInterpolator {
1736 private float focalLength;
1738 public ZInterpolator(float foc) {
1742 public float getInterpolation(float input) {
1743 return (1.0f - focalLength / (focalLength + input)) /
1744 (1.0f - focalLength / (focalLength + 1.0f));
1748 // Returns an interpolated value for the page/film transition.
1749 // When ratio = 0, the result is from.
1750 // When ratio = 1, the result is to.
1751 private static float interpolate(float ratio, float from, float to) {
1752 return from + (to - from) * ratio * ratio;
1755 // Returns the alpha factor in film mode if a picture is not in the center.
1756 // The 0.03 lower bound is to make the item always visible a bit.
1757 private float getOffsetAlpha(float offset) {
1759 float alpha = (offset > 0) ? (1 - offset) : (1 + offset);
1760 return Utils.clamp(alpha, 0.03f, 1f);
1763 ////////////////////////////////////////////////////////////////////////////
1764 // Simple public utilities
1765 ////////////////////////////////////////////////////////////////////////////
1767 public void setListener(Listener listener) {
1768 mListener = listener;
1771 public Rect getPhotoRect(int index) {
1772 return mPositionController.getPosition(index);
1775 public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) {
1776 Rect location = new Rect();
1777 Utils.assertTrue(root.getBoundsOf(this, location));
1779 Rect fullRect = bounds();
1780 PhotoFallbackEffect effect = new PhotoFallbackEffect();
1781 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
1782 MediaItem item = mModel.getMediaItem(i);
1783 if (item == null) continue;
1784 ScreenNail sc = mModel.getScreenNail(i);
1785 if (!(sc instanceof BitmapScreenNail)
1786 || ((BitmapScreenNail) sc).isShowingPlaceholder()) continue;
1788 // Now, sc is BitmapScreenNail and is not showing placeholder
1789 Rect rect = new Rect(getPhotoRect(i));
1790 if (!Rect.intersects(fullRect, rect)) continue;
1791 rect.offset(location.left, location.top);
1793 int width = sc.getWidth();
1794 int height = sc.getHeight();
1796 int rotation = mModel.getImageRotation(i);
1798 if ((rotation % 180) == 0) {
1799 texture = new RawTexture(width, height, true);
1800 canvas.beginRenderTarget(texture);
1801 canvas.translate(width / 2f, height / 2f);
1803 texture = new RawTexture(height, width, true);
1804 canvas.beginRenderTarget(texture);
1805 canvas.translate(height / 2f, width / 2f);
1808 canvas.rotate(rotation, 0, 0, 1);
1809 canvas.translate(-width / 2f, -height / 2f);
1810 sc.draw(canvas, 0, 0, width, height);
1811 canvas.endRenderTarget();
1812 effect.addEntry(item.getPath(), rect, texture);