import android.content.Context;
import android.graphics.Color;
+import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Message;
+import android.util.FloatMath;
import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
import android.view.animation.AccelerateInterpolator;
import com.android.gallery3d.R;
import com.android.gallery3d.app.GalleryActivity;
import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.GalleryUtils;
import com.android.gallery3d.util.RangeArray;
-import java.util.Arrays;
-
public class PhotoView extends GLView {
@SuppressWarnings("unused")
private static final String TAG = "PhotoView";
// not avaiable, width = height = 0.
public void getImageSize(int offset, Size size);
+ // Returns the media item for the specified picture.
+ public MediaItem getMediaItem(int offset);
+
// Returns the rotation for the specified picture.
public int getImageRotation(int offset);
// Returns true if the item is the Camera preview.
public boolean isCamera(int offset);
+
+ // Returns true if the item is the Panorama.
+ public boolean isPanorama(int offset);
+
+ // Returns true if the item is a Video.
+ public boolean isVideo(int offset);
+
+ // Returns true if the item can be deleted.
+ public boolean isDeletable(int offset);
+
+ public static final int LOADING_INIT = 0;
+ public static final int LOADING_COMPLETE = 1;
+ public static final int LOADING_FAIL = 2;
+
+ public int getLoadingState(int offset);
+
+ // When data change happens, we need to decide which MediaItem to focus
+ // on.
+ //
+ // 1. If focus hint path != null, we try to focus on it if we can find
+ // it. This is used for undo a deletion, so we can focus on the
+ // undeleted item.
+ //
+ // 2. Otherwise try to focus on the MediaItem that is currently focused,
+ // if we can find it.
+ //
+ // 3. Otherwise try to focus on the previous MediaItem or the next
+ // MediaItem, depending on the value of focus hint direction.
+ public static final int FOCUS_HINT_NEXT = 0;
+ public static final int FOCUS_HINT_PREVIOUS = 1;
+ public void setFocusHintDirection(int direction);
+ public void setFocusHintPath(Path path);
}
public interface Listener {
public void unlockOrientation();
public void onFullScreenChanged(boolean full);
public void onActionBarAllowed(boolean allowed);
+ public void onActionBarWanted();
+ public void onCurrentImageUpdated();
+ public void onDeleteImage(Path path, int offset);
+ public void onUndoDeleteImage();
+ public void onCommitDeleteImage();
}
- // Here is a graph showing the places we need to lock/unlock device
- // orientation:
+ // The rules about orientation locking:
//
- // +------------+ A +------------+
- // Page mode | Camera |<---| Photo |
- // | [locked] |--->| [unlocked] |
- // +------------+ B +------------+
- // ^ ^
- // | C | D
- // +------------+ +------------+
- // | Camera | | Photo |
- // Film mode | [*] | | [*] |
- // +------------+ +------------+
+ // (1) We need to lock the orientation if we are in page mode camera
+ // preview, so there is no (unwanted) rotation animation when the user
+ // rotates the device.
//
- // In Page mode, we want to lock in Camera because we don't want the system
- // rotation animation. We also want to unlock in Photo because we want to
- // show the system action bar in the right place.
+ // (2) We need to unlock the orientation if we want to show the action bar
+ // because the action bar follows the system orientation.
//
- // We don't show action bar in Film mode, so it's fine for it to be locked
- // or unlocked in Film mode.
+ // The rules about action bar:
//
- // There are four transitions we need to check if we need to
- // lock/unlock. Marked as A to D above and in the code.
-
- private static final int MSG_SHOW_LOADING = 1;
+ // (1) If we are in film mode, we don't show action bar.
+ //
+ // (2) If we go from camera to gallery with capture animation, we show
+ // action bar.
private static final int MSG_CANCEL_EXTRA_SCALING = 2;
private static final int MSG_SWITCH_FOCUS = 3;
private static final int MSG_CAPTURE_ANIMATION_DONE = 4;
-
- private static final long DELAY_SHOW_LOADING = 250; // 250ms;
-
- private static final int LOADING_INIT = 0;
- private static final int LOADING_TIMEOUT = 1;
- private static final int LOADING_COMPLETE = 2;
- private static final int LOADING_FAIL = 3;
+ private static final int MSG_DELETE_ANIMATION_DONE = 5;
+ private static final int MSG_DELETE_DONE = 6;
+ private static final int MSG_HIDE_UNDO_BAR = 7;
private static final int MOVE_THRESHOLD = 256;
private static final float SWIPE_THRESHOLD = 300f;
private static final float DEFAULT_TEXT_SIZE = 20;
private static float TRANSITION_SCALE_FACTOR = 0.74f;
+ private static final int ICON_RATIO = 6;
// whether we want to apply card deck effect in page mode.
private static final boolean CARD_EFFECT = true;
- // Used to calculate the scaling factor for the fading animation.
+ // whether we want to apply offset effect in film mode.
+ private static final boolean OFFSET_EFFECT = true;
+
+ // Used to calculate the scaling factor for the card deck effect.
private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f);
// Used to calculate the alpha factor for the fading animation.
// We keep this many previous ScreenNails. (also this many next ScreenNails)
public static final int SCREEN_NAIL_MAX = 3;
+ // These are constants for the delete gesture.
+ private static final int SWIPE_ESCAPE_VELOCITY = 500; // dp/sec
+ private static final int MAX_DISMISS_VELOCITY = 2000; // dp/sec
+
// The picture entries, the valid index is from -SCREEN_NAIL_MAX to
// SCREEN_NAIL_MAX.
private final RangeArray<Picture> mPictures =
new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX);
+ private Size[] mSizes = new Size[2 * SCREEN_NAIL_MAX + 1];
- private final long mDataVersion[] = new long[2 * SCREEN_NAIL_MAX + 1];
- private final int mFromIndex[] = new int[2 * SCREEN_NAIL_MAX + 1];
-
+ private final MyGestureListener mGestureListener;
private final GestureRecognizer mGestureRecognizer;
private final PositionController mPositionController;
private StringTexture mNoThumbnailText;
private TileImageView mTileView;
private EdgeView mEdgeView;
+ private UndoBarView mUndoBar;
private Texture mVideoPlayIcon;
- private boolean mShowVideoPlayIcon;
- private ProgressSpinner mLoadingSpinner;
-
private SynchronizedHandler mHandler;
- private int mLoadingState = LOADING_COMPLETE;
-
private Point mImageCenter = new Point();
private boolean mCancelExtraScalingPending;
private boolean mFilmMode = false;
private int mDisplayRotation = 0;
private int mCompensation = 0;
- private boolean mFullScreen = true;
- private Rect mCameraNaturalFrame = new Rect();
+ private boolean mFullScreenCamera;
+ private Rect mCameraRelativeFrame = new Rect();
private Rect mCameraRect = new Rect();
// [mPrevBound, mNextBound] is the range of index for all pictures in the
private int mHolding;
private static final int HOLD_TOUCH_DOWN = 1;
private static final int HOLD_CAPTURE_ANIMATION = 2;
+ private static final int HOLD_DELETE = 4;
+
+ // mTouchBoxIndex is the index of the box that is touched by the down
+ // gesture in film mode. The value Integer.MAX_VALUE means no box was
+ // touched.
+ private int mTouchBoxIndex = Integer.MAX_VALUE;
+ // Whether the box indicated by mTouchBoxIndex is deletable. Only meaningful
+ // if mTouchBoxIndex is not Integer.MAX_VALUE.
+ private boolean mTouchBoxDeletable;
+ // This is the index of the last deleted item. This is only used as a hint
+ // to hide the undo button when we are too far away from the deleted
+ // item. The value Integer.MAX_VALUE means there is no such hint.
+ private int mUndoIndexHint = Integer.MAX_VALUE;
public PhotoView(GalleryActivity activity) {
mTileView = new TileImageView(activity);
Context context = activity.getAndroidContext();
mEdgeView = new EdgeView(context);
addComponent(mEdgeView);
- mLoadingSpinner = new ProgressSpinner(context);
+ mUndoBar = new UndoBarView(context);
+ addComponent(mUndoBar);
+ mUndoBar.setVisibility(GLView.INVISIBLE);
+ mUndoBar.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(GLView v) {
+ mListener.onUndoDeleteImage();
+ }
+ });
mLoadingText = StringTexture.newInstance(
context.getString(R.string.loading),
DEFAULT_TEXT_SIZE, Color.WHITE);
mHandler = new MyHandler(activity.getGLRoot());
- mGestureRecognizer = new GestureRecognizer(
- context, new MyGestureListener());
+ mGestureListener = new MyGestureListener();
+ mGestureRecognizer = new GestureRecognizer(context, mGestureListener);
mPositionController = new PositionController(context,
new PositionController.Listener() {
public void invalidate() {
PhotoView.this.invalidate();
}
- public boolean isHolding() {
- return mHolding != 0;
+ public boolean isHoldingDown() {
+ return (mHolding & HOLD_TOUCH_DOWN) != 0;
+ }
+ public boolean isHoldingDelete() {
+ return (mHolding & HOLD_DELETE) != 0;
}
public void onPull(int offset, int direction) {
mEdgeView.onPull(offset, direction);
}
});
mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
- Arrays.fill(mDataVersion, INVALID_DATA_VERSION);
for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
if (i == 0) {
mPictures.put(i, new FullPicture());
@Override
public void handleMessage(Message message) {
switch (message.what) {
- case MSG_SHOW_LOADING: {
- if (mLoadingState == LOADING_INIT) {
- // We don't need the opening animation
- mPositionController.setOpenAnimationRect(null);
-
- mLoadingSpinner.startAnimation();
- mLoadingState = LOADING_TIMEOUT;
- invalidate();
- }
- break;
- }
case MSG_CANCEL_EXTRA_SCALING: {
mGestureRecognizer.cancelScale();
mPositionController.setExtraScalingRange(false);
captureAnimationDone(message.arg1);
break;
}
+ case MSG_DELETE_ANIMATION_DONE: {
+ // message.obj is the Path of the MediaItem which should be
+ // deleted. message.arg1 is the offset of the image.
+ mListener.onDeleteImage((Path) message.obj, message.arg1);
+ // Normally a box which finishes delete animation will hold
+ // position until the underlying MediaItem is actually
+ // deleted, and HOLD_DELETE will be cancelled that time. In
+ // case the MediaItem didn't actually get deleted in 2
+ // seconds, we will cancel HOLD_DELETE and make it bounce
+ // back.
+
+ // We make sure there is at most one MSG_DELETE_DONE
+ // in the handler.
+ mHandler.removeMessages(MSG_DELETE_DONE);
+ Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
+ mHandler.sendMessageDelayed(m, 2000);
+ break;
+ }
+ case MSG_DELETE_DONE: {
+ if (!mHandler.hasMessages(MSG_DELETE_ANIMATION_DONE)) {
+ mHolding &= ~HOLD_DELETE;
+ snapback();
+ }
+ break;
+ }
+ case MSG_HIDE_UNDO_BAR: {
+ checkHideUndoBar(UNDO_BAR_TIMEOUT);
+ break;
+ }
default: throw new AssertionError(message.what);
}
}
};
- private void updateLoadingState() {
- // Possible transitions of mLoadingState:
- // INIT --> TIMEOUT, COMPLETE, FAIL
- // TIMEOUT --> COMPLETE, FAIL, INIT
- // COMPLETE --> INIT
- // FAIL --> INIT
- if (mModel.getLevelCount() != 0 || mModel.getScreenNail() != null) {
- mHandler.removeMessages(MSG_SHOW_LOADING);
- mLoadingState = LOADING_COMPLETE;
- } else if (mModel.isFailedToLoad()) {
- mHandler.removeMessages(MSG_SHOW_LOADING);
- mLoadingState = LOADING_FAIL;
- // We don't want the opening animation after loading failure
- mPositionController.setOpenAnimationRect(null);
- } else if (mLoadingState != LOADING_INIT) {
- mLoadingState = LOADING_INIT;
- mHandler.removeMessages(MSG_SHOW_LOADING);
- mHandler.sendEmptyMessageDelayed(
- MSG_SHOW_LOADING, DELAY_SHOW_LOADING);
- }
- }
-
////////////////////////////////////////////////////////////////////////////
// Data/Image change notifications
////////////////////////////////////////////////////////////////////////////
- public void notifyDataChange(long[] versions, int prevBound, int nextBound) {
+ public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) {
mPrevBound = prevBound;
mNextBound = nextBound;
- // Check if the data version actually changed.
- boolean changed = false;
- int N = 2 * SCREEN_NAIL_MAX + 1;
- for (int i = 0; i < N; i++) {
- if (versions[i] != mDataVersion[i]) {
- changed = true;
- break;
+ // Update mTouchBoxIndex
+ if (mTouchBoxIndex != Integer.MAX_VALUE) {
+ int k = mTouchBoxIndex;
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ for (int i = 0; i < 2 * SCREEN_NAIL_MAX + 1; i++) {
+ if (fromIndex[i] == k) {
+ mTouchBoxIndex = i - SCREEN_NAIL_MAX;
+ break;
+ }
}
}
- if (!changed) return;
- // Create the mFromIndex array, which records the index where the picture
- // come from. The value Integer.MAX_VALUE means it's a new picture.
- for (int i = 0; i < N; i++) {
- long v = versions[i];
- if (v == INVALID_DATA_VERSION) {
- mFromIndex[i] = Integer.MAX_VALUE;
- continue;
+ // Hide undo button if we are too far away
+ if (mUndoIndexHint != Integer.MAX_VALUE) {
+ if (Math.abs(mUndoIndexHint - mModel.getCurrentIndex()) >= 3) {
+ hideUndoBar();
}
-
- // Try to find the same version number in the old array
- int j;
- for (j = 0; j < N; j++) {
- if (mDataVersion[j] == v) {
- break;
- }
- }
- mFromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE;
}
- // Copy the new data version
- for (int i = 0; i < N; i++) {
- mDataVersion[i] = versions[i];
+ // Update the ScreenNails.
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ Picture p = mPictures.get(i);
+ p.reload();
+ mSizes[i + SCREEN_NAIL_MAX] = p.getSize();
}
+ boolean wasDeleting = mPositionController.hasDeletingBox();
+
// Move the boxes
- mPositionController.moveBox(mFromIndex, mPrevBound < 0, mNextBound > 0,
- mModel.isCamera(0));
+ mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0,
+ mModel.isCamera(0), mSizes);
- // Update the ScreenNails.
for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
- mPictures.get(i).reload();
+ setPictureSize(i);
+ }
+
+ boolean isDeleting = mPositionController.hasDeletingBox();
+
+ // If the deletion is done, make HOLD_DELETE persist for only the time
+ // needed for a snapback animation.
+ if (wasDeleting && !isDeleting) {
+ mHandler.removeMessages(MSG_DELETE_DONE);
+ Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
+ mHandler.sendMessageDelayed(
+ m, PositionController.SNAPBACK_ANIMATION_TIME);
}
invalidate();
}
+ public boolean isDeleting() {
+ return (mHolding & HOLD_DELETE) != 0
+ && mPositionController.hasDeletingBox();
+ }
+
public void notifyImageChange(int index) {
+ if (index == 0) {
+ mListener.onCurrentImageUpdated();
+ }
mPictures.get(index).reload();
+ setPictureSize(index);
invalidate();
}
- @Override
- protected void onOrient(int displayRotation, int compensation) {
- // onLayout will be called soon. We need to change the size and rotation
- // of the Camera ScreenNail, but we don't want it start moving because
- // the view size will be changed soon.
- mDisplayRotation = displayRotation;
- mCompensation = compensation;
- for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
- Picture p = mPictures.get(i);
- if (p.isCamera()) {
- p.updateSize(true);
- }
- }
+ private void setPictureSize(int index) {
+ Picture p = mPictures.get(index);
+ mPositionController.setImageSize(index, p.getSize(),
+ index == 0 && p.isCamera() ? mCameraRect : null);
}
@Override
protected void onLayout(
boolean changeSize, int left, int top, int right, int bottom) {
- mTileView.layout(left, top, right, bottom);
- mEdgeView.layout(left, top, right, bottom);
- updateConstrainedFrame();
+ int w = right - left;
+ int h = bottom - top;
+ mTileView.layout(0, 0, w, h);
+ mEdgeView.layout(0, 0, w, h);
+ mUndoBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ mUndoBar.layout(0, h - mUndoBar.getMeasuredHeight(), w, h);
+
+ GLRoot root = getGLRoot();
+ int displayRotation = root.getDisplayRotation();
+ int compensation = root.getCompensation();
+ if (mDisplayRotation != displayRotation
+ || mCompensation != compensation) {
+ mDisplayRotation = displayRotation;
+ mCompensation = compensation;
+
+ // We need to change the size and rotation of the Camera ScreenNail,
+ // but we don't want it to animate because the size doen't actually
+ // change in the eye of the user.
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ Picture p = mPictures.get(i);
+ if (p.isCamera()) {
+ p.forceSize();
+ }
+ }
+ }
+
+ updateCameraRect();
+ mPositionController.setConstrainedFrame(mCameraRect);
if (changeSize) {
mPositionController.setViewSize(getWidth(), getHeight());
}
}
- // Update the constrained frame due to layout change.
- private void updateConstrainedFrame() {
+ // Update the camera rectangle due to layout change or camera relative frame
+ // change.
+ private void updateCameraRect() {
+ // Get the width and height in framework orientation because the given
+ // mCameraRelativeFrame is in that coordinates.
int w = getWidth();
int h = getHeight();
- int rotation = getCameraRotation();
- if (rotation % 180 != 0) {
+ if (mCompensation % 180 != 0) {
int tmp = w;
w = h;
h = tmp;
}
+ int l = mCameraRelativeFrame.left;
+ int t = mCameraRelativeFrame.top;
+ int r = mCameraRelativeFrame.right;
+ int b = mCameraRelativeFrame.bottom;
- int l = mCameraNaturalFrame.left;
- int t = mCameraNaturalFrame.top;
- int r = mCameraNaturalFrame.right;
- int b = mCameraNaturalFrame.bottom;
-
- switch (rotation) {
+ // Now convert it to the coordinates we are using.
+ switch (mCompensation) {
case 0: mCameraRect.set(l, t, r, b); break;
case 90: mCameraRect.set(h - b, l, h - t, r); break;
case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break;
case 270: mCameraRect.set(t, w - r, b, w - l); break;
}
- mPositionController.setConstrainedFrame(mCameraRect);
+ Log.d(TAG, "compensation = " + mCompensation
+ + ", CameraRelativeFrame = " + mCameraRelativeFrame
+ + ", mCameraRect = " + mCameraRect);
}
- public void setCameraNaturalFrame(Rect frame) {
- mCameraNaturalFrame.set(frame);
+ public void setCameraRelativeFrame(Rect frame) {
+ mCameraRelativeFrame.set(frame);
+ updateCameraRect();
+ // Originally we do
+ // mPositionController.setConstrainedFrame(mCameraRect);
+ // here, but it is moved to a parameter of the setImageSize() call, so
+ // it can be updated atomically with the CameraScreenNail's size change.
}
// Returns the rotation we need to do to the camera texture before drawing
return (mCompensation - mDisplayRotation + 360) % 360;
}
+ private int getPanoramaRotation() {
+ return mCompensation;
+ }
+
////////////////////////////////////////////////////////////////////////////
// Pictures
////////////////////////////////////////////////////////////////////////////
void draw(GLCanvas canvas, Rect r);
void setScreenNail(ScreenNail s);
boolean isCamera(); // whether the picture is a camera preview
- void updateSize(boolean force); // called when mCompensation changes
+ boolean isDeletable(); // whether the picture can be deleted
+ void forceSize(); // called when mCompensation changes
+ Size getSize();
};
class FullPicture implements Picture {
private int mRotation;
private boolean mIsCamera;
+ private boolean mIsPanorama;
+ private boolean mIsVideo;
+ private boolean mIsDeletable;
+ private int mLoadingState = Model.LOADING_INIT;
+ private Size mSize = new Size();
private boolean mWasCameraCenter;
-
public void FullPicture(TileImageView tileView) {
mTileView = tileView;
}
public void reload() {
// mImageWidth and mImageHeight will get updated
mTileView.notifyModelInvalidated();
- mTileView.setAlpha(1.0f);
mIsCamera = mModel.isCamera(0);
+ mIsPanorama = mModel.isPanorama(0);
+ mIsVideo = mModel.isVideo(0);
+ mIsDeletable = mModel.isDeletable(0);
+ mLoadingState = mModel.getLoadingState(0);
setScreenNail(mModel.getScreenNail(0));
- updateSize(false);
- updateLoadingState();
+ updateSize();
+ }
+
+ @Override
+ public Size getSize() {
+ return mSize;
}
@Override
- public void updateSize(boolean force) {
- if (mIsCamera) {
+ public void forceSize() {
+ updateSize();
+ mPositionController.forceImageSize(0, mSize);
+ }
+
+ private void updateSize() {
+ if (mIsPanorama) {
+ mRotation = getPanoramaRotation();
+ } else if (mIsCamera) {
mRotation = getCameraRotation();
} else {
mRotation = mModel.getImageRotation(0);
int w = mTileView.mImageWidth;
int h = mTileView.mImageHeight;
- mPositionController.setImageSize(0,
- getRotated(mRotation, w, h),
- getRotated(mRotation, h, w),
- force);
+ mSize.width = getRotated(mRotation, w, h);
+ mSize.height = getRotated(mRotation, h, w);
}
@Override
public void draw(GLCanvas canvas, Rect r) {
- boolean isCenter = mPositionController.isCenter();
-
- if (mLoadingState == LOADING_COMPLETE) {
- setTileViewPosition(r);
- PhotoView.super.render(canvas);
- }
- renderMessage(canvas, r.centerX(), r.centerY());
-
- if (mIsCamera) {
- boolean full = !mFilmMode && isCenter
- && mPositionController.isAtMinimalScale();
- if (full != mFullScreen) {
- mFullScreen = full;
- mListener.onFullScreenChanged(full);
- }
- }
+ drawTileView(canvas, r);
// We want to have the following transitions:
// (1) Move camera preview out of its place: switch to film mode
// Holdings except touch-down prevent the transitions.
if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return;
+ boolean isCenter = mPositionController.isCenter();
boolean isCameraCenter = mIsCamera && isCenter;
if (mWasCameraCenter && mIsCamera && !isCenter && !mFilmMode) {
setFilmMode(false);
}
- if (isCenter && !mFilmMode) {
- if (mIsCamera) {
- // move into camera, lock
- mListener.lockOrientation(); // Transition A
- } else {
- // move out of camera, unlock
- mListener.unlockOrientation(); // Transition B
- }
+ if (isCameraCenter && !mFilmMode) {
+ // Move into camera in page mode, lock
+ mListener.lockOrientation();
}
mWasCameraCenter = isCameraCenter;
return mIsCamera;
}
- private void setTileViewPosition(Rect r) {
- TileImageView t = mTileView;
+ @Override
+ public boolean isDeletable() {
+ return mIsDeletable;
+ }
- // Find out the bitmap coordinates of the center of the view
- int imageW = mPositionController.getImageWidth();
- int imageH = mPositionController.getImageHeight();
- float scale = mPositionController.getImageScale();
+ private void drawTileView(GLCanvas canvas, Rect r) {
+ float imageScale = mPositionController.getImageScale();
int viewW = getWidth();
int viewH = getHeight();
- int centerX = (int) (imageW / 2f +
- (viewW / 2f - r.exactCenterX()) / scale + 0.5f);
- int centerY = (int) (imageH / 2f +
- (viewH / 2f - r.exactCenterY()) / scale + 0.5f);
-
- boolean wantsCardEffect = CARD_EFFECT && !mFilmMode
- && !mIsCamera && !mPictures.get(-1).isCamera();
+ float cx = r.exactCenterX();
+ float cy = r.exactCenterY();
+ float scale = 1f; // the scaling factor due to card effect
+
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
+ float filmRatio = mPositionController.getFilmRatio();
+ boolean wantsCardEffect = CARD_EFFECT && !mIsCamera
+ && filmRatio != 1f && !mPictures.get(-1).isCamera()
+ && !mPositionController.inOpeningAnimation();
+ boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
+ && filmRatio == 1f && r.centerY() != viewH / 2;
if (wantsCardEffect) {
// Calculate the move-out progress value.
int left = r.left;
// We only want to apply the fading animation if the scrolling
// movement is to the right.
if (progress < 0) {
- if (right - left < viewW) {
+ scale = getScrollScale(progress);
+ float alpha = getScrollAlpha(progress);
+ scale = interpolate(filmRatio, scale, 1f);
+ alpha = interpolate(filmRatio, alpha, 1f);
+
+ imageScale *= scale;
+ canvas.multiplyAlpha(alpha);
+
+ float cxPage; // the cx value in page mode
+ if (right - left <= viewW) {
// If the picture is narrower than the view, keep it at
// the center of the view.
- centerX = imageW / 2;
+ cxPage = viewW / 2f;
} else {
// If the picture is wider than the view (it's
// zoomed-in), keep the left edge of the object align
// the the left edge of the view.
- centerX = Math.round(viewW / 2f / scale);
+ cxPage = (right - left) * scale / 2f;
}
- scale *= getScrollScale(progress);
- t.setAlpha(getScrollAlpha(progress));
+ cx = interpolate(filmRatio, cxPage, cx);
}
+ } else if (wantsOffsetEffect) {
+ float offset = (float) (r.centerY() - viewH / 2) / viewH;
+ float alpha = getOffsetAlpha(offset);
+ canvas.multiplyAlpha(alpha);
}
- // set the position of the tile view
- int inverseX = imageW - centerX;
- int inverseY = imageH - centerY;
- int rotation = mRotation;
- switch (rotation) {
- case 0: t.setPosition(centerX, centerY, scale, 0); break;
- case 90: t.setPosition(centerY, inverseX, scale, 90); break;
- case 180: t.setPosition(inverseX, inverseY, scale, 180); break;
- case 270: t.setPosition(inverseY, centerX, scale, 270); break;
- default:
- throw new IllegalArgumentException(String.valueOf(rotation));
- }
- }
+ // Draw the tile view.
+ setTileViewPosition(cx, cy, viewW, viewH, imageScale);
+ renderChild(canvas, mTileView);
- private void renderMessage(GLCanvas canvas, int x, int y) {
- // Draw the progress spinner and the text below it
- //
- // (x, y) is where we put the center of the spinner.
- // s is the size of the video play icon, and we use s to layout text
- // because we want to keep the text at the same place when the video
- // play icon is shown instead of the spinner.
- int w = getWidth();
- int h = getHeight();
- int s = Math.min(w, h) / 6;
-
- if (mLoadingState == LOADING_TIMEOUT) {
- StringTexture m = mLoadingText;
- ProgressSpinner p = mLoadingSpinner;
- p.draw(canvas, x - p.getWidth() / 2, y - p.getHeight() / 2);
- m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
- invalidate(); // we need to keep the spinner rotating
- } else if (mLoadingState == LOADING_FAIL) {
- StringTexture m = mNoThumbnailText;
- m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
+ // Draw the play video icon and the message.
+ canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f));
+ int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f);
+ if (mIsVideo) drawVideoPlayIcon(canvas, s);
+ if (mLoadingState == Model.LOADING_FAIL) {
+ drawLoadingFailMessage(canvas);
}
// Draw a debug indicator showing which picture has focus (index ==
// 0).
- // canvas.fillRect(x - 10, y - 10, 20, 20, 0x80FF00FF);
+ //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF);
+
+ canvas.restore();
+ }
+
+ // Set the position of the tile view
+ private void setTileViewPosition(float cx, float cy,
+ int viewW, int viewH, float scale) {
+ // Find out the bitmap coordinates of the center of the view
+ int imageW = mPositionController.getImageWidth();
+ int imageH = mPositionController.getImageHeight();
+ int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f);
+ int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f);
- // Draw the video play icon (in the place where the spinner was)
- if (mShowVideoPlayIcon
- && mLoadingState != LOADING_INIT
- && mLoadingState != LOADING_TIMEOUT) {
- mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s);
+ int inverseX = imageW - centerX;
+ int inverseY = imageH - centerY;
+ int x, y;
+ switch (mRotation) {
+ case 0: x = centerX; y = centerY; break;
+ case 90: x = centerY; y = inverseX; break;
+ case 180: x = inverseX; y = inverseY; break;
+ case 270: x = inverseY; y = centerX; break;
+ default:
+ throw new RuntimeException(String.valueOf(mRotation));
}
+ mTileView.setPosition(x, y, scale, mRotation);
}
}
private int mIndex;
private int mRotation;
private ScreenNail mScreenNail;
- private Size mSize = new Size();
private boolean mIsCamera;
+ private boolean mIsPanorama;
+ private boolean mIsVideo;
+ private boolean mIsDeletable;
+ private int mLoadingState = Model.LOADING_INIT;
+ private Size mSize = new Size();
public ScreenNailPicture(int index) {
mIndex = index;
@Override
public void reload() {
mIsCamera = mModel.isCamera(mIndex);
+ mIsPanorama = mModel.isPanorama(mIndex);
+ mIsVideo = mModel.isVideo(mIndex);
+ mIsDeletable = mModel.isDeletable(mIndex);
+ mLoadingState = mModel.getLoadingState(mIndex);
setScreenNail(mModel.getScreenNail(mIndex));
+ updateSize();
+ }
+
+ @Override
+ public Size getSize() {
+ return mSize;
}
@Override
public void draw(GLCanvas canvas, Rect r) {
if (mScreenNail == null) {
- // Draw a placeholder rectange if there will be a picture in
- // this position.
+ // Draw a placeholder rectange if there should be a picture in
+ // this position (but somehow there isn't).
if (mIndex >= mPrevBound && mIndex <= mNextBound) {
- canvas.fillRect(r.left, r.top, r.width(), r.height(),
- PLACEHOLDER_COLOR);
+ drawPlaceHolder(canvas, r);
}
return;
}
- if (r.left >= getWidth() || r.right <= 0 ||
- r.top >= getHeight() || r.bottom <= 0) {
+ int w = getWidth();
+ int h = getHeight();
+ if (r.left >= w || r.right <= 0 || r.top >= h || r.bottom <= 0) {
mScreenNail.noDraw();
return;
}
- if (mIsCamera && mFullScreen != false) {
- mFullScreen = false;
- mListener.onFullScreenChanged(false);
- }
-
- boolean wantsCardEffect = CARD_EFFECT && !mFilmMode
- && (mIndex > 0) && !mPictures.get(0).isCamera();
-
- int w = getWidth();
- int drawW = getRotated(mRotation, r.width(), r.height());
- int drawH = getRotated(mRotation, r.height(), r.width());
- int cx = wantsCardEffect ? w / 2 : r.centerX();
+ float filmRatio = mPositionController.getFilmRatio();
+ boolean wantsCardEffect = CARD_EFFECT && mIndex > 0
+ && filmRatio != 1f && !mPictures.get(0).isCamera();
+ boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
+ && filmRatio == 1f && r.centerY() != h / 2;
+ int cx = wantsCardEffect
+ ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f)
+ : r.centerX();
int cy = r.centerY();
- int flags = GLCanvas.SAVE_FLAG_MATRIX;
-
- if (wantsCardEffect) flags |= GLCanvas.SAVE_FLAG_ALPHA;
- canvas.save(flags);
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
canvas.translate(cx, cy);
if (wantsCardEffect) {
float progress = (float) (w / 2 - r.centerX()) / w;
progress = Utils.clamp(progress, -1, 1);
float alpha = getScrollAlpha(progress);
float scale = getScrollScale(progress);
+ alpha = interpolate(filmRatio, alpha, 1f);
+ scale = interpolate(filmRatio, scale, 1f);
canvas.multiplyAlpha(alpha);
canvas.scale(scale, scale, 1);
+ } else if (wantsOffsetEffect) {
+ float offset = (float) (r.centerY() - h / 2) / h;
+ float alpha = getOffsetAlpha(offset);
+ canvas.multiplyAlpha(alpha);
}
if (mRotation != 0) {
canvas.rotate(mRotation, 0, 0, 1);
}
+ int drawW = getRotated(mRotation, r.width(), r.height());
+ int drawH = getRotated(mRotation, r.height(), r.width());
mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH);
+ if (isScreenNailAnimating()) {
+ invalidate();
+ }
+ int s = Math.min(drawW, drawH);
+ if (mIsVideo) drawVideoPlayIcon(canvas, s);
+ if (mLoadingState == Model.LOADING_FAIL) {
+ drawLoadingFailMessage(canvas);
+ }
canvas.restore();
}
+ private boolean isScreenNailAnimating() {
+ return (mScreenNail instanceof BitmapScreenNail)
+ && ((BitmapScreenNail) mScreenNail).isAnimating();
+ }
+
@Override
public void setScreenNail(ScreenNail s) {
- if (mScreenNail == s) return;
mScreenNail = s;
- updateSize(false);
}
@Override
- public void updateSize(boolean force) {
- if (mIsCamera) {
+ public void forceSize() {
+ updateSize();
+ mPositionController.forceImageSize(mIndex, mSize);
+ }
+
+ private void updateSize() {
+ if (mIsPanorama) {
+ mRotation = getPanoramaRotation();
+ } else if (mIsCamera) {
mRotation = getCameraRotation();
} else {
mRotation = mModel.getImageRotation(mIndex);
}
- int w = 0, h = 0;
if (mScreenNail != null) {
- w = mScreenNail.getWidth();
- h = mScreenNail.getHeight();
- } else if (mModel != null) {
+ mSize.width = mScreenNail.getWidth();
+ mSize.height = mScreenNail.getHeight();
+ } else {
// If we don't have ScreenNail available, we can still try to
// get the size information of it.
mModel.getImageSize(mIndex, mSize);
- w = mSize.width;
- h = mSize.height;
}
- if (w != 0 && h != 0) {
- mPositionController.setImageSize(mIndex,
- getRotated(mRotation, w, h),
- getRotated(mRotation, h, w),
- force);
- }
+ int w = mSize.width;
+ int h = mSize.height;
+ mSize.width = getRotated(mRotation, w, h);
+ mSize.height = getRotated(mRotation, h, w);
}
@Override
public boolean isCamera() {
return mIsCamera;
}
+
+ @Override
+ public boolean isDeletable() {
+ return mIsDeletable;
+ }
+ }
+
+ // Draw a gray placeholder in the specified rectangle.
+ private void drawPlaceHolder(GLCanvas canvas, Rect r) {
+ canvas.fillRect(r.left, r.top, r.width(), r.height(), PLACEHOLDER_COLOR);
+ }
+
+ // Draw the video play icon (in the place where the spinner was)
+ private void drawVideoPlayIcon(GLCanvas canvas, int side) {
+ int s = side / ICON_RATIO;
+ // Draw the video play icon at the center
+ mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s);
+ }
+
+ // Draw the "no thumbnail" message
+ private void drawLoadingFailMessage(GLCanvas canvas) {
+ StringTexture m = mNoThumbnailText;
+ m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2);
}
private static int getRotated(int degree, int original, int theother) {
private boolean mModeChanged;
// If this scaling gesture should be ignored.
private boolean mIgnoreScalingGesture;
+ // If we have seen a scaling gesture.
+ private boolean mSeenScaling;
+ // whether the down action happened while the view is scrolling.
+ private boolean mDownInScrolling;
+ // If we should ignore all gestures other than onSingleTapUp.
+ private boolean mIgnoreSwipingGesture;
+ // If a scrolling has happened after a down gesture.
+ private boolean mScrolledAfterDown;
+ // If the first scrolling move is in X direction. In the film mode, X
+ // direction scrolling is normal scrolling. but Y direction scrolling is
+ // a delete gesture.
+ private boolean mFirstScrollX;
+ // The accumulated Y delta that has been sent to mPositionController.
+ private int mDeltaY;
+ // The accumulated scaling change from a scaling gesture.
+ private float mAccScale;
@Override
public boolean onSingleTapUp(float x, float y) {
- if (mFilmMode) {
+ // We do this in addition to onUp() because we want the snapback of
+ // setFilmMode to happen.
+ mHolding &= ~HOLD_TOUCH_DOWN;
+
+ if (mFilmMode && !mDownInScrolling) {
+ switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f));
setFilmMode(false);
+ mIgnoreUpEvent = true;
return true;
}
if (mListener != null) {
- mListener.onSingleTapUp((int) x, (int) y);
+ // Do the inverse transform of the touch coordinates.
+ Matrix m = getGLRoot().getCompensationMatrix();
+ Matrix inv = new Matrix();
+ m.invert(inv);
+ float[] pts = new float[] {x, y};
+ inv.mapPoints(pts);
+ mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f));
}
return true;
}
@Override
public boolean onDoubleTap(float x, float y) {
+ if (mIgnoreSwipingGesture) return true;
if (mPictures.get(0).isCamera()) return false;
PositionController controller = mPositionController;
float scale = controller.getImageScale();
}
@Override
- public boolean onScroll(float dx, float dy) {
- mPositionController.startScroll(-dx, -dy);
+ public boolean onScroll(float dx, float dy, float totalX, float totalY) {
+ if (mIgnoreSwipingGesture) return true;
+ if (!mScrolledAfterDown) {
+ mScrolledAfterDown = true;
+ mFirstScrollX = (Math.abs(dx) > Math.abs(dy));
+ }
+
+ int dxi = (int) (-dx + 0.5f);
+ int dyi = (int) (-dy + 0.5f);
+ if (mFilmMode) {
+ if (mFirstScrollX) {
+ mPositionController.scrollFilmX(dxi);
+ } else {
+ if (mTouchBoxIndex == Integer.MAX_VALUE) return true;
+ int newDeltaY = calculateDeltaY(totalY);
+ int d = newDeltaY - mDeltaY;
+ if (d != 0) {
+ mPositionController.scrollFilmY(mTouchBoxIndex, d);
+ mDeltaY = newDeltaY;
+ }
+ }
+ } else {
+ mPositionController.scrollPage(dxi, dyi);
+ }
return true;
}
+ private int calculateDeltaY(float delta) {
+ if (mTouchBoxDeletable) return (int) (delta + 0.5f);
+
+ // don't let items that can't be deleted be dragged more than
+ // maxScrollDistance, and make it harder and harder to drag.
+ int size = getHeight();
+ float maxScrollDistance = 0.15f * size;
+ if (Math.abs(delta) >= size) {
+ delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
+ } else {
+ delta = maxScrollDistance *
+ FloatMath.sin((delta / size) * (float) (Math.PI / 2));
+ }
+ return (int) (delta + 0.5f);
+ }
+
@Override
public boolean onFling(float velocityX, float velocityY) {
+ if (mIgnoreSwipingGesture) return true;
+ if (mSeenScaling) return true;
if (swipeImages(velocityX, velocityY)) {
mIgnoreUpEvent = true;
- } else if (mPositionController.fling(velocityX, velocityY)) {
- mIgnoreUpEvent = true;
+ } else {
+ flingImages(velocityX, velocityY);
}
return true;
}
+ private boolean flingImages(float velocityX, float velocityY) {
+ int vx = (int) (velocityX + 0.5f);
+ int vy = (int) (velocityY + 0.5f);
+ if (!mFilmMode) {
+ return mPositionController.flingPage(vx, vy);
+ }
+ if (Math.abs(velocityX) > Math.abs(velocityY)) {
+ return mPositionController.flingFilmX(vx);
+ }
+ // If we scrolled in Y direction fast enough, treat it as a delete
+ // gesture.
+ if (!mFilmMode || mTouchBoxIndex == Integer.MAX_VALUE
+ || !mTouchBoxDeletable) {
+ return false;
+ }
+ int maxVelocity = (int) GalleryUtils.dpToPixel(MAX_DISMISS_VELOCITY);
+ int escapeVelocity =
+ (int) GalleryUtils.dpToPixel(SWIPE_ESCAPE_VELOCITY);
+ int centerY = mPositionController.getPosition(mTouchBoxIndex)
+ .centerY();
+ boolean fastEnough = (Math.abs(vy) > escapeVelocity)
+ && (Math.abs(vy) > Math.abs(vx))
+ && ((vy > 0) == (centerY > getHeight() / 2));
+ if (fastEnough) {
+ vy = Math.min(vy, maxVelocity);
+ int duration = mPositionController.flingFilmY(mTouchBoxIndex, vy);
+ if (duration >= 0) {
+ mPositionController.setPopFromTop(vy < 0);
+ deleteAfterAnimation(duration);
+ // We reset mTouchBoxIndex, so up() won't check if Y
+ // scrolled far enough to be a delete gesture.
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void deleteAfterAnimation(int duration) {
+ MediaItem item = mModel.getMediaItem(mTouchBoxIndex);
+ if (item == null) return;
+ mListener.onCommitDeleteImage();
+ mUndoIndexHint = mModel.getCurrentIndex() + mTouchBoxIndex;
+ mHolding |= HOLD_DELETE;
+ Message m = mHandler.obtainMessage(MSG_DELETE_ANIMATION_DONE);
+ m.obj = item.getPath();
+ m.arg1 = mTouchBoxIndex;
+ mHandler.sendMessageDelayed(m, duration);
+ }
+
@Override
public boolean onScaleBegin(float focusX, float focusY) {
+ if (mIgnoreSwipingGesture) return true;
// We ignore the scaling gesture if it is a camera preview.
mIgnoreScalingGesture = mPictures.get(0).isCamera();
if (mIgnoreScalingGesture) {
mCanChangeMode = mFilmMode
|| mPositionController.isAtMinimalScale();
mModeChanged = false;
+ mSeenScaling = true;
+ mAccScale = 1f;
return true;
}
@Override
public boolean onScale(float focusX, float focusY, float scale) {
- if (mIgnoreScalingGesture) {
- return true;
- }
+ if (mIgnoreSwipingGesture) return true;
+ if (mIgnoreScalingGesture) return true;
if (mModeChanged) return true;
if (Float.isNaN(scale) || Float.isInfinite(scale)) return false;
- // We wait for the scale change accumulated to a large enough change
- // before reacting to it. Otherwise we may mistakenly treat a
- // zoom-in gesture as zoom-out or vice versa.
- if (scale > 0.99f && scale < 1.01f) return false;
-
int outOfRange = mPositionController.scaleBy(scale, focusX, focusY);
+ // We wait for a large enough scale change before changing mode.
+ // Otherwise we may mistakenly treat a zoom-in gesture as zoom-out
+ // or vice versa.
+ mAccScale *= scale;
+ boolean largeEnough = (mAccScale < 0.97f || mAccScale > 1.03f);
+
// If mode changes, we treat this scaling gesture has ended.
- if (mCanChangeMode) {
+ if (mCanChangeMode && largeEnough) {
if ((outOfRange < 0 && !mFilmMode) ||
(outOfRange > 0 && mFilmMode)) {
stopExtraScalingIfNeeded();
@Override
public void onScaleEnd() {
- if (mIgnoreScalingGesture) {
- return;
- }
+ if (mIgnoreSwipingGesture) return;
+ if (mIgnoreScalingGesture) return;
if (mModeChanged) return;
mPositionController.endScale();
}
}
@Override
- public void onDown() {
+ public void onDown(float x, float y) {
+ checkHideUndoBar(UNDO_BAR_TOUCHED);
+
+ mDeltaY = 0;
+ mSeenScaling = false;
+
+ if (mIgnoreSwipingGesture) return;
+
mHolding |= HOLD_TOUCH_DOWN;
+
+ if (mFilmMode && mPositionController.isScrolling()) {
+ mDownInScrolling = true;
+ mPositionController.stopScrolling();
+ } else {
+ mDownInScrolling = false;
+ }
+
+ mScrolledAfterDown = false;
+ if (mFilmMode) {
+ int xi = (int) (x + 0.5f);
+ int yi = (int) (y + 0.5f);
+ mTouchBoxIndex = mPositionController.hitTest(xi, yi);
+ if (mTouchBoxIndex < mPrevBound || mTouchBoxIndex > mNextBound) {
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ } else {
+ mTouchBoxDeletable =
+ mPictures.get(mTouchBoxIndex).isDeletable();
+ }
+ } else {
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ }
}
@Override
public void onUp() {
+ if (mIgnoreSwipingGesture) return;
+
mHolding &= ~HOLD_TOUCH_DOWN;
mEdgeView.onRelease();
+ // If we scrolled in Y direction far enough, treat it as a delete
+ // gesture.
+ if (mFilmMode && mScrolledAfterDown && !mFirstScrollX
+ && mTouchBoxIndex != Integer.MAX_VALUE) {
+ Rect r = mPositionController.getPosition(mTouchBoxIndex);
+ int h = getHeight();
+ if (Math.abs(r.centerY() - h * 0.5f) > 0.4f * h) {
+ int duration = mPositionController
+ .flingFilmY(mTouchBoxIndex, 0);
+ if (duration >= 0) {
+ mPositionController.setPopFromTop(r.centerY() < h * 0.5f);
+ deleteAfterAnimation(duration);
+ }
+ }
+ }
+
if (mIgnoreUpEvent) {
mIgnoreUpEvent = false;
return;
snapback();
}
+
+ public void setSwipingEnabled(boolean enabled) {
+ mIgnoreSwipingGesture = !enabled;
+ }
+ }
+
+ public void setSwipingEnabled(boolean enabled) {
+ mGestureListener.setSwipingEnabled(enabled);
}
private void setFilmMode(boolean enabled) {
mFilmMode = enabled;
mPositionController.setFilmMode(mFilmMode);
mModel.setNeedFullImage(!enabled);
+ mModel.setFocusHintDirection(
+ mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT);
mListener.onActionBarAllowed(!enabled);
- // If we leave filmstrip mode, we should lock/unlock
- if (!enabled) {
- if (mPictures.get(0).isCamera()) {
- mListener.lockOrientation(); // Transition C
- } else {
- mListener.unlockOrientation(); // Transition D
- }
+ // Move into camera in page mode, lock
+ if (!enabled && mPictures.get(0).isCamera()) {
+ mListener.lockOrientation();
}
}
}
////////////////////////////////////////////////////////////////////////////
+ // Undo Bar
+ ////////////////////////////////////////////////////////////////////////////
+
+ private int mUndoBarState;
+ private static final int UNDO_BAR_SHOW = 1;
+ private static final int UNDO_BAR_TIMEOUT = 2;
+ private static final int UNDO_BAR_TOUCHED = 4;
+
+ public void showUndoBar() {
+ mHandler.removeMessages(MSG_HIDE_UNDO_BAR);
+ mUndoBarState = UNDO_BAR_SHOW;
+ mUndoBar.animateVisibility(GLView.VISIBLE);
+ mHandler.sendEmptyMessageDelayed(MSG_HIDE_UNDO_BAR, 3000);
+ }
+
+ public void hideUndoBar() {
+ mHandler.removeMessages(MSG_HIDE_UNDO_BAR);
+ mListener.onCommitDeleteImage();
+ mUndoBar.animateVisibility(GLView.INVISIBLE);
+ mUndoBarState = 0;
+ mUndoIndexHint = Integer.MAX_VALUE;
+ }
+
+ // Check if the all conditions for hiding the undo bar have been met. The
+ // conditions are: it has been three seconds since last showing, and the
+ // user has touched.
+ private void checkHideUndoBar(int addition) {
+ mUndoBarState |= addition;
+ if (mUndoBarState ==
+ (UNDO_BAR_SHOW | UNDO_BAR_TIMEOUT | UNDO_BAR_TOUCHED)) {
+ hideUndoBar();
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
// Rendering
////////////////////////////////////////////////////////////////////////////
@Override
protected void render(GLCanvas canvas) {
- // Draw next photos
- for (int i = 1; i <= SCREEN_NAIL_MAX; i++) {
- Rect r = mPositionController.getPosition(i);
- mPictures.get(i).draw(canvas, r);
- // In page mode, we draw only one next photo.
- if (!mFilmMode) break;
+ // Check if the camera preview occupies the full screen.
+ boolean full = !mFilmMode && mPictures.get(0).isCamera()
+ && mPositionController.isCenter()
+ && mPositionController.isAtMinimalScale();
+ if (full != mFullScreenCamera) {
+ mFullScreenCamera = full;
+ mListener.onFullScreenChanged(full);
}
- // Draw current photo
- mPictures.get(0).draw(canvas, mPositionController.getPosition(0));
+ // Determine how many photos we need to draw in addition to the center
+ // one.
+ int neighbors;
+ if (mFullScreenCamera) {
+ neighbors = 0;
+ } else {
+ // In page mode, we draw only one previous/next photo. But if we are
+ // doing capture animation, we want to draw all photos.
+ boolean inPageMode = (mPositionController.getFilmRatio() == 0f);
+ boolean inCaptureAnimation =
+ ((mHolding & HOLD_CAPTURE_ANIMATION) != 0);
+ if (inPageMode && !inCaptureAnimation) {
+ neighbors = 1;
+ } else {
+ neighbors = SCREEN_NAIL_MAX;
+ }
+ }
- // Draw previous photos
- for (int i = -1; i >= -SCREEN_NAIL_MAX; i--) {
+ // Draw photos from back to front
+ for (int i = neighbors; i >= -neighbors; i--) {
Rect r = mPositionController.getPosition(i);
mPictures.get(i).draw(canvas, r);
- // In page mode, we draw only one previous photo.
- if (!mFilmMode) break;
}
+ renderChild(canvas, mEdgeView);
+ renderChild(canvas, mUndoBar);
+
mPositionController.advanceAnimation();
checkFocusSwitching();
}
return 0;
}
+ // Switch to the previous or next picture if the hit position is inside
+ // one of their boxes. This runs in main thread.
+ private void switchToHitPicture(int x, int y) {
+ if (mPrevBound < 0) {
+ Rect r = mPositionController.getPosition(-1);
+ if (r.right >= x) {
+ slideToPrevPicture();
+ return;
+ }
+ }
+
+ if (mNextBound > 0) {
+ Rect r = mPositionController.getPosition(1);
+ if (r.left <= x) {
+ slideToNextPicture();
+ return;
+ }
+ }
+ }
+
////////////////////////////////////////////////////////////////////////////
// Page mode focus switching
//
}
private void snapback() {
- if (mHolding != 0) return;
+ if ((mHolding & ~HOLD_DELETE) != 0) return;
if (!snapToNeighborImage()) {
mPositionController.snapback();
}
mPositionController.startCaptureAnimationSlide(-1);
} else if (offset == -1) {
if (mPrevBound >= 0) return false;
+ if (mFilmMode) setFilmMode(false);
+
+ // If we are too far away from the first image (so that we don't
+ // have all the ScreenNails in-between), we go directly without
+ // animation.
+ if (mModel.getCurrentIndex() > SCREEN_NAIL_MAX) {
+ switchToFirstImage();
+ mPositionController.skipToFinalPosition();
+ return true;
+ }
+
switchToFirstImage();
mPositionController.startCaptureAnimationSlide(1);
} else {
}
mHolding |= HOLD_CAPTURE_ANIMATION;
Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0);
- mHandler.sendMessageDelayed(m, 800);
+ mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME);
return true;
}
private void captureAnimationDone(int offset) {
mHolding &= ~HOLD_CAPTURE_ANIMATION;
- if (offset == 1) {
- // move out of camera, unlock
- if (!mFilmMode) {
- // Now the capture animation is done, enable the action bar.
- mListener.onActionBarAllowed(true);
- }
+ if (offset == 1 && !mFilmMode) {
+ // Now the capture animation is done, enable the action bar.
+ mListener.onActionBarAllowed(true);
+ mListener.onActionBarWanted();
}
snapback();
}
}
}
+ // Returns an interpolated value for the page/film transition.
+ // When ratio = 0, the result is from.
+ // When ratio = 1, the result is to.
+ private static float interpolate(float ratio, float from, float to) {
+ return from + (to - from) * ratio * ratio;
+ }
+
+ // Returns the alpha factor in film mode if a picture is not in the center.
+ // The 0.03 lower bound is to make the item always visible a bit.
+ private float getOffsetAlpha(float offset) {
+ offset /= 0.5f;
+ float alpha = (offset > 0) ? (1 - offset) : (1 + offset);
+ return Utils.clamp(alpha, 0.03f, 1f);
+ }
+
////////////////////////////////////////////////////////////////////////////
// Simple public utilities
////////////////////////////////////////////////////////////////////////////
mListener = listener;
}
- public void showVideoPlayIcon(boolean show) {
- mShowVideoPlayIcon = show;
+ public Rect getPhotoRect(int index) {
+ return mPositionController.getPosition(index);
+ }
+
+ public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) {
+ Rect location = new Rect();
+ Utils.assertTrue(root.getBoundsOf(this, location));
+
+ Rect fullRect = bounds();
+ PhotoFallbackEffect effect = new PhotoFallbackEffect();
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+ MediaItem item = mModel.getMediaItem(i);
+ if (item == null) continue;
+ ScreenNail sc = mModel.getScreenNail(i);
+ if (!(sc instanceof BitmapScreenNail)
+ || ((BitmapScreenNail) sc).isShowingPlaceholder()) continue;
+
+ // Now, sc is BitmapScreenNail and is not showing placeholder
+ Rect rect = new Rect(getPhotoRect(i));
+ if (!Rect.intersects(fullRect, rect)) continue;
+ rect.offset(location.left, location.top);
+
+ RawTexture texture = new RawTexture(sc.getWidth(), sc.getHeight(), true);
+ canvas.beginRenderTarget(texture);
+ sc.draw(canvas, 0, 0, sc.getWidth(), sc.getHeight());
+ canvas.endRenderTarget();
+ effect.addEntry(item.getPath(), rect, texture);
+ }
+ return effect;
}
}