Allow multiple range selections using the shift key.

- Introduce an API on MultiSelectManager for starting/ending range
  selections.

- Navigation with the shift key pressed extends the current range
  selection (or starts a new one, if one isn't in progress).

- Navigation without the shift key pressed will end the current range
  selection.

BUG=27124371

Change-Id: Ieddf3ee816812bf5210463536fe63179ef1809ad
(cherry picked from commit 09792ef150)
This commit is contained in:
Ben Kwa
2016-02-10 14:01:19 -08:00
parent fd32b894ff
commit 83df50f997
4 changed files with 100 additions and 36 deletions

View File

@@ -101,7 +101,6 @@ import com.android.documentsui.model.RootInfo;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.services.FileOperations;
import com.google.common.collect.Lists;
import java.lang.annotation.Retention;
@@ -264,7 +263,7 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
mSelectionManager.addCallback(selectionListener);
// Make sure this is done after the RecyclerView is set up.
mFocusManager = new FocusManager(mRecView, mSelectionManager);
mFocusManager = new FocusManager(mRecView);
mModel = new Model();
mModel.addUpdateListener(mAdapter);
@@ -1262,6 +1261,18 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
}
if (mFocusManager.handleKey(doc, keyCode, event)) {
// Handle range selection adjustments. Extending the selection will adjust the
// bounds of the in-progress range selection. Each time an unshifted navigation
// event is received, the range selection is restarted.
if (shouldExtendSelection(event)) {
if (!mSelectionManager.isRangeSelectionActive()) {
// Start a range selection if one isn't active
mSelectionManager.startRangeSelection(doc.getAdapterPosition());
}
mSelectionManager.snapRangeSelection(mFocusManager.getFocusPosition());
} else {
mSelectionManager.endRangeSelection();
}
return true;
}
@@ -1272,6 +1283,11 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
return false;
}
private boolean shouldExtendSelection(KeyEvent event) {
return Events.isNavigationKeyCode(event.getKeyCode()) &&
event.isShiftPressed();
}
}
private final class ModelUpdateListener implements Model.UpdateListener {

View File

@@ -33,15 +33,13 @@ class FocusManager implements View.OnFocusChangeListener {
private RecyclerView mView;
private RecyclerView.Adapter<?> mAdapter;
private LinearLayoutManager mLayout;
private MultiSelectManager mSelectionManager;
private int mLastFocusPosition = RecyclerView.NO_POSITION;
public FocusManager(RecyclerView view, MultiSelectManager selectionManager) {
public FocusManager(RecyclerView view) {
mView = view;
mAdapter = view.getAdapter();
mLayout = (LinearLayoutManager) view.getLayoutManager();
mSelectionManager = selectionManager;
}
/**
@@ -60,13 +58,6 @@ class FocusManager implements View.OnFocusChangeListener {
if (endPos != RecyclerView.NO_POSITION) {
focusItem(endPos);
boolean extendSelection = event.isShiftPressed();
// Handle any necessary adjustments to selection.
if (extendSelection) {
int startPos = doc.getAdapterPosition();
mSelectionManager.selectRange(startPos, endPos);
}
}
// Swallow all navigation keystrokes. Otherwise they go to the app's global
// key-handler, which will route them back to the DF and cause focus to be reset.
@@ -96,6 +87,13 @@ class FocusManager implements View.OnFocusChangeListener {
}
}
/**
* @return The adapter position of the last focused item.
*/
public int getFocusPosition() {
return mLastFocusPosition;
}
/**
* Finds the destination position where the focus should land for a given navigation event.
*

View File

@@ -370,39 +370,41 @@ public final class MultiSelectManager {
}
/**
* Handle a range selection event.
* <li> If the MSM is currently in single-select mode, only the last item in the range will
* actually be selected.
* <li>If a range selection is not already active, one will be started, and the given range of
* items will be selected. The given startPos becomes the anchor for the range selection.
* <li>If a range selection is already active, the anchor is not changed. The range is extended
* from its current anchor to endPos.
* Starts a range selection. If a range selection is already active, this will start a new range
* selection (which will reset the range anchor).
*
* @param startPos
* @param endPos
* @param pos The anchor position for the selection range.
*/
public void selectRange(int startPos, int endPos) {
// In single-select mode, just select the last item in the range.
if (mSingleSelect) {
attemptSelect(mAdapter.getModelId(endPos));
return;
}
void startRangeSelection(int pos) {
attemptSelect(mAdapter.getModelId(pos));
setSelectionRangeBegin(pos);
}
// In regular (i.e. multi-select) mode
if (!isRangeSelectionActive()) {
// If a range selection isn't active, start one up
attemptSelect(mAdapter.getModelId(startPos));
setSelectionRangeBegin(startPos);
}
// Extend the range selection
mRanger.snapSelection(endPos);
/**
* 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.
*
* @param pos The new end position for the selection range.
*/
void snapRangeSelection(int pos) {
checkNotNull(mRanger);
mRanger.snapSelection(pos);
notifySelectionChanged();
}
/**
* Stops an in-progress range selection.
*/
void endRangeSelection() {
mRanger = null;
}
/**
* @return Whether or not there is a current range selection active.
*/
private boolean isRangeSelectionActive() {
boolean isRangeSelectionActive() {
return mRanger != null;
}

View File

@@ -189,6 +189,54 @@ public class MultiSelectManagerTest extends AndroidTestCase {
assertSelection(items.get(20));
}
public void testRangeSelection() {
mManager.startRangeSelection(15);
mManager.snapRangeSelection(19);
assertRangeSelection(15, 19);
}
public void testRangeSelection_snapExpand() {
mManager.startRangeSelection(15);
mManager.snapRangeSelection(19);
mManager.snapRangeSelection(27);
assertRangeSelection(15, 27);
}
public void testRangeSelection_snapContract() {
mManager.startRangeSelection(15);
mManager.snapRangeSelection(27);
mManager.snapRangeSelection(19);
assertRangeSelection(15, 19);
}
public void testRangeSelection_snapInvert() {
mManager.startRangeSelection(15);
mManager.snapRangeSelection(27);
mManager.snapRangeSelection(3);
assertRangeSelection(3, 15);
}
public void testRangeSelection_multiple() {
mManager.startRangeSelection(15);
mManager.snapRangeSelection(27);
mManager.endRangeSelection();
mManager.startRangeSelection(42);
mManager.snapRangeSelection(57);
assertSelectionSize(29);
assertRangeSelected(15, 27);
assertRangeSelected(42, 57);
}
public void testRangeSelection_singleSelect() {
mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null);
mManager.addCallback(mCallback);
mManager.startRangeSelection(11);
mManager.snapRangeSelection(19);
assertSelectionSize(1);
assertSelection(items.get(19));
}
public void testProvisionalSelection() {
Selection s = mManager.getSelection();
assertSelection();