Loading src/com/android/documentsui/dirlist/GestureMultiSelectHelper.java +15 −50 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.support.v7.widget.RecyclerView; import android.view.MotionEvent; import android.view.View; import com.android.documentsui.Events; import com.android.documentsui.Events.InputEvent; import com.android.documentsui.Events.MotionInputEvent; import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate; Loading @@ -29,8 +30,6 @@ import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.HashSet; import java.util.Set; import java.util.function.Function; import java.util.function.IntSupplier; Loading Loading @@ -72,11 +71,9 @@ class GestureMultiSelectHelper { private final int mAutoScrollEdgeHeight; private final int mColumnCount; private final IntSupplier mHeight; private final Set<String> mCurrentSelectedIds = new HashSet<>(); private int mLastGlidedItemPos = -1; private int mLastStartedItemPos = -1; private boolean mEnabled = false; private Point mLastStartedPoint; private Point mLastDownPoint; private Point mLastInterceptedPoint; private @SelectType int mType = TYPE_NONE; private @GestureSelectIntent int mUserIntent = TYPE_UNKNOWN; Loading Loading @@ -152,7 +149,7 @@ class GestureMultiSelectHelper { public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { if (!mEnabled) { if (!mEnabled || Events.isMouseEvent(e)) { return false; } Loading Loading @@ -196,10 +193,9 @@ class GestureMultiSelectHelper { private boolean handleInterceptedDownEvent(RecyclerView rv, MotionEvent e) { View itemView = rv.findChildViewUnder(e.getX(), e.getY()); try (InputEvent event = MotionInputEvent.obtain(e, rv)) { mLastStartedPoint = event.getOrigin(); mLastDownPoint = event.getOrigin(); if (itemView != null) { mLastStartedItemPos = rv.getChildAdapterPosition(itemView); mLastGlidedItemPos = mLastStartedItemPos; String modelId = mModelIdFinder.apply(mLastStartedItemPos); if (mSelectionMgr.getSelection().contains(modelId)) { mType = TYPE_ERASE; Loading @@ -214,10 +210,7 @@ class GestureMultiSelectHelper { // Called when an ACTION_MOVE event is intercepted. private boolean handleInterceptedMoveEvent(RecyclerView rv, MotionEvent e) { if (shouldInterceptMoveEvent(rv, e)) { View itemView = rv.findChildViewUnder(e.getX(), e.getY()); int pos = rv.getChildAdapterPosition(itemView); String modelId = mModelIdFinder.apply(pos); mCurrentSelectedIds.add(modelId); mSelectionMgr.startRangeSelection(mLastStartedItemPos); return true; } return false; Loading @@ -227,11 +220,10 @@ class GestureMultiSelectHelper { // Essentially, since this means all gesture movement is over, reset everything. private boolean handleUpEvent(RecyclerView rv, MotionEvent e) { mType = TYPE_NONE; mLastGlidedItemPos = -1; mLastStartedItemPos = -1; mLastStartedPoint = null; mLastDownPoint = null; mUserIntent = TYPE_UNKNOWN; mCurrentSelectedIds.clear(); mSelectionMgr.getSelection().applyProvisionalSelection(); return false; } Loading @@ -254,10 +246,8 @@ class GestureMultiSelectHelper { // item position. int lastGlidedItemPos = (bottomRight) ? rv.getAdapter().getItemCount() - 1 : rv.getChildAdapterPosition(rv.findChildViewUnder(e.getX(), e.getY())); if (lastGlidedItemPos != RecyclerView.NO_POSITION && mLastGlidedItemPos != lastGlidedItemPos) { doGestureMultiSelect(mLastStartedItemPos, lastGlidedItemPos); mLastGlidedItemPos = lastGlidedItemPos; if (lastGlidedItemPos != RecyclerView.NO_POSITION) { doGestureMultiSelect(lastGlidedItemPos); } if (insideDragZone(rv)) { mDragScroller.run(); Loading @@ -269,34 +259,9 @@ class GestureMultiSelectHelper { * @param startPos The adapter position of the start item. * @param endPos The adapter position of the end item. */ private void doGestureMultiSelect(int startPos, int endPos) { boolean selectionMode = (mType == TYPE_SELECTION); // First, reset everything that's currently selected/erased except the start item mCurrentSelectedIds.remove(mModelIdFinder.apply(startPos)); mSelectionMgr.setItemsSelected(mCurrentSelectedIds, !selectionMode); // Then clear the set mCurrentSelectedIds.clear(); // Add everything to be selected/erased if (startPos > endPos) { addItemsToModelIds(endPos, startPos); } else { addItemsToModelIds(startPos, endPos); } mSelectionMgr.setItemsSelected(mCurrentSelectedIds, selectionMode); } // Helper for {@code doGestureMultiSelect (int, int)}. Add all items from startPos <= i <= // endPos into mModelIds. private void addItemsToModelIds(int startPos, int endPos) { for (int i = startPos; i <= endPos; i++) { String modelId = mModelIdFinder.apply(i); if (modelId != null) { mCurrentSelectedIds.add(modelId); } private void doGestureMultiSelect(int endPos) { if (mType == TYPE_SELECTION) { mSelectionMgr.snapProvisionalRangeSelection(endPos); } } Loading @@ -313,8 +278,8 @@ class GestureMultiSelectHelper { return true; } int startItemPos = rv.getChildAdapterPosition(rv.findChildViewUnder(mLastStartedPoint.x, mLastStartedPoint.y)); int startItemPos = rv.getChildAdapterPosition(rv.findChildViewUnder(mLastDownPoint.x, mLastDownPoint.y)); int currentItemPos = rv .getChildAdapterPosition(rv.findChildViewUnder(e.getX(), e.getY())); if (startItemPos == RecyclerView.NO_POSITION || Loading @@ -326,7 +291,7 @@ class GestureMultiSelectHelper { return false; } if (mLastGlidedItemPos != currentItemPos) { if (startItemPos != currentItemPos) { int diff = Math.abs(startItemPos - currentItemPos); if (diff == 1 && mSelectionMgr.hasSelection()) { mUserIntent = TYPE_SELECT; Loading src/com/android/documentsui/dirlist/MultiSelectManager.java +133 −73 Original line number Diff line number Diff line Loading @@ -54,6 +54,15 @@ public final class MultiSelectManager { public static final int MODE_MULTIPLE = 0; public static final int MODE_SINGLE = 1; @IntDef({ RANGE_REGULAR, RANGE_PROVISIONAL }) @Retention(RetentionPolicy.SOURCE) public @interface RangeType {} public static final int RANGE_REGULAR = 0; public static final int RANGE_PROVISIONAL = 1; private static final String TAG = "MultiSelectManager"; private final Selection mSelection = new Selection(); Loading Loading @@ -222,15 +231,6 @@ public final class MultiSelectManager { } } void snapSelection(int position) { mRanger.snapSelection(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(); } /** * Toggles selection on the item with the given model ID. * Loading Loading @@ -262,28 +262,47 @@ public final class MultiSelectManager { setSelectionRangeBegin(pos); } void snapRangeSelection(int pos) { snapRangeSelection(pos, RANGE_REGULAR); } void snapProvisionalRangeSelection(int pos) { snapRangeSelection(pos, RANGE_PROVISIONAL); } /** * Sets the end point for the current range selection, started by a call to * {@link #startRangeSelection(int)}. This function should only be called when a range selection * is active (see {@link #isRangeSelectionActive()}. Items in the range [anchor, end] will be * selected. * selected or in provisional select, depending on the type supplied. Note that if the type is * provisional select, one should do {@link Selection#applyProvisionalSelection()} at some point * before calling on {@link #endRangeSelection()}. * * @param pos The new end position for the selection range. * @param type The type of selection the range should utilize. */ void snapRangeSelection(int pos) { private void snapRangeSelection(int pos, @RangeType int type) { if (!isRangeSelectionActive()) { throw new IllegalStateException("Range start point not set."); } mRanger.snapSelection(pos); mRanger.snapSelection(pos, type); // 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(); } /** * Stops an in-progress range selection. * Stops an in-progress range selection. All selection done with * {@link #snapRangeSelection(int, int)} with type RANGE_PROVISIONAL will be lost if * {@link Selection#applyProvisionalSelection()} is not called beforehand. */ void endRangeSelection() { mRanger = null; // Clean up in case there was any leftover provisional selection mSelection.cancelProvisionalSelection(); } /** Loading @@ -297,9 +316,6 @@ public final class MultiSelectManager { * Sets the magic location at which a selection range begins (the selection anchor). This value * is consulted when determining how to extend, and modify selection ranges. Calling this when a * range selection is active will reset the range selection. * * @throws IllegalStateException if {@code position} is not already be selected * @param position */ void setSelectionRangeBegin(int position) { if (position == RecyclerView.NO_POSITION) { Loading @@ -307,38 +323,7 @@ public final class MultiSelectManager { } if (mSelection.contains(mAdapter.getModelId(position))) { mRanger = new Range(position); } } /** * Try to set selection state for all elements in range. Not that callbacks can cancel selection * of specific items, so some or even all items may not reflect the desired state after the * update is complete. * * @param begin Adapter position for range start (inclusive). * @param end Adapter position for range end (inclusive). * @param selected New selection state. */ private void updateRange(int begin, int end, boolean selected) { assert(end >= begin); for (int i = begin; i <= end; i++) { String id = mAdapter.getModelId(i); if (id == null) { continue; } if (selected) { boolean canSelect = notifyBeforeItemStateChange(id, true); if (canSelect) { if (mSingleSelect && hasSelection()) { clearSelectionQuietly(); } selectAndNotify(id); } } else { attemptDeselect(id); } mRanger = new Range(this::updateForRange, position); } } Loading Loading @@ -425,50 +410,103 @@ public final class MultiSelectManager { } } private void updateForRange(int begin, int end, boolean selected, @RangeType int type) { switch (type) { case RANGE_REGULAR: updateForRegularRange(begin, end, selected); break; case RANGE_PROVISIONAL: updateForProvisionalRange(begin, end, selected); break; default: throw new IllegalArgumentException("Invalid range type: " + type); } } private void updateForRegularRange(int begin, int end, boolean selected) { assert(end >= begin); for (int i = begin; i <= end; i++) { String id = mAdapter.getModelId(i); if (id == null) { continue; } if (selected) { boolean canSelect = notifyBeforeItemStateChange(id, true); if (canSelect) { if (mSingleSelect && hasSelection()) { clearSelectionQuietly(); } selectAndNotify(id); } } else { attemptDeselect(id); } } } private void updateForProvisionalRange(int begin, int end, boolean selected) { assert (end >= begin); for (int i = begin; i <= end; i++) { String id = mAdapter.getModelId(i); if (id == null) { continue; } if (selected) { mSelection.mProvisionalSelection.add(id); } else { mSelection.mProvisionalSelection.remove(id); } notifyItemStateChanged(id, selected); } notifySelectionChanged(); } /** * Class providing support for managing range selections. */ private final class Range { private static final class Range { private static final int UNDEFINED = -1; final int mBegin; int mEnd = UNDEFINED; private final RangeUpdater mUpdater; private final int mBegin; private int mEnd = UNDEFINED; public Range(int begin) { public Range(RangeUpdater updater, int begin) { if (DEBUG) Log.d(TAG, "New Ranger created beginning @ " + begin); mUpdater = updater; mBegin = begin; } private void snapSelection(int position) { assert(mRanger != null); private void snapSelection(int position, @RangeType int type) { assert(position != RecyclerView.NO_POSITION); if (mEnd == UNDEFINED || mEnd == mBegin) { // Reset mEnd so it can be established in establishRange. mEnd = UNDEFINED; establishRange(position); establishRange(position, type); } else { reviseRange(position); reviseRange(position, type); } } private void establishRange(int position) { assert(mRanger.mEnd == UNDEFINED); private void establishRange(int position, @RangeType int type) { assert(mEnd == UNDEFINED); if (position == mBegin) { mEnd = position; } if (position > mBegin) { updateRange(mBegin + 1, position, true); updateRange(mBegin + 1, position, true, type); } else if (position < mBegin) { updateRange(position, mBegin - 1, true); updateRange(position, mBegin - 1, true, type); } mEnd = position; } private void reviseRange(int position) { private void reviseRange(int position, @RangeType int type) { assert(mEnd != UNDEFINED); assert(mBegin != mEnd); Loading @@ -477,9 +515,9 @@ public final class MultiSelectManager { } if (mEnd > mBegin) { reviseAscendingRange(position); reviseAscendingRange(position, type); } else if (mEnd < mBegin) { reviseDescendingRange(position); reviseDescendingRange(position, type); } // the "else" case is covered by checkState at beginning of method. Loading @@ -490,39 +528,61 @@ public final class MultiSelectManager { * Updates an existing ascending seleciton. * @param position */ private void reviseAscendingRange(int position) { private void reviseAscendingRange(int position, @RangeType int type) { // Reducing or reversing the range.... if (position < mEnd) { if (position < mBegin) { updateRange(mBegin + 1, mEnd, false); updateRange(position, mBegin -1, true); updateRange(mBegin + 1, mEnd, false, type); updateRange(position, mBegin -1, true, type); } else { updateRange(position + 1, mEnd, false); updateRange(position + 1, mEnd, false, type); } } // Extending the range... else if (position > mEnd) { updateRange(mEnd + 1, position, true); updateRange(mEnd + 1, position, true, type); } } private void reviseDescendingRange(int position) { private void reviseDescendingRange(int position, @RangeType int type) { // Reducing or reversing the range.... if (position > mEnd) { if (position > mBegin) { updateRange(mEnd, mBegin - 1, false); updateRange(mBegin + 1, position, true); updateRange(mEnd, mBegin - 1, false, type); updateRange(mBegin + 1, position, true, type); } else { updateRange(mEnd, position - 1, false); updateRange(mEnd, position - 1, false, type); } } // Extending the range... else if (position < mEnd) { updateRange(position, mEnd - 1, true); updateRange(position, mEnd - 1, true, type); } } /** * Try to set selection state for all elements in range. Not that callbacks can cancel * selection of specific items, so some or even all items may not reflect the desired state * after the update is complete. * * @param begin Adapter position for range start (inclusive). * @param end Adapter position for range end (inclusive). * @param selected New selection state. */ private void updateRange(int begin, int end, boolean selected, @RangeType int type) { mUpdater.updateForRange(begin, end, selected, type); } /* * @see {@link MultiSelectManager#updateForRegularRange(int, int , boolean)} and {@link * MultiSelectManager#updateForProvisionalRange(int, int, boolean)} */ @FunctionalInterface private interface RangeUpdater { void updateForRange(int begin, int end, boolean selected, @RangeType int type); } } /** Loading src/com/android/documentsui/dirlist/UserInputHandler.java +2 −2 Original line number Diff line number Diff line Loading @@ -182,7 +182,7 @@ public final class UserInputHandler<T extends InputEvent> } private void extendSelectionRange(T event) { mSelectionMgr.snapSelection(event.getItemPosition()); mSelectionMgr.snapRangeSelection(event.getItemPosition()); } private final class TouchInputDelegate { Loading @@ -200,7 +200,7 @@ public final class UserInputHandler<T extends InputEvent> if (mSelectionMgr.hasSelection()) { if (isRangeExtension(event)) { mSelectionMgr.snapSelection(event.getItemPosition()); mSelectionMgr.snapRangeSelection(event.getItemPosition()); } else { selectDocument(mDocFinder.apply(event)); } Loading tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java +43 −0 Original line number Diff line number Diff line Loading @@ -102,7 +102,50 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertSelectionSize(29); mSelection.assertRangeSelected(15, 27); mSelection.assertRangeSelected(42, 57); } public void testProvisionalRangeSelection() { mManager.startRangeSelection(13); mManager.snapProvisionalRangeSelection(15); mSelection.assertRangeSelection(13, 15); mManager.getSelection().applyProvisionalSelection(); mManager.endRangeSelection(); mSelection.assertSelectionSize(3); } public void testProvisionalRangeSelection_endEarly() { mManager.startRangeSelection(13); mManager.snapProvisionalRangeSelection(15); mSelection.assertRangeSelection(13, 15); mManager.endRangeSelection(); // If we end range selection prematurely for provision selection, nothing should be selected // except the first item mSelection.assertSelectionSize(1); } public void testProvisionalRangeSelection_snapExpand() { mManager.startRangeSelection(13); mManager.snapProvisionalRangeSelection(15); mSelection.assertRangeSelection(13, 15); mManager.getSelection().applyProvisionalSelection(); mManager.snapRangeSelection(18); mSelection.assertRangeSelection(13, 18); } public void testCombinationRangeSelection_IntersectsOldSelection() { mManager.startRangeSelection(13); mManager.snapRangeSelection(15); mSelection.assertRangeSelection(13, 15); mManager.startRangeSelection(11); mManager.snapProvisionalRangeSelection(18); mSelection.assertRangeSelected(11, 18); mManager.endRangeSelection(); mSelection.assertRangeSelected(13, 15); mSelection.assertRangeSelected(11, 11); mSelection.assertSelectionSize(4); } public void testProvisionalSelection() { Loading Loading
src/com/android/documentsui/dirlist/GestureMultiSelectHelper.java +15 −50 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.support.v7.widget.RecyclerView; import android.view.MotionEvent; import android.view.View; import com.android.documentsui.Events; import com.android.documentsui.Events.InputEvent; import com.android.documentsui.Events.MotionInputEvent; import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate; Loading @@ -29,8 +30,6 @@ import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.HashSet; import java.util.Set; import java.util.function.Function; import java.util.function.IntSupplier; Loading Loading @@ -72,11 +71,9 @@ class GestureMultiSelectHelper { private final int mAutoScrollEdgeHeight; private final int mColumnCount; private final IntSupplier mHeight; private final Set<String> mCurrentSelectedIds = new HashSet<>(); private int mLastGlidedItemPos = -1; private int mLastStartedItemPos = -1; private boolean mEnabled = false; private Point mLastStartedPoint; private Point mLastDownPoint; private Point mLastInterceptedPoint; private @SelectType int mType = TYPE_NONE; private @GestureSelectIntent int mUserIntent = TYPE_UNKNOWN; Loading Loading @@ -152,7 +149,7 @@ class GestureMultiSelectHelper { public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { if (!mEnabled) { if (!mEnabled || Events.isMouseEvent(e)) { return false; } Loading Loading @@ -196,10 +193,9 @@ class GestureMultiSelectHelper { private boolean handleInterceptedDownEvent(RecyclerView rv, MotionEvent e) { View itemView = rv.findChildViewUnder(e.getX(), e.getY()); try (InputEvent event = MotionInputEvent.obtain(e, rv)) { mLastStartedPoint = event.getOrigin(); mLastDownPoint = event.getOrigin(); if (itemView != null) { mLastStartedItemPos = rv.getChildAdapterPosition(itemView); mLastGlidedItemPos = mLastStartedItemPos; String modelId = mModelIdFinder.apply(mLastStartedItemPos); if (mSelectionMgr.getSelection().contains(modelId)) { mType = TYPE_ERASE; Loading @@ -214,10 +210,7 @@ class GestureMultiSelectHelper { // Called when an ACTION_MOVE event is intercepted. private boolean handleInterceptedMoveEvent(RecyclerView rv, MotionEvent e) { if (shouldInterceptMoveEvent(rv, e)) { View itemView = rv.findChildViewUnder(e.getX(), e.getY()); int pos = rv.getChildAdapterPosition(itemView); String modelId = mModelIdFinder.apply(pos); mCurrentSelectedIds.add(modelId); mSelectionMgr.startRangeSelection(mLastStartedItemPos); return true; } return false; Loading @@ -227,11 +220,10 @@ class GestureMultiSelectHelper { // Essentially, since this means all gesture movement is over, reset everything. private boolean handleUpEvent(RecyclerView rv, MotionEvent e) { mType = TYPE_NONE; mLastGlidedItemPos = -1; mLastStartedItemPos = -1; mLastStartedPoint = null; mLastDownPoint = null; mUserIntent = TYPE_UNKNOWN; mCurrentSelectedIds.clear(); mSelectionMgr.getSelection().applyProvisionalSelection(); return false; } Loading @@ -254,10 +246,8 @@ class GestureMultiSelectHelper { // item position. int lastGlidedItemPos = (bottomRight) ? rv.getAdapter().getItemCount() - 1 : rv.getChildAdapterPosition(rv.findChildViewUnder(e.getX(), e.getY())); if (lastGlidedItemPos != RecyclerView.NO_POSITION && mLastGlidedItemPos != lastGlidedItemPos) { doGestureMultiSelect(mLastStartedItemPos, lastGlidedItemPos); mLastGlidedItemPos = lastGlidedItemPos; if (lastGlidedItemPos != RecyclerView.NO_POSITION) { doGestureMultiSelect(lastGlidedItemPos); } if (insideDragZone(rv)) { mDragScroller.run(); Loading @@ -269,34 +259,9 @@ class GestureMultiSelectHelper { * @param startPos The adapter position of the start item. * @param endPos The adapter position of the end item. */ private void doGestureMultiSelect(int startPos, int endPos) { boolean selectionMode = (mType == TYPE_SELECTION); // First, reset everything that's currently selected/erased except the start item mCurrentSelectedIds.remove(mModelIdFinder.apply(startPos)); mSelectionMgr.setItemsSelected(mCurrentSelectedIds, !selectionMode); // Then clear the set mCurrentSelectedIds.clear(); // Add everything to be selected/erased if (startPos > endPos) { addItemsToModelIds(endPos, startPos); } else { addItemsToModelIds(startPos, endPos); } mSelectionMgr.setItemsSelected(mCurrentSelectedIds, selectionMode); } // Helper for {@code doGestureMultiSelect (int, int)}. Add all items from startPos <= i <= // endPos into mModelIds. private void addItemsToModelIds(int startPos, int endPos) { for (int i = startPos; i <= endPos; i++) { String modelId = mModelIdFinder.apply(i); if (modelId != null) { mCurrentSelectedIds.add(modelId); } private void doGestureMultiSelect(int endPos) { if (mType == TYPE_SELECTION) { mSelectionMgr.snapProvisionalRangeSelection(endPos); } } Loading @@ -313,8 +278,8 @@ class GestureMultiSelectHelper { return true; } int startItemPos = rv.getChildAdapterPosition(rv.findChildViewUnder(mLastStartedPoint.x, mLastStartedPoint.y)); int startItemPos = rv.getChildAdapterPosition(rv.findChildViewUnder(mLastDownPoint.x, mLastDownPoint.y)); int currentItemPos = rv .getChildAdapterPosition(rv.findChildViewUnder(e.getX(), e.getY())); if (startItemPos == RecyclerView.NO_POSITION || Loading @@ -326,7 +291,7 @@ class GestureMultiSelectHelper { return false; } if (mLastGlidedItemPos != currentItemPos) { if (startItemPos != currentItemPos) { int diff = Math.abs(startItemPos - currentItemPos); if (diff == 1 && mSelectionMgr.hasSelection()) { mUserIntent = TYPE_SELECT; Loading
src/com/android/documentsui/dirlist/MultiSelectManager.java +133 −73 Original line number Diff line number Diff line Loading @@ -54,6 +54,15 @@ public final class MultiSelectManager { public static final int MODE_MULTIPLE = 0; public static final int MODE_SINGLE = 1; @IntDef({ RANGE_REGULAR, RANGE_PROVISIONAL }) @Retention(RetentionPolicy.SOURCE) public @interface RangeType {} public static final int RANGE_REGULAR = 0; public static final int RANGE_PROVISIONAL = 1; private static final String TAG = "MultiSelectManager"; private final Selection mSelection = new Selection(); Loading Loading @@ -222,15 +231,6 @@ public final class MultiSelectManager { } } void snapSelection(int position) { mRanger.snapSelection(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(); } /** * Toggles selection on the item with the given model ID. * Loading Loading @@ -262,28 +262,47 @@ public final class MultiSelectManager { setSelectionRangeBegin(pos); } void snapRangeSelection(int pos) { snapRangeSelection(pos, RANGE_REGULAR); } void snapProvisionalRangeSelection(int pos) { snapRangeSelection(pos, RANGE_PROVISIONAL); } /** * Sets the end point for the current range selection, started by a call to * {@link #startRangeSelection(int)}. This function should only be called when a range selection * is active (see {@link #isRangeSelectionActive()}. Items in the range [anchor, end] will be * selected. * selected or in provisional select, depending on the type supplied. Note that if the type is * provisional select, one should do {@link Selection#applyProvisionalSelection()} at some point * before calling on {@link #endRangeSelection()}. * * @param pos The new end position for the selection range. * @param type The type of selection the range should utilize. */ void snapRangeSelection(int pos) { private void snapRangeSelection(int pos, @RangeType int type) { if (!isRangeSelectionActive()) { throw new IllegalStateException("Range start point not set."); } mRanger.snapSelection(pos); mRanger.snapSelection(pos, type); // 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(); } /** * Stops an in-progress range selection. * Stops an in-progress range selection. All selection done with * {@link #snapRangeSelection(int, int)} with type RANGE_PROVISIONAL will be lost if * {@link Selection#applyProvisionalSelection()} is not called beforehand. */ void endRangeSelection() { mRanger = null; // Clean up in case there was any leftover provisional selection mSelection.cancelProvisionalSelection(); } /** Loading @@ -297,9 +316,6 @@ public final class MultiSelectManager { * Sets the magic location at which a selection range begins (the selection anchor). This value * is consulted when determining how to extend, and modify selection ranges. Calling this when a * range selection is active will reset the range selection. * * @throws IllegalStateException if {@code position} is not already be selected * @param position */ void setSelectionRangeBegin(int position) { if (position == RecyclerView.NO_POSITION) { Loading @@ -307,38 +323,7 @@ public final class MultiSelectManager { } if (mSelection.contains(mAdapter.getModelId(position))) { mRanger = new Range(position); } } /** * Try to set selection state for all elements in range. Not that callbacks can cancel selection * of specific items, so some or even all items may not reflect the desired state after the * update is complete. * * @param begin Adapter position for range start (inclusive). * @param end Adapter position for range end (inclusive). * @param selected New selection state. */ private void updateRange(int begin, int end, boolean selected) { assert(end >= begin); for (int i = begin; i <= end; i++) { String id = mAdapter.getModelId(i); if (id == null) { continue; } if (selected) { boolean canSelect = notifyBeforeItemStateChange(id, true); if (canSelect) { if (mSingleSelect && hasSelection()) { clearSelectionQuietly(); } selectAndNotify(id); } } else { attemptDeselect(id); } mRanger = new Range(this::updateForRange, position); } } Loading Loading @@ -425,50 +410,103 @@ public final class MultiSelectManager { } } private void updateForRange(int begin, int end, boolean selected, @RangeType int type) { switch (type) { case RANGE_REGULAR: updateForRegularRange(begin, end, selected); break; case RANGE_PROVISIONAL: updateForProvisionalRange(begin, end, selected); break; default: throw new IllegalArgumentException("Invalid range type: " + type); } } private void updateForRegularRange(int begin, int end, boolean selected) { assert(end >= begin); for (int i = begin; i <= end; i++) { String id = mAdapter.getModelId(i); if (id == null) { continue; } if (selected) { boolean canSelect = notifyBeforeItemStateChange(id, true); if (canSelect) { if (mSingleSelect && hasSelection()) { clearSelectionQuietly(); } selectAndNotify(id); } } else { attemptDeselect(id); } } } private void updateForProvisionalRange(int begin, int end, boolean selected) { assert (end >= begin); for (int i = begin; i <= end; i++) { String id = mAdapter.getModelId(i); if (id == null) { continue; } if (selected) { mSelection.mProvisionalSelection.add(id); } else { mSelection.mProvisionalSelection.remove(id); } notifyItemStateChanged(id, selected); } notifySelectionChanged(); } /** * Class providing support for managing range selections. */ private final class Range { private static final class Range { private static final int UNDEFINED = -1; final int mBegin; int mEnd = UNDEFINED; private final RangeUpdater mUpdater; private final int mBegin; private int mEnd = UNDEFINED; public Range(int begin) { public Range(RangeUpdater updater, int begin) { if (DEBUG) Log.d(TAG, "New Ranger created beginning @ " + begin); mUpdater = updater; mBegin = begin; } private void snapSelection(int position) { assert(mRanger != null); private void snapSelection(int position, @RangeType int type) { assert(position != RecyclerView.NO_POSITION); if (mEnd == UNDEFINED || mEnd == mBegin) { // Reset mEnd so it can be established in establishRange. mEnd = UNDEFINED; establishRange(position); establishRange(position, type); } else { reviseRange(position); reviseRange(position, type); } } private void establishRange(int position) { assert(mRanger.mEnd == UNDEFINED); private void establishRange(int position, @RangeType int type) { assert(mEnd == UNDEFINED); if (position == mBegin) { mEnd = position; } if (position > mBegin) { updateRange(mBegin + 1, position, true); updateRange(mBegin + 1, position, true, type); } else if (position < mBegin) { updateRange(position, mBegin - 1, true); updateRange(position, mBegin - 1, true, type); } mEnd = position; } private void reviseRange(int position) { private void reviseRange(int position, @RangeType int type) { assert(mEnd != UNDEFINED); assert(mBegin != mEnd); Loading @@ -477,9 +515,9 @@ public final class MultiSelectManager { } if (mEnd > mBegin) { reviseAscendingRange(position); reviseAscendingRange(position, type); } else if (mEnd < mBegin) { reviseDescendingRange(position); reviseDescendingRange(position, type); } // the "else" case is covered by checkState at beginning of method. Loading @@ -490,39 +528,61 @@ public final class MultiSelectManager { * Updates an existing ascending seleciton. * @param position */ private void reviseAscendingRange(int position) { private void reviseAscendingRange(int position, @RangeType int type) { // Reducing or reversing the range.... if (position < mEnd) { if (position < mBegin) { updateRange(mBegin + 1, mEnd, false); updateRange(position, mBegin -1, true); updateRange(mBegin + 1, mEnd, false, type); updateRange(position, mBegin -1, true, type); } else { updateRange(position + 1, mEnd, false); updateRange(position + 1, mEnd, false, type); } } // Extending the range... else if (position > mEnd) { updateRange(mEnd + 1, position, true); updateRange(mEnd + 1, position, true, type); } } private void reviseDescendingRange(int position) { private void reviseDescendingRange(int position, @RangeType int type) { // Reducing or reversing the range.... if (position > mEnd) { if (position > mBegin) { updateRange(mEnd, mBegin - 1, false); updateRange(mBegin + 1, position, true); updateRange(mEnd, mBegin - 1, false, type); updateRange(mBegin + 1, position, true, type); } else { updateRange(mEnd, position - 1, false); updateRange(mEnd, position - 1, false, type); } } // Extending the range... else if (position < mEnd) { updateRange(position, mEnd - 1, true); updateRange(position, mEnd - 1, true, type); } } /** * Try to set selection state for all elements in range. Not that callbacks can cancel * selection of specific items, so some or even all items may not reflect the desired state * after the update is complete. * * @param begin Adapter position for range start (inclusive). * @param end Adapter position for range end (inclusive). * @param selected New selection state. */ private void updateRange(int begin, int end, boolean selected, @RangeType int type) { mUpdater.updateForRange(begin, end, selected, type); } /* * @see {@link MultiSelectManager#updateForRegularRange(int, int , boolean)} and {@link * MultiSelectManager#updateForProvisionalRange(int, int, boolean)} */ @FunctionalInterface private interface RangeUpdater { void updateForRange(int begin, int end, boolean selected, @RangeType int type); } } /** Loading
src/com/android/documentsui/dirlist/UserInputHandler.java +2 −2 Original line number Diff line number Diff line Loading @@ -182,7 +182,7 @@ public final class UserInputHandler<T extends InputEvent> } private void extendSelectionRange(T event) { mSelectionMgr.snapSelection(event.getItemPosition()); mSelectionMgr.snapRangeSelection(event.getItemPosition()); } private final class TouchInputDelegate { Loading @@ -200,7 +200,7 @@ public final class UserInputHandler<T extends InputEvent> if (mSelectionMgr.hasSelection()) { if (isRangeExtension(event)) { mSelectionMgr.snapSelection(event.getItemPosition()); mSelectionMgr.snapRangeSelection(event.getItemPosition()); } else { selectDocument(mDocFinder.apply(event)); } Loading
tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java +43 −0 Original line number Diff line number Diff line Loading @@ -102,7 +102,50 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertSelectionSize(29); mSelection.assertRangeSelected(15, 27); mSelection.assertRangeSelected(42, 57); } public void testProvisionalRangeSelection() { mManager.startRangeSelection(13); mManager.snapProvisionalRangeSelection(15); mSelection.assertRangeSelection(13, 15); mManager.getSelection().applyProvisionalSelection(); mManager.endRangeSelection(); mSelection.assertSelectionSize(3); } public void testProvisionalRangeSelection_endEarly() { mManager.startRangeSelection(13); mManager.snapProvisionalRangeSelection(15); mSelection.assertRangeSelection(13, 15); mManager.endRangeSelection(); // If we end range selection prematurely for provision selection, nothing should be selected // except the first item mSelection.assertSelectionSize(1); } public void testProvisionalRangeSelection_snapExpand() { mManager.startRangeSelection(13); mManager.snapProvisionalRangeSelection(15); mSelection.assertRangeSelection(13, 15); mManager.getSelection().applyProvisionalSelection(); mManager.snapRangeSelection(18); mSelection.assertRangeSelection(13, 18); } public void testCombinationRangeSelection_IntersectsOldSelection() { mManager.startRangeSelection(13); mManager.snapRangeSelection(15); mSelection.assertRangeSelection(13, 15); mManager.startRangeSelection(11); mManager.snapProvisionalRangeSelection(18); mSelection.assertRangeSelected(11, 18); mManager.endRangeSelection(); mSelection.assertRangeSelected(13, 15); mSelection.assertRangeSelected(11, 11); mSelection.assertSelectionSize(4); } public void testProvisionalSelection() { Loading