diff options
4 files changed, 153 insertions, 58 deletions
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java index 7e6ec8bc66f8..5223d760e96d 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java @@ -85,7 +85,6 @@ import android.view.View; import android.view.View.OnLayoutChangeListener; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; @@ -153,7 +152,6 @@ public class DirectoryFragment extends Fragment { // These are lazily initialized. private LinearLayoutManager mListLayout; private GridLayoutManager mGridLayout; - private OnLayoutChangeListener mRecyclerLayoutListener; private int mColumnCount = 1; // This will get updated when layout changes. public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) { @@ -294,7 +292,13 @@ public class DirectoryFragment extends Fragment { } }; - mSelectionManager = new MultiSelectManager(mRecView, listener); + mSelectionManager = new MultiSelectManager( + mRecView, + listener, + state.allowMultiple + ? MultiSelectManager.MODE_MULTIPLE + : MultiSelectManager.MODE_SINGLE); + mSelectionManager.addCallback(new SelectionModeListener()); mType = getArguments().getInt(EXTRA_TYPE); @@ -431,7 +435,7 @@ public class DirectoryFragment extends Fragment { } private boolean onSingleTapUp(MotionEvent e) { - if (!Events.isMouseEvent(e)) { + if (Events.isTouchEvent(e) && mSelectionManager.getSelection().isEmpty()) { int position = getEventAdapterPosition(e); if (position != RecyclerView.NO_POSITION) { return handleViewItem(position); @@ -531,13 +535,6 @@ public class DirectoryFragment extends Fragment { updateLayout(state.derivedMode); - final int choiceMode; - if (state.allowMultiple) { - choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL; - } else { - choiceMode = ListView.CHOICE_MODE_NONE; - } - final int thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size); mThumbSize = new Point(thumbSize, thumbSize); mRecView.setAdapter(mAdapter); @@ -622,7 +619,10 @@ public class DirectoryFragment extends Fragment { if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) { mNoDeleteCount += selected ? 1 : -1; } + } + @Override + public void onSelectionChanged() { mSelectionManager.getSelection(mSelected); if (mSelected.size() > 0) { if (DEBUG) Log.d(TAG, "Maybe starting action mode."); diff --git a/packages/DocumentsUI/src/com/android/documentsui/Events.java b/packages/DocumentsUI/src/com/android/documentsui/Events.java index 2e069036e4d1..025b94f2f3ac 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/Events.java +++ b/packages/DocumentsUI/src/com/android/documentsui/Events.java @@ -28,7 +28,14 @@ final class Events { * Returns true if event was triggered by a mouse. */ static boolean isMouseEvent(MotionEvent e) { - return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; + return isMouseType(e.getToolType(0)); + } + + /** + * Returns true if event was triggered by a finger or stylus touch. + */ + static boolean isTouchEvent(MotionEvent e) { + return isTouchType(e.getToolType(0)); } /** @@ -39,6 +46,14 @@ final class Events { } /** + * Returns true if event was triggered by a finger or stylus touch. + */ + static boolean isTouchType(int toolType) { + return toolType == MotionEvent.TOOL_TYPE_FINGER + || toolType == MotionEvent.TOOL_TYPE_STYLUS; + } + + /** * Returns true if the shift is pressed. */ boolean isShiftPressed(MotionEvent e) { diff --git a/packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java index 91b44564193d..02edd0c87448 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java +++ b/packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java @@ -37,10 +37,18 @@ import java.util.ArrayList; import java.util.List; /** - * MultiSelectManager adds traditional multi-item selection support to RecyclerView. + * MultiSelectManager provides support traditional multi-item selection support to RecyclerView. + * Additionally it can be configured to restrict selection to a single element, @see + * #setSelectMode. */ public final class MultiSelectManager { + /** Selection mode for multiple select. **/ + public static final int MODE_MULTIPLE = 0; + + /** Selection mode for multiple select. **/ + public static final int MODE_SINGLE = 1; + private static final String TAG = "MultiSelectManager"; private static final boolean DEBUG = false; @@ -54,15 +62,18 @@ public final class MultiSelectManager { private Adapter<?> mAdapter; private RecyclerViewHelper mHelper; + private boolean mSingleSelect; /** * @param recyclerView * @param gestureDelegate Option delage gesture listener. + * @param mode Selection mode * @template A gestureDelegate that implements both {@link OnGestureListener} * and {@link OnDoubleTapListener} */ public <L extends OnGestureListener & OnDoubleTapListener> MultiSelectManager( - final RecyclerView recyclerView, L gestureDelegate) { + final RecyclerView recyclerView, L gestureDelegate, int mode) { + this( recyclerView.getAdapter(), new RecyclerViewHelper() { @@ -73,7 +84,8 @@ public final class MultiSelectManager { ? recyclerView.getChildAdapterPosition(view) : RecyclerView.NO_POSITION; } - }); + }, + mode); GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener() { @@ -110,15 +122,15 @@ public final class MultiSelectManager { /** * Constructs a new instance with {@code adapter} and {@code helper}. - * @param adapter - * @param helper * @hide */ @VisibleForTesting - MultiSelectManager(Adapter<?> adapter, RecyclerViewHelper helper) { + MultiSelectManager(Adapter<?> adapter, RecyclerViewHelper helper, int mode) { checkNotNull(adapter, "'adapter' cannot be null."); checkNotNull(helper, "'helper' cannot be null."); + mSingleSelect = mode == MODE_SINGLE; + mHelper = helper; mAdapter = adapter; @@ -196,34 +208,44 @@ public final class MultiSelectManager { * @return True if the selection state of the item changed. */ public boolean setItemSelected(int position, boolean selected) { - boolean changed = (selected) - ? mSelection.add(position) - : mSelection.remove(position); - - if (changed) { - notifyItemStateChanged(position, true); + if (mSingleSelect && !mSelection.isEmpty()) { + clearSelectionQuietly(); } - return changed; + return setItemsSelected(position, 1, selected); } /** - * @param position - * @param length - * @param selected + * Sets the selected state of the specified items. Note that the callback will NOT + * be consulted to see if an item can be selected. + * * @return True if the selection state of any of the items changed. */ public boolean setItemsSelected(int position, int length, boolean selected) { boolean changed = false; for (int i = position; i < position + length; i++) { - changed |= setItemSelected(i, selected); + boolean itemChanged = selected ? mSelection.add(i) : mSelection.remove(i); + if (itemChanged) { + notifyItemStateChanged(i, selected); + } + changed |= itemChanged; } + + notifySelectionChanged(); return changed; } /** - * Clears the selection. + * Clears the selection and notifies (even if nothing changes). */ public void clearSelection() { + clearSelectionQuietly(); + notifySelectionChanged(); + } + + /** + * Clears the selection, without notifying anyone. + */ + private void clearSelectionQuietly() { mRanger = null; if (mSelection.isEmpty()) { @@ -265,7 +287,9 @@ public final class MultiSelectManager { if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); } - toggleSelection(position); + if (toggleSelection(position)) { + notifySelectionChanged(); + } } /** @@ -309,6 +333,10 @@ public final class MultiSelectManager { toggleSelection(position); } + // We're being lazy here notifying even when something might not have changed. + // To make this more correct, we'd need to update the Ranger class to return + // information about what has changed. + notifySelectionChanged(); return false; } @@ -327,20 +355,29 @@ public final class MultiSelectManager { return false; } + boolean changed = false; if (mSelection.contains(position)) { - return attemptDeselect(position); + changed = attemptDeselect(position); } else { - boolean selected = attemptSelect(position); + boolean canSelect = notifyBeforeItemStateChange(position, true); + if (!canSelect) { + return false; + } + if (mSingleSelect && !mSelection.isEmpty()) { + clearSelectionQuietly(); + } + // Here we're already in selection mode. In that case // When a simple click/tap (without SHIFT) creates causes // an item to be selected. // By recreating Ranger at this point, we allow the user to create // multiple separate contiguous ranges with SHIFT+Click & Click. - if (selected) { - setSelectionFocusBegin(position); - } - return selected; + selectAndNotify(position); + setSelectionFocusBegin(position); + changed = true; } + + return changed; } /** @@ -367,10 +404,15 @@ public final class MultiSelectManager { */ private void updateRange(int begin, int end, boolean selected) { checkState(end >= begin); - if (DEBUG) Log.i(TAG, String.format("Updating range begin=%d, end=%d, selected=%b.", begin, end, selected)); for (int i = begin; i <= end; i++) { if (selected) { - attemptSelect(i); + boolean canSelect = notifyBeforeItemStateChange(i, true); + if (canSelect) { + if (mSingleSelect && !mSelection.isEmpty()) { + clearSelectionQuietly(); + } + selectAndNotify(i); + } } else { attemptDeselect(i); } @@ -381,16 +423,12 @@ public final class MultiSelectManager { * @param position * @return True if the update was applied. */ - private boolean attemptSelect(int position) { - if (notifyBeforeItemStateChange(position, true)) { - mSelection.add(position); + private boolean selectAndNotify(int position) { + boolean changed = mSelection.add(position); + if (changed) { notifyItemStateChanged(position, true); - if (DEBUG) Log.d(TAG, "Selection after select: " + mSelection); - return true; - } else { - if (DEBUG) Log.d(TAG, "Select cancelled by listener."); - return false; } + return changed; } /** @@ -420,10 +458,8 @@ public final class MultiSelectManager { } /** - * Notifies registered listeners when a selection changes. - * - * @param position - * @param selected + * Notifies registered listeners when the selection status of a single item + * (identified by {@code position}) changes. */ private void notifyItemStateChanged(int position, boolean selected) { int lastListener = mCallbacks.size() - 1; @@ -434,6 +470,19 @@ public final class MultiSelectManager { } /** + * Notifies registered listeners when the selection has changed. This + * notification should be sent only once a full series of changes + * is complete, e.g. clearingSelection, or updating the single + * selection from one item to another. + */ + private void notifySelectionChanged() { + int lastListener = mCallbacks.size() - 1; + for (int i = lastListener; i > -1; i--) { + mCallbacks.get(i).onSelectionChanged(); + } + } + + /** * Class providing support for managing range selections. */ private final class Range { @@ -443,7 +492,7 @@ public final class MultiSelectManager { int mEnd = UNDEFINED; public Range(int begin) { - if (DEBUG) Log.d(TAG, String.format("New Ranger(%d) created.", begin)); + if (DEBUG) Log.d(TAG, "New Ranger created beginning @ " + begin); mBegin = begin; } @@ -680,8 +729,10 @@ public final class MultiSelectManager { } StringBuilder buffer = new StringBuilder(mSelection.size() * 28); - buffer.append(String.format("{size=%d, ", mSelection.size())); - buffer.append("items=["); + buffer.append("{size=") + .append(mSelection.size()) + .append(", ") + .append("items=["); for (int i=0; i < mSelection.size(); i++) { if (i > 0) { buffer.append(", "); @@ -726,11 +777,19 @@ public final class MultiSelectManager { public void onItemStateChanged(int position, boolean selected); /** - * @param position - * @param selected - * @return false to cancel the change. + * Called prior to an item changing state. Callbacks can cancel + * the change at {@code position} by returning {@code false}. + * + * @param position Adapter position of the item that was checked or unchecked + * @param selected <code>true</code> if the item is to be selected, <code>false</code> + * if the item is to be unselected. */ public boolean onBeforeItemStateChange(int position, boolean selected); + + /** + * Called immediately after completion of any set of changes. + */ + public void onSelectionChanged(); } /** diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java index d9f226154d9a..03ad3d4df1fd 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java @@ -60,7 +60,7 @@ public class MultiSelectManagerTest { mAdapter = new TestAdapter(items); mCallback = new TestCallback(); mEventHelper = new EventHelper(); - mManager = new MultiSelectManager(mAdapter, mEventHelper); + mManager = new MultiSelectManager(mAdapter, mEventHelper, MultiSelectManager.MODE_MULTIPLE); mManager.addCallback(mCallback); } @@ -188,6 +188,24 @@ public class MultiSelectManagerTest { assertRangeSelection(0, 7); } + @Test + public void singleSelectMode() { + mManager = new MultiSelectManager(mAdapter, mEventHelper, MultiSelectManager.MODE_SINGLE); + mManager.addCallback(mCallback); + tap(20); + tap(13); + assertSelection(13); + } + + @Test + public void singleSelectMode_ShiftTap() { + mManager = new MultiSelectManager(mAdapter, mEventHelper, MultiSelectManager.MODE_SINGLE); + mManager.addCallback(mCallback); + tap(13); + shiftTap(20); + assertSelection(20); + } + private void tap(int position) { mManager.onSingleTapUp(position, 0, MotionEvent.TOOL_TYPE_MOUSE); } @@ -257,6 +275,9 @@ public class MultiSelectManagerTest { public boolean onBeforeItemStateChange(int position, boolean selected) { return !ignored.contains(position); } + + @Override + public void onSelectionChanged() {} } private static final class TestHolder extends RecyclerView.ViewHolder { |