diff options
author | Chih-Chung Chang <chihchung@google.com> | 2012-06-07 20:09:13 +0800 |
---|---|---|
committer | Chih-Chung Chang <chihchung@google.com> | 2012-06-18 17:59:58 +0800 |
commit | 4ddc93e1f61858195a62729e54450502628fe28a (patch) | |
tree | 6a3bb2cc609e01f488b2f1a484be082e0d859272 /src/com/android/gallery3d/ui | |
parent | e2bd0fa7863f5912073bcdee88ebc281b8d125c6 (diff) | |
download | android_packages_apps_Snap-4ddc93e1f61858195a62729e54450502628fe28a.tar.gz android_packages_apps_Snap-4ddc93e1f61858195a62729e54450502628fe28a.tar.bz2 android_packages_apps_Snap-4ddc93e1f61858195a62729e54450502628fe28a.zip |
Add swipe-to-delete gesture.
Change-Id: I992e59702f9dfff17da2f4464e48c9228d42b1b3
Diffstat (limited to 'src/com/android/gallery3d/ui')
-rw-r--r-- | src/com/android/gallery3d/ui/GLView.java | 4 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/GestureRecognizer.java | 9 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/MenuExecutor.java | 28 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/PhotoView.java | 383 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/PositionController.java | 313 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/StringTexture.java | 6 | ||||
-rw-r--r-- | src/com/android/gallery3d/ui/UndoBarView.java | 146 |
7 files changed, 750 insertions, 139 deletions
diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java index bb71312fb..3924c6e9d 100644 --- a/src/com/android/gallery3d/ui/GLView.java +++ b/src/com/android/gallery3d/ui/GLView.java @@ -50,6 +50,10 @@ public class GLView { private static final int FLAG_SET_MEASURED_SIZE = 2; private static final int FLAG_LAYOUT_REQUESTED = 4; + public interface OnClickListener { + void onClick(GLView v); + } + protected final Rect mBounds = new Rect(); protected final Rect mPaddings = new Rect(); diff --git a/src/com/android/gallery3d/ui/GestureRecognizer.java b/src/com/android/gallery3d/ui/GestureRecognizer.java index 4a17d4364..780c548d0 100644 --- a/src/com/android/gallery3d/ui/GestureRecognizer.java +++ b/src/com/android/gallery3d/ui/GestureRecognizer.java @@ -30,12 +30,12 @@ public class GestureRecognizer { public interface Listener { boolean onSingleTapUp(float x, float y); boolean onDoubleTap(float x, float y); - boolean onScroll(float dx, float dy); + boolean onScroll(float dx, float dy, float totalX, float totalY); boolean onFling(float velocityX, float velocityY); boolean onScaleBegin(float focusX, float focusY); boolean onScale(float focusX, float focusY, float scale); void onScaleEnd(); - void onDown(); + void onDown(float x, float y); void onUp(); } @@ -86,7 +86,8 @@ public class GestureRecognizer { @Override public boolean onScroll( MotionEvent e1, MotionEvent e2, float dx, float dy) { - return mListener.onScroll(dx, dy); + return mListener.onScroll( + dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY()); } @Override @@ -119,7 +120,7 @@ public class GestureRecognizer { private class MyDownUpListener implements DownUpDetector.DownUpListener { @Override public void onDown(MotionEvent e) { - mListener.onDown(); + mListener.onDown(e.getX(), e.getY()); } @Override diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java index 7de07e821..3619ca0f6 100644 --- a/src/com/android/gallery3d/ui/MenuExecutor.java +++ b/src/com/android/gallery3d/ui/MenuExecutor.java @@ -58,12 +58,14 @@ public class MenuExecutor { private ProgressDialog mDialog; private Future<?> mTask; + // wait the operation to finish when we want to stop it. + private boolean mWaitOnStop; private final GalleryActivity mActivity; private final SelectionManager mSelectionManager; private final Handler mHandler; - private static ProgressDialog showProgressDialog( + private static ProgressDialog createProgressDialog( Context context, int titleId, int progressMax) { ProgressDialog dialog = new ProgressDialog(context); dialog.setTitle(titleId); @@ -73,7 +75,6 @@ public class MenuExecutor { if (progressMax > 1) { dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); } - dialog.show(); return dialog; } @@ -120,7 +121,7 @@ public class MenuExecutor { private void stopTaskAndDismissDialog() { if (mTask != null) { - mTask.cancel(); + if (!mWaitOnStop) mTask.cancel(); mTask.waitDone(); mDialog.dismiss(); mDialog = null; @@ -185,6 +186,11 @@ public class MenuExecutor { } private void onMenuClicked(int action, ProgressListener listener) { + onMenuClicked(action, listener, false, true); + } + + public void onMenuClicked(int action, ProgressListener listener, + boolean waitOnStop, boolean showDialog) { int title; switch (action) { case R.id.action_select_all: @@ -232,7 +238,7 @@ public class MenuExecutor { default: return; } - startAction(action, title, listener); + startAction(action, title, listener, waitOnStop, showDialog); } private class ConfirmDialogListener implements OnClickListener, OnCancelListener { @@ -285,13 +291,22 @@ public class MenuExecutor { } public void startAction(int action, int title, ProgressListener listener) { + startAction(action, title, listener, false, true); + } + + public void startAction(int action, int title, ProgressListener listener, + boolean waitOnStop, boolean showDialog) { ArrayList<Path> ids = mSelectionManager.getSelected(false); stopTaskAndDismissDialog(); Activity activity = (Activity) mActivity; - mDialog = showProgressDialog(activity, title, ids.size()); + mDialog = createProgressDialog(activity, title, ids.size()); + if (showDialog) { + mDialog.show(); + } MediaOperation operation = new MediaOperation(action, ids, listener); mTask = mActivity.getThreadPool().submit(operation, null); + mWaitOnStop = waitOnStop; } public static String getMimeType(int type) { @@ -358,7 +373,8 @@ public class MenuExecutor { private final int mOperation; private final ProgressListener mListener; - public MediaOperation(int operation, ArrayList<Path> items, ProgressListener listener) { + public MediaOperation(int operation, ArrayList<Path> items, + ProgressListener listener) { mOperation = operation; mItems = items; mListener = listener; diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java index a7ecd062d..6aace393f 100644 --- a/src/com/android/gallery3d/ui/PhotoView.java +++ b/src/com/android/gallery3d/ui/PhotoView.java @@ -22,7 +22,9 @@ 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; @@ -30,6 +32,8 @@ 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; public class PhotoView extends GLView { @@ -77,11 +81,31 @@ public class PhotoView extends GLView { // 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 { @@ -92,6 +116,9 @@ public class PhotoView extends GLView { public void onActionBarAllowed(boolean allowed); public void onActionBarWanted(); public void onCurrentImageUpdated(); + public void onDeleteImage(Path path, int offset); + public void onUndoDeleteImage(); + public void onCommitDeleteImage(); } // The rules about orientation locking: @@ -112,6 +139,8 @@ public class PhotoView extends GLView { 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 int MSG_DELETE_ANIMATION_DONE = 5; + private static final int MSG_DELETE_DONE = 6; private static final int MOVE_THRESHOLD = 256; private static final float SWIPE_THRESHOLD = 300f; @@ -123,7 +152,10 @@ public class PhotoView extends GLView { // 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. @@ -133,10 +165,15 @@ public class PhotoView extends GLView { // 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 MyGestureListener mGestureListener; private final GestureRecognizer mGestureRecognizer; @@ -148,6 +185,7 @@ public class PhotoView extends GLView { private StringTexture mNoThumbnailText; private TileImageView mTileView; private EdgeView mEdgeView; + private UndoBarView mUndoBar; private Texture mVideoPlayIcon; private SynchronizedHandler mHandler; @@ -174,6 +212,15 @@ public class PhotoView extends GLView { 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; public PhotoView(GalleryActivity activity) { mTileView = new TileImageView(activity); @@ -181,6 +228,15 @@ public class PhotoView extends GLView { Context context = activity.getAndroidContext(); mEdgeView = new EdgeView(context); addComponent(mEdgeView); + 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); @@ -198,8 +254,11 @@ public class PhotoView extends GLView { 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); @@ -250,6 +309,31 @@ public class PhotoView extends GLView { 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; + } default: throw new AssertionError(message.what); } } @@ -263,26 +347,69 @@ public class PhotoView extends GLView { mPrevBound = prevBound; mNextBound = nextBound; + // 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; + } + } + } + + // 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(fromIndex, mPrevBound < 0, mNextBound > 0, - mModel.isCamera(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(); } + 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) { @@ -290,6 +417,8 @@ public class PhotoView extends GLView { 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(); @@ -376,7 +505,9 @@ public class PhotoView extends GLView { void draw(GLCanvas canvas, Rect r); void setScreenNail(ScreenNail s); boolean isCamera(); // whether the picture is a camera preview + boolean isDeletable(); // whether the picture can be deleted void forceSize(); // called when mCompensation changes + Size getSize(); }; class FullPicture implements Picture { @@ -384,10 +515,10 @@ public class PhotoView extends GLView { 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; - private int mWidth, mHeight; - public void FullPicture(TileImageView tileView) { mTileView = tileView; } @@ -400,21 +531,21 @@ public class PhotoView extends GLView { mIsCamera = mModel.isCamera(0); mIsPanorama = mModel.isPanorama(0); mIsVideo = mModel.isVideo(0); + mIsDeletable = mModel.isDeletable(0); mLoadingState = mModel.getLoadingState(0); setScreenNail(mModel.getScreenNail(0)); - setSize(); + updateSize(); } - private void setSize() { - updateSize(); - mPositionController.setImageSize(0, mWidth, mHeight, - mIsCamera ? mCameraRect : null); + @Override + public Size getSize() { + return mSize; } @Override public void forceSize() { updateSize(); - mPositionController.forceImageSize(0, mWidth, mHeight); + mPositionController.forceImageSize(0, mSize); } private void updateSize() { @@ -428,8 +559,8 @@ public class PhotoView extends GLView { int w = mTileView.mImageWidth; int h = mTileView.mImageHeight; - mWidth = getRotated(mRotation, w, h); - mHeight = getRotated(mRotation, h, w); + mSize.width = getRotated(mRotation, w, h); + mSize.height = getRotated(mRotation, h, w); } @Override @@ -473,6 +604,11 @@ public class PhotoView extends GLView { return mIsCamera; } + @Override + public boolean isDeletable() { + return mIsDeletable; + } + private void drawTileView(GLCanvas canvas, Rect r) { float imageScale = mPositionController.getImageScale(); int viewW = getWidth(); @@ -486,6 +622,8 @@ public class PhotoView extends GLView { 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; @@ -517,11 +655,15 @@ public class PhotoView extends GLView { } cx = interpolate(filmRatio, cxPage, cx); } + } else if (wantsOffsetEffect) { + float offset = (float) (r.centerY() - viewH / 2) / viewH; + float alpha = getOffsetAlpha(offset); + canvas.multiplyAlpha(alpha); } // Draw the tile view. setTileViewPosition(cx, cy, viewW, viewH, imageScale); - PhotoView.super.render(canvas); + renderChild(canvas, mTileView); // Draw the play video icon and the message. canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f)); @@ -566,12 +708,12 @@ public class PhotoView extends GLView { 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 int mWidth, mHeight; + private Size mSize = new Size(); public ScreenNailPicture(int index) { mIndex = index; @@ -582,9 +724,15 @@ public class PhotoView extends GLView { mIsCamera = mModel.isCamera(mIndex); mIsPanorama = mModel.isPanorama(mIndex); mIsVideo = mModel.isVideo(mIndex); + mIsDeletable = mModel.isDeletable(mIndex); mLoadingState = mModel.getLoadingState(mIndex); setScreenNail(mModel.getScreenNail(mIndex)); - setSize(); + updateSize(); + } + + @Override + public Size getSize() { + return mSize; } @Override @@ -597,8 +745,9 @@ public class PhotoView extends GLView { } 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; } @@ -606,7 +755,8 @@ public class PhotoView extends GLView { float filmRatio = mPositionController.getFilmRatio(); boolean wantsCardEffect = CARD_EFFECT && mIndex > 0 && filmRatio != 1f && !mPictures.get(0).isCamera(); - int w = getWidth(); + 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(); @@ -622,6 +772,10 @@ public class PhotoView extends GLView { 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); @@ -650,15 +804,10 @@ public class PhotoView extends GLView { mScreenNail = s; } - private void setSize() { - updateSize(); - mPositionController.setImageSize(mIndex, mWidth, mHeight, null); - } - @Override public void forceSize() { updateSize(); - mPositionController.forceImageSize(mIndex, mWidth, mHeight); + mPositionController.forceImageSize(mIndex, mSize); } private void updateSize() { @@ -670,26 +819,30 @@ public class PhotoView extends GLView { 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; } - mWidth = getRotated(mRotation, w, h); - mHeight = getRotated(mRotation, h, w); + 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. @@ -736,6 +889,14 @@ public class PhotoView extends GLView { 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; @Override public boolean onSingleTapUp(float x, float y) { @@ -780,23 +941,108 @@ public class PhotoView extends GLView { } @Override - public boolean onScroll(float dx, float dy) { + public boolean onScroll(float dx, float dy, float totalX, float totalY) { if (mIgnoreSwipingGesture) return true; - mPositionController.startScroll(-dx, -dy); + 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 (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; + 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; @@ -881,7 +1127,10 @@ public class PhotoView extends GLView { } @Override - public void onDown() { + public void onDown(float x, float y) { + mDeltaY = 0; + mListener.onCommitDeleteImage(); + if (mIgnoreSwipingGesture) return; mHolding |= HOLD_TOUCH_DOWN; @@ -892,6 +1141,21 @@ public class PhotoView extends GLView { } 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 @@ -901,6 +1165,22 @@ public class PhotoView extends GLView { 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; @@ -923,6 +1203,8 @@ public class PhotoView extends GLView { mFilmMode = enabled; mPositionController.setFilmMode(mFilmMode); mModel.setNeedFullImage(!enabled); + mModel.setFocusHintDirection( + mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT); mListener.onActionBarAllowed(!enabled); // Move into camera in page mode, lock @@ -957,6 +1239,10 @@ public class PhotoView extends GLView { setFilmMode(false); } + public void showUndoButton(boolean show) { + mUndoBar.setVisibility(show ? GLView.VISIBLE : GLView.INVISIBLE); + } + //////////////////////////////////////////////////////////////////////////// // Rendering //////////////////////////////////////////////////////////////////////////// @@ -996,6 +1282,9 @@ public class PhotoView extends GLView { mPictures.get(i).draw(canvas, r); } + renderChild(canvas, mEdgeView); + renderChild(canvas, mUndoBar); + mPositionController.advanceAnimation(); checkFocusSwitching(); } @@ -1106,7 +1395,7 @@ public class PhotoView extends GLView { } private void snapback() { - if (mHolding != 0) return; + if ((mHolding & ~HOLD_DELETE) != 0) return; if (!snapToNeighborImage()) { mPositionController.snapback(); } @@ -1319,6 +1608,14 @@ public class PhotoView extends GLView { 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 //////////////////////////////////////////////////////////////////////////// diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java index 65334d584..2b91fcbfe 100644 --- a/src/com/android/gallery3d/ui/PositionController.java +++ b/src/com/android/gallery3d/ui/PositionController.java @@ -25,6 +25,7 @@ import com.android.gallery3d.common.Utils; import com.android.gallery3d.util.GalleryUtils; import com.android.gallery3d.util.RangeArray; import com.android.gallery3d.util.RangeIntArray; +import com.android.gallery3d.ui.PhotoView.Size; class PositionController { private static final String TAG = "PositionController"; @@ -35,11 +36,13 @@ class PositionController { public static final int IMAGE_AT_BOTTOM_EDGE = 8; public static final int CAPTURE_ANIMATION_TIME = 700; + public static final int SNAPBACK_ANIMATION_TIME = 600; // Special values for animation time. private static final long NO_ANIMATION = -1; private static final long LAST_ANIMATION = -2; + private static final int ANIM_KIND_NONE = -1; private static final int ANIM_KIND_SCROLL = 0; private static final int ANIM_KIND_SCALE = 1; private static final int ANIM_KIND_SNAPBACK = 2; @@ -47,17 +50,26 @@ class PositionController { private static final int ANIM_KIND_ZOOM = 4; private static final int ANIM_KIND_OPENING = 5; private static final int ANIM_KIND_FLING = 6; - private static final int ANIM_KIND_CAPTURE = 7; + private static final int ANIM_KIND_FLING_X = 7; + private static final int ANIM_KIND_DELETE = 8; + private static final int ANIM_KIND_CAPTURE = 9; // Animation time in milliseconds. The order must match ANIM_KIND_* above. + // + // The values for ANIM_KIND_FLING_X does't matter because we use + // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's + // faster for Animatable.advanceAnimation() to calculate the progress + // (always 1). private static final int ANIM_TIME[] = { 0, // ANIM_KIND_SCROLL 50, // ANIM_KIND_SCALE - 600, // ANIM_KIND_SNAPBACK + SNAPBACK_ANIMATION_TIME, // ANIM_KIND_SNAPBACK 400, // ANIM_KIND_SLIDE 300, // ANIM_KIND_ZOOM 400, // ANIM_KIND_OPENING 0, // ANIM_KIND_FLING (the duration is calculated dynamically) + 0, // ANIM_KIND_FLING_X (see the comment above) + 0, // ANIM_KIND_DELETE (the duration is calculated dynamically) CAPTURE_ANIMATION_TIME, // ANIM_KIND_CAPTURE }; @@ -86,10 +98,15 @@ class PositionController { // In addition to the focused box (index == 0). We also keep information // about this many boxes on each side. private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX; + private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1]; private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16); private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12); + // These are constants for the delete gesture. + private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms + private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms + private Listener mListener; private volatile Rect mOpenAnimationRect; @@ -164,9 +181,14 @@ class PositionController { // The output of the PositionController. Available throught getPosition(). private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX); + // The direction of a new picture should appear. New pictures pop from top + // if this value is true, or from bottom if this value is false. + boolean mPopFromTop; + public interface Listener { void invalidate(); - boolean isHolding(); + boolean isHoldingDown(); + boolean isHoldingDelete(); // EdgeView void onPull(int offset, int direction); @@ -174,6 +196,17 @@ class PositionController { void onAbsorb(int velocity, int direction); } + static { + // Initialize the CENTER_OUT_INDEX array. + // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX + // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX + for (int i = 0; i < CENTER_OUT_INDEX.length; i++) { + int j = (i + 1) / 2; + if ((i & 1) == 0) j = -j; + CENTER_OUT_INDEX[i] = j; + } + } + public PositionController(Context context, Listener listener) { mListener = listener; mPageScroller = new FlingScroller(); @@ -234,16 +267,16 @@ class PositionController { snapAndRedraw(); } - public void forceImageSize(int index, int width, int height) { - if (width == 0 || height == 0) return; + public void forceImageSize(int index, Size s) { + if (s.width == 0 || s.height == 0) return; Box b = mBoxes.get(index); - b.mImageW = width; - b.mImageH = height; + b.mImageW = s.width; + b.mImageH = s.height; return; } - public void setImageSize(int index, int width, int height, Rect cFrame) { - if (width == 0 || height == 0) return; + public void setImageSize(int index, Size s, Rect cFrame) { + if (s.width == 0 || s.height == 0) return; boolean needUpdate = false; if (cFrame != null && !mConstrainedFrame.equals(cFrame)) { @@ -251,7 +284,7 @@ class PositionController { mPlatform.updateDefaultXY(); needUpdate = true; } - needUpdate |= setBoxSize(index, width, height, false); + needUpdate |= setBoxSize(index, s.width, s.height, false); if (!needUpdate) return; updateScaleAndGapLimit(); @@ -527,37 +560,31 @@ class PositionController { redraw(); } - public void startScroll(float dx, float dy) { + // Only allow scrolling when we are not currently in an animation or we + // are in some animation with can be interrupted. + private boolean canScroll() { Box b = mBoxes.get(0); - Platform p = mPlatform; - - // Only allow scrolling when we are not currently in an animation or we - // are in some animation with can be interrupted. - if (b.mAnimationStartTime != NO_ANIMATION) { - switch (b.mAnimationKind) { - case ANIM_KIND_SCROLL: - case ANIM_KIND_FLING: - break; - default: - return; - } - } - - int x = p.mCurrentX + (int) (dx + 0.5f); - int y = b.mCurrentY + (int) (dy + 0.5f); - - if (mFilmMode) { - scrollToFilm(x, y); - } else { - scrollToPage(x, y); + if (b.mAnimationStartTime == NO_ANIMATION) return true; + switch (b.mAnimationKind) { + case ANIM_KIND_SCROLL: + case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + return true; } + return false; } - private void scrollToPage(int x, int y) { + public void scrollPage(int dx, int dy) { + if (!canScroll()) return; + Box b = mBoxes.get(0); + Platform p = mPlatform; calculateStableBound(b.mCurrentScale); + int x = p.mCurrentX + dx; + int y = b.mCurrentY + dy; + // Vertical direction: If we have space to move in the vertical // direction, we show the edge effect when scrolling reaches the edge. if (mBoundTop != mBoundBottom) { @@ -585,8 +612,26 @@ class PositionController { startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL); } - private void scrollToFilm(int x, int y) { + public void scrollFilmX(int dx) { + if (!canScroll()) return; + Box b = mBoxes.get(0); + Platform p = mPlatform; + + // Only allow scrolling when we are not currently in an animation or we + // are in some animation with can be interrupted. + if (b.mAnimationStartTime != NO_ANIMATION) { + switch (b.mAnimationKind) { + case ANIM_KIND_SCROLL: + case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + break; + default: + return; + } + } + + int x = p.mCurrentX + dx; // Horizontal direction: we show the edge effect when the scrolling // tries to go left of the first image or go right of the last image. @@ -599,16 +644,19 @@ class PositionController { x = 0; } x += mPlatform.mDefaultX; - startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL); + startAnimation(x, b.mCurrentY, b.mCurrentScale, ANIM_KIND_SCROLL); } - public boolean fling(float velocityX, float velocityY) { - int vx = (int) (velocityX + 0.5f); - int vy = (int) (velocityY + 0.5f); - return mFilmMode ? flingFilm(vx, vy) : flingPage(vx, vy); + public void scrollFilmY(int boxIndex, int dy) { + if (!canScroll()) return; + + Box b = mBoxes.get(boxIndex); + int y = b.mCurrentY + dy; + b.doAnimation(y, b.mCurrentScale, ANIM_KIND_SCROLL); + redraw(); } - private boolean flingPage(int velocityX, int velocityY) { + public boolean flingPage(int velocityX, int velocityY) { Box b = mBoxes.get(0); Platform p = mPlatform; @@ -637,11 +685,12 @@ class PositionController { int targetX = mPageScroller.getFinalX(); int targetY = mPageScroller.getFinalY(); ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration(); - startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING); - return true; + return startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING); } - private boolean flingFilm(int velocityX, int velocityY) { + public boolean flingFilmX(int velocityX) { + if (velocityX == 0) return false; + Box b = mBoxes.get(0); Platform p = mPlatform; @@ -652,17 +701,62 @@ class PositionController { return false; } - if (velocityX == 0) return false; - mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); int targetX = mFilmScroller.getFinalX(); - // This value doesn't matter because we use mFilmScroller.isFinished() - // to decide when to stop. We set this to 0 so it's faster for - // Animatable.advanceAnimation() to calculate the progress (always 1). - ANIM_TIME[ANIM_KIND_FLING] = 0; - startAnimation(targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING); - return true; + return startAnimation( + targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING_X); + } + + // Moves the specified box out of screen. If velocityY is 0, a default + // velocity is used. Returns the time for the duration, or -1 if we cannot + // not do the animation. + public int flingFilmY(int boxIndex, int velocityY) { + Box b = mBoxes.get(boxIndex); + + // Calculate targetY + int h = heightOf(b); + int targetY; + int FUZZY = 3; // TODO: figure out why this is needed. + if (velocityY < 0 || (velocityY == 0 && b.mCurrentY <= 0)) { + targetY = -mViewH / 2 - (h + 1) / 2 - FUZZY; + } else { + targetY = (mViewH + 1) / 2 + h / 2 + FUZZY; + } + + // Calculate duration + int duration; + if (velocityY != 0) { + duration = (int) (Math.abs(targetY - b.mCurrentY) * 1000f + / Math.abs(velocityY)); + duration = Math.min(MAX_DELETE_ANIMATION_DURATION, duration); + } else { + duration = DEFAULT_DELETE_ANIMATION_DURATION; + } + + // Start animation + ANIM_TIME[ANIM_KIND_DELETE] = duration; + if (b.doAnimation(targetY, b.mCurrentScale, ANIM_KIND_DELETE)) { + redraw(); + return duration; + } + return -1; + } + + // Returns the index of the box which contains the given point (x, y) + // Returns Integer.MAX_VALUE if there is no hit. There may be more than + // one box contains the given point, and we want to give priority to the + // one closer to the focused index (0). + public int hitTest(int x, int y) { + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + int j = CENTER_OUT_INDEX[i]; + Rect r = mRects.get(j); + if (r.contains(x, y)) { + return j; + } + } + + return Integer.MAX_VALUE; } //////////////////////////////////////////////////////////////////////////// @@ -697,12 +791,13 @@ class PositionController { redraw(); } - private void startAnimation(int targetX, int targetY, float targetScale, + private boolean startAnimation(int targetX, int targetY, float targetScale, int kind) { boolean changed = false; changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind); changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind); if (changed) redraw(); + return changed; } public void advanceAnimation() { @@ -752,15 +847,11 @@ class PositionController { // Convert the information in mPlatform and mBoxes to mRects, so the user // can get the position of each box by getPosition(). // - // Note the loop index goes from inside-out because each box's X coordinate + // Note we go from center-out because each box's X coordinate // is relative to its anchor box (except the focused box). private void layoutAndSetPosition() { - // layout box 0 (focused box) - convertBoxToRect(0); - for (int i = 1; i <= BOX_MAX; i++) { - // layout box i and -i - convertBoxToRect(i); - convertBoxToRect(-i); + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + convertBoxToRect(CENTER_OUT_INDEX[i]); } //dumpState(); } @@ -770,10 +861,8 @@ class PositionController { Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap); } - dumpRect(0); - for (int i = 1; i <= BOX_MAX; i++) { - dumpRect(i); - dumpRect(-i); + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + dumpRect(CENTER_OUT_INDEX[i]); } for (int i = -BOX_MAX; i <= BOX_MAX; i++) { @@ -854,6 +943,25 @@ class PositionController { b.mCurrentY = 0; b.mCurrentScale = b.mScaleMin; b.mAnimationStartTime = NO_ANIMATION; + b.mAnimationKind = ANIM_KIND_NONE; + } + + // Initialize a box to a given size. + private void initBox(int index, Size size) { + if (size.width == 0 || size.height == 0) { + initBox(index); + return; + } + Box b = mBoxes.get(index); + b.mImageW = size.width; + b.mImageH = size.height; + b.mUseViewSize = false; + b.mScaleMin = getMinimalScale(b); + b.mScaleMax = getMaximalScale(b); + b.mCurrentY = 0; + b.mCurrentScale = b.mScaleMin; + b.mAnimationStartTime = NO_ANIMATION; + b.mAnimationKind = ANIM_KIND_NONE; } // Initialize a gap. This can only be called after the boxes around the gap @@ -904,7 +1012,7 @@ class PositionController { // focused box. constrained indicates whether the focused box should be put // into the constrained frame. public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext, - boolean constrained) { + boolean constrained, Size[] sizes) { //debugMoveBox(fromIndex); mHasPrev = hasPrev; mHasNext = hasNext; @@ -957,7 +1065,7 @@ class PositionController { k++; } mBoxes.put(i, mTempBoxes.get(k++)); - initBox(i); + initBox(i, sizes[i + BOX_MAX]); } // 6. Now give the recycled box a reasonable absolute X position. @@ -977,13 +1085,41 @@ class PositionController { mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX; first = last = 0; } - // Now for those boxes between first and last, just assign the same - // position as the next box. (We can do better, but this should be - // rare). For the boxes before first or after last, we will use a new - // default gap size below. - for (int i = last - 1; i > first; i--) { + // Now for those boxes between first and last, assign their position to + // align to the previous box or the next box with known position. For + // the boxes before first or after last, we will use a new default gap + // size below. + + // Align to the previous box + for (int i = Math.max(0, first + 1); i < last; i++) { if (from.get(i) != Integer.MAX_VALUE) continue; - mBoxes.get(i).mAbsoluteX = mBoxes.get(i + 1).mAbsoluteX; + Box a = mBoxes.get(i - 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + + getDefaultGapSize(i); + if (mPopFromTop) { + b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); + } else { + b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); + } + } + + // Align to the next box + for (int i = Math.min(-1, last - 1); i > first; i--) { + if (from.get(i) != Integer.MAX_VALUE) continue; + Box a = mBoxes.get(i + 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) + - getDefaultGapSize(i); + if (mPopFromTop) { + b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); + } else { + b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); + } } // 7. recycle the gaps that are not used in the new array. @@ -1107,6 +1243,19 @@ class PositionController { return mFilmRatio.mCurrentRatio; } + public void setPopFromTop(boolean top) { + mPopFromTop = top; + } + + public boolean hasDeletingBox() { + for(int i = -BOX_MAX; i <= BOX_MAX; i++) { + if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) { + return true; + } + } + return false; + } + //////////////////////////////////////////////////////////////////////////// // Private utilities //////////////////////////////////////////////////////////////////////////// @@ -1262,6 +1411,8 @@ class PositionController { switch (kind) { case ANIM_KIND_SCROLL: case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + case ANIM_KIND_DELETE: case ANIM_KIND_CAPTURE: progress = 1 - f; // linear break; @@ -1293,7 +1444,7 @@ class PositionController { public boolean startSnapback() { if (mAnimationStartTime != NO_ANIMATION) return false; if (mAnimationKind == ANIM_KIND_SCROLL - && mListener.isHolding()) return false; + && mListener.isHoldingDown()) return false; if (mInScale) return false; Box b = mBoxes.get(0); @@ -1367,9 +1518,9 @@ class PositionController { @Override protected boolean interpolate(float progress) { if (mAnimationKind == ANIM_KIND_FLING) { - return mFilmMode - ? interpolateFlingFilm(progress) - : interpolateFlingPage(progress); + return interpolateFlingPage(progress); + } else if (mAnimationKind == ANIM_KIND_FLING_X) { + return interpolateFlingFilm(progress); } else { return interpolateLinear(progress); } @@ -1469,7 +1620,9 @@ class PositionController { public boolean startSnapback() { if (mAnimationStartTime != NO_ANIMATION) return false; if (mAnimationKind == ANIM_KIND_SCROLL - && mListener.isHolding()) return false; + && mListener.isHoldingDown()) return false; + if (mAnimationKind == ANIM_KIND_DELETE + && mListener.isHoldingDelete()) return false; if (mInScale && this == mBoxes.get(0)) return false; int y = mCurrentY; @@ -1508,13 +1661,6 @@ class PositionController { private boolean doAnimation(int targetY, float targetScale, int kind) { targetScale = clampScale(targetScale); - // If the scaled height is smaller than the view height, force it to be - // in the center. (We do this for height only, not width, because the - // user may want to scroll to the previous/next image.) - if (!mInScale && viewTallerThanScaledImage(targetScale)) { - targetY = 0; - } - if (mCurrentY == targetY && mCurrentScale == targetScale && kind != ANIM_KIND_CAPTURE) { return false; @@ -1542,7 +1688,6 @@ class PositionController { @Override protected boolean interpolate(float progress) { if (mAnimationKind == ANIM_KIND_FLING) { - // Currently a Box can only be flung in page mode. return interpolateFlingPage(progress); } else { return interpolateLinear(progress); diff --git a/src/com/android/gallery3d/ui/StringTexture.java b/src/com/android/gallery3d/ui/StringTexture.java index 2db2de4a2..97995c8a5 100644 --- a/src/com/android/gallery3d/ui/StringTexture.java +++ b/src/com/android/gallery3d/ui/StringTexture.java @@ -63,8 +63,10 @@ class StringTexture extends CanvasTexture { if (isBold) { paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); } - text = TextUtils.ellipsize( - text, paint, lengthLimit, TextUtils.TruncateAt.END).toString(); + if (lengthLimit > 0) { + text = TextUtils.ellipsize( + text, paint, lengthLimit, TextUtils.TruncateAt.END).toString(); + } return newInstance(text, paint); } diff --git a/src/com/android/gallery3d/ui/UndoBarView.java b/src/com/android/gallery3d/ui/UndoBarView.java new file mode 100644 index 000000000..9ddd1d755 --- /dev/null +++ b/src/com/android/gallery3d/ui/UndoBarView.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.view.MotionEvent; + +import com.android.gallery3d.R; +import com.android.gallery3d.util.GalleryUtils; + +public class UndoBarView extends GLView { + private static final String TAG = "UndoBarView"; + + private static final int WHITE = 0xFFFFFFFF; + private static final int GRAY = 0xFFAAAAAA; + + private final NinePatchTexture mPanel; + private final StringTexture mUndoText; + private final StringTexture mDeletedText; + private final ResourceTexture mUndoIcon; + private final int mBarHeight; + private final int mBarMargin; + private final int mUndoTextMargin; + private final int mIconSize; + private final int mIconMargin; + private final int mSeparatorTopMargin; + private final int mSeparatorBottomMargin; + private final int mSeparatorRightMargin; + private final int mSeparatorWidth; + private final int mDeletedTextMargin; + private final int mClickRegion; + + private OnClickListener mOnClickListener; + private boolean mDownOnButton; + + // This is the layout of UndoBarView. The unit is dp. + // + // +-+----+----------------+-+--+----+-+------+--+-+ + // 48 | | | Deleted | | | <- | | UNDO | | | + // +-+----+----------------+-+--+----+-+------+--+-+ + // 4 16 1 12 32 8 16 4 + public UndoBarView(Context context) { + mBarHeight = (int) GalleryUtils.dpToPixel(48); + mBarMargin = (int) GalleryUtils.dpToPixel(4); + mUndoTextMargin = (int) GalleryUtils.dpToPixel(16); + mIconMargin = (int) GalleryUtils.dpToPixel(8); + mIconSize = (int) GalleryUtils.dpToPixel(32); + mSeparatorRightMargin = (int) GalleryUtils.dpToPixel(12); + mSeparatorTopMargin = (int) GalleryUtils.dpToPixel(10); + mSeparatorBottomMargin = (int) GalleryUtils.dpToPixel(10); + mSeparatorWidth = (int) GalleryUtils.dpToPixel(1); + mDeletedTextMargin = (int) GalleryUtils.dpToPixel(16); + + mPanel = new NinePatchTexture(context, R.drawable.panel_undo_holo); + mUndoText = StringTexture.newInstance(context.getString(R.string.undo), + GalleryUtils.dpToPixel(12), GRAY, 0, true); + mDeletedText = StringTexture.newInstance( + context.getString(R.string.deleted), + GalleryUtils.dpToPixel(16), WHITE); + mUndoIcon = new ResourceTexture( + context, R.drawable.ic_menu_revert_holo_dark); + mClickRegion = mBarMargin + mUndoTextMargin + mUndoText.getWidth() + + mIconMargin + mIconSize + mSeparatorRightMargin; + } + + public void setOnClickListener(OnClickListener listener) { + mOnClickListener = listener; + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + setMeasuredSize(0 /* unused */, mBarHeight); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + int w = getWidth(); + int h = getHeight(); + mPanel.draw(canvas, mBarMargin, 0, w - mBarMargin * 2, mBarHeight); + + int x = w - mBarMargin; + int y; + + x -= mUndoTextMargin + mUndoText.getWidth(); + y = (mBarHeight - mUndoText.getHeight()) / 2; + mUndoText.draw(canvas, x, y); + + x -= mIconMargin + mIconSize; + y = (mBarHeight - mIconSize) / 2; + mUndoIcon.draw(canvas, x, y, mIconSize, mIconSize); + + x -= mSeparatorRightMargin + mSeparatorWidth; + y = mSeparatorTopMargin; + canvas.fillRect(x, y, mSeparatorWidth, + mBarHeight - mSeparatorTopMargin - mSeparatorBottomMargin, GRAY); + + x = mBarMargin + mDeletedTextMargin; + y = (mBarHeight - mDeletedText.getHeight()) / 2; + mDeletedText.draw(canvas, x, y); + } + + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mDownOnButton = inUndoButton(event); + break; + case MotionEvent.ACTION_UP: + if (mDownOnButton) { + if (mOnClickListener != null && inUndoButton(event)) { + mOnClickListener.onClick(this); + } + mDownOnButton = false; + } + break; + case MotionEvent.ACTION_CANCEL: + mDownOnButton = false; + break; + } + return true; + } + + // Check if the event is on the right of the separator + private boolean inUndoButton(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + int w = getWidth(); + int h = getHeight(); + return (x >= w - mClickRegion && x < w && y >= 0 && y < h); + } +} |