From 6b891c6a3739f8c49d42f9db6fc76cb92c7c5f25 Mon Sep 17 00:00:00 2001 From: Chih-Chung Chang Date: Thu, 7 Jun 2012 20:09:13 +0800 Subject: Add swipe-to-delete gesture. Change-Id: I992e59702f9dfff17da2f4464e48c9228d42b1b3 --- res/drawable-hdpi/ic_menu_revert_holo_dark.png | Bin 0 -> 436 bytes res/drawable-hdpi/panel_undo_holo.9.png | Bin 0 -> 599 bytes res/drawable-mdpi/ic_menu_revert_holo_dark.png | Bin 0 -> 308 bytes res/drawable-mdpi/panel_undo_holo.9.png | Bin 0 -> 374 bytes res/drawable-xhdpi/ic_menu_revert_holo_dark.png | Bin 0 -> 575 bytes res/drawable-xhdpi/panel_undo_holo.9.png | Bin 0 -> 814 bytes res/values/strings.xml | 6 + .../android/gallery3d/app/PhotoDataAdapter.java | 111 ++++-- src/com/android/gallery3d/app/PhotoPage.java | 56 ++- .../gallery3d/app/SinglePhotoDataAdapter.java | 15 + .../android/gallery3d/data/FilterDeleteSet.java | 115 +++++++ src/com/android/gallery3d/data/FilterSet.java | 137 -------- src/com/android/gallery3d/data/FilterSource.java | 24 +- src/com/android/gallery3d/data/FilterTypeSet.java | 135 ++++++++ src/com/android/gallery3d/data/MtpSource.java | 6 +- src/com/android/gallery3d/ui/GLView.java | 4 + .../android/gallery3d/ui/GestureRecognizer.java | 9 +- src/com/android/gallery3d/ui/MenuExecutor.java | 28 +- src/com/android/gallery3d/ui/PhotoView.java | 383 ++++++++++++++++++--- .../android/gallery3d/ui/PositionController.java | 313 ++++++++++++----- src/com/android/gallery3d/ui/StringTexture.java | 6 +- src/com/android/gallery3d/ui/UndoBarView.java | 146 ++++++++ 22 files changed, 1175 insertions(+), 319 deletions(-) create mode 100644 res/drawable-hdpi/ic_menu_revert_holo_dark.png create mode 100644 res/drawable-hdpi/panel_undo_holo.9.png create mode 100644 res/drawable-mdpi/ic_menu_revert_holo_dark.png create mode 100644 res/drawable-mdpi/panel_undo_holo.9.png create mode 100644 res/drawable-xhdpi/ic_menu_revert_holo_dark.png create mode 100644 res/drawable-xhdpi/panel_undo_holo.9.png create mode 100644 src/com/android/gallery3d/data/FilterDeleteSet.java delete mode 100644 src/com/android/gallery3d/data/FilterSet.java create mode 100644 src/com/android/gallery3d/data/FilterTypeSet.java create mode 100644 src/com/android/gallery3d/ui/UndoBarView.java 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 000000000..6165a98a7 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 index 000000000..2396b2631 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 index 000000000..97ee13da3 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 index 000000000..291a9368d 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 index 000000000..48ff5bcda 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 index 000000000..1dc492792 Binary files /dev/null and b/res/drawable-xhdpi/panel_undo_holo.9.png differ diff --git a/res/values/strings.xml b/res/values/strings.xml index 529480c48..899c1e697 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -108,6 +108,12 @@ Cancel Share + + Deleted + + + UNDO + Select all Deselect all diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java index 6a4961eb2..555ea897c 100644 --- a/src/com/android/gallery3d/app/PhotoDataAdapter.java +++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java @@ -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); } @@ -465,6 +468,14 @@ public class PhotoDataAdapter implements PhotoPage.Model { : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO; } + @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)); @@ -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 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 36ccc6741..1d9344ba3 100644 --- a/src/com/android/gallery3d/app/PhotoPage.java +++ b/src/com/android/gallery3d/app/PhotoPage.java @@ -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(); } diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java index 111333e3b..f5b22d15c 100644 --- a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java +++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java @@ -205,6 +205,11 @@ public class SinglePhotoDataAdapter extends TileImageViewAdapter return mItem.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO; } + @Override + public boolean isDeletable(int offset) { + return (mItem.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0; + } + @Override public MediaItem getMediaItem(int offset) { return offset == 0 ? mItem : null; @@ -220,6 +225,16 @@ public class SinglePhotoDataAdapter extends TileImageViewAdapter // ignore } + @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 000000000..fc94eb8e0 --- /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 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 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 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/FilterSet.java b/src/com/android/gallery3d/data/FilterSet.java deleted file mode 100644 index 9cb7e02ef..000000000 --- a/src/com/android/gallery3d/data/FilterSet.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2010 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; - -// 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 { - @SuppressWarnings("unused") - private static final String TAG = "FilterSet"; - - private final DataManager mDataManager; - private final MediaSet mBaseSet; - private final int mMediaType; - private final ArrayList mPaths = new ArrayList(); - private final ArrayList mAlbums = new ArrayList(); - - public FilterSet(Path path, DataManager dataManager, MediaSet baseSet, - int mediaType) { - super(path, INVALID_DATA_VERSION); - mDataManager = dataManager; - mBaseSet = baseSet; - mMediaType = mediaType; - mBaseSet.addContentListener(this); - } - - @Override - public String getName() { - return mBaseSet.getName(); - } - - @Override - public MediaSet getSubMediaSet(int index) { - return mAlbums.get(index); - } - - @Override - public int getSubMediaSetCount() { - return mAlbums.size(); - } - - @Override - public int getMediaItemCount() { - return mPaths.size(); - } - - @Override - public ArrayList getMediaItem(int start, int count) { - return ClusterAlbum.getMediaItemFromPath( - mPaths, start, count, mDataManager); - } - - @Override - public long reload() { - if (mBaseSet.reload() > mDataVersion) { - updateData(); - mDataVersion = nextVersionNumber(); - } - return mDataVersion; - } - - @Override - public void onContentDirty() { - notifyContentChanged(); - } - - private void updateData() { - // Albums - mAlbums.clear(); - String basePath = "/filter/mediatype/" + mMediaType; - - for (int i = 0, n = mBaseSet.getSubMediaSetCount(); i < n; i++) { - MediaSet set = mBaseSet.getSubMediaSet(i); - String filteredPath = basePath + "/{" + set.getPath().toString() + "}"; - MediaSet filteredSet = mDataManager.getMediaSet(filteredPath); - filteredSet.reload(); - if (filteredSet.getMediaItemCount() > 0 - || filteredSet.getSubMediaSetCount() > 0) { - mAlbums.add(filteredSet); - } - } - - // Items - mPaths.clear(); - final int total = mBaseSet.getMediaItemCount(); - final Path[] buf = new Path[total]; - - mBaseSet.enumerateMediaItems(new MediaSet.ItemConsumer() { - public void consume(int index, MediaItem item) { - if (item.getMediaType() == mMediaType) { - if (index < 0 || index >= total) return; - Path path = item.getPath(); - buf[index] = path; - } - } - }); - - for (int i = 0; i < total; i++) { - if (buf[i] != null) { - mPaths.add(buf[i]); - } - } - } - - @Override - public int getSupportedOperations() { - return SUPPORT_SHARE | SUPPORT_DELETE; - } - - @Override - public void delete() { - ItemConsumer consumer = new ItemConsumer() { - public void consume(int index, MediaItem item) { - if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) { - item.delete(); - } - } - }; - mDataManager.mapMediaItems(mPaths, consumer, 0); - } -} diff --git a/src/com/android/gallery3d/data/FilterSource.java b/src/com/android/gallery3d/data/FilterSource.java index d1a04c995..b3e6ee356 100644 --- a/src/com/android/gallery3d/data/FilterSource.java +++ b/src/com/android/gallery3d/data/FilterSource.java @@ -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); } diff --git a/src/com/android/gallery3d/data/FilterTypeSet.java b/src/com/android/gallery3d/data/FilterTypeSet.java new file mode 100644 index 000000000..1983a39f1 --- /dev/null +++ b/src/com/android/gallery3d/data/FilterTypeSet.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2010 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; + +// 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 = "FilterTypeSet"; + + private final DataManager mDataManager; + private final MediaSet mBaseSet; + private final int mMediaType; + private final ArrayList mPaths = new ArrayList(); + private final ArrayList mAlbums = new ArrayList(); + + public FilterTypeSet(Path path, DataManager dataManager, MediaSet baseSet, + int mediaType) { + super(path, INVALID_DATA_VERSION); + mDataManager = dataManager; + mBaseSet = baseSet; + mMediaType = mediaType; + mBaseSet.addContentListener(this); + } + + @Override + public String getName() { + return mBaseSet.getName(); + } + + @Override + public MediaSet getSubMediaSet(int index) { + return mAlbums.get(index); + } + + @Override + public int getSubMediaSetCount() { + return mAlbums.size(); + } + + @Override + public int getMediaItemCount() { + return mPaths.size(); + } + + @Override + public ArrayList getMediaItem(int start, int count) { + return ClusterAlbum.getMediaItemFromPath( + mPaths, start, count, mDataManager); + } + + @Override + public long reload() { + if (mBaseSet.reload() > mDataVersion) { + updateData(); + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + private void updateData() { + // Albums + mAlbums.clear(); + String basePath = "/filter/mediatype/" + mMediaType; + + for (int i = 0, n = mBaseSet.getSubMediaSetCount(); i < n; i++) { + MediaSet set = mBaseSet.getSubMediaSet(i); + String filteredPath = basePath + "/{" + set.getPath().toString() + "}"; + MediaSet filteredSet = mDataManager.getMediaSet(filteredPath); + filteredSet.reload(); + if (filteredSet.getMediaItemCount() > 0 + || filteredSet.getSubMediaSetCount() > 0) { + mAlbums.add(filteredSet); + } + } + + // Items + mPaths.clear(); + final int total = mBaseSet.getMediaItemCount(); + final Path[] buf = new Path[total]; + + mBaseSet.enumerateMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + if (item.getMediaType() == mMediaType) { + if (index < 0 || index >= total) return; + Path path = item.getPath(); + buf[index] = path; + } + } + }); + + for (int i = 0; i < total; i++) { + if (buf[i] != null) { + mPaths.add(buf[i]); + } + } + } + + @Override + public int getSupportedOperations() { + return SUPPORT_SHARE | SUPPORT_DELETE; + } + + @Override + public void delete() { + ItemConsumer consumer = new ItemConsumer() { + public void consume(int index, MediaItem item) { + if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) { + item.delete(); + } + } + }; + mDataManager.mapMediaItems(mPaths, consumer, 0); + } +} diff --git a/src/com/android/gallery3d/data/MtpSource.java b/src/com/android/gallery3d/data/MtpSource.java index 683a40291..aaf50ad4c 100644 --- a/src/com/android/gallery3d/data/MtpSource.java +++ b/src/com/android/gallery3d/data/MtpSource.java @@ -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"); + } } diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java index bb71312fb..3924c6e9d 100644 --- a/src/com/android/gallery3d/ui/GLView.java +++ b/src/com/android/gallery3d/ui/GLView.java @@ -50,6 +50,10 @@ public class GLView { private static final int FLAG_SET_MEASURED_SIZE = 2; private static final int FLAG_LAYOUT_REQUESTED = 4; + public interface OnClickListener { + void onClick(GLView v); + } + protected final Rect mBounds = new Rect(); protected final Rect mPaddings = new Rect(); diff --git a/src/com/android/gallery3d/ui/GestureRecognizer.java b/src/com/android/gallery3d/ui/GestureRecognizer.java index 4a17d4364..780c548d0 100644 --- a/src/com/android/gallery3d/ui/GestureRecognizer.java +++ b/src/com/android/gallery3d/ui/GestureRecognizer.java @@ -30,12 +30,12 @@ public class GestureRecognizer { public interface Listener { boolean onSingleTapUp(float x, float y); boolean onDoubleTap(float x, float y); - boolean onScroll(float dx, float dy); + boolean onScroll(float dx, float dy, float totalX, float totalY); boolean onFling(float velocityX, float velocityY); boolean onScaleBegin(float focusX, float focusY); boolean onScale(float focusX, float focusY, float scale); void onScaleEnd(); - void onDown(); + void onDown(float x, float y); void onUp(); } @@ -86,7 +86,8 @@ public class GestureRecognizer { @Override public boolean onScroll( MotionEvent e1, MotionEvent e2, float dx, float dy) { - return mListener.onScroll(dx, dy); + return mListener.onScroll( + dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY()); } @Override @@ -119,7 +120,7 @@ public class GestureRecognizer { private class MyDownUpListener implements DownUpDetector.DownUpListener { @Override public void onDown(MotionEvent e) { - mListener.onDown(); + mListener.onDown(e.getX(), e.getY()); } @Override diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java index 7de07e821..3619ca0f6 100644 --- a/src/com/android/gallery3d/ui/MenuExecutor.java +++ b/src/com/android/gallery3d/ui/MenuExecutor.java @@ -58,12 +58,14 @@ public class MenuExecutor { private ProgressDialog mDialog; private Future mTask; + // wait the operation to finish when we want to stop it. + private boolean mWaitOnStop; private final GalleryActivity mActivity; private final SelectionManager mSelectionManager; private final Handler mHandler; - private static ProgressDialog showProgressDialog( + private static ProgressDialog createProgressDialog( Context context, int titleId, int progressMax) { ProgressDialog dialog = new ProgressDialog(context); dialog.setTitle(titleId); @@ -73,7 +75,6 @@ public class MenuExecutor { if (progressMax > 1) { dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); } - dialog.show(); return dialog; } @@ -120,7 +121,7 @@ public class MenuExecutor { private void stopTaskAndDismissDialog() { if (mTask != null) { - mTask.cancel(); + if (!mWaitOnStop) mTask.cancel(); mTask.waitDone(); mDialog.dismiss(); mDialog = null; @@ -185,6 +186,11 @@ public class MenuExecutor { } private void onMenuClicked(int action, ProgressListener listener) { + onMenuClicked(action, listener, false, true); + } + + public void onMenuClicked(int action, ProgressListener listener, + boolean waitOnStop, boolean showDialog) { int title; switch (action) { case R.id.action_select_all: @@ -232,7 +238,7 @@ public class MenuExecutor { default: return; } - startAction(action, title, listener); + startAction(action, title, listener, waitOnStop, showDialog); } private class ConfirmDialogListener implements OnClickListener, OnCancelListener { @@ -285,13 +291,22 @@ public class MenuExecutor { } public void startAction(int action, int title, ProgressListener listener) { + startAction(action, title, listener, false, true); + } + + public void startAction(int action, int title, ProgressListener listener, + boolean waitOnStop, boolean showDialog) { ArrayList 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 items, ProgressListener listener) { + public MediaOperation(int operation, ArrayList items, + ProgressListener listener) { mOperation = operation; mItems = items; mListener = listener; diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java index a7ecd062d..6aace393f 100644 --- a/src/com/android/gallery3d/ui/PhotoView.java +++ b/src/com/android/gallery3d/ui/PhotoView.java @@ -22,7 +22,9 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; import android.os.Message; +import android.util.FloatMath; import android.view.MotionEvent; +import android.view.View.MeasureSpec; import android.view.animation.AccelerateInterpolator; import com.android.gallery3d.R; @@ -30,6 +32,8 @@ import com.android.gallery3d.app.GalleryActivity; import com.android.gallery3d.common.Utils; import com.android.gallery3d.data.MediaItem; import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.util.GalleryUtils; import com.android.gallery3d.util.RangeArray; public class PhotoView extends GLView { @@ -77,11 +81,31 @@ public class PhotoView extends GLView { // Returns true if the item is a Video. public boolean isVideo(int offset); + // Returns true if the item can be deleted. + public boolean isDeletable(int offset); + public static final int LOADING_INIT = 0; public static final int LOADING_COMPLETE = 1; public static final int LOADING_FAIL = 2; public int getLoadingState(int offset); + + // When data change happens, we need to decide which MediaItem to focus + // on. + // + // 1. If focus hint path != null, we try to focus on it if we can find + // it. This is used for undo a deletion, so we can focus on the + // undeleted item. + // + // 2. Otherwise try to focus on the MediaItem that is currently focused, + // if we can find it. + // + // 3. Otherwise try to focus on the previous MediaItem or the next + // MediaItem, depending on the value of focus hint direction. + public static final int FOCUS_HINT_NEXT = 0; + public static final int FOCUS_HINT_PREVIOUS = 1; + public void setFocusHintDirection(int direction); + public void setFocusHintPath(Path path); } public interface Listener { @@ -92,6 +116,9 @@ public class PhotoView extends GLView { public void onActionBarAllowed(boolean allowed); public void onActionBarWanted(); public void onCurrentImageUpdated(); + public void onDeleteImage(Path path, int offset); + public void onUndoDeleteImage(); + public void onCommitDeleteImage(); } // The rules about orientation locking: @@ -112,6 +139,8 @@ public class PhotoView extends GLView { private static final int MSG_CANCEL_EXTRA_SCALING = 2; private static final int MSG_SWITCH_FOCUS = 3; private static final int MSG_CAPTURE_ANIMATION_DONE = 4; + private static final int MSG_DELETE_ANIMATION_DONE = 5; + private static final int MSG_DELETE_DONE = 6; private static final int MOVE_THRESHOLD = 256; private static final float SWIPE_THRESHOLD = 300f; @@ -123,7 +152,10 @@ public class PhotoView extends GLView { // whether we want to apply card deck effect in page mode. private static final boolean CARD_EFFECT = true; - // Used to calculate the scaling factor for the fading animation. + // whether we want to apply offset effect in film mode. + private static final boolean OFFSET_EFFECT = true; + + // Used to calculate the scaling factor for the card deck effect. private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f); // Used to calculate the alpha factor for the fading animation. @@ -133,10 +165,15 @@ public class PhotoView extends GLView { // We keep this many previous ScreenNails. (also this many next ScreenNails) public static final int SCREEN_NAIL_MAX = 3; + // These are constants for the delete gesture. + private static final int SWIPE_ESCAPE_VELOCITY = 500; // dp/sec + private static final int MAX_DISMISS_VELOCITY = 2000; // dp/sec + // The picture entries, the valid index is from -SCREEN_NAIL_MAX to // SCREEN_NAIL_MAX. private final RangeArray mPictures = new RangeArray(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX); + private Size[] mSizes = new Size[2 * SCREEN_NAIL_MAX + 1]; private final MyGestureListener mGestureListener; private final GestureRecognizer mGestureRecognizer; @@ -148,6 +185,7 @@ public class PhotoView extends GLView { private StringTexture mNoThumbnailText; private TileImageView mTileView; private EdgeView mEdgeView; + private UndoBarView mUndoBar; private Texture mVideoPlayIcon; private SynchronizedHandler mHandler; @@ -174,6 +212,15 @@ public class PhotoView extends GLView { private int mHolding; private static final int HOLD_TOUCH_DOWN = 1; private static final int HOLD_CAPTURE_ANIMATION = 2; + private static final int HOLD_DELETE = 4; + + // mTouchBoxIndex is the index of the box that is touched by the down + // gesture in film mode. The value Integer.MAX_VALUE means no box was + // touched. + private int mTouchBoxIndex = Integer.MAX_VALUE; + // Whether the box indicated by mTouchBoxIndex is deletable. Only meaningful + // if mTouchBoxIndex is not Integer.MAX_VALUE. + private boolean mTouchBoxDeletable; public PhotoView(GalleryActivity activity) { mTileView = new TileImageView(activity); @@ -181,6 +228,15 @@ public class PhotoView extends GLView { Context context = activity.getAndroidContext(); mEdgeView = new EdgeView(context); addComponent(mEdgeView); + mUndoBar = new UndoBarView(context); + addComponent(mUndoBar); + mUndoBar.setVisibility(GLView.INVISIBLE); + mUndoBar.setOnClickListener(new OnClickListener() { + @Override + public void onClick(GLView v) { + mListener.onUndoDeleteImage(); + } + }); mLoadingText = StringTexture.newInstance( context.getString(R.string.loading), DEFAULT_TEXT_SIZE, Color.WHITE); @@ -198,8 +254,11 @@ public class PhotoView extends GLView { public void invalidate() { PhotoView.this.invalidate(); } - public boolean isHolding() { - return mHolding != 0; + public boolean isHoldingDown() { + return (mHolding & HOLD_TOUCH_DOWN) != 0; + } + public boolean isHoldingDelete() { + return (mHolding & HOLD_DELETE) != 0; } public void onPull(int offset, int direction) { mEdgeView.onPull(offset, direction); @@ -250,6 +309,31 @@ public class PhotoView extends GLView { captureAnimationDone(message.arg1); break; } + case MSG_DELETE_ANIMATION_DONE: { + // message.obj is the Path of the MediaItem which should be + // deleted. message.arg1 is the offset of the image. + mListener.onDeleteImage((Path) message.obj, message.arg1); + // Normally a box which finishes delete animation will hold + // position until the underlying MediaItem is actually + // deleted, and HOLD_DELETE will be cancelled that time. In + // case the MediaItem didn't actually get deleted in 2 + // seconds, we will cancel HOLD_DELETE and make it bounce + // back. + + // We make sure there is at most one MSG_DELETE_DONE + // in the handler. + mHandler.removeMessages(MSG_DELETE_DONE); + Message m = mHandler.obtainMessage(MSG_DELETE_DONE); + mHandler.sendMessageDelayed(m, 2000); + break; + } + case MSG_DELETE_DONE: { + if (!mHandler.hasMessages(MSG_DELETE_ANIMATION_DONE)) { + mHolding &= ~HOLD_DELETE; + snapback(); + } + break; + } default: throw new AssertionError(message.what); } } @@ -263,26 +347,69 @@ public class PhotoView extends GLView { mPrevBound = prevBound; mNextBound = nextBound; + // Update mTouchBoxIndex + if (mTouchBoxIndex != Integer.MAX_VALUE) { + int k = mTouchBoxIndex; + mTouchBoxIndex = Integer.MAX_VALUE; + for (int i = 0; i < 2 * SCREEN_NAIL_MAX + 1; i++) { + if (fromIndex[i] == k) { + mTouchBoxIndex = i - SCREEN_NAIL_MAX; + break; + } + } + } + + // Update the ScreenNails. + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { + Picture p = mPictures.get(i); + p.reload(); + mSizes[i + SCREEN_NAIL_MAX] = p.getSize(); + } + + boolean wasDeleting = mPositionController.hasDeletingBox(); + // Move the boxes mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0, - mModel.isCamera(0)); + mModel.isCamera(0), mSizes); - // Update the ScreenNails. for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { - mPictures.get(i).reload(); + setPictureSize(i); + } + + boolean isDeleting = mPositionController.hasDeletingBox(); + + // If the deletion is done, make HOLD_DELETE persist for only the time + // needed for a snapback animation. + if (wasDeleting && !isDeleting) { + mHandler.removeMessages(MSG_DELETE_DONE); + Message m = mHandler.obtainMessage(MSG_DELETE_DONE); + mHandler.sendMessageDelayed( + m, PositionController.SNAPBACK_ANIMATION_TIME); } invalidate(); } + public boolean isDeleting() { + return (mHolding & HOLD_DELETE) != 0 + && mPositionController.hasDeletingBox(); + } + public void notifyImageChange(int index) { if (index == 0) { mListener.onCurrentImageUpdated(); } mPictures.get(index).reload(); + setPictureSize(index); invalidate(); } + private void setPictureSize(int index) { + Picture p = mPictures.get(index); + mPositionController.setImageSize(index, p.getSize(), + index == 0 && p.isCamera() ? mCameraRect : null); + } + @Override protected void onLayout( boolean changeSize, int left, int top, int right, int bottom) { @@ -290,6 +417,8 @@ public class PhotoView extends GLView { int h = bottom - top; mTileView.layout(0, 0, w, h); mEdgeView.layout(0, 0, w, h); + mUndoBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mUndoBar.layout(0, h - mUndoBar.getMeasuredHeight(), w, h); GLRoot root = getGLRoot(); int displayRotation = root.getDisplayRotation(); @@ -376,7 +505,9 @@ public class PhotoView extends GLView { void draw(GLCanvas canvas, Rect r); void setScreenNail(ScreenNail s); boolean isCamera(); // whether the picture is a camera preview + boolean isDeletable(); // whether the picture can be deleted void forceSize(); // called when mCompensation changes + Size getSize(); }; class FullPicture implements Picture { @@ -384,10 +515,10 @@ public class PhotoView extends GLView { private boolean mIsCamera; private boolean mIsPanorama; private boolean mIsVideo; + private boolean mIsDeletable; private int mLoadingState = Model.LOADING_INIT; + private Size mSize = new Size(); private boolean mWasCameraCenter; - private int mWidth, mHeight; - public void FullPicture(TileImageView tileView) { mTileView = tileView; } @@ -400,21 +531,21 @@ public class PhotoView extends GLView { mIsCamera = mModel.isCamera(0); mIsPanorama = mModel.isPanorama(0); mIsVideo = mModel.isVideo(0); + mIsDeletable = mModel.isDeletable(0); mLoadingState = mModel.getLoadingState(0); setScreenNail(mModel.getScreenNail(0)); - setSize(); + updateSize(); } - private void setSize() { - updateSize(); - mPositionController.setImageSize(0, mWidth, mHeight, - mIsCamera ? mCameraRect : null); + @Override + public Size getSize() { + return mSize; } @Override public void forceSize() { updateSize(); - mPositionController.forceImageSize(0, mWidth, mHeight); + mPositionController.forceImageSize(0, mSize); } private void updateSize() { @@ -428,8 +559,8 @@ public class PhotoView extends GLView { int w = mTileView.mImageWidth; int h = mTileView.mImageHeight; - mWidth = getRotated(mRotation, w, h); - mHeight = getRotated(mRotation, h, w); + mSize.width = getRotated(mRotation, w, h); + mSize.height = getRotated(mRotation, h, w); } @Override @@ -473,6 +604,11 @@ public class PhotoView extends GLView { return mIsCamera; } + @Override + public boolean isDeletable() { + return mIsDeletable; + } + private void drawTileView(GLCanvas canvas, Rect r) { float imageScale = mPositionController.getImageScale(); int viewW = getWidth(); @@ -486,6 +622,8 @@ public class PhotoView extends GLView { boolean wantsCardEffect = CARD_EFFECT && !mIsCamera && filmRatio != 1f && !mPictures.get(-1).isCamera() && !mPositionController.inOpeningAnimation(); + boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable + && filmRatio == 1f && r.centerY() != viewH / 2; if (wantsCardEffect) { // Calculate the move-out progress value. int left = r.left; @@ -517,11 +655,15 @@ public class PhotoView extends GLView { } cx = interpolate(filmRatio, cxPage, cx); } + } else if (wantsOffsetEffect) { + float offset = (float) (r.centerY() - viewH / 2) / viewH; + float alpha = getOffsetAlpha(offset); + canvas.multiplyAlpha(alpha); } // Draw the tile view. setTileViewPosition(cx, cy, viewW, viewH, imageScale); - PhotoView.super.render(canvas); + renderChild(canvas, mTileView); // Draw the play video icon and the message. canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f)); @@ -566,12 +708,12 @@ public class PhotoView extends GLView { private int mIndex; private int mRotation; private ScreenNail mScreenNail; - private Size mSize = new Size(); private boolean mIsCamera; private boolean mIsPanorama; private boolean mIsVideo; + private boolean mIsDeletable; private int mLoadingState = Model.LOADING_INIT; - private int mWidth, mHeight; + private Size mSize = new Size(); public ScreenNailPicture(int index) { mIndex = index; @@ -582,9 +724,15 @@ public class PhotoView extends GLView { mIsCamera = mModel.isCamera(mIndex); mIsPanorama = mModel.isPanorama(mIndex); mIsVideo = mModel.isVideo(mIndex); + mIsDeletable = mModel.isDeletable(mIndex); mLoadingState = mModel.getLoadingState(mIndex); setScreenNail(mModel.getScreenNail(mIndex)); - setSize(); + updateSize(); + } + + @Override + public Size getSize() { + return mSize; } @Override @@ -597,8 +745,9 @@ public class PhotoView extends GLView { } return; } - if (r.left >= getWidth() || r.right <= 0 || - r.top >= getHeight() || r.bottom <= 0) { + int w = getWidth(); + int h = getHeight(); + if (r.left >= w || r.right <= 0 || r.top >= h || r.bottom <= 0) { mScreenNail.noDraw(); return; } @@ -606,7 +755,8 @@ public class PhotoView extends GLView { float filmRatio = mPositionController.getFilmRatio(); boolean wantsCardEffect = CARD_EFFECT && mIndex > 0 && filmRatio != 1f && !mPictures.get(0).isCamera(); - int w = getWidth(); + boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable + && filmRatio == 1f && r.centerY() != h / 2; int cx = wantsCardEffect ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f) : r.centerX(); @@ -622,6 +772,10 @@ public class PhotoView extends GLView { scale = interpolate(filmRatio, scale, 1f); canvas.multiplyAlpha(alpha); canvas.scale(scale, scale, 1); + } else if (wantsOffsetEffect) { + float offset = (float) (r.centerY() - h / 2) / h; + float alpha = getOffsetAlpha(offset); + canvas.multiplyAlpha(alpha); } if (mRotation != 0) { canvas.rotate(mRotation, 0, 0, 1); @@ -650,15 +804,10 @@ public class PhotoView extends GLView { mScreenNail = s; } - private void setSize() { - updateSize(); - mPositionController.setImageSize(mIndex, mWidth, mHeight, null); - } - @Override public void forceSize() { updateSize(); - mPositionController.forceImageSize(mIndex, mWidth, mHeight); + mPositionController.forceImageSize(mIndex, mSize); } private void updateSize() { @@ -670,26 +819,30 @@ public class PhotoView extends GLView { mRotation = mModel.getImageRotation(mIndex); } - int w = 0, h = 0; if (mScreenNail != null) { - w = mScreenNail.getWidth(); - h = mScreenNail.getHeight(); - } else if (mModel != null) { + mSize.width = mScreenNail.getWidth(); + mSize.height = mScreenNail.getHeight(); + } else { // If we don't have ScreenNail available, we can still try to // get the size information of it. mModel.getImageSize(mIndex, mSize); - w = mSize.width; - h = mSize.height; } - mWidth = getRotated(mRotation, w, h); - mHeight = getRotated(mRotation, h, w); + int w = mSize.width; + int h = mSize.height; + mSize.width = getRotated(mRotation, w, h); + mSize.height = getRotated(mRotation, h, w); } @Override public boolean isCamera() { return mIsCamera; } + + @Override + public boolean isDeletable() { + return mIsDeletable; + } } // Draw a gray placeholder in the specified rectangle. @@ -736,6 +889,14 @@ public class PhotoView extends GLView { private boolean mDownInScrolling; // If we should ignore all gestures other than onSingleTapUp. private boolean mIgnoreSwipingGesture; + // If a scrolling has happened after a down gesture. + private boolean mScrolledAfterDown; + // If the first scrolling move is in X direction. In the film mode, X + // direction scrolling is normal scrolling. but Y direction scrolling is + // a delete gesture. + private boolean mFirstScrollX; + // The accumulated Y delta that has been sent to mPositionController. + private int mDeltaY; @Override public boolean onSingleTapUp(float x, float y) { @@ -780,23 +941,108 @@ public class PhotoView extends GLView { } @Override - public boolean onScroll(float dx, float dy) { + public boolean onScroll(float dx, float dy, float totalX, float totalY) { if (mIgnoreSwipingGesture) return true; - mPositionController.startScroll(-dx, -dy); + if (!mScrolledAfterDown) { + mScrolledAfterDown = true; + mFirstScrollX = (Math.abs(dx) > Math.abs(dy)); + } + + int dxi = (int) (-dx + 0.5f); + int dyi = (int) (-dy + 0.5f); + if (mFilmMode) { + if (mFirstScrollX) { + mPositionController.scrollFilmX(dxi); + } else { + if (mTouchBoxIndex == Integer.MAX_VALUE) return true; + int newDeltaY = calculateDeltaY(totalY); + int d = newDeltaY - mDeltaY; + if (d != 0) { + mPositionController.scrollFilmY(mTouchBoxIndex, d); + mDeltaY = newDeltaY; + } + } + } else { + mPositionController.scrollPage(dxi, dyi); + } return true; } + private int calculateDeltaY(float delta) { + if (mTouchBoxDeletable) return (int) (delta + 0.5f); + + // don't let items that can't be deleted be dragged more than + // maxScrollDistance, and make it harder and harder to drag. + int size = getHeight(); + float maxScrollDistance = 0.15f * size; + if (Math.abs(delta) >= size) { + delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; + } else { + delta = maxScrollDistance * + FloatMath.sin((delta / size) * (float) (Math.PI / 2)); + } + return (int) (delta + 0.5f); + } + @Override public boolean onFling(float velocityX, float velocityY) { if (mIgnoreSwipingGesture) return true; if (swipeImages(velocityX, velocityY)) { mIgnoreUpEvent = true; - } else if (mPositionController.fling(velocityX, velocityY)) { - mIgnoreUpEvent = true; + } else { + flingImages(velocityX, velocityY); } return true; } + private boolean flingImages(float velocityX, float velocityY) { + int vx = (int) (velocityX + 0.5f); + int vy = (int) (velocityY + 0.5f); + if (!mFilmMode) { + return mPositionController.flingPage(vx, vy); + } + if (Math.abs(velocityX) > Math.abs(velocityY)) { + return mPositionController.flingFilmX(vx); + } + // If we scrolled in Y direction fast enough, treat it as a delete + // gesture. + if (!mFilmMode || mTouchBoxIndex == Integer.MAX_VALUE + || !mTouchBoxDeletable) { + return false; + } + int maxVelocity = (int) GalleryUtils.dpToPixel(MAX_DISMISS_VELOCITY); + int escapeVelocity = + (int) GalleryUtils.dpToPixel(SWIPE_ESCAPE_VELOCITY); + int centerY = mPositionController.getPosition(mTouchBoxIndex) + .centerY(); + boolean fastEnough = (Math.abs(vy) > escapeVelocity) + && (Math.abs(vy) > Math.abs(vx)) + && ((vy > 0) == (centerY > getHeight() / 2)); + if (fastEnough) { + vy = Math.min(vy, maxVelocity); + int duration = mPositionController.flingFilmY(mTouchBoxIndex, vy); + if (duration >= 0) { + mPositionController.setPopFromTop(vy < 0); + deleteAfterAnimation(duration); + // We reset mTouchBoxIndex, so up() won't check if Y + // scrolled far enough to be a delete gesture. + mTouchBoxIndex = Integer.MAX_VALUE; + return true; + } + } + return false; + } + + private void deleteAfterAnimation(int duration) { + MediaItem item = mModel.getMediaItem(mTouchBoxIndex); + if (item == null) return; + mHolding |= HOLD_DELETE; + Message m = mHandler.obtainMessage(MSG_DELETE_ANIMATION_DONE); + m.obj = item.getPath(); + m.arg1 = mTouchBoxIndex; + mHandler.sendMessageDelayed(m, duration); + } + @Override public boolean onScaleBegin(float focusX, float focusY) { if (mIgnoreSwipingGesture) return true; @@ -881,7 +1127,10 @@ public class PhotoView extends GLView { } @Override - public void onDown() { + public void onDown(float x, float y) { + mDeltaY = 0; + mListener.onCommitDeleteImage(); + if (mIgnoreSwipingGesture) return; mHolding |= HOLD_TOUCH_DOWN; @@ -892,6 +1141,21 @@ public class PhotoView extends GLView { } else { mDownInScrolling = false; } + + mScrolledAfterDown = false; + if (mFilmMode) { + int xi = (int) (x + 0.5f); + int yi = (int) (y + 0.5f); + mTouchBoxIndex = mPositionController.hitTest(xi, yi); + if (mTouchBoxIndex < mPrevBound || mTouchBoxIndex > mNextBound) { + mTouchBoxIndex = Integer.MAX_VALUE; + } else { + mTouchBoxDeletable = + mPictures.get(mTouchBoxIndex).isDeletable(); + } + } else { + mTouchBoxIndex = Integer.MAX_VALUE; + } } @Override @@ -901,6 +1165,22 @@ public class PhotoView extends GLView { mHolding &= ~HOLD_TOUCH_DOWN; mEdgeView.onRelease(); + // If we scrolled in Y direction far enough, treat it as a delete + // gesture. + if (mFilmMode && mScrolledAfterDown && !mFirstScrollX + && mTouchBoxIndex != Integer.MAX_VALUE) { + Rect r = mPositionController.getPosition(mTouchBoxIndex); + int h = getHeight(); + if (Math.abs(r.centerY() - h * 0.5f) > 0.4f * h) { + int duration = mPositionController + .flingFilmY(mTouchBoxIndex, 0); + if (duration >= 0) { + mPositionController.setPopFromTop(r.centerY() < h * 0.5f); + deleteAfterAnimation(duration); + } + } + } + if (mIgnoreUpEvent) { mIgnoreUpEvent = false; return; @@ -923,6 +1203,8 @@ public class PhotoView extends GLView { mFilmMode = enabled; mPositionController.setFilmMode(mFilmMode); mModel.setNeedFullImage(!enabled); + mModel.setFocusHintDirection( + mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT); mListener.onActionBarAllowed(!enabled); // Move into camera in page mode, lock @@ -957,6 +1239,10 @@ public class PhotoView extends GLView { setFilmMode(false); } + public void showUndoButton(boolean show) { + mUndoBar.setVisibility(show ? GLView.VISIBLE : GLView.INVISIBLE); + } + //////////////////////////////////////////////////////////////////////////// // Rendering //////////////////////////////////////////////////////////////////////////// @@ -996,6 +1282,9 @@ public class PhotoView extends GLView { mPictures.get(i).draw(canvas, r); } + renderChild(canvas, mEdgeView); + renderChild(canvas, mUndoBar); + mPositionController.advanceAnimation(); checkFocusSwitching(); } @@ -1106,7 +1395,7 @@ public class PhotoView extends GLView { } private void snapback() { - if (mHolding != 0) return; + if ((mHolding & ~HOLD_DELETE) != 0) return; if (!snapToNeighborImage()) { mPositionController.snapback(); } @@ -1319,6 +1608,14 @@ public class PhotoView extends GLView { return from + (to - from) * ratio * ratio; } + // Returns the alpha factor in film mode if a picture is not in the center. + // The 0.03 lower bound is to make the item always visible a bit. + private float getOffsetAlpha(float offset) { + offset /= 0.5f; + float alpha = (offset > 0) ? (1 - offset) : (1 + offset); + return Utils.clamp(alpha, 0.03f, 1f); + } + //////////////////////////////////////////////////////////////////////////// // Simple public utilities //////////////////////////////////////////////////////////////////////////// diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java index 65334d584..2b91fcbfe 100644 --- a/src/com/android/gallery3d/ui/PositionController.java +++ b/src/com/android/gallery3d/ui/PositionController.java @@ -25,6 +25,7 @@ import com.android.gallery3d.common.Utils; import com.android.gallery3d.util.GalleryUtils; import com.android.gallery3d.util.RangeArray; import com.android.gallery3d.util.RangeIntArray; +import com.android.gallery3d.ui.PhotoView.Size; class PositionController { private static final String TAG = "PositionController"; @@ -35,11 +36,13 @@ class PositionController { public static final int IMAGE_AT_BOTTOM_EDGE = 8; public static final int CAPTURE_ANIMATION_TIME = 700; + public static final int SNAPBACK_ANIMATION_TIME = 600; // Special values for animation time. private static final long NO_ANIMATION = -1; private static final long LAST_ANIMATION = -2; + private static final int ANIM_KIND_NONE = -1; private static final int ANIM_KIND_SCROLL = 0; private static final int ANIM_KIND_SCALE = 1; private static final int ANIM_KIND_SNAPBACK = 2; @@ -47,17 +50,26 @@ class PositionController { private static final int ANIM_KIND_ZOOM = 4; private static final int ANIM_KIND_OPENING = 5; private static final int ANIM_KIND_FLING = 6; - private static final int ANIM_KIND_CAPTURE = 7; + private static final int ANIM_KIND_FLING_X = 7; + private static final int ANIM_KIND_DELETE = 8; + private static final int ANIM_KIND_CAPTURE = 9; // Animation time in milliseconds. The order must match ANIM_KIND_* above. + // + // The values for ANIM_KIND_FLING_X does't matter because we use + // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's + // faster for Animatable.advanceAnimation() to calculate the progress + // (always 1). private static final int ANIM_TIME[] = { 0, // ANIM_KIND_SCROLL 50, // ANIM_KIND_SCALE - 600, // ANIM_KIND_SNAPBACK + SNAPBACK_ANIMATION_TIME, // ANIM_KIND_SNAPBACK 400, // ANIM_KIND_SLIDE 300, // ANIM_KIND_ZOOM 400, // ANIM_KIND_OPENING 0, // ANIM_KIND_FLING (the duration is calculated dynamically) + 0, // ANIM_KIND_FLING_X (see the comment above) + 0, // ANIM_KIND_DELETE (the duration is calculated dynamically) CAPTURE_ANIMATION_TIME, // ANIM_KIND_CAPTURE }; @@ -86,10 +98,15 @@ class PositionController { // In addition to the focused box (index == 0). We also keep information // about this many boxes on each side. private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX; + private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1]; private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16); private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12); + // These are constants for the delete gesture. + private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms + private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms + private Listener mListener; private volatile Rect mOpenAnimationRect; @@ -164,9 +181,14 @@ class PositionController { // The output of the PositionController. Available throught getPosition(). private RangeArray mRects = new RangeArray(-BOX_MAX, BOX_MAX); + // The direction of a new picture should appear. New pictures pop from top + // if this value is true, or from bottom if this value is false. + boolean mPopFromTop; + public interface Listener { void invalidate(); - boolean isHolding(); + boolean isHoldingDown(); + boolean isHoldingDelete(); // EdgeView void onPull(int offset, int direction); @@ -174,6 +196,17 @@ class PositionController { void onAbsorb(int velocity, int direction); } + static { + // Initialize the CENTER_OUT_INDEX array. + // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX + // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX + for (int i = 0; i < CENTER_OUT_INDEX.length; i++) { + int j = (i + 1) / 2; + if ((i & 1) == 0) j = -j; + CENTER_OUT_INDEX[i] = j; + } + } + public PositionController(Context context, Listener listener) { mListener = listener; mPageScroller = new FlingScroller(); @@ -234,16 +267,16 @@ class PositionController { snapAndRedraw(); } - public void forceImageSize(int index, int width, int height) { - if (width == 0 || height == 0) return; + public void forceImageSize(int index, Size s) { + if (s.width == 0 || s.height == 0) return; Box b = mBoxes.get(index); - b.mImageW = width; - b.mImageH = height; + b.mImageW = s.width; + b.mImageH = s.height; return; } - public void setImageSize(int index, int width, int height, Rect cFrame) { - if (width == 0 || height == 0) return; + public void setImageSize(int index, Size s, Rect cFrame) { + if (s.width == 0 || s.height == 0) return; boolean needUpdate = false; if (cFrame != null && !mConstrainedFrame.equals(cFrame)) { @@ -251,7 +284,7 @@ class PositionController { mPlatform.updateDefaultXY(); needUpdate = true; } - needUpdate |= setBoxSize(index, width, height, false); + needUpdate |= setBoxSize(index, s.width, s.height, false); if (!needUpdate) return; updateScaleAndGapLimit(); @@ -527,37 +560,31 @@ class PositionController { redraw(); } - public void startScroll(float dx, float dy) { + // Only allow scrolling when we are not currently in an animation or we + // are in some animation with can be interrupted. + private boolean canScroll() { Box b = mBoxes.get(0); - Platform p = mPlatform; - - // Only allow scrolling when we are not currently in an animation or we - // are in some animation with can be interrupted. - if (b.mAnimationStartTime != NO_ANIMATION) { - switch (b.mAnimationKind) { - case ANIM_KIND_SCROLL: - case ANIM_KIND_FLING: - break; - default: - return; - } - } - - int x = p.mCurrentX + (int) (dx + 0.5f); - int y = b.mCurrentY + (int) (dy + 0.5f); - - if (mFilmMode) { - scrollToFilm(x, y); - } else { - scrollToPage(x, y); + if (b.mAnimationStartTime == NO_ANIMATION) return true; + switch (b.mAnimationKind) { + case ANIM_KIND_SCROLL: + case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + return true; } + return false; } - private void scrollToPage(int x, int y) { + public void scrollPage(int dx, int dy) { + if (!canScroll()) return; + Box b = mBoxes.get(0); + Platform p = mPlatform; calculateStableBound(b.mCurrentScale); + int x = p.mCurrentX + dx; + int y = b.mCurrentY + dy; + // Vertical direction: If we have space to move in the vertical // direction, we show the edge effect when scrolling reaches the edge. if (mBoundTop != mBoundBottom) { @@ -585,8 +612,26 @@ class PositionController { startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL); } - private void scrollToFilm(int x, int y) { + public void scrollFilmX(int dx) { + if (!canScroll()) return; + Box b = mBoxes.get(0); + Platform p = mPlatform; + + // Only allow scrolling when we are not currently in an animation or we + // are in some animation with can be interrupted. + if (b.mAnimationStartTime != NO_ANIMATION) { + switch (b.mAnimationKind) { + case ANIM_KIND_SCROLL: + case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + break; + default: + return; + } + } + + int x = p.mCurrentX + dx; // Horizontal direction: we show the edge effect when the scrolling // tries to go left of the first image or go right of the last image. @@ -599,16 +644,19 @@ class PositionController { x = 0; } x += mPlatform.mDefaultX; - startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL); + startAnimation(x, b.mCurrentY, b.mCurrentScale, ANIM_KIND_SCROLL); } - public boolean fling(float velocityX, float velocityY) { - int vx = (int) (velocityX + 0.5f); - int vy = (int) (velocityY + 0.5f); - return mFilmMode ? flingFilm(vx, vy) : flingPage(vx, vy); + public void scrollFilmY(int boxIndex, int dy) { + if (!canScroll()) return; + + Box b = mBoxes.get(boxIndex); + int y = b.mCurrentY + dy; + b.doAnimation(y, b.mCurrentScale, ANIM_KIND_SCROLL); + redraw(); } - private boolean flingPage(int velocityX, int velocityY) { + public boolean flingPage(int velocityX, int velocityY) { Box b = mBoxes.get(0); Platform p = mPlatform; @@ -637,11 +685,12 @@ class PositionController { int targetX = mPageScroller.getFinalX(); int targetY = mPageScroller.getFinalY(); ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration(); - startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING); - return true; + return startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING); } - private boolean flingFilm(int velocityX, int velocityY) { + public boolean flingFilmX(int velocityX) { + if (velocityX == 0) return false; + Box b = mBoxes.get(0); Platform p = mPlatform; @@ -652,17 +701,62 @@ class PositionController { return false; } - if (velocityX == 0) return false; - mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); int targetX = mFilmScroller.getFinalX(); - // This value doesn't matter because we use mFilmScroller.isFinished() - // to decide when to stop. We set this to 0 so it's faster for - // Animatable.advanceAnimation() to calculate the progress (always 1). - ANIM_TIME[ANIM_KIND_FLING] = 0; - startAnimation(targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING); - return true; + return startAnimation( + targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING_X); + } + + // Moves the specified box out of screen. If velocityY is 0, a default + // velocity is used. Returns the time for the duration, or -1 if we cannot + // not do the animation. + public int flingFilmY(int boxIndex, int velocityY) { + Box b = mBoxes.get(boxIndex); + + // Calculate targetY + int h = heightOf(b); + int targetY; + int FUZZY = 3; // TODO: figure out why this is needed. + if (velocityY < 0 || (velocityY == 0 && b.mCurrentY <= 0)) { + targetY = -mViewH / 2 - (h + 1) / 2 - FUZZY; + } else { + targetY = (mViewH + 1) / 2 + h / 2 + FUZZY; + } + + // Calculate duration + int duration; + if (velocityY != 0) { + duration = (int) (Math.abs(targetY - b.mCurrentY) * 1000f + / Math.abs(velocityY)); + duration = Math.min(MAX_DELETE_ANIMATION_DURATION, duration); + } else { + duration = DEFAULT_DELETE_ANIMATION_DURATION; + } + + // Start animation + ANIM_TIME[ANIM_KIND_DELETE] = duration; + if (b.doAnimation(targetY, b.mCurrentScale, ANIM_KIND_DELETE)) { + redraw(); + return duration; + } + return -1; + } + + // Returns the index of the box which contains the given point (x, y) + // Returns Integer.MAX_VALUE if there is no hit. There may be more than + // one box contains the given point, and we want to give priority to the + // one closer to the focused index (0). + public int hitTest(int x, int y) { + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + int j = CENTER_OUT_INDEX[i]; + Rect r = mRects.get(j); + if (r.contains(x, y)) { + return j; + } + } + + return Integer.MAX_VALUE; } //////////////////////////////////////////////////////////////////////////// @@ -697,12 +791,13 @@ class PositionController { redraw(); } - private void startAnimation(int targetX, int targetY, float targetScale, + private boolean startAnimation(int targetX, int targetY, float targetScale, int kind) { boolean changed = false; changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind); changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind); if (changed) redraw(); + return changed; } public void advanceAnimation() { @@ -752,15 +847,11 @@ class PositionController { // Convert the information in mPlatform and mBoxes to mRects, so the user // can get the position of each box by getPosition(). // - // Note the loop index goes from inside-out because each box's X coordinate + // Note we go from center-out because each box's X coordinate // is relative to its anchor box (except the focused box). private void layoutAndSetPosition() { - // layout box 0 (focused box) - convertBoxToRect(0); - for (int i = 1; i <= BOX_MAX; i++) { - // layout box i and -i - convertBoxToRect(i); - convertBoxToRect(-i); + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + convertBoxToRect(CENTER_OUT_INDEX[i]); } //dumpState(); } @@ -770,10 +861,8 @@ class PositionController { Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap); } - dumpRect(0); - for (int i = 1; i <= BOX_MAX; i++) { - dumpRect(i); - dumpRect(-i); + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + dumpRect(CENTER_OUT_INDEX[i]); } for (int i = -BOX_MAX; i <= BOX_MAX; i++) { @@ -854,6 +943,25 @@ class PositionController { b.mCurrentY = 0; b.mCurrentScale = b.mScaleMin; b.mAnimationStartTime = NO_ANIMATION; + b.mAnimationKind = ANIM_KIND_NONE; + } + + // Initialize a box to a given size. + private void initBox(int index, Size size) { + if (size.width == 0 || size.height == 0) { + initBox(index); + return; + } + Box b = mBoxes.get(index); + b.mImageW = size.width; + b.mImageH = size.height; + b.mUseViewSize = false; + b.mScaleMin = getMinimalScale(b); + b.mScaleMax = getMaximalScale(b); + b.mCurrentY = 0; + b.mCurrentScale = b.mScaleMin; + b.mAnimationStartTime = NO_ANIMATION; + b.mAnimationKind = ANIM_KIND_NONE; } // Initialize a gap. This can only be called after the boxes around the gap @@ -904,7 +1012,7 @@ class PositionController { // focused box. constrained indicates whether the focused box should be put // into the constrained frame. public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext, - boolean constrained) { + boolean constrained, Size[] sizes) { //debugMoveBox(fromIndex); mHasPrev = hasPrev; mHasNext = hasNext; @@ -957,7 +1065,7 @@ class PositionController { k++; } mBoxes.put(i, mTempBoxes.get(k++)); - initBox(i); + initBox(i, sizes[i + BOX_MAX]); } // 6. Now give the recycled box a reasonable absolute X position. @@ -977,13 +1085,41 @@ class PositionController { mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX; first = last = 0; } - // Now for those boxes between first and last, just assign the same - // position as the next box. (We can do better, but this should be - // rare). For the boxes before first or after last, we will use a new - // default gap size below. - for (int i = last - 1; i > first; i--) { + // Now for those boxes between first and last, assign their position to + // align to the previous box or the next box with known position. For + // the boxes before first or after last, we will use a new default gap + // size below. + + // Align to the previous box + for (int i = Math.max(0, first + 1); i < last; i++) { if (from.get(i) != Integer.MAX_VALUE) continue; - mBoxes.get(i).mAbsoluteX = mBoxes.get(i + 1).mAbsoluteX; + Box a = mBoxes.get(i - 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + + getDefaultGapSize(i); + if (mPopFromTop) { + b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); + } else { + b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); + } + } + + // Align to the next box + for (int i = Math.min(-1, last - 1); i > first; i--) { + if (from.get(i) != Integer.MAX_VALUE) continue; + Box a = mBoxes.get(i + 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) + - getDefaultGapSize(i); + if (mPopFromTop) { + b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); + } else { + b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); + } } // 7. recycle the gaps that are not used in the new array. @@ -1107,6 +1243,19 @@ class PositionController { return mFilmRatio.mCurrentRatio; } + public void setPopFromTop(boolean top) { + mPopFromTop = top; + } + + public boolean hasDeletingBox() { + for(int i = -BOX_MAX; i <= BOX_MAX; i++) { + if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) { + return true; + } + } + return false; + } + //////////////////////////////////////////////////////////////////////////// // Private utilities //////////////////////////////////////////////////////////////////////////// @@ -1262,6 +1411,8 @@ class PositionController { switch (kind) { case ANIM_KIND_SCROLL: case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + case ANIM_KIND_DELETE: case ANIM_KIND_CAPTURE: progress = 1 - f; // linear break; @@ -1293,7 +1444,7 @@ class PositionController { public boolean startSnapback() { if (mAnimationStartTime != NO_ANIMATION) return false; if (mAnimationKind == ANIM_KIND_SCROLL - && mListener.isHolding()) return false; + && mListener.isHoldingDown()) return false; if (mInScale) return false; Box b = mBoxes.get(0); @@ -1367,9 +1518,9 @@ class PositionController { @Override protected boolean interpolate(float progress) { if (mAnimationKind == ANIM_KIND_FLING) { - return mFilmMode - ? interpolateFlingFilm(progress) - : interpolateFlingPage(progress); + return interpolateFlingPage(progress); + } else if (mAnimationKind == ANIM_KIND_FLING_X) { + return interpolateFlingFilm(progress); } else { return interpolateLinear(progress); } @@ -1469,7 +1620,9 @@ class PositionController { public boolean startSnapback() { if (mAnimationStartTime != NO_ANIMATION) return false; if (mAnimationKind == ANIM_KIND_SCROLL - && mListener.isHolding()) return false; + && mListener.isHoldingDown()) return false; + if (mAnimationKind == ANIM_KIND_DELETE + && mListener.isHoldingDelete()) return false; if (mInScale && this == mBoxes.get(0)) return false; int y = mCurrentY; @@ -1508,13 +1661,6 @@ class PositionController { private boolean doAnimation(int targetY, float targetScale, int kind) { targetScale = clampScale(targetScale); - // If the scaled height is smaller than the view height, force it to be - // in the center. (We do this for height only, not width, because the - // user may want to scroll to the previous/next image.) - if (!mInScale && viewTallerThanScaledImage(targetScale)) { - targetY = 0; - } - if (mCurrentY == targetY && mCurrentScale == targetScale && kind != ANIM_KIND_CAPTURE) { return false; @@ -1542,7 +1688,6 @@ class PositionController { @Override protected boolean interpolate(float progress) { if (mAnimationKind == ANIM_KIND_FLING) { - // Currently a Box can only be flung in page mode. return interpolateFlingPage(progress); } else { return interpolateLinear(progress); diff --git a/src/com/android/gallery3d/ui/StringTexture.java b/src/com/android/gallery3d/ui/StringTexture.java index 2db2de4a2..97995c8a5 100644 --- a/src/com/android/gallery3d/ui/StringTexture.java +++ b/src/com/android/gallery3d/ui/StringTexture.java @@ -63,8 +63,10 @@ class StringTexture extends CanvasTexture { if (isBold) { paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); } - text = TextUtils.ellipsize( - text, paint, lengthLimit, TextUtils.TruncateAt.END).toString(); + if (lengthLimit > 0) { + text = TextUtils.ellipsize( + text, paint, lengthLimit, TextUtils.TruncateAt.END).toString(); + } return newInstance(text, paint); } diff --git a/src/com/android/gallery3d/ui/UndoBarView.java b/src/com/android/gallery3d/ui/UndoBarView.java new file mode 100644 index 000000000..9ddd1d755 --- /dev/null +++ b/src/com/android/gallery3d/ui/UndoBarView.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.view.MotionEvent; + +import com.android.gallery3d.R; +import com.android.gallery3d.util.GalleryUtils; + +public class UndoBarView extends GLView { + private static final String TAG = "UndoBarView"; + + private static final int WHITE = 0xFFFFFFFF; + private static final int GRAY = 0xFFAAAAAA; + + private final NinePatchTexture mPanel; + private final StringTexture mUndoText; + private final StringTexture mDeletedText; + private final ResourceTexture mUndoIcon; + private final int mBarHeight; + private final int mBarMargin; + private final int mUndoTextMargin; + private final int mIconSize; + private final int mIconMargin; + private final int mSeparatorTopMargin; + private final int mSeparatorBottomMargin; + private final int mSeparatorRightMargin; + private final int mSeparatorWidth; + private final int mDeletedTextMargin; + private final int mClickRegion; + + private OnClickListener mOnClickListener; + private boolean mDownOnButton; + + // This is the layout of UndoBarView. The unit is dp. + // + // +-+----+----------------+-+--+----+-+------+--+-+ + // 48 | | | Deleted | | | <- | | UNDO | | | + // +-+----+----------------+-+--+----+-+------+--+-+ + // 4 16 1 12 32 8 16 4 + public UndoBarView(Context context) { + mBarHeight = (int) GalleryUtils.dpToPixel(48); + mBarMargin = (int) GalleryUtils.dpToPixel(4); + mUndoTextMargin = (int) GalleryUtils.dpToPixel(16); + mIconMargin = (int) GalleryUtils.dpToPixel(8); + mIconSize = (int) GalleryUtils.dpToPixel(32); + mSeparatorRightMargin = (int) GalleryUtils.dpToPixel(12); + mSeparatorTopMargin = (int) GalleryUtils.dpToPixel(10); + mSeparatorBottomMargin = (int) GalleryUtils.dpToPixel(10); + mSeparatorWidth = (int) GalleryUtils.dpToPixel(1); + mDeletedTextMargin = (int) GalleryUtils.dpToPixel(16); + + mPanel = new NinePatchTexture(context, R.drawable.panel_undo_holo); + mUndoText = StringTexture.newInstance(context.getString(R.string.undo), + GalleryUtils.dpToPixel(12), GRAY, 0, true); + mDeletedText = StringTexture.newInstance( + context.getString(R.string.deleted), + GalleryUtils.dpToPixel(16), WHITE); + mUndoIcon = new ResourceTexture( + context, R.drawable.ic_menu_revert_holo_dark); + mClickRegion = mBarMargin + mUndoTextMargin + mUndoText.getWidth() + + mIconMargin + mIconSize + mSeparatorRightMargin; + } + + public void setOnClickListener(OnClickListener listener) { + mOnClickListener = listener; + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + setMeasuredSize(0 /* unused */, mBarHeight); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + int w = getWidth(); + int h = getHeight(); + mPanel.draw(canvas, mBarMargin, 0, w - mBarMargin * 2, mBarHeight); + + int x = w - mBarMargin; + int y; + + x -= mUndoTextMargin + mUndoText.getWidth(); + y = (mBarHeight - mUndoText.getHeight()) / 2; + mUndoText.draw(canvas, x, y); + + x -= mIconMargin + mIconSize; + y = (mBarHeight - mIconSize) / 2; + mUndoIcon.draw(canvas, x, y, mIconSize, mIconSize); + + x -= mSeparatorRightMargin + mSeparatorWidth; + y = mSeparatorTopMargin; + canvas.fillRect(x, y, mSeparatorWidth, + mBarHeight - mSeparatorTopMargin - mSeparatorBottomMargin, GRAY); + + x = mBarMargin + mDeletedTextMargin; + y = (mBarHeight - mDeletedText.getHeight()) / 2; + mDeletedText.draw(canvas, x, y); + } + + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mDownOnButton = inUndoButton(event); + break; + case MotionEvent.ACTION_UP: + if (mDownOnButton) { + if (mOnClickListener != null && inUndoButton(event)) { + mOnClickListener.onClick(this); + } + mDownOnButton = false; + } + break; + case MotionEvent.ACTION_CANCEL: + mDownOnButton = false; + break; + } + return true; + } + + // Check if the event is on the right of the separator + private boolean inUndoButton(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + int w = getWidth(); + int h = getHeight(); + return (x >= w - mClickRegion && x < w && y >= 0 && y < h); + } +} -- cgit v1.2.3