Add swipe-to-delete gesture.
Change-Id: I992e59702f9dfff17da2f4464e48c9228d42b1b3
diff --git a/res/drawable-hdpi/ic_menu_revert_holo_dark.png b/res/drawable-hdpi/ic_menu_revert_holo_dark.png
new file mode 100644
index 0000000..6165a98
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_revert_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/panel_undo_holo.9.png b/res/drawable-hdpi/panel_undo_holo.9.png
new file mode 100644
index 0000000..2396b26
--- /dev/null
+++ b/res/drawable-hdpi/panel_undo_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_revert_holo_dark.png b/res/drawable-mdpi/ic_menu_revert_holo_dark.png
new file mode 100644
index 0000000..97ee13d
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_revert_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/panel_undo_holo.9.png b/res/drawable-mdpi/panel_undo_holo.9.png
new file mode 100644
index 0000000..291a936
--- /dev/null
+++ b/res/drawable-mdpi/panel_undo_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_revert_holo_dark.png b/res/drawable-xhdpi/ic_menu_revert_holo_dark.png
new file mode 100644
index 0000000..48ff5bc
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_revert_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/panel_undo_holo.9.png b/res/drawable-xhdpi/panel_undo_holo.9.png
new file mode 100644
index 0000000..1dc4927
--- /dev/null
+++ b/res/drawable-xhdpi/panel_undo_holo.9.png
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 529480c..899c1e6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -108,6 +108,12 @@
<string name="cancel">Cancel</string>
<string name="share">Share</string>
+ <!-- The label shown after an image is deleted [CHAR LIMIT=16] -->
+ <string name="deleted">Deleted</string>
+
+ <!-- The label on the button which when clicked will undo a deletion of image [CHAR LIMIT=16]-->
+ <string name="undo">UNDO</string>
+
<!-- String indicating more actions are available -->
<string name="select_all">Select all</string>
<string name="deselect_all">Deselect all</string>
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
index 6a4961e..555ea89 100644
--- a/src/com/android/gallery3d/app/PhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -152,6 +152,8 @@
private boolean mIsPanorama;
private boolean mIsActive;
private boolean mNeedFullImage;
+ private int mFocusHintDirection = FOCUS_HINT_NEXT;
+ private Path mFocusHintPath = null;
public interface DataListener extends LoadingListener {
public void onPhotoChanged(int index, Path item);
@@ -414,8 +416,9 @@
ImageEntry entry = mImageCache.get(item.getPath());
if (entry == null) return null;
- // Create a default ScreenNail if the real one is not available yet.
- if (entry.screenNail == null) {
+ // Create a default ScreenNail if the real one is not available yet,
+ // except for camera that a black screen is better than a gray tile.
+ if (entry.screenNail == null && !isCamera(offset)) {
entry.screenNail = newPlaceholderScreenNail(item);
if (offset == 0) updateTileProvider(entry);
}
@@ -466,6 +469,14 @@
}
@Override
+ public boolean isDeletable(int offset) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ return (item == null)
+ ? false
+ : (item.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0;
+ }
+
+ @Override
public int getLoadingState(int offset) {
ImageEntry entry = mImageCache.get(getPath(mCurrentIndex + offset));
if (entry == null) return LOADING_INIT;
@@ -475,7 +486,7 @@
}
public ScreenNail getScreenNail() {
- return mTileProvider.getScreenNail();
+ return getScreenNail(0);
}
public int getImageHeight() {
@@ -526,6 +537,14 @@
}
}
+ public void setFocusHintDirection(int direction) {
+ mFocusHintDirection = direction;
+ }
+
+ public void setFocusHintPath(Path path) {
+ mFocusHintPath = path;
+ }
+
private void updateTileProvider() {
ImageEntry entry = mImageCache.get(getPath(mCurrentIndex));
if (entry == null) { // in loading
@@ -902,15 +921,7 @@
if (mActiveEnd > mSize) mActiveEnd = mSize;
}
- if (info.indexHint == MediaSet.INDEX_NOT_FOUND) {
- // The image has been deleted, clear mItemPath, the
- // mCurrentIndex will be updated in the updateCurrentItem().
- mItemPath = null;
- updateCurrentItem();
- } else {
- mCurrentIndex = info.indexHint;
- }
-
+ mCurrentIndex = info.indexHint;
updateSlidingWindow();
if (info.items != null) {
@@ -922,23 +933,17 @@
if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0;
}
}
- if (mItemPath == null) {
- MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
- mItemPath = current == null ? null : current.getPath();
- }
+
+ // update mItemPath
+ MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
+ mItemPath = current == null ? null : current.getPath();
+
updateImageCache();
updateTileProvider();
updateImageRequests();
fireDataChange();
return null;
}
-
- private void updateCurrentItem() {
- if (mSize == 0) return;
- if (mCurrentIndex >= mSize) {
- mCurrentIndex = mSize - 1;
- }
- }
}
private class ReloadTask extends Thread {
@@ -973,12 +978,49 @@
info.size = mSource.getMediaItemCount();
}
if (!info.reloadContent) continue;
- info.items = mSource.getMediaItem(info.contentStart, info.contentEnd);
- MediaItem item = findCurrentMediaItem(info);
- if (item == null || item.getPath() != info.target) {
- info.indexHint = findIndexOfTarget(info);
+ info.items = mSource.getMediaItem(
+ info.contentStart, info.contentEnd);
+
+ int index = MediaSet.INDEX_NOT_FOUND;
+
+ // First try to focus on the given hint path if there is one.
+ if (mFocusHintPath != null) {
+ index = findIndexOfPathInCache(info, mFocusHintPath);
+ mFocusHintPath = null;
+ }
+
+ // Otherwise try to see if the currently focused item can be found.
+ if (index == MediaSet.INDEX_NOT_FOUND) {
+ MediaItem item = findCurrentMediaItem(info);
+ if (item != null && item.getPath() == info.target) {
+ index = info.indexHint;
+ } else {
+ index = findIndexOfTarget(info);
+ }
+ }
+
+ // The image has been deleted. Focus on the next image (keep
+ // mCurrentIndex unchanged) or the previous image (decrease
+ // mCurrentIndex by 1). In page mode we want to see the next
+ // image, so we focus on the next one. In film mode we want the
+ // later images to shift left to fill the empty space, so we
+ // focus on the previous image (so it will not move). In any
+ // case the index needs to be limited to [0, mSize).
+ if (index == MediaSet.INDEX_NOT_FOUND) {
+ index = info.indexHint;
+ if (mFocusHintDirection == FOCUS_HINT_PREVIOUS
+ && index > 0) {
+ index--;
+ }
+ }
+
+ // Don't change index if mSize == 0
+ if (mSize > 0) {
+ if (index >= mSize) index = mSize - 1;
+ info.indexHint = index;
}
}
+
executeAndWait(new UpdateContent(info));
}
}
@@ -1005,13 +1047,22 @@
// First, try to find the item in the data just loaded
if (items != null) {
- for (int i = 0, n = items.size(); i < n; ++i) {
- if (items.get(i).getPath() == info.target) return i + info.contentStart;
- }
+ int i = findIndexOfPathInCache(info, info.target);
+ if (i != MediaSet.INDEX_NOT_FOUND) return i;
}
// Not found, find it in mSource.
return mSource.getIndexOfItem(info.target, info.indexHint);
}
+
+ private int findIndexOfPathInCache(UpdateInfo info, Path path) {
+ ArrayList<MediaItem> items = info.items;
+ for (int i = 0, n = items.size(); i < n; ++i) {
+ if (items.get(i).getPath() == path) {
+ return i + info.contentStart;
+ }
+ }
+ return MediaSet.INDEX_NOT_FOUND;
+ }
}
}
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index 36ccc67..1d9344b 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -39,11 +39,12 @@
import com.android.gallery3d.R;
import com.android.gallery3d.common.Utils;
import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.FilterDeleteSet;
import com.android.gallery3d.data.MediaDetails;
import com.android.gallery3d.data.MediaItem;
import com.android.gallery3d.data.MediaObject;
import com.android.gallery3d.data.MediaSet;
-import com.android.gallery3d.data.MtpDevice;
+import com.android.gallery3d.data.MtpSource;
import com.android.gallery3d.data.Path;
import com.android.gallery3d.data.SnailAlbum;
import com.android.gallery3d.data.SnailItem;
@@ -106,7 +107,7 @@
// mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied.
// E.g., viewing a photo in gmail attachment
- private MediaSet mMediaSet;
+ private FilterDeleteSet mMediaSet;
private Menu mMenu;
private int mCurrentIndex = 0;
@@ -130,6 +131,10 @@
private boolean mHasActivityResult;
private boolean mTreatBackAsUp;
+ // The item that is deleted (but it can still be undeleted before commiting)
+ private Path mDeletePath;
+ private boolean mDeleteIsFocus; // whether the deleted item was in focus
+
private NfcAdapter mNfcAdapter;
public static interface Model extends PhotoView.Model {
@@ -213,7 +218,9 @@
mShowBars = false;
}
- mMediaSet = mActivity.getDataManager().getMediaSet(mSetPathString);
+ mSetPathString = "/filter/delete/{" + mSetPathString + "}";
+ mMediaSet = (FilterDeleteSet) mActivity.getDataManager()
+ .getMediaSet(mSetPathString);
mSelectionManager.setSourceMediaSet(mMediaSet);
mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
if (mMediaSet == null) {
@@ -369,7 +376,7 @@
if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
return false;
}
- if (mMediaSet instanceof MtpDevice) {
+ if (MtpSource.isMtpPath(mOriginalSetPathString)) {
return false;
}
return true;
@@ -702,6 +709,46 @@
m.sendToTarget();
}
+ // How we do delete/undo:
+ //
+ // When the user choose to delete a media item, we just tell the
+ // FilterDeleteSet to hide that item. If the user choose to undo it, we
+ // again tell FilterDeleteSet not to hide it. If the user choose to commit
+ // the deletion, we then actually delete the media item.
+ @Override
+ public void onDeleteImage(Path path, int offset) {
+ commitDeletion(); // commit the previous deletion
+ mDeletePath = path;
+ mDeleteIsFocus = (offset == 0);
+ mMediaSet.setDeletion(path, mCurrentIndex + offset);
+ mPhotoView.showUndoButton(true);
+ }
+
+ @Override
+ public void onUndoDeleteImage() {
+ // If the deletion was done on the focused item, we want the model to
+ // focus on it when it is undeleted.
+ if (mDeleteIsFocus) mModel.setFocusHintPath(mDeletePath);
+ mMediaSet.setDeletion(null, 0);
+ mDeletePath = null;
+ mPhotoView.showUndoButton(false);
+ }
+
+ @Override
+ public void onCommitDeleteImage() {
+ if (mDeletePath == null) return;
+ commitDeletion();
+ mPhotoView.showUndoButton(false);
+ }
+
+ private void commitDeletion() {
+ if (mDeletePath == null) return;
+ mSelectionManager.deSelectAll();
+ mSelectionManager.toggle(mDeletePath);
+ mMenuExecutor.onMenuClicked(R.id.action_delete, null, true, false);
+ mDeletePath = null;
+ }
+
public static void playVideo(Activity activity, Uri uri, String title) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW)
@@ -808,6 +855,7 @@
mHandler.removeMessages(MSG_HIDE_BARS);
mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
+ onCommitDeleteImage();
mMenuExecutor.pause();
}
diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
index 111333e..f5b22d1 100644
--- a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
@@ -206,6 +206,11 @@
}
@Override
+ public boolean isDeletable(int offset) {
+ return (mItem.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0;
+ }
+
+ @Override
public MediaItem getMediaItem(int offset) {
return offset == 0 ? mItem : null;
}
@@ -221,6 +226,16 @@
}
@Override
+ public void setFocusHintDirection(int direction) {
+ // ignore
+ }
+
+ @Override
+ public void setFocusHintPath(Path path) {
+ // ignore
+ }
+
+ @Override
public int getLoadingState(int offset) {
return mLoadingState;
}
diff --git a/src/com/android/gallery3d/data/FilterDeleteSet.java b/src/com/android/gallery3d/data/FilterDeleteSet.java
new file mode 100644
index 0000000..fc94eb8
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterDeleteSet.java
@@ -0,0 +1,115 @@
+/*
+ * 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.data;
+
+import java.util.ArrayList;
+
+// FilterDeleteSet filters a base MediaSet to remove a deletion item. The user
+// can use the following method to change the deletion item:
+//
+// void setDeletion(Path path, int index);
+//
+// If the path is null, there is no deletion item.
+public class FilterDeleteSet extends MediaSet implements ContentListener {
+ private static final String TAG = "FilterDeleteSet";
+
+ private final MediaSet mBaseSet;
+ private Path mDeletionPath;
+ private int mDeletionIndexHint;
+ private boolean mNewDeletionSettingPending = false;
+
+ // This is set to true or false in reload(), so we know if the given
+ // mDelectionPath is still in the mBaseSet, and if so we can adjust the
+ // index and items.
+ private boolean mDeletionInEffect;
+ private int mDeletionIndex;
+
+ public FilterDeleteSet(Path path, MediaSet baseSet) {
+ super(path, INVALID_DATA_VERSION);
+ mBaseSet = baseSet;
+ mBaseSet.addContentListener(this);
+ }
+
+ @Override
+ public String getName() {
+ return mBaseSet.getName();
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ if (mDeletionInEffect) {
+ return mBaseSet.getMediaItemCount() - 1;
+ } else {
+ return mBaseSet.getMediaItemCount();
+ }
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ if (!mDeletionInEffect || mDeletionIndex >= start + count) {
+ return mBaseSet.getMediaItem(start, count);
+ }
+ if (mDeletionIndex < start) {
+ return mBaseSet.getMediaItem(start + 1, count);
+ }
+ ArrayList<MediaItem> base = mBaseSet.getMediaItem(start, count + 1);
+ base.remove(mDeletionIndex - start);
+ return base;
+ }
+
+ @Override
+ public long reload() {
+ boolean newData = mBaseSet.reload() > mDataVersion;
+ if (!newData && !mNewDeletionSettingPending) return mDataVersion;
+ mNewDeletionSettingPending = false;
+ mDeletionInEffect = false;
+ if (mDeletionPath != null) {
+ // See if mDeletionPath can be found in the MediaSet. We don't want
+ // to search the whole mBaseSet, so we just search a small window
+ // that is close the the index hint.
+ int n = mBaseSet.getMediaItemCount();
+ int from = Math.max(mDeletionIndexHint - 5, 0);
+ int to = Math.min(mDeletionIndexHint + 5, n);
+ ArrayList<MediaItem> items = mBaseSet.getMediaItem(from, to - from);
+ for (int i = 0; i < items.size(); i++) {
+ MediaItem item = items.get(i);
+ if (item != null && item.getPath() == mDeletionPath) {
+ mDeletionInEffect = true;
+ mDeletionIndex = i + from;
+ }
+ }
+ // We cannot find this path. Set it to null to avoid further search.
+ if (!mDeletionInEffect) {
+ mDeletionPath = null;
+ }
+ }
+ mDataVersion = nextVersionNumber();
+ return mDataVersion;
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ public void setDeletion(Path path, int indexHint) {
+ mDeletionPath = path;
+ mDeletionIndexHint = indexHint;
+ mNewDeletionSettingPending = true;
+ notifyContentChanged();
+ }
+}
diff --git a/src/com/android/gallery3d/data/FilterSource.java b/src/com/android/gallery3d/data/FilterSource.java
index d1a04c9..b3e6ee3 100644
--- a/src/com/android/gallery3d/data/FilterSource.java
+++ b/src/com/android/gallery3d/data/FilterSource.java
@@ -21,6 +21,7 @@
class FilterSource extends MediaSource {
private static final String TAG = "FilterSource";
private static final int FILTER_BY_MEDIATYPE = 0;
+ private static final int FILTER_BY_DELETE = 1;
private GalleryApp mApplication;
private PathMatcher mMatcher;
@@ -30,21 +31,28 @@
mApplication = application;
mMatcher = new PathMatcher();
mMatcher.add("/filter/mediatype/*/*", FILTER_BY_MEDIATYPE);
+ mMatcher.add("/filter/delete/*", FILTER_BY_DELETE);
}
- // The name we accept is:
- // /filter/mediatype/k/{set}
- // where k is the media type we want.
+ // The name we accept are:
+ // /filter/mediatype/k/{set} where k is the media type we want.
+ // /filter/delete/{set}
@Override
public MediaObject createMediaObject(Path path) {
int matchType = mMatcher.match(path);
- int mediaType = mMatcher.getIntVar(0);
- String setsName = mMatcher.getVar(1);
DataManager dataManager = mApplication.getDataManager();
- MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
switch (matchType) {
- case FILTER_BY_MEDIATYPE:
- return new FilterSet(path, dataManager, sets[0], mediaType);
+ case FILTER_BY_MEDIATYPE: {
+ int mediaType = mMatcher.getIntVar(0);
+ String setsName = mMatcher.getVar(1);
+ MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+ return new FilterTypeSet(path, dataManager, sets[0], mediaType);
+ }
+ case FILTER_BY_DELETE: {
+ String setsName = mMatcher.getVar(0);
+ MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+ return new FilterDeleteSet(path, sets[0]);
+ }
default:
throw new RuntimeException("bad path: " + path);
}
diff --git a/src/com/android/gallery3d/data/FilterSet.java b/src/com/android/gallery3d/data/FilterTypeSet.java
similarity index 91%
rename from src/com/android/gallery3d/data/FilterSet.java
rename to src/com/android/gallery3d/data/FilterTypeSet.java
index 9cb7e02..1983a39 100644
--- a/src/com/android/gallery3d/data/FilterSet.java
+++ b/src/com/android/gallery3d/data/FilterTypeSet.java
@@ -18,12 +18,10 @@
import java.util.ArrayList;
-// FilterSet filters a base MediaSet according to a condition. Currently the
-// condition is a matching media type. It can be extended to other conditions
-// if needed.
-public class FilterSet extends MediaSet implements ContentListener {
+// FilterTypeSet filters a base MediaSet according to a matching media type.
+public class FilterTypeSet extends MediaSet implements ContentListener {
@SuppressWarnings("unused")
- private static final String TAG = "FilterSet";
+ private static final String TAG = "FilterTypeSet";
private final DataManager mDataManager;
private final MediaSet mBaseSet;
@@ -31,7 +29,7 @@
private final ArrayList<Path> mPaths = new ArrayList<Path>();
private final ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>();
- public FilterSet(Path path, DataManager dataManager, MediaSet baseSet,
+ public FilterTypeSet(Path path, DataManager dataManager, MediaSet baseSet,
int mediaType) {
super(path, INVALID_DATA_VERSION);
mDataManager = dataManager;
diff --git a/src/com/android/gallery3d/data/MtpSource.java b/src/com/android/gallery3d/data/MtpSource.java
index 683a402..aaf50ad 100644
--- a/src/com/android/gallery3d/data/MtpSource.java
+++ b/src/com/android/gallery3d/data/MtpSource.java
@@ -18,7 +18,7 @@
import com.android.gallery3d.app.GalleryApp;
-class MtpSource extends MediaSource {
+public class MtpSource extends MediaSource {
private static final String TAG = "MtpSource";
private static final int MTP_DEVICESET = 0;
@@ -68,4 +68,8 @@
public void resume() {
mMtpContext.resume();
}
+
+ public static boolean isMtpPath(String s) {
+ return s != null && Path.fromString(s).getPrefix().equals("mtp");
+ }
}
diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java
index bb71312..3924c6e 100644
--- a/src/com/android/gallery3d/ui/GLView.java
+++ b/src/com/android/gallery3d/ui/GLView.java
@@ -50,6 +50,10 @@
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 4a17d43..780c548 100644
--- a/src/com/android/gallery3d/ui/GestureRecognizer.java
+++ b/src/com/android/gallery3d/ui/GestureRecognizer.java
@@ -30,12 +30,12 @@
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 @@
@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 @@
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 7de07e8..3619ca0 100644
--- a/src/com/android/gallery3d/ui/MenuExecutor.java
+++ b/src/com/android/gallery3d/ui/MenuExecutor.java
@@ -58,12 +58,14 @@
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 @@
if (progressMax > 1) {
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
}
- dialog.show();
return dialog;
}
@@ -120,7 +121,7 @@
private void stopTaskAndDismissDialog() {
if (mTask != null) {
- mTask.cancel();
+ if (!mWaitOnStop) mTask.cancel();
mTask.waitDone();
mDialog.dismiss();
mDialog = null;
@@ -185,6 +186,11 @@
}
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 @@
default:
return;
}
- startAction(action, title, listener);
+ startAction(action, title, listener, waitOnStop, showDialog);
}
private class ConfirmDialogListener implements OnClickListener, OnCancelListener {
@@ -285,13 +291,22 @@
}
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 @@
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 a7ecd06..6aace39 100644
--- a/src/com/android/gallery3d/ui/PhotoView.java
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -22,7 +22,9 @@
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.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 @@
// 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 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 @@
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 @@
// 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 @@
// 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 @@
private StringTexture mNoThumbnailText;
private TileImageView mTileView;
private EdgeView mEdgeView;
+ private UndoBarView mUndoBar;
private Texture mVideoPlayIcon;
private SynchronizedHandler mHandler;
@@ -174,6 +212,15 @@
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 @@
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 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 @@
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 @@
mPrevBound = prevBound;
mNextBound = nextBound;
- // Move the boxes
- mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0,
- mModel.isCamera(0));
+ // 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++) {
- mPictures.get(i).reload();
+ 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), mSizes);
+
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
@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 @@
}
@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 @@
} 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 @@
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 @@
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 @@
setFilmMode(false);
}
+ public void showUndoButton(boolean show) {
+ mUndoBar.setVisibility(show ? GLView.VISIBLE : GLView.INVISIBLE);
+ }
+
////////////////////////////////////////////////////////////////////////////
// Rendering
////////////////////////////////////////////////////////////////////////////
@@ -996,6 +1282,9 @@
mPictures.get(i).draw(canvas, r);
}
+ renderChild(canvas, mEdgeView);
+ renderChild(canvas, mUndoBar);
+
mPositionController.advanceAnimation();
checkFocusSwitching();
}
@@ -1106,7 +1395,7 @@
}
private void snapback() {
- if (mHolding != 0) return;
+ if ((mHolding & ~HOLD_DELETE) != 0) return;
if (!snapToNeighborImage()) {
mPositionController.snapback();
}
@@ -1319,6 +1608,14 @@
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 65334d5..2b91fcb 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.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 @@
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 @@
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 @@
// 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 @@
// 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 @@
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 @@
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 @@
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 @@
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);
+ 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;
+ }
+
+ public void scrollPage(int dx, int dy) {
+ 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:
- 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);
- }
- }
-
- private void scrollToPage(int x, int y) {
- Box b = mBoxes.get(0);
-
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
@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 2db2de4..97995c8 100644
--- a/src/com/android/gallery3d/ui/StringTexture.java
+++ b/src/com/android/gallery3d/ui/StringTexture.java
@@ -63,8 +63,10 @@
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 0000000..9ddd1d7
--- /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);
+ }
+}