OSDN Git Service

Add swipe-to-delete gesture.
authorChih-Chung Chang <chihchung@google.com>
Thu, 7 Jun 2012 12:09:13 +0000 (20:09 +0800)
committerChih-Chung Chang <chihchung@google.com>
Mon, 18 Jun 2012 09:59:58 +0000 (17:59 +0800)
Change-Id: I992e59702f9dfff17da2f4464e48c9228d42b1b3

21 files changed:
res/drawable-hdpi/ic_menu_revert_holo_dark.png [new file with mode: 0644]
res/drawable-hdpi/panel_undo_holo.9.png [new file with mode: 0644]
res/drawable-mdpi/ic_menu_revert_holo_dark.png [new file with mode: 0644]
res/drawable-mdpi/panel_undo_holo.9.png [new file with mode: 0644]
res/drawable-xhdpi/ic_menu_revert_holo_dark.png [new file with mode: 0644]
res/drawable-xhdpi/panel_undo_holo.9.png [new file with mode: 0644]
res/values/strings.xml
src/com/android/gallery3d/app/PhotoDataAdapter.java
src/com/android/gallery3d/app/PhotoPage.java
src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
src/com/android/gallery3d/data/FilterDeleteSet.java [new file with mode: 0644]
src/com/android/gallery3d/data/FilterSource.java
src/com/android/gallery3d/data/FilterTypeSet.java [moved from src/com/android/gallery3d/data/FilterSet.java with 91% similarity]
src/com/android/gallery3d/data/MtpSource.java
src/com/android/gallery3d/ui/GLView.java
src/com/android/gallery3d/ui/GestureRecognizer.java
src/com/android/gallery3d/ui/MenuExecutor.java
src/com/android/gallery3d/ui/PhotoView.java
src/com/android/gallery3d/ui/PositionController.java
src/com/android/gallery3d/ui/StringTexture.java
src/com/android/gallery3d/ui/UndoBarView.java [new file with mode: 0644]

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 (file)
index 0000000..6165a98
Binary files /dev/null and b/res/drawable-hdpi/ic_menu_revert_holo_dark.png 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 (file)
index 0000000..2396b26
Binary files /dev/null and b/res/drawable-hdpi/panel_undo_holo.9.png 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 (file)
index 0000000..97ee13d
Binary files /dev/null and b/res/drawable-mdpi/ic_menu_revert_holo_dark.png 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 (file)
index 0000000..291a936
Binary files /dev/null and b/res/drawable-mdpi/panel_undo_holo.9.png 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 (file)
index 0000000..48ff5bc
Binary files /dev/null and b/res/drawable-xhdpi/ic_menu_revert_holo_dark.png 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 (file)
index 0000000..1dc4927
Binary files /dev/null and b/res/drawable-xhdpi/panel_undo_holo.9.png differ
index 529480c..899c1e6 100644 (file)
     <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>
index 6a4961e..555ea89 100644 (file)
@@ -152,6 +152,8 @@ public class PhotoDataAdapter implements PhotoPage.Model {
     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 @@ public class PhotoDataAdapter implements PhotoPage.Model {
         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 @@ public class PhotoDataAdapter implements PhotoPage.Model {
     }
 
     @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 class PhotoDataAdapter implements PhotoPage.Model {
     }
 
     public ScreenNail getScreenNail() {
-        return mTileProvider.getScreenNail();
+        return getScreenNail(0);
     }
 
     public int getImageHeight() {
@@ -526,6 +537,14 @@ public class PhotoDataAdapter implements PhotoPage.Model {
         }
     }
 
+    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 @@ public class PhotoDataAdapter implements PhotoPage.Model {
                 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 @@ public class PhotoDataAdapter implements PhotoPage.Model {
                     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 @@ public class PhotoDataAdapter implements PhotoPage.Model {
                         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 @@ public class PhotoDataAdapter implements PhotoPage.Model {
 
             // 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;
+        }
     }
 }
index 36ccc67..1d9344b 100644 (file)
@@ -39,11 +39,12 @@ import android.widget.Toast;
 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 @@ public class PhotoPage extends ActivityState implements
 
     // 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 @@ public class PhotoPage extends ActivityState implements
     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 @@ public class PhotoPage extends ActivityState implements
                 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 @@ public class PhotoPage extends ActivityState implements
         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 @@ public class PhotoPage extends ActivityState implements
         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 @@ public class PhotoPage extends ActivityState implements
         mHandler.removeMessages(MSG_HIDE_BARS);
         mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
 
+        onCommitDeleteImage();
         mMenuExecutor.pause();
     }
 
index 111333e..f5b22d1 100644 (file)
@@ -206,6 +206,11 @@ public class SinglePhotoDataAdapter extends TileImageViewAdapter
     }
 
     @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 @@ public class SinglePhotoDataAdapter extends TileImageViewAdapter
     }
 
     @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 (file)
index 0000000..fc94eb8
--- /dev/null
@@ -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();
+    }
+}
index d1a04c9..b3e6ee3 100644 (file)
@@ -21,6 +21,7 @@ import com.android.gallery3d.app.GalleryApp;
 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 @@ class FilterSource extends MediaSource {
         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);
         }
@@ -18,12 +18,10 @@ package com.android.gallery3d.data;
 
 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 @@ public class FilterSet extends MediaSet implements ContentListener {
     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;
index 683a402..aaf50ad 100644 (file)
@@ -18,7 +18,7 @@ package com.android.gallery3d.data;
 
 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 @@ class MtpSource extends MediaSource {
     public void resume() {
         mMtpContext.resume();
     }
+
+    public static boolean isMtpPath(String s) {
+        return s != null && Path.fromString(s).getPrefix().equals("mtp");
+    }
 }
index bb71312..3924c6e 100644 (file)
@@ -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();
 
index 4a17d43..780c548 100644 (file)
@@ -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
index 7de07e8..3619ca0 100644 (file)
@@ -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;
index a7ecd06..6aace39 100644 (file)
@@ -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
     ////////////////////////////////////////////////////////////////////////////
index 65334d5..2b91fcb 100644 (file)
@@ -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);
index 2db2de4..97995c8 100644 (file)
@@ -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 (file)
index 0000000..9ddd1d7
--- /dev/null
@@ -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);
+    }
+}