Loading packages/DocumentsUI/src/com/android/documentsui/Events.java +21 −0 Original line number Diff line number Diff line Loading @@ -77,6 +77,27 @@ public final class Events { return hasShiftBit(e.getMetaState()); } /** * Whether or not the given keyCode represents a navigation keystroke (e.g. up, down, home). * * @param keyCode * @return */ public static boolean isNavigationKeyCode(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_MOVE_HOME: case KeyEvent.KEYCODE_MOVE_END: return true; default: return false; } } /** * Returns true if the "SHIFT" bit is set. */ Loading packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +132 −3 Original line number Diff line number Diff line Loading @@ -66,6 +66,7 @@ import android.util.TypedValue; import android.view.ActionMode; import android.view.DragEvent; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; Loading Loading @@ -99,7 +100,6 @@ import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.RootInfo; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperations; import com.google.common.collect.Lists; import java.util.ArrayList; Loading Loading @@ -529,6 +529,9 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi final Cursor cursor = mModel.getItem(modelId); checkNotNull(cursor, "Cursor cannot be null."); // TODO: Should this be happening in onSelectionChanged? Technically this callback is // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized // selection changes here) final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) { mNoDeleteCount += selected ? 1 : -1; Loading Loading @@ -827,7 +830,6 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi @Override public void initDocumentHolder(DocumentHolder holder) { holder.addEventListener(mItemEventListener); holder.addOnKeyListener(mSelectionManager); } @Override Loading Loading @@ -1230,7 +1232,12 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi private class ItemEventListener implements DocumentHolder.EventListener { @Override public boolean onActivate(DocumentHolder doc) { // Toggle selection if we're in selection mode, othewise, view item. if (mSelectionManager.hasSelection()) { mSelectionManager.toggleSelection(doc.modelId); } else { handleViewItem(doc.modelId); } return true; } Loading @@ -1240,6 +1247,128 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition()); return true; } @Override public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) { // Only handle key-down events. This is simpler, consistent with most other UIs, and // enables the handling of repeated key events from holding down a key. if (event.getAction() != KeyEvent.ACTION_DOWN) { return false; } boolean handled = false; if (Events.isNavigationKeyCode(keyCode)) { // Find the target item and focus it. int endPos = findTargetPosition(doc.itemView, keyCode); if (endPos != RecyclerView.NO_POSITION) { focusItem(endPos); // Handle any necessary adjustments to selection. boolean extendSelection = event.isShiftPressed(); if (extendSelection) { int startPos = doc.getAdapterPosition(); mSelectionManager.selectRange(startPos, endPos); } handled = true; } } else { // Handle enter key events if (keyCode == KeyEvent.KEYCODE_ENTER) { handled = onActivate(doc); } } return handled; } /** * Finds the destination position where the focus should land for a given navigation event. * * @param view The view that received the event. * @param keyCode The key code for the event. * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION. */ private int findTargetPosition(View view, int keyCode) { if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) { return 0; } if (keyCode == KeyEvent.KEYCODE_MOVE_END) { return mAdapter.getItemCount() - 1; } // Find a navigation target based on the arrow key that the user pressed. int searchDir = -1; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: searchDir = View.FOCUS_UP; break; case KeyEvent.KEYCODE_DPAD_DOWN: searchDir = View.FOCUS_DOWN; break; case KeyEvent.KEYCODE_DPAD_LEFT: searchDir = View.FOCUS_LEFT; break; case KeyEvent.KEYCODE_DPAD_RIGHT: searchDir = View.FOCUS_RIGHT; break; } if (searchDir != -1) { View targetView = view.focusSearch(searchDir); // TargetView can be null, for example, if the user pressed <down> at the bottom // of the list. if (targetView != null) { // Ignore navigation targets that aren't items in the RecyclerView. if (targetView.getParent() == mRecView) { return mRecView.getChildAdapterPosition(targetView); } } } return RecyclerView.NO_POSITION; } /** * Requests focus for the item in the given adapter position, scrolling the RecyclerView if * necessary. * * @param pos */ public void focusItem(final int pos) { // If the item is already in view, focus it; otherwise, scroll to it and focus it. RecyclerView.ViewHolder vh = mRecView.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { mRecView.smoothScrollToPosition(pos); // Set a one-time listener to request focus when the scroll has completed. mRecView.addOnScrollListener( new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged (RecyclerView view, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { // When scrolling stops, find the item and focus it. RecyclerView.ViewHolder vh = view.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { // This might happen in weird corner cases, e.g. if the user is // scrolling while a delete operation is in progress. In that // case, just don't attempt to focus the missing item. Log.w( TAG, "Unable to focus position " + pos + " after a scroll"); } view.removeOnScrollListener(this); } } }); } } } private final class ModelUpdateListener implements Model.UpdateListener { Loading packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java +15 −7 Original line number Diff line number Diff line Loading @@ -84,13 +84,7 @@ public abstract class DocumentHolder public boolean onKey(View v, int keyCode, KeyEvent event) { // Event listener should always be set. checkNotNull(mEventListener); // Intercept enter key-up events, and treat them as clicks. Forward other events. if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_ENTER) { return mEventListener.onActivate(this); } else if (mKeyListener != null) { return mKeyListener.onKey(v, keyCode, event); } return false; return mEventListener.onKey(this, keyCode, event); } public void addEventListener(DocumentHolder.EventListener listener) { Loading Loading @@ -159,15 +153,29 @@ public abstract class DocumentHolder */ interface EventListener { /** * Handles activation events on the document holder. * * @param doc The target DocumentHolder * @return Whether the event was handled. */ public boolean onActivate(DocumentHolder doc); /** * Handles selection events on the document holder. * * @param doc The target DocumentHolder * @return Whether the event was handled. */ public boolean onSelect(DocumentHolder doc); /** * Handles key events on the document holder. * * @param doc The target DocumentHolder. * @param keyCode Key code for the event. * @param event KeyEvent for the event. * @return Whether the event was handled. */ public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event); } } packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java +65 −163 Original line number Diff line number Diff line Loading @@ -34,7 +34,6 @@ import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; Loading @@ -57,7 +56,7 @@ import java.util.Set; * Additionally it can be configured to restrict selection to a single element, @see * #setSelectMode. */ public final class MultiSelectManager implements View.OnKeyListener { public final class MultiSelectManager { /** Selection mode for multiple select. **/ public static final int MODE_MULTIPLE = 0; Loading Loading @@ -239,7 +238,8 @@ public final class MultiSelectManager implements View.OnKeyListener { } /** * Clears the selection, without notifying anyone. * Clears the selection, without notifying selection listeners. UI elements still need to be * notified about state changes so that they can update their appearance. */ private void clearSelectionQuietly() { mRanger = null; Loading @@ -248,10 +248,10 @@ public final class MultiSelectManager implements View.OnKeyListener { return; } Selection intermediateSelection = getSelection(new Selection()); Selection oldSelection = getSelection(new Selection()); mSelection.clear(); for (String id: intermediateSelection.getAll()) { for (String id: oldSelection.getAll()) { notifyItemStateChanged(id, false); } } Loading Loading @@ -334,32 +334,55 @@ public final class MultiSelectManager implements View.OnKeyListener { if (mSelection.contains(modelId)) { changed = attemptDeselect(modelId); } else { boolean canSelect = notifyBeforeItemStateChange(modelId, true); if (!canSelect) { return; changed = attemptSelect(modelId); } if (changed) { notifySelectionChanged(); } if (mSingleSelect && hasSelection()) { clearSelectionQuietly(); } // Here we're already in selection mode. In that case // When a simple click/tap (without SHIFT) creates causes // an item to be selected. // By recreating Ranger at this point, we allow the user to create // multiple separate contiguous ranges with SHIFT+Click & Click. selectAndNotify(modelId); changed = true; /** * 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. * * @param startPos * @param endPos */ 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; } if (changed) { // 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); notifySelectionChanged(); } /** * @return Whether or not there is a current range selection active. */ private boolean isRangeSelectionActive() { return mRanger != null; } /** * Sets the magic location at which a selection range begins. This * value is consulted when determining how to extend, and modify * selection ranges. * 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 Loading Loading @@ -434,6 +457,24 @@ public final class MultiSelectManager implements View.OnKeyListener { } } /** * @param id * @return True if the update was applied. */ private boolean attemptSelect(String id) { checkArgument(id != null); boolean canSelect = notifyBeforeItemStateChange(id, true); if (!canSelect) { return false; } if (mSingleSelect && hasSelection()) { clearSelectionQuietly(); } selectAndNotify(id); return true; } private boolean notifyBeforeItemStateChange(String id, boolean nextState) { int lastListener = mCallbacks.size() - 1; for (int i = lastListener; i > -1; i--) { Loading Loading @@ -786,12 +827,10 @@ public final class MultiSelectManager implements View.OnKeyListener { Point createAbsolutePoint(Point relativePoint); Rect getAbsoluteRectForChildViewAt(int index); int getAdapterPositionAt(int index); int getAdapterPositionForChildView(View view); int getColumnCount(); int getRowCount(); int getChildCount(); int getVisibleChildCount(); void focusItem(int position); /** * Layout items are excluded from the GridModel. */ Loading @@ -811,18 +850,9 @@ public final class MultiSelectManager implements View.OnKeyListener { mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay); } @Override public int getAdapterPositionForChildView(View view) { if (view.getParent() == mView) { return mView.getChildAdapterPosition(view); } else { return RecyclerView.NO_POSITION; } } @Override public int getAdapterPositionAt(int index) { return getAdapterPositionForChildView(mView.getChildAt(index)); return mView.getChildAdapterPosition(mView.getChildAt(index)); } @Override Loading Loading @@ -920,39 +950,6 @@ public final class MultiSelectManager implements View.OnKeyListener { mView.getOverlay().remove(mBand); } @Override public void focusItem(final int pos) { // If the item is already in view, focus it; otherwise, scroll to it and focus it. RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { mView.smoothScrollToPosition(pos); // Set a one-time listener to request focus when the scroll has completed. mView.addOnScrollListener( new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged (RecyclerView view, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { // When scrolling stops, find the item and focus it. RecyclerView.ViewHolder vh = view.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { // This might happen in weird corner cases, e.g. if the user is // scrolling while a delete operation is in progress. In that // case, just don't attempt to focus the missing item. Log.w( TAG, "Unable to focus position " + pos + " after a scroll"); } view.removeOnScrollListener(this); } } }); } } @Override public boolean isLayoutItem(int pos) { // The band selection model only operates on documents and directories. Exclude other Loading Loading @@ -1907,99 +1904,4 @@ public final class MultiSelectManager implements View.OnKeyListener { return true; } } // TODO: Might have to move this to a more global level. e.g. What should happen if the // user taps a file and then presses shift-down? Currently the RecyclerView never even sees // the key event. Perhaps install a global key handler to catch those events while in // selection mode? @Override public boolean onKey(View view, int keyCode, KeyEvent event) { // Listen for key-down events. This allows the handler to respond appropriately when // the user holds down the arrow keys for navigation. if (event.getAction() != KeyEvent.ACTION_DOWN) { return false; } // Here we unpack information from the event and pass it to an more // easily tested method....basically eliminating the need to synthesize // events and views and so on in our tests. int endPos = findTargetPosition(view, keyCode); if (endPos == RecyclerView.NO_POSITION) { // If there is no valid navigation target, don't handle the keypress. return false; } int startPos = mEnvironment.getAdapterPositionForChildView(view); return changeFocus(startPos, endPos, event.isShiftPressed()); } /** * @param startPosition The current focus position. * @param targetPosition The adapter position to focus. * @param extendSelection */ @VisibleForTesting boolean changeFocus(int startPosition, int targetPosition, boolean extendSelection) { // Focus the new file. mEnvironment.focusItem(targetPosition); if (extendSelection) { if (mSingleSelect) { // We're in single select and have an existing selection. // Our best guess as to what the user would expect is to advance the selection. clearSelection(); toggleSelection(targetPosition); } else { if (!hasSelection()) { // No selection - start a selection when the user presses shift-arrow. toggleSelection(startPosition); setSelectionRangeBegin(startPosition); } mRanger.snapSelection(targetPosition); notifySelectionChanged(); } } return true; } /** * Returns the adapter position that the key combo is targeted at. */ private int findTargetPosition(View view, int keyCode) { int position = RecyclerView.NO_POSITION; if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) { position = 0; } else if (keyCode == KeyEvent.KEYCODE_MOVE_END) { position = mAdapter.getItemCount() - 1; } else { // Find a navigation target based on the arrow key that the user pressed. Ignore // navigation targets that aren't items in the recycler view. int searchDir = -1; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: searchDir = View.FOCUS_UP; break; case KeyEvent.KEYCODE_DPAD_DOWN: searchDir = View.FOCUS_DOWN; break; case KeyEvent.KEYCODE_DPAD_LEFT: searchDir = View.FOCUS_LEFT; break; case KeyEvent.KEYCODE_DPAD_RIGHT: searchDir = View.FOCUS_RIGHT; break; } if (searchDir != -1) { View targetView = view.focusSearch(searchDir); // TargetView can be null, for example, if the user pressed <down> at the bottom of // the list. if (targetView != null) { position = mEnvironment.getAdapterPositionForChildView(targetView); } } } return position; } } packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java +6 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.graphics.Rect; import android.os.SystemClock; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; Loading Loading @@ -130,5 +131,10 @@ public class DocumentHolderTest extends AndroidTestCase { return true; } @Override public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) { return false; } } } Loading
packages/DocumentsUI/src/com/android/documentsui/Events.java +21 −0 Original line number Diff line number Diff line Loading @@ -77,6 +77,27 @@ public final class Events { return hasShiftBit(e.getMetaState()); } /** * Whether or not the given keyCode represents a navigation keystroke (e.g. up, down, home). * * @param keyCode * @return */ public static boolean isNavigationKeyCode(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_MOVE_HOME: case KeyEvent.KEYCODE_MOVE_END: return true; default: return false; } } /** * Returns true if the "SHIFT" bit is set. */ Loading
packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +132 −3 Original line number Diff line number Diff line Loading @@ -66,6 +66,7 @@ import android.util.TypedValue; import android.view.ActionMode; import android.view.DragEvent; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; Loading Loading @@ -99,7 +100,6 @@ import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.RootInfo; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperations; import com.google.common.collect.Lists; import java.util.ArrayList; Loading Loading @@ -529,6 +529,9 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi final Cursor cursor = mModel.getItem(modelId); checkNotNull(cursor, "Cursor cannot be null."); // TODO: Should this be happening in onSelectionChanged? Technically this callback is // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized // selection changes here) final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) { mNoDeleteCount += selected ? 1 : -1; Loading Loading @@ -827,7 +830,6 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi @Override public void initDocumentHolder(DocumentHolder holder) { holder.addEventListener(mItemEventListener); holder.addOnKeyListener(mSelectionManager); } @Override Loading Loading @@ -1230,7 +1232,12 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi private class ItemEventListener implements DocumentHolder.EventListener { @Override public boolean onActivate(DocumentHolder doc) { // Toggle selection if we're in selection mode, othewise, view item. if (mSelectionManager.hasSelection()) { mSelectionManager.toggleSelection(doc.modelId); } else { handleViewItem(doc.modelId); } return true; } Loading @@ -1240,6 +1247,128 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition()); return true; } @Override public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) { // Only handle key-down events. This is simpler, consistent with most other UIs, and // enables the handling of repeated key events from holding down a key. if (event.getAction() != KeyEvent.ACTION_DOWN) { return false; } boolean handled = false; if (Events.isNavigationKeyCode(keyCode)) { // Find the target item and focus it. int endPos = findTargetPosition(doc.itemView, keyCode); if (endPos != RecyclerView.NO_POSITION) { focusItem(endPos); // Handle any necessary adjustments to selection. boolean extendSelection = event.isShiftPressed(); if (extendSelection) { int startPos = doc.getAdapterPosition(); mSelectionManager.selectRange(startPos, endPos); } handled = true; } } else { // Handle enter key events if (keyCode == KeyEvent.KEYCODE_ENTER) { handled = onActivate(doc); } } return handled; } /** * Finds the destination position where the focus should land for a given navigation event. * * @param view The view that received the event. * @param keyCode The key code for the event. * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION. */ private int findTargetPosition(View view, int keyCode) { if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) { return 0; } if (keyCode == KeyEvent.KEYCODE_MOVE_END) { return mAdapter.getItemCount() - 1; } // Find a navigation target based on the arrow key that the user pressed. int searchDir = -1; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: searchDir = View.FOCUS_UP; break; case KeyEvent.KEYCODE_DPAD_DOWN: searchDir = View.FOCUS_DOWN; break; case KeyEvent.KEYCODE_DPAD_LEFT: searchDir = View.FOCUS_LEFT; break; case KeyEvent.KEYCODE_DPAD_RIGHT: searchDir = View.FOCUS_RIGHT; break; } if (searchDir != -1) { View targetView = view.focusSearch(searchDir); // TargetView can be null, for example, if the user pressed <down> at the bottom // of the list. if (targetView != null) { // Ignore navigation targets that aren't items in the RecyclerView. if (targetView.getParent() == mRecView) { return mRecView.getChildAdapterPosition(targetView); } } } return RecyclerView.NO_POSITION; } /** * Requests focus for the item in the given adapter position, scrolling the RecyclerView if * necessary. * * @param pos */ public void focusItem(final int pos) { // If the item is already in view, focus it; otherwise, scroll to it and focus it. RecyclerView.ViewHolder vh = mRecView.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { mRecView.smoothScrollToPosition(pos); // Set a one-time listener to request focus when the scroll has completed. mRecView.addOnScrollListener( new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged (RecyclerView view, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { // When scrolling stops, find the item and focus it. RecyclerView.ViewHolder vh = view.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { // This might happen in weird corner cases, e.g. if the user is // scrolling while a delete operation is in progress. In that // case, just don't attempt to focus the missing item. Log.w( TAG, "Unable to focus position " + pos + " after a scroll"); } view.removeOnScrollListener(this); } } }); } } } private final class ModelUpdateListener implements Model.UpdateListener { Loading
packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java +15 −7 Original line number Diff line number Diff line Loading @@ -84,13 +84,7 @@ public abstract class DocumentHolder public boolean onKey(View v, int keyCode, KeyEvent event) { // Event listener should always be set. checkNotNull(mEventListener); // Intercept enter key-up events, and treat them as clicks. Forward other events. if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_ENTER) { return mEventListener.onActivate(this); } else if (mKeyListener != null) { return mKeyListener.onKey(v, keyCode, event); } return false; return mEventListener.onKey(this, keyCode, event); } public void addEventListener(DocumentHolder.EventListener listener) { Loading Loading @@ -159,15 +153,29 @@ public abstract class DocumentHolder */ interface EventListener { /** * Handles activation events on the document holder. * * @param doc The target DocumentHolder * @return Whether the event was handled. */ public boolean onActivate(DocumentHolder doc); /** * Handles selection events on the document holder. * * @param doc The target DocumentHolder * @return Whether the event was handled. */ public boolean onSelect(DocumentHolder doc); /** * Handles key events on the document holder. * * @param doc The target DocumentHolder. * @param keyCode Key code for the event. * @param event KeyEvent for the event. * @return Whether the event was handled. */ public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event); } }
packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java +65 −163 Original line number Diff line number Diff line Loading @@ -34,7 +34,6 @@ import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; Loading @@ -57,7 +56,7 @@ import java.util.Set; * Additionally it can be configured to restrict selection to a single element, @see * #setSelectMode. */ public final class MultiSelectManager implements View.OnKeyListener { public final class MultiSelectManager { /** Selection mode for multiple select. **/ public static final int MODE_MULTIPLE = 0; Loading Loading @@ -239,7 +238,8 @@ public final class MultiSelectManager implements View.OnKeyListener { } /** * Clears the selection, without notifying anyone. * Clears the selection, without notifying selection listeners. UI elements still need to be * notified about state changes so that they can update their appearance. */ private void clearSelectionQuietly() { mRanger = null; Loading @@ -248,10 +248,10 @@ public final class MultiSelectManager implements View.OnKeyListener { return; } Selection intermediateSelection = getSelection(new Selection()); Selection oldSelection = getSelection(new Selection()); mSelection.clear(); for (String id: intermediateSelection.getAll()) { for (String id: oldSelection.getAll()) { notifyItemStateChanged(id, false); } } Loading Loading @@ -334,32 +334,55 @@ public final class MultiSelectManager implements View.OnKeyListener { if (mSelection.contains(modelId)) { changed = attemptDeselect(modelId); } else { boolean canSelect = notifyBeforeItemStateChange(modelId, true); if (!canSelect) { return; changed = attemptSelect(modelId); } if (changed) { notifySelectionChanged(); } if (mSingleSelect && hasSelection()) { clearSelectionQuietly(); } // Here we're already in selection mode. In that case // When a simple click/tap (without SHIFT) creates causes // an item to be selected. // By recreating Ranger at this point, we allow the user to create // multiple separate contiguous ranges with SHIFT+Click & Click. selectAndNotify(modelId); changed = true; /** * 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. * * @param startPos * @param endPos */ 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; } if (changed) { // 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); notifySelectionChanged(); } /** * @return Whether or not there is a current range selection active. */ private boolean isRangeSelectionActive() { return mRanger != null; } /** * Sets the magic location at which a selection range begins. This * value is consulted when determining how to extend, and modify * selection ranges. * 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 Loading Loading @@ -434,6 +457,24 @@ public final class MultiSelectManager implements View.OnKeyListener { } } /** * @param id * @return True if the update was applied. */ private boolean attemptSelect(String id) { checkArgument(id != null); boolean canSelect = notifyBeforeItemStateChange(id, true); if (!canSelect) { return false; } if (mSingleSelect && hasSelection()) { clearSelectionQuietly(); } selectAndNotify(id); return true; } private boolean notifyBeforeItemStateChange(String id, boolean nextState) { int lastListener = mCallbacks.size() - 1; for (int i = lastListener; i > -1; i--) { Loading Loading @@ -786,12 +827,10 @@ public final class MultiSelectManager implements View.OnKeyListener { Point createAbsolutePoint(Point relativePoint); Rect getAbsoluteRectForChildViewAt(int index); int getAdapterPositionAt(int index); int getAdapterPositionForChildView(View view); int getColumnCount(); int getRowCount(); int getChildCount(); int getVisibleChildCount(); void focusItem(int position); /** * Layout items are excluded from the GridModel. */ Loading @@ -811,18 +850,9 @@ public final class MultiSelectManager implements View.OnKeyListener { mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay); } @Override public int getAdapterPositionForChildView(View view) { if (view.getParent() == mView) { return mView.getChildAdapterPosition(view); } else { return RecyclerView.NO_POSITION; } } @Override public int getAdapterPositionAt(int index) { return getAdapterPositionForChildView(mView.getChildAt(index)); return mView.getChildAdapterPosition(mView.getChildAt(index)); } @Override Loading Loading @@ -920,39 +950,6 @@ public final class MultiSelectManager implements View.OnKeyListener { mView.getOverlay().remove(mBand); } @Override public void focusItem(final int pos) { // If the item is already in view, focus it; otherwise, scroll to it and focus it. RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { mView.smoothScrollToPosition(pos); // Set a one-time listener to request focus when the scroll has completed. mView.addOnScrollListener( new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged (RecyclerView view, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { // When scrolling stops, find the item and focus it. RecyclerView.ViewHolder vh = view.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { // This might happen in weird corner cases, e.g. if the user is // scrolling while a delete operation is in progress. In that // case, just don't attempt to focus the missing item. Log.w( TAG, "Unable to focus position " + pos + " after a scroll"); } view.removeOnScrollListener(this); } } }); } } @Override public boolean isLayoutItem(int pos) { // The band selection model only operates on documents and directories. Exclude other Loading Loading @@ -1907,99 +1904,4 @@ public final class MultiSelectManager implements View.OnKeyListener { return true; } } // TODO: Might have to move this to a more global level. e.g. What should happen if the // user taps a file and then presses shift-down? Currently the RecyclerView never even sees // the key event. Perhaps install a global key handler to catch those events while in // selection mode? @Override public boolean onKey(View view, int keyCode, KeyEvent event) { // Listen for key-down events. This allows the handler to respond appropriately when // the user holds down the arrow keys for navigation. if (event.getAction() != KeyEvent.ACTION_DOWN) { return false; } // Here we unpack information from the event and pass it to an more // easily tested method....basically eliminating the need to synthesize // events and views and so on in our tests. int endPos = findTargetPosition(view, keyCode); if (endPos == RecyclerView.NO_POSITION) { // If there is no valid navigation target, don't handle the keypress. return false; } int startPos = mEnvironment.getAdapterPositionForChildView(view); return changeFocus(startPos, endPos, event.isShiftPressed()); } /** * @param startPosition The current focus position. * @param targetPosition The adapter position to focus. * @param extendSelection */ @VisibleForTesting boolean changeFocus(int startPosition, int targetPosition, boolean extendSelection) { // Focus the new file. mEnvironment.focusItem(targetPosition); if (extendSelection) { if (mSingleSelect) { // We're in single select and have an existing selection. // Our best guess as to what the user would expect is to advance the selection. clearSelection(); toggleSelection(targetPosition); } else { if (!hasSelection()) { // No selection - start a selection when the user presses shift-arrow. toggleSelection(startPosition); setSelectionRangeBegin(startPosition); } mRanger.snapSelection(targetPosition); notifySelectionChanged(); } } return true; } /** * Returns the adapter position that the key combo is targeted at. */ private int findTargetPosition(View view, int keyCode) { int position = RecyclerView.NO_POSITION; if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) { position = 0; } else if (keyCode == KeyEvent.KEYCODE_MOVE_END) { position = mAdapter.getItemCount() - 1; } else { // Find a navigation target based on the arrow key that the user pressed. Ignore // navigation targets that aren't items in the recycler view. int searchDir = -1; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: searchDir = View.FOCUS_UP; break; case KeyEvent.KEYCODE_DPAD_DOWN: searchDir = View.FOCUS_DOWN; break; case KeyEvent.KEYCODE_DPAD_LEFT: searchDir = View.FOCUS_LEFT; break; case KeyEvent.KEYCODE_DPAD_RIGHT: searchDir = View.FOCUS_RIGHT; break; } if (searchDir != -1) { View targetView = view.focusSearch(searchDir); // TargetView can be null, for example, if the user pressed <down> at the bottom of // the list. if (targetView != null) { position = mEnvironment.getAdapterPositionForChildView(targetView); } } } return position; } }
packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java +6 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.graphics.Rect; import android.os.SystemClock; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; Loading Loading @@ -130,5 +131,10 @@ public class DocumentHolderTest extends AndroidTestCase { return true; } @Override public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) { return false; } } }