Loading packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java +66 −15 Original line number Diff line number Diff line Loading @@ -16,7 +16,9 @@ package com.android.documentsui; import static com.android.documentsui.Events.isMouseEvent; import static com.android.internal.util.Preconditions.checkState; import static java.lang.String.format; import android.graphics.Point; import android.graphics.Rect; Loading @@ -24,6 +26,7 @@ import android.graphics.drawable.Drawable; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.ViewHolder; import android.util.Log; import android.util.SparseBooleanArray; import android.view.MotionEvent; import android.view.View; Loading @@ -34,6 +37,8 @@ import android.view.View; */ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { private static final int NOT_SELECTED = -1; // For debugging purposes. private static final String TAG = "BandSelectManager"; private static final boolean DEBUG = false; Loading @@ -41,10 +46,17 @@ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { private final RecyclerView mRecyclerView; private final MultiSelectManager mSelectManager; private final Drawable mRegionSelectorDrawable; private final SparseBooleanArray mSelectedByBand = new SparseBooleanArray(); private boolean mIsBandSelectActive = false; private Point mOrigin; private Rect mRegionBounds; private Rect mBounds; // Maintain the last selection made by band, so if bounds shink back, we can unselect // the respective items. // Track information private int mCursorDeltaY = 0; private int mFirstSelected = NOT_SELECTED; /** * @param recyclerView Loading Loading @@ -109,11 +121,17 @@ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { * @param pointerPosition */ private void resizeBandSelectRectangle(Point pointerPosition) { mRegionBounds = new Rect(Math.min(mOrigin.x, pointerPosition.x), if (mBounds != null) { mCursorDeltaY = pointerPosition.y - mBounds.bottom; } mBounds = new Rect(Math.min(mOrigin.x, pointerPosition.x), Math.min(mOrigin.y, pointerPosition.y), Math.max(mOrigin.x, pointerPosition.x), Math.max(mOrigin.y, pointerPosition.y)); mRegionSelectorDrawable.setBounds(mRegionBounds); mRegionSelectorDrawable.setBounds(mBounds); } /** Loading @@ -122,14 +140,53 @@ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { * Final optimized implementation, with support for managing offscreen selection to come. */ private void selectChildrenCoveredBySelection() { // track top and bottom selections. Details on why this is useful below. int first = NOT_SELECTED; int last = NOT_SELECTED; for (int i = 0; i < mRecyclerView.getChildCount(); i++) { View child = mRecyclerView.getChildAt(i); ViewHolder holder = mRecyclerView.getChildViewHolder(child); Rect childRect = new Rect(); child.getHitRect(childRect); boolean doRectsOverlap = Rect.intersects(childRect, mRegionBounds); mSelectManager.setItemSelected(holder.getAdapterPosition(), doRectsOverlap); boolean shouldSelect = Rect.intersects(childRect, mBounds); int position = holder.getAdapterPosition(); // This also allows us to clear the selection of elements // that only temporarily entered the bounds of the band. if (mSelectedByBand.get(position) && !shouldSelect) { mSelectManager.setItemSelected(position, false); mSelectedByBand.delete(position); } // We need to keep track of the first and last items selected. // We'll use this information along with cursor direction // to determine the starting point of the selection. // We provide this information to selection manager // to enable more natural user interaction when working // with Shift+Click and multiple contiguous selection ranges. if (shouldSelect) { if (first == NOT_SELECTED) { first = position; } else { last = position; } mSelectManager.setItemSelected(position, true); mSelectedByBand.put(position, true); } } // Remember which is the last selected item, so we can // share that with selection manager when band select ends. // It'll use that as it's begin selection point when // user SHIFT+Clicks. if (mCursorDeltaY < 0 && last != NOT_SELECTED) { mFirstSelected = last; } else if (mCursorDeltaY > 0 && first != NOT_SELECTED) { mFirstSelected = first; } } Loading @@ -139,16 +196,10 @@ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { private void endBandSelect() { if (DEBUG) Log.d(TAG, "Ending band select."); mIsBandSelectActive = false; mSelectedByBand.clear(); mRecyclerView.getOverlay().remove(mRegionSelectorDrawable); if (mFirstSelected != NOT_SELECTED) { mSelectManager.setSelectionFocusBegin(mFirstSelected); } /** * Determines whether the provided event was triggered by a mouse (as opposed to a finger or * stylus). * @param e * @return */ private static boolean isMouseEvent(MotionEvent e) { return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; } } packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java +32 −10 Original line number Diff line number Diff line Loading @@ -286,6 +286,11 @@ public class DirectoryFragment extends Fragment { public boolean onSingleTapUp(MotionEvent e) { return DirectoryFragment.this.onSingleTapUp(e); } @Override public boolean onDoubleTap(MotionEvent e) { Log.d(TAG, "Handling double tap."); return DirectoryFragment.this.onDoubleTap(e); } }; mSelectionManager = new MultiSelectManager(mRecView, listener); Loading Loading @@ -425,9 +430,27 @@ public class DirectoryFragment extends Fragment { } private boolean onSingleTapUp(MotionEvent e) { if (!Events.isMouseEvent(e)) { int position = getEventAdapterPosition(e); if (position != RecyclerView.NO_POSITION) { return handleViewItem(position); } } return false; } protected boolean onDoubleTap(MotionEvent e) { if (Events.isMouseEvent(e)) { Log.d(TAG, "Handling double tap from mouse."); int position = getEventAdapterPosition(e); if (position != RecyclerView.NO_POSITION) { return handleViewItem(position); } } return false; } private boolean handleViewItem(int position) { final Cursor cursor = mAdapter.getItem(position); checkNotNull(cursor, "Cursor cannot be null."); final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); Loading @@ -435,10 +458,9 @@ public class DirectoryFragment extends Fragment { if (isDocumentEnabled(docMimeType, docFlags)) { final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); ((BaseActivity) getActivity()).onDocumentPicked(doc); mSelectionManager.clearSelection(); return true; } } return false; } Loading packages/DocumentsUI/src/com/android/documentsui/Events.java 0 → 100644 +54 −0 Original line number Diff line number Diff line /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui; import android.view.KeyEvent; import android.view.MotionEvent; /** * Utility code for dealing with MotionEvents. */ 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; } /** * Returns true if event was triggered by a mouse. */ static boolean isMouseType(int toolType) { return toolType == MotionEvent.TOOL_TYPE_MOUSE; } /** * Returns true if the shift is pressed. */ boolean isShiftPressed(MotionEvent e) { return hasShiftBit(e.getMetaState()); } /** * Returns true if the "SHIFT" bit is set. */ static boolean hasShiftBit(int metaState) { return (metaState & KeyEvent.META_SHIFT_ON) != 0; } } packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java +103 −52 Original line number Diff line number Diff line Loading @@ -26,13 +26,11 @@ import android.support.v7.widget.RecyclerView.AdapterDataObserver; import android.util.Log; import android.util.SparseBooleanArray; import android.view.GestureDetector; import android.view.GestureDetector.OnDoubleTapListener; import android.view.GestureDetector.OnGestureListener; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import com.android.internal.util.Preconditions; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; Loading @@ -51,7 +49,7 @@ public final class MultiSelectManager { // Only created when selection is cleared. private Selection mIntermediateSelection; private Ranger mRanger; private Range mRanger; private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1); private Adapter<?> mAdapter; Loading @@ -60,8 +58,11 @@ public final class MultiSelectManager { /** * @param recyclerView * @param gestureDelegate Option delage gesture listener. * @template A gestureDelegate that implements both {@link OnGestureListener} * and {@link OnDoubleTapListener} */ public MultiSelectManager(final RecyclerView recyclerView, OnGestureListener gestureDelegate) { public <L extends OnGestureListener & OnDoubleTapListener> MultiSelectManager( final RecyclerView recyclerView, L gestureDelegate) { this( recyclerView.getAdapter(), new RecyclerViewHelper() { Loading @@ -86,11 +87,15 @@ public final class MultiSelectManager { } }; CompositeOnGestureListener<? extends Object> compositeListener = new CompositeOnGestureListener<>(listener, gestureDelegate); final GestureDetector detector = new GestureDetector( recyclerView.getContext(), gestureDelegate == null ? listener : new CompositeOnGestureListener(listener, gestureDelegate)); : compositeListener); detector.setOnDoubleTapListener(compositeListener); recyclerView.addOnItemTouchListener( new RecyclerView.OnItemTouchListener() { Loading Loading @@ -236,17 +241,40 @@ public final class MultiSelectManager { } } private void onLongPress(MotionEvent e) { if (DEBUG) Log.d(TAG, "Handling long press event."); int position = mHelper.findEventPosition(e); if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); } onLongPress(position); } /** * TODO: Roll this back into {@link #onLongPress(MotionEvent)} once MotionEvent * can be mocked. * * @param position * @hide */ @VisibleForTesting void onLongPress(int position) { if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); } toggleSelection(position); } /** * @param e * @return true if the event was consumed. */ private boolean onSingleTapUp(MotionEvent e) { if (DEBUG) Log.d(TAG, "Handling tap event."); if (mSelection.isEmpty()) { return false; } return onSingleTapUp(mHelper.findEventPosition(e), e.getMetaState()); return onSingleTapUp(mHelper.findEventPosition(e), e.getMetaState(), e.getToolType(0)); } /** Loading @@ -255,58 +283,33 @@ public final class MultiSelectManager { * * @param position * @param metaState as returned from {@link MotionEvent#getMetaState()}. * @param toolType * @return true if the event was consumed. * @hide */ @VisibleForTesting boolean onSingleTapUp(int position, int metaState) { boolean onSingleTapUp(int position, int metaState, int toolType) { if (mSelection.isEmpty()) { // if this is a mouse click on an item, start selection mode. if (position != RecyclerView.NO_POSITION && Events.isMouseType(toolType)) { toggleSelection(position); } return false; } if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.d(TAG, "View is null. Canceling selection."); clearSelection(); return true; return false; } if (isShiftPressed(metaState) && mRanger != null) { if (Events.hasShiftBit(metaState) && mRanger != null) { mRanger.snapSelection(position); } else { toggleSelection(position); } return true; } private static boolean isShiftPressed(int metaState) { return (metaState & KeyEvent.META_SHIFT_ON) != 0; } private void onLongPress(MotionEvent e) { if (DEBUG) Log.d(TAG, "Handling long press event."); int position = mHelper.findEventPosition(e); if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); } onLongPress(position); } /** * TODO: Roll this back into {@link #onLongPress(MotionEvent)} once MotionEvent * can be mocked. * * @param position * @hide */ @VisibleForTesting void onLongPress(int position) { if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); } toggleSelection(position); return false; } /** Loading Loading @@ -334,12 +337,25 @@ public final class MultiSelectManager { // 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); setSelectionFocusBegin(position); } return selected; } } /** * Sets the magic location at which a selection range begins. This * value is consulted when determining how to extend, and modify * selection ranges. * * @throws IllegalStateException if {@code position} is not already be selected * @param position */ void setSelectionFocusBegin(int position) { checkState(mSelection.contains(position)); mRanger = new Range(position); } /** * 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 Loading Loading @@ -420,18 +436,18 @@ public final class MultiSelectManager { /** * Class providing support for managing range selections. */ private final class Ranger { private final class Range { private static final int UNDEFINED = -1; final int mBegin; int mEnd = UNDEFINED; public Ranger(int begin) { public Range(int begin) { if (DEBUG) Log.d(TAG, String.format("New Ranger(%d) created.", begin)); mBegin = begin; } void snapSelection(int position) { private void snapSelection(int position) { checkState(mRanger != null); checkArgument(position != RecyclerView.NO_POSITION); Loading Loading @@ -720,12 +736,17 @@ public final class MultiSelectManager { /** * A composite {@code OnGestureDetector} that allows us to delegate unhandled * events to an outside party (presumably DirectoryFragment). * @template A gestureDelegate that implements both {@link OnGestureListener} * and {@link OnDoubleTapListener} */ private static final class CompositeOnGestureListener implements OnGestureListener { private static final class CompositeOnGestureListener<L extends OnGestureListener & OnDoubleTapListener> implements OnGestureListener, OnDoubleTapListener { private OnGestureListener[] mListeners; private L[] mListeners; public CompositeOnGestureListener(OnGestureListener... listeners) { @SafeVarargs public CompositeOnGestureListener(L... listeners) { mListeners = listeners; } Loading Loading @@ -782,5 +803,35 @@ public final class MultiSelectManager { } return false; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { for (int i = 0; i < mListeners.length; i++) { if (mListeners[i].onSingleTapConfirmed(e)) { return true; } } return false; } @Override public boolean onDoubleTap(MotionEvent e) { for (int i = 0; i < mListeners.length; i++) { if (mListeners[i].onDoubleTap(e)) { return true; } } return false; } @Override public boolean onDoubleTapEvent(MotionEvent e) { for (int i = 0; i < mListeners.length; i++) { if (mListeners[i].onDoubleTapEvent(e)) { return true; } } return false; } } } packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java +65 −26 Original line number Diff line number Diff line Loading @@ -64,6 +64,35 @@ public class MultiSelectManagerTest { mManager.addCallback(mCallback); } @Test public void mouseClick_StartsSelectionMode() { click(7); assertSelection(7); } @Test public void mouseClick_ShiftClickExtendsSelection() { click(7); shiftClick(11); assertRangeSelection(7, 11); } @Test public void mouseClick_NoPosition_ClearsSelection() { mManager.onLongPress(7); click(11); click(RecyclerView.NO_POSITION); assertSelection(); } @Test public void setSelectionFocusBegin() { mManager.setItemSelected(7, true); mManager.setSelectionFocusBegin(7); shiftClick(11); assertRangeSelection(7, 11); } @Test public void longPress_StartsSelectionMode() { mManager.onLongPress(7); Loading @@ -77,64 +106,58 @@ public class MultiSelectManagerTest { assertSelection(7, 99); } @Test public void singleTapUp_DoesNotSelectBeforeLongPress() { mManager.onSingleTapUp(99, 0); assertSelection(); } @Test public void singleTapUp_UnselectsSelectedItem() { mManager.onLongPress(7); mManager.onSingleTapUp(7, 0); tap(7); assertSelection(); } @Test public void singleTapUp_NoPositionClearsSelection() { public void singleTapUp_NoPosition_ClearsSelection() { mManager.onLongPress(7); mManager.onSingleTapUp(11, 0); mManager.onSingleTapUp(RecyclerView.NO_POSITION, 0); tap(11); tap(RecyclerView.NO_POSITION); assertSelection(); } @Test public void singleTapUp_ExtendsSelection() { mManager.onLongPress(99); mManager.onSingleTapUp(7, 0); mManager.onSingleTapUp(13, 0); mManager.onSingleTapUp(129899, 0); tap(7); tap(13); tap(129899); assertSelection(7, 99, 13, 129899); } @Test public void singleTapUp_ShiftCreatesRangeSelection() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); shiftTap(17); assertRangeSelection(7, 17); } @Test public void singleTapUp_ShiftCreatesRangeSeletion_Backwards() { mManager.onLongPress(17); mManager.onSingleTapUp(7, KeyEvent.META_SHIFT_ON); shiftTap(7); 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); shiftTap(11); shiftTap(17); 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); shiftTap(11); tap(20); shiftTap(25); assertRangeSelected(7, 11); assertRangeSelected(20, 25); assertSelectionSize(11); Loading @@ -143,16 +166,16 @@ public class MultiSelectManagerTest { @Test public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(10, KeyEvent.META_SHIFT_ON); shiftTap(17); shiftTap(10); 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); shiftTap(7); shiftTap(14); assertRangeSelection(14, 17); } Loading @@ -160,11 +183,27 @@ public class MultiSelectManagerTest { @Test public void singleTapUp_ShiftReversesSelectionDirection() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(0, KeyEvent.META_SHIFT_ON); shiftTap(17); shiftTap(0); assertRangeSelection(0, 7); } private void tap(int position) { mManager.onSingleTapUp(position, 0, MotionEvent.TOOL_TYPE_MOUSE); } private void shiftTap(int position) { mManager.onSingleTapUp(position, KeyEvent.META_SHIFT_ON, MotionEvent.TOOL_TYPE_FINGER); } private void click(int position) { mManager.onSingleTapUp(position, 0, MotionEvent.TOOL_TYPE_MOUSE); } private void shiftClick(int position) { mManager.onSingleTapUp(position, KeyEvent.META_SHIFT_ON, MotionEvent.TOOL_TYPE_MOUSE); } private void assertSelected(int... expected) { for (int i = 0; i < expected.length; i++) { Selection selection = mManager.getSelection(); Loading Loading
packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java +66 −15 Original line number Diff line number Diff line Loading @@ -16,7 +16,9 @@ package com.android.documentsui; import static com.android.documentsui.Events.isMouseEvent; import static com.android.internal.util.Preconditions.checkState; import static java.lang.String.format; import android.graphics.Point; import android.graphics.Rect; Loading @@ -24,6 +26,7 @@ import android.graphics.drawable.Drawable; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.ViewHolder; import android.util.Log; import android.util.SparseBooleanArray; import android.view.MotionEvent; import android.view.View; Loading @@ -34,6 +37,8 @@ import android.view.View; */ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { private static final int NOT_SELECTED = -1; // For debugging purposes. private static final String TAG = "BandSelectManager"; private static final boolean DEBUG = false; Loading @@ -41,10 +46,17 @@ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { private final RecyclerView mRecyclerView; private final MultiSelectManager mSelectManager; private final Drawable mRegionSelectorDrawable; private final SparseBooleanArray mSelectedByBand = new SparseBooleanArray(); private boolean mIsBandSelectActive = false; private Point mOrigin; private Rect mRegionBounds; private Rect mBounds; // Maintain the last selection made by band, so if bounds shink back, we can unselect // the respective items. // Track information private int mCursorDeltaY = 0; private int mFirstSelected = NOT_SELECTED; /** * @param recyclerView Loading Loading @@ -109,11 +121,17 @@ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { * @param pointerPosition */ private void resizeBandSelectRectangle(Point pointerPosition) { mRegionBounds = new Rect(Math.min(mOrigin.x, pointerPosition.x), if (mBounds != null) { mCursorDeltaY = pointerPosition.y - mBounds.bottom; } mBounds = new Rect(Math.min(mOrigin.x, pointerPosition.x), Math.min(mOrigin.y, pointerPosition.y), Math.max(mOrigin.x, pointerPosition.x), Math.max(mOrigin.y, pointerPosition.y)); mRegionSelectorDrawable.setBounds(mRegionBounds); mRegionSelectorDrawable.setBounds(mBounds); } /** Loading @@ -122,14 +140,53 @@ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { * Final optimized implementation, with support for managing offscreen selection to come. */ private void selectChildrenCoveredBySelection() { // track top and bottom selections. Details on why this is useful below. int first = NOT_SELECTED; int last = NOT_SELECTED; for (int i = 0; i < mRecyclerView.getChildCount(); i++) { View child = mRecyclerView.getChildAt(i); ViewHolder holder = mRecyclerView.getChildViewHolder(child); Rect childRect = new Rect(); child.getHitRect(childRect); boolean doRectsOverlap = Rect.intersects(childRect, mRegionBounds); mSelectManager.setItemSelected(holder.getAdapterPosition(), doRectsOverlap); boolean shouldSelect = Rect.intersects(childRect, mBounds); int position = holder.getAdapterPosition(); // This also allows us to clear the selection of elements // that only temporarily entered the bounds of the band. if (mSelectedByBand.get(position) && !shouldSelect) { mSelectManager.setItemSelected(position, false); mSelectedByBand.delete(position); } // We need to keep track of the first and last items selected. // We'll use this information along with cursor direction // to determine the starting point of the selection. // We provide this information to selection manager // to enable more natural user interaction when working // with Shift+Click and multiple contiguous selection ranges. if (shouldSelect) { if (first == NOT_SELECTED) { first = position; } else { last = position; } mSelectManager.setItemSelected(position, true); mSelectedByBand.put(position, true); } } // Remember which is the last selected item, so we can // share that with selection manager when band select ends. // It'll use that as it's begin selection point when // user SHIFT+Clicks. if (mCursorDeltaY < 0 && last != NOT_SELECTED) { mFirstSelected = last; } else if (mCursorDeltaY > 0 && first != NOT_SELECTED) { mFirstSelected = first; } } Loading @@ -139,16 +196,10 @@ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { private void endBandSelect() { if (DEBUG) Log.d(TAG, "Ending band select."); mIsBandSelectActive = false; mSelectedByBand.clear(); mRecyclerView.getOverlay().remove(mRegionSelectorDrawable); if (mFirstSelected != NOT_SELECTED) { mSelectManager.setSelectionFocusBegin(mFirstSelected); } /** * Determines whether the provided event was triggered by a mouse (as opposed to a finger or * stylus). * @param e * @return */ private static boolean isMouseEvent(MotionEvent e) { return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; } }
packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java +32 −10 Original line number Diff line number Diff line Loading @@ -286,6 +286,11 @@ public class DirectoryFragment extends Fragment { public boolean onSingleTapUp(MotionEvent e) { return DirectoryFragment.this.onSingleTapUp(e); } @Override public boolean onDoubleTap(MotionEvent e) { Log.d(TAG, "Handling double tap."); return DirectoryFragment.this.onDoubleTap(e); } }; mSelectionManager = new MultiSelectManager(mRecView, listener); Loading Loading @@ -425,9 +430,27 @@ public class DirectoryFragment extends Fragment { } private boolean onSingleTapUp(MotionEvent e) { if (!Events.isMouseEvent(e)) { int position = getEventAdapterPosition(e); if (position != RecyclerView.NO_POSITION) { return handleViewItem(position); } } return false; } protected boolean onDoubleTap(MotionEvent e) { if (Events.isMouseEvent(e)) { Log.d(TAG, "Handling double tap from mouse."); int position = getEventAdapterPosition(e); if (position != RecyclerView.NO_POSITION) { return handleViewItem(position); } } return false; } private boolean handleViewItem(int position) { final Cursor cursor = mAdapter.getItem(position); checkNotNull(cursor, "Cursor cannot be null."); final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); Loading @@ -435,10 +458,9 @@ public class DirectoryFragment extends Fragment { if (isDocumentEnabled(docMimeType, docFlags)) { final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); ((BaseActivity) getActivity()).onDocumentPicked(doc); mSelectionManager.clearSelection(); return true; } } return false; } Loading
packages/DocumentsUI/src/com/android/documentsui/Events.java 0 → 100644 +54 −0 Original line number Diff line number Diff line /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui; import android.view.KeyEvent; import android.view.MotionEvent; /** * Utility code for dealing with MotionEvents. */ 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; } /** * Returns true if event was triggered by a mouse. */ static boolean isMouseType(int toolType) { return toolType == MotionEvent.TOOL_TYPE_MOUSE; } /** * Returns true if the shift is pressed. */ boolean isShiftPressed(MotionEvent e) { return hasShiftBit(e.getMetaState()); } /** * Returns true if the "SHIFT" bit is set. */ static boolean hasShiftBit(int metaState) { return (metaState & KeyEvent.META_SHIFT_ON) != 0; } }
packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java +103 −52 Original line number Diff line number Diff line Loading @@ -26,13 +26,11 @@ import android.support.v7.widget.RecyclerView.AdapterDataObserver; import android.util.Log; import android.util.SparseBooleanArray; import android.view.GestureDetector; import android.view.GestureDetector.OnDoubleTapListener; import android.view.GestureDetector.OnGestureListener; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import com.android.internal.util.Preconditions; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; Loading @@ -51,7 +49,7 @@ public final class MultiSelectManager { // Only created when selection is cleared. private Selection mIntermediateSelection; private Ranger mRanger; private Range mRanger; private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1); private Adapter<?> mAdapter; Loading @@ -60,8 +58,11 @@ public final class MultiSelectManager { /** * @param recyclerView * @param gestureDelegate Option delage gesture listener. * @template A gestureDelegate that implements both {@link OnGestureListener} * and {@link OnDoubleTapListener} */ public MultiSelectManager(final RecyclerView recyclerView, OnGestureListener gestureDelegate) { public <L extends OnGestureListener & OnDoubleTapListener> MultiSelectManager( final RecyclerView recyclerView, L gestureDelegate) { this( recyclerView.getAdapter(), new RecyclerViewHelper() { Loading @@ -86,11 +87,15 @@ public final class MultiSelectManager { } }; CompositeOnGestureListener<? extends Object> compositeListener = new CompositeOnGestureListener<>(listener, gestureDelegate); final GestureDetector detector = new GestureDetector( recyclerView.getContext(), gestureDelegate == null ? listener : new CompositeOnGestureListener(listener, gestureDelegate)); : compositeListener); detector.setOnDoubleTapListener(compositeListener); recyclerView.addOnItemTouchListener( new RecyclerView.OnItemTouchListener() { Loading Loading @@ -236,17 +241,40 @@ public final class MultiSelectManager { } } private void onLongPress(MotionEvent e) { if (DEBUG) Log.d(TAG, "Handling long press event."); int position = mHelper.findEventPosition(e); if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); } onLongPress(position); } /** * TODO: Roll this back into {@link #onLongPress(MotionEvent)} once MotionEvent * can be mocked. * * @param position * @hide */ @VisibleForTesting void onLongPress(int position) { if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); } toggleSelection(position); } /** * @param e * @return true if the event was consumed. */ private boolean onSingleTapUp(MotionEvent e) { if (DEBUG) Log.d(TAG, "Handling tap event."); if (mSelection.isEmpty()) { return false; } return onSingleTapUp(mHelper.findEventPosition(e), e.getMetaState()); return onSingleTapUp(mHelper.findEventPosition(e), e.getMetaState(), e.getToolType(0)); } /** Loading @@ -255,58 +283,33 @@ public final class MultiSelectManager { * * @param position * @param metaState as returned from {@link MotionEvent#getMetaState()}. * @param toolType * @return true if the event was consumed. * @hide */ @VisibleForTesting boolean onSingleTapUp(int position, int metaState) { boolean onSingleTapUp(int position, int metaState, int toolType) { if (mSelection.isEmpty()) { // if this is a mouse click on an item, start selection mode. if (position != RecyclerView.NO_POSITION && Events.isMouseType(toolType)) { toggleSelection(position); } return false; } if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.d(TAG, "View is null. Canceling selection."); clearSelection(); return true; return false; } if (isShiftPressed(metaState) && mRanger != null) { if (Events.hasShiftBit(metaState) && mRanger != null) { mRanger.snapSelection(position); } else { toggleSelection(position); } return true; } private static boolean isShiftPressed(int metaState) { return (metaState & KeyEvent.META_SHIFT_ON) != 0; } private void onLongPress(MotionEvent e) { if (DEBUG) Log.d(TAG, "Handling long press event."); int position = mHelper.findEventPosition(e); if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); } onLongPress(position); } /** * TODO: Roll this back into {@link #onLongPress(MotionEvent)} once MotionEvent * can be mocked. * * @param position * @hide */ @VisibleForTesting void onLongPress(int position) { if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event."); } toggleSelection(position); return false; } /** Loading Loading @@ -334,12 +337,25 @@ public final class MultiSelectManager { // 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); setSelectionFocusBegin(position); } return selected; } } /** * Sets the magic location at which a selection range begins. This * value is consulted when determining how to extend, and modify * selection ranges. * * @throws IllegalStateException if {@code position} is not already be selected * @param position */ void setSelectionFocusBegin(int position) { checkState(mSelection.contains(position)); mRanger = new Range(position); } /** * 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 Loading Loading @@ -420,18 +436,18 @@ public final class MultiSelectManager { /** * Class providing support for managing range selections. */ private final class Ranger { private final class Range { private static final int UNDEFINED = -1; final int mBegin; int mEnd = UNDEFINED; public Ranger(int begin) { public Range(int begin) { if (DEBUG) Log.d(TAG, String.format("New Ranger(%d) created.", begin)); mBegin = begin; } void snapSelection(int position) { private void snapSelection(int position) { checkState(mRanger != null); checkArgument(position != RecyclerView.NO_POSITION); Loading Loading @@ -720,12 +736,17 @@ public final class MultiSelectManager { /** * A composite {@code OnGestureDetector} that allows us to delegate unhandled * events to an outside party (presumably DirectoryFragment). * @template A gestureDelegate that implements both {@link OnGestureListener} * and {@link OnDoubleTapListener} */ private static final class CompositeOnGestureListener implements OnGestureListener { private static final class CompositeOnGestureListener<L extends OnGestureListener & OnDoubleTapListener> implements OnGestureListener, OnDoubleTapListener { private OnGestureListener[] mListeners; private L[] mListeners; public CompositeOnGestureListener(OnGestureListener... listeners) { @SafeVarargs public CompositeOnGestureListener(L... listeners) { mListeners = listeners; } Loading Loading @@ -782,5 +803,35 @@ public final class MultiSelectManager { } return false; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { for (int i = 0; i < mListeners.length; i++) { if (mListeners[i].onSingleTapConfirmed(e)) { return true; } } return false; } @Override public boolean onDoubleTap(MotionEvent e) { for (int i = 0; i < mListeners.length; i++) { if (mListeners[i].onDoubleTap(e)) { return true; } } return false; } @Override public boolean onDoubleTapEvent(MotionEvent e) { for (int i = 0; i < mListeners.length; i++) { if (mListeners[i].onDoubleTapEvent(e)) { return true; } } return false; } } }
packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java +65 −26 Original line number Diff line number Diff line Loading @@ -64,6 +64,35 @@ public class MultiSelectManagerTest { mManager.addCallback(mCallback); } @Test public void mouseClick_StartsSelectionMode() { click(7); assertSelection(7); } @Test public void mouseClick_ShiftClickExtendsSelection() { click(7); shiftClick(11); assertRangeSelection(7, 11); } @Test public void mouseClick_NoPosition_ClearsSelection() { mManager.onLongPress(7); click(11); click(RecyclerView.NO_POSITION); assertSelection(); } @Test public void setSelectionFocusBegin() { mManager.setItemSelected(7, true); mManager.setSelectionFocusBegin(7); shiftClick(11); assertRangeSelection(7, 11); } @Test public void longPress_StartsSelectionMode() { mManager.onLongPress(7); Loading @@ -77,64 +106,58 @@ public class MultiSelectManagerTest { assertSelection(7, 99); } @Test public void singleTapUp_DoesNotSelectBeforeLongPress() { mManager.onSingleTapUp(99, 0); assertSelection(); } @Test public void singleTapUp_UnselectsSelectedItem() { mManager.onLongPress(7); mManager.onSingleTapUp(7, 0); tap(7); assertSelection(); } @Test public void singleTapUp_NoPositionClearsSelection() { public void singleTapUp_NoPosition_ClearsSelection() { mManager.onLongPress(7); mManager.onSingleTapUp(11, 0); mManager.onSingleTapUp(RecyclerView.NO_POSITION, 0); tap(11); tap(RecyclerView.NO_POSITION); assertSelection(); } @Test public void singleTapUp_ExtendsSelection() { mManager.onLongPress(99); mManager.onSingleTapUp(7, 0); mManager.onSingleTapUp(13, 0); mManager.onSingleTapUp(129899, 0); tap(7); tap(13); tap(129899); assertSelection(7, 99, 13, 129899); } @Test public void singleTapUp_ShiftCreatesRangeSelection() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); shiftTap(17); assertRangeSelection(7, 17); } @Test public void singleTapUp_ShiftCreatesRangeSeletion_Backwards() { mManager.onLongPress(17); mManager.onSingleTapUp(7, KeyEvent.META_SHIFT_ON); shiftTap(7); 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); shiftTap(11); shiftTap(17); 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); shiftTap(11); tap(20); shiftTap(25); assertRangeSelected(7, 11); assertRangeSelected(20, 25); assertSelectionSize(11); Loading @@ -143,16 +166,16 @@ public class MultiSelectManagerTest { @Test public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(10, KeyEvent.META_SHIFT_ON); shiftTap(17); shiftTap(10); 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); shiftTap(7); shiftTap(14); assertRangeSelection(14, 17); } Loading @@ -160,11 +183,27 @@ public class MultiSelectManagerTest { @Test public void singleTapUp_ShiftReversesSelectionDirection() { mManager.onLongPress(7); mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON); mManager.onSingleTapUp(0, KeyEvent.META_SHIFT_ON); shiftTap(17); shiftTap(0); assertRangeSelection(0, 7); } private void tap(int position) { mManager.onSingleTapUp(position, 0, MotionEvent.TOOL_TYPE_MOUSE); } private void shiftTap(int position) { mManager.onSingleTapUp(position, KeyEvent.META_SHIFT_ON, MotionEvent.TOOL_TYPE_FINGER); } private void click(int position) { mManager.onSingleTapUp(position, 0, MotionEvent.TOOL_TYPE_MOUSE); } private void shiftClick(int position) { mManager.onSingleTapUp(position, KeyEvent.META_SHIFT_ON, MotionEvent.TOOL_TYPE_MOUSE); } private void assertSelected(int... expected) { for (int i = 0; i < expected.length; i++) { Selection selection = mManager.getSelection(); Loading