Loading packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java +195 −13 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.documentsui; package com.android.documentsui; import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.internal.util.Preconditions.checkState; import static com.android.internal.util.Preconditions.checkState; Loading @@ -26,9 +27,12 @@ import android.util.Log; import android.util.SparseBooleanArray; import android.util.SparseBooleanArray; import android.view.GestureDetector; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.GestureDetector.OnGestureListener; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.MotionEvent; import android.view.View; import android.view.View; import com.android.internal.util.Preconditions; import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.ArrayList; Loading @@ -43,9 +47,11 @@ public final class MultiSelectManager { private static final boolean DEBUG = false; private static final boolean DEBUG = false; private final Selection mSelection = new Selection(); private final Selection mSelection = new Selection(); // Only created when selection is cleared. // Only created when selection is cleared. private Selection mIntermediateSelection; private Selection mIntermediateSelection; private Ranger mRanger; private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1); private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1); private Adapter<?> mAdapter; private Adapter<?> mAdapter; Loading Loading @@ -213,6 +219,8 @@ public final class MultiSelectManager { * Clears the selection. * Clears the selection. */ */ public void clearSelection() { public void clearSelection() { mRanger = null; if (mSelection.isEmpty()) { if (mSelection.isEmpty()) { return; return; } } Loading @@ -238,7 +246,7 @@ public final class MultiSelectManager { return false; return false; } } return onSingleTapUp(mHelper.findEventPosition(e)); return onSingleTapUp(mHelper.findEventPosition(e), e.getMetaState()); } } /** /** Loading @@ -246,11 +254,12 @@ public final class MultiSelectManager { * can be mocked. * can be mocked. * * * @param position * @param position * @param metaState as returned from {@link MotionEvent#getMetaState()}. * @return true if the event was consumed. * @return true if the event was consumed. * @hide * @hide */ */ @VisibleForTesting @VisibleForTesting boolean onSingleTapUp(int position) { boolean onSingleTapUp(int position, int metaState) { if (mSelection.isEmpty()) { if (mSelection.isEmpty()) { return false; return false; } } Loading @@ -261,10 +270,18 @@ public final class MultiSelectManager { return true; return true; } } if (isShiftPressed(metaState) && mRanger != null) { mRanger.snapSelection(position); } else { toggleSelection(position); toggleSelection(position); } return true; return true; } } private static boolean isShiftPressed(int metaState) { return (metaState & KeyEvent.META_SHIFT_ON) != 0; } private void onLongPress(MotionEvent e) { private void onLongPress(MotionEvent e) { if (DEBUG) Log.d(TAG, "Handling long press event."); if (DEBUG) Log.d(TAG, "Handling long press event."); Loading @@ -273,7 +290,7 @@ public final class MultiSelectManager { if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); } } toggleSelection(position); onLongPress(position); } } /** /** Loading @@ -292,22 +309,87 @@ public final class MultiSelectManager { toggleSelection(position); toggleSelection(position); } } private void toggleSelection(int position) { /** * Toggles the selection state at position. If an item does end up selected * a new Ranger (range selection manager) at that point is created. * * @param position * @return True if state changed. */ private boolean toggleSelection(int position) { // Position may be special "no position" during certain // Position may be special "no position" during certain // transitional phases. If so, skip handling of the event. // transitional phases. If so, skip handling of the event. if (position == RecyclerView.NO_POSITION) { if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position."); if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position."); return; return false; } if (mSelection.contains(position)) { return attemptDeselect(position); } else { boolean selected = attemptSelect(position); // 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) { mRanger = new Ranger(position); } return selected; } } /** * Try to select 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 inclusive * @param end inclusive * @param selected */ 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); } else { attemptDeselect(i); } } } /** * @param position * @return True if the update was applied. */ private boolean attemptSelect(int position) { if (notifyBeforeItemStateChange(position, true)) { mSelection.add(position); 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; } } } if (DEBUG) Log.d(TAG, "Handling long press on view: " + position); /** boolean nextState = !mSelection.contains(position); * @param position if (notifyBeforeItemStateChange(position, nextState)) { * @return True if the update was applied. boolean selected = mSelection.flip(position); */ notifyItemStateChanged(position, selected); private boolean attemptDeselect(int position) { if (DEBUG) Log.d(TAG, "Selection after long press: " + mSelection); if (notifyBeforeItemStateChange(position, false)) { mSelection.remove(position); notifyItemStateChanged(position, false); if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection); return true; } else { } else { Log.i(TAG, "Selection change cancelled by listener."); if (DEBUG) Log.d(TAG, "Select cancelled by listener."); return false; } } } } Loading Loading @@ -335,6 +417,106 @@ public final class MultiSelectManager { mAdapter.notifyItemChanged(position); mAdapter.notifyItemChanged(position); } } /** * Class providing support for managing range selections. */ private final class Ranger { private static final int UNDEFINED = -1; final int mBegin; int mEnd = UNDEFINED; public Ranger(int begin) { if (DEBUG) Log.d(TAG, String.format("New Ranger(%d) created.", begin)); mBegin = begin; } void snapSelection(int position) { checkState(mRanger != null); checkArgument(position != RecyclerView.NO_POSITION); if (mEnd == UNDEFINED || mEnd == mBegin) { // Reset mEnd so it can be established in establishRange. mEnd = UNDEFINED; establishRange(position); } else { reviseRange(position); } } private void establishRange(int position) { checkState(mRanger.mEnd == UNDEFINED); if (position == mBegin) { mEnd = position; } if (position > mBegin) { updateRange(mBegin + 1, position, true); } else if (position < mBegin) { updateRange(position, mBegin - 1, true); } mEnd = position; } private void reviseRange(int position) { checkState(mEnd != UNDEFINED); checkState(mBegin != mEnd); if (position == mEnd) { if (DEBUG) Log.i(TAG, "Skipping no-op revision click on mEndRange."); } if (mEnd > mBegin) { reviseAscendingRange(position); } else if (mEnd < mBegin) { reviseDescendingRange(position); } // the "else" case is covered by checkState at beginning of method. mEnd = position; } /** * Updates an existing ascending seleciton. * @param position */ private void reviseAscendingRange(int position) { // Reducing or reversing the range.... if (position < mEnd) { if (position < mBegin) { updateRange(mBegin + 1, mEnd, false); updateRange(position, mBegin -1, true); } else { updateRange(position + 1, mEnd, false); } } // Extending the range... else if (position > mEnd) { updateRange(mEnd + 1, position, true); } } private void reviseDescendingRange(int position) { // Reducing or reversing the range.... if (position > mEnd) { if (position > mBegin) { updateRange(mEnd, mBegin - 1, false); updateRange(mBegin + 1, position, true); } else { updateRange(mEnd, position - 1, false); } } // Extending the range... else if (position < mEnd) { updateRange(position, mEnd - 1, true); } } } /** /** * Object representing the current selection. Provides read only access * Object representing the current selection. Provides read only access * public access, and private write access. * public access, and private write access. Loading packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java +78 −8 Original line number Original line Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.documentsui; import static org.junit.Assert.*; import static org.junit.Assert.*; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.MotionEvent; import android.view.View; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup; Loading Loading @@ -78,34 +79,92 @@ public class MultiSelectManagerTest { @Test @Test public void singleTapUp_DoesNotSelectBeforeLongPress() { public void singleTapUp_DoesNotSelectBeforeLongPress() { mManager.onSingleTapUp(99); mManager.onSingleTapUp(99, 0); assertSelection(); assertSelection(); } } @Test @Test public void singleTapUp_UnselectsSelectedItem() { public void singleTapUp_UnselectsSelectedItem() { mManager.onLongPress(7); mManager.onLongPress(7); mManager.onSingleTapUp(7); mManager.onSingleTapUp(7, 0); assertSelection(); assertSelection(); } } @Test @Test public void singleTapUp_NoPositionClearsSelection() { public void singleTapUp_NoPositionClearsSelection() { mManager.onLongPress(7); mManager.onLongPress(7); mManager.onSingleTapUp(11); mManager.onSingleTapUp(11, 0); mManager.onSingleTapUp(RecyclerView.NO_POSITION); mManager.onSingleTapUp(RecyclerView.NO_POSITION, 0); assertSelection(); assertSelection(); } } @Test @Test public void singleTapUp_ExtendsSelection() { public void singleTapUp_ExtendsSelection() { mManager.onLongPress(99); mManager.onLongPress(99); mManager.onSingleTapUp(7); mManager.onSingleTapUp(7, 0); mManager.onSingleTapUp(13); mManager.onSingleTapUp(13, 0); mManager.onSingleTapUp(129899); mManager.onSingleTapUp(129899, 0); assertSelection(7, 99, 13, 129899); assertSelection(7, 99, 13, 129899); } } @Test public void singleTapUp_ShiftCreatesRangeSelection() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); assertRangeSelection(7, 17); } @Test public void singleTapUp_ShiftCreatesRangeSeletion_Backwards() { mManager.onLongPress(17); mManager.onSingleTapUp(7, KeyEvent.META_SHIFT_ON); assertRangeSelection(7, 17); } @Test public void singleTapUp_SecondShiftClickExtendsSelection() { mManager.onLongPress(7); mManager.onSingleTapUp(11, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); assertRangeSelection(7, 17); } @Test public void singleTapUp_MultipleContiguousRangesSelected() { mManager.onLongPress(7); mManager.onSingleTapUp(11, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(20, 0); mManager.onSingleTapUp(25, KeyEvent.META_SHIFT_ON); assertRangeSelected(7, 11); assertRangeSelected(20, 25); assertSelectionSize(11); } @Test public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(10, KeyEvent.META_SHIFT_ON); assertRangeSelection(7, 10); } @Test public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick_Backwards() { mManager.onLongPress(17); mManager.onSingleTapUp(7, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(14, KeyEvent.META_SHIFT_ON); assertRangeSelection(14, 17); } @Test public void singleTapUp_ShiftReversesSelectionDirection() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(0, KeyEvent.META_SHIFT_ON); assertRangeSelection(0, 7); } private void assertSelected(int... expected) { private void assertSelected(int... expected) { for (int i = 0; i < expected.length; i++) { for (int i = 0; i < expected.length; i++) { Selection selection = mManager.getSelection(); Selection selection = mManager.getSelection(); Loading @@ -120,9 +179,20 @@ public class MultiSelectManagerTest { assertSelected(expected); assertSelected(expected); } } private void assertRangeSelected(int begin, int end) { for (int i = begin; i <= end; i++) { assertSelected(i); } } private void assertRangeSelection(int begin, int end) { assertSelectionSize(end - begin + 1); assertRangeSelected(begin, end); } private void assertSelectionSize(int expected) { private void assertSelectionSize(int expected) { Selection selection = mManager.getSelection(); Selection selection = mManager.getSelection(); assertEquals(expected, selection.size()); assertEquals(selection.toString(), expected, selection.size()); } } private static final class EventHelper implements RecyclerViewHelper { private static final class EventHelper implements RecyclerViewHelper { Loading Loading
packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java +195 −13 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.documentsui; package com.android.documentsui; import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.internal.util.Preconditions.checkState; import static com.android.internal.util.Preconditions.checkState; Loading @@ -26,9 +27,12 @@ import android.util.Log; import android.util.SparseBooleanArray; import android.util.SparseBooleanArray; import android.view.GestureDetector; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.GestureDetector.OnGestureListener; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.MotionEvent; import android.view.View; import android.view.View; import com.android.internal.util.Preconditions; import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.ArrayList; Loading @@ -43,9 +47,11 @@ public final class MultiSelectManager { private static final boolean DEBUG = false; private static final boolean DEBUG = false; private final Selection mSelection = new Selection(); private final Selection mSelection = new Selection(); // Only created when selection is cleared. // Only created when selection is cleared. private Selection mIntermediateSelection; private Selection mIntermediateSelection; private Ranger mRanger; private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1); private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1); private Adapter<?> mAdapter; private Adapter<?> mAdapter; Loading Loading @@ -213,6 +219,8 @@ public final class MultiSelectManager { * Clears the selection. * Clears the selection. */ */ public void clearSelection() { public void clearSelection() { mRanger = null; if (mSelection.isEmpty()) { if (mSelection.isEmpty()) { return; return; } } Loading @@ -238,7 +246,7 @@ public final class MultiSelectManager { return false; return false; } } return onSingleTapUp(mHelper.findEventPosition(e)); return onSingleTapUp(mHelper.findEventPosition(e), e.getMetaState()); } } /** /** Loading @@ -246,11 +254,12 @@ public final class MultiSelectManager { * can be mocked. * can be mocked. * * * @param position * @param position * @param metaState as returned from {@link MotionEvent#getMetaState()}. * @return true if the event was consumed. * @return true if the event was consumed. * @hide * @hide */ */ @VisibleForTesting @VisibleForTesting boolean onSingleTapUp(int position) { boolean onSingleTapUp(int position, int metaState) { if (mSelection.isEmpty()) { if (mSelection.isEmpty()) { return false; return false; } } Loading @@ -261,10 +270,18 @@ public final class MultiSelectManager { return true; return true; } } if (isShiftPressed(metaState) && mRanger != null) { mRanger.snapSelection(position); } else { toggleSelection(position); toggleSelection(position); } return true; return true; } } private static boolean isShiftPressed(int metaState) { return (metaState & KeyEvent.META_SHIFT_ON) != 0; } private void onLongPress(MotionEvent e) { private void onLongPress(MotionEvent e) { if (DEBUG) Log.d(TAG, "Handling long press event."); if (DEBUG) Log.d(TAG, "Handling long press event."); Loading @@ -273,7 +290,7 @@ public final class MultiSelectManager { if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); } } toggleSelection(position); onLongPress(position); } } /** /** Loading @@ -292,22 +309,87 @@ public final class MultiSelectManager { toggleSelection(position); toggleSelection(position); } } private void toggleSelection(int position) { /** * Toggles the selection state at position. If an item does end up selected * a new Ranger (range selection manager) at that point is created. * * @param position * @return True if state changed. */ private boolean toggleSelection(int position) { // Position may be special "no position" during certain // Position may be special "no position" during certain // transitional phases. If so, skip handling of the event. // transitional phases. If so, skip handling of the event. if (position == RecyclerView.NO_POSITION) { if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position."); if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position."); return; return false; } if (mSelection.contains(position)) { return attemptDeselect(position); } else { boolean selected = attemptSelect(position); // 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) { mRanger = new Ranger(position); } return selected; } } /** * Try to select 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 inclusive * @param end inclusive * @param selected */ 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); } else { attemptDeselect(i); } } } /** * @param position * @return True if the update was applied. */ private boolean attemptSelect(int position) { if (notifyBeforeItemStateChange(position, true)) { mSelection.add(position); 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; } } } if (DEBUG) Log.d(TAG, "Handling long press on view: " + position); /** boolean nextState = !mSelection.contains(position); * @param position if (notifyBeforeItemStateChange(position, nextState)) { * @return True if the update was applied. boolean selected = mSelection.flip(position); */ notifyItemStateChanged(position, selected); private boolean attemptDeselect(int position) { if (DEBUG) Log.d(TAG, "Selection after long press: " + mSelection); if (notifyBeforeItemStateChange(position, false)) { mSelection.remove(position); notifyItemStateChanged(position, false); if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection); return true; } else { } else { Log.i(TAG, "Selection change cancelled by listener."); if (DEBUG) Log.d(TAG, "Select cancelled by listener."); return false; } } } } Loading Loading @@ -335,6 +417,106 @@ public final class MultiSelectManager { mAdapter.notifyItemChanged(position); mAdapter.notifyItemChanged(position); } } /** * Class providing support for managing range selections. */ private final class Ranger { private static final int UNDEFINED = -1; final int mBegin; int mEnd = UNDEFINED; public Ranger(int begin) { if (DEBUG) Log.d(TAG, String.format("New Ranger(%d) created.", begin)); mBegin = begin; } void snapSelection(int position) { checkState(mRanger != null); checkArgument(position != RecyclerView.NO_POSITION); if (mEnd == UNDEFINED || mEnd == mBegin) { // Reset mEnd so it can be established in establishRange. mEnd = UNDEFINED; establishRange(position); } else { reviseRange(position); } } private void establishRange(int position) { checkState(mRanger.mEnd == UNDEFINED); if (position == mBegin) { mEnd = position; } if (position > mBegin) { updateRange(mBegin + 1, position, true); } else if (position < mBegin) { updateRange(position, mBegin - 1, true); } mEnd = position; } private void reviseRange(int position) { checkState(mEnd != UNDEFINED); checkState(mBegin != mEnd); if (position == mEnd) { if (DEBUG) Log.i(TAG, "Skipping no-op revision click on mEndRange."); } if (mEnd > mBegin) { reviseAscendingRange(position); } else if (mEnd < mBegin) { reviseDescendingRange(position); } // the "else" case is covered by checkState at beginning of method. mEnd = position; } /** * Updates an existing ascending seleciton. * @param position */ private void reviseAscendingRange(int position) { // Reducing or reversing the range.... if (position < mEnd) { if (position < mBegin) { updateRange(mBegin + 1, mEnd, false); updateRange(position, mBegin -1, true); } else { updateRange(position + 1, mEnd, false); } } // Extending the range... else if (position > mEnd) { updateRange(mEnd + 1, position, true); } } private void reviseDescendingRange(int position) { // Reducing or reversing the range.... if (position > mEnd) { if (position > mBegin) { updateRange(mEnd, mBegin - 1, false); updateRange(mBegin + 1, position, true); } else { updateRange(mEnd, position - 1, false); } } // Extending the range... else if (position < mEnd) { updateRange(position, mEnd - 1, true); } } } /** /** * Object representing the current selection. Provides read only access * Object representing the current selection. Provides read only access * public access, and private write access. * public access, and private write access. Loading
packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java +78 −8 Original line number Original line Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.documentsui; import static org.junit.Assert.*; import static org.junit.Assert.*; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.MotionEvent; import android.view.View; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup; Loading Loading @@ -78,34 +79,92 @@ public class MultiSelectManagerTest { @Test @Test public void singleTapUp_DoesNotSelectBeforeLongPress() { public void singleTapUp_DoesNotSelectBeforeLongPress() { mManager.onSingleTapUp(99); mManager.onSingleTapUp(99, 0); assertSelection(); assertSelection(); } } @Test @Test public void singleTapUp_UnselectsSelectedItem() { public void singleTapUp_UnselectsSelectedItem() { mManager.onLongPress(7); mManager.onLongPress(7); mManager.onSingleTapUp(7); mManager.onSingleTapUp(7, 0); assertSelection(); assertSelection(); } } @Test @Test public void singleTapUp_NoPositionClearsSelection() { public void singleTapUp_NoPositionClearsSelection() { mManager.onLongPress(7); mManager.onLongPress(7); mManager.onSingleTapUp(11); mManager.onSingleTapUp(11, 0); mManager.onSingleTapUp(RecyclerView.NO_POSITION); mManager.onSingleTapUp(RecyclerView.NO_POSITION, 0); assertSelection(); assertSelection(); } } @Test @Test public void singleTapUp_ExtendsSelection() { public void singleTapUp_ExtendsSelection() { mManager.onLongPress(99); mManager.onLongPress(99); mManager.onSingleTapUp(7); mManager.onSingleTapUp(7, 0); mManager.onSingleTapUp(13); mManager.onSingleTapUp(13, 0); mManager.onSingleTapUp(129899); mManager.onSingleTapUp(129899, 0); assertSelection(7, 99, 13, 129899); assertSelection(7, 99, 13, 129899); } } @Test public void singleTapUp_ShiftCreatesRangeSelection() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); assertRangeSelection(7, 17); } @Test public void singleTapUp_ShiftCreatesRangeSeletion_Backwards() { mManager.onLongPress(17); mManager.onSingleTapUp(7, KeyEvent.META_SHIFT_ON); assertRangeSelection(7, 17); } @Test public void singleTapUp_SecondShiftClickExtendsSelection() { mManager.onLongPress(7); mManager.onSingleTapUp(11, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); assertRangeSelection(7, 17); } @Test public void singleTapUp_MultipleContiguousRangesSelected() { mManager.onLongPress(7); mManager.onSingleTapUp(11, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(20, 0); mManager.onSingleTapUp(25, KeyEvent.META_SHIFT_ON); assertRangeSelected(7, 11); assertRangeSelected(20, 25); assertSelectionSize(11); } @Test public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(10, KeyEvent.META_SHIFT_ON); assertRangeSelection(7, 10); } @Test public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick_Backwards() { mManager.onLongPress(17); mManager.onSingleTapUp(7, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(14, KeyEvent.META_SHIFT_ON); assertRangeSelection(14, 17); } @Test public void singleTapUp_ShiftReversesSelectionDirection() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(0, KeyEvent.META_SHIFT_ON); assertRangeSelection(0, 7); } private void assertSelected(int... expected) { private void assertSelected(int... expected) { for (int i = 0; i < expected.length; i++) { for (int i = 0; i < expected.length; i++) { Selection selection = mManager.getSelection(); Selection selection = mManager.getSelection(); Loading @@ -120,9 +179,20 @@ public class MultiSelectManagerTest { assertSelected(expected); assertSelected(expected); } } private void assertRangeSelected(int begin, int end) { for (int i = begin; i <= end; i++) { assertSelected(i); } } private void assertRangeSelection(int begin, int end) { assertSelectionSize(end - begin + 1); assertRangeSelected(begin, end); } private void assertSelectionSize(int expected) { private void assertSelectionSize(int expected) { Selection selection = mManager.getSelection(); Selection selection = mManager.getSelection(); assertEquals(expected, selection.size()); assertEquals(selection.toString(), expected, selection.size()); } } private static final class EventHelper implements RecyclerViewHelper { private static final class EventHelper implements RecyclerViewHelper { Loading