summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java22
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/Events.java17
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java149
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java23
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 {