From 10419c2882a5dfdc9b1c1908775f3899dbd6391e Mon Sep 17 00:00:00 2001 From: Steve McKay Date: Thu, 23 Jun 2016 11:08:19 -0700 Subject: [PATCH] Move event handling code out of DirectoryFragment Bug:29575607 Change-Id: Ieae3bcee030973bf83551a74f722236a24832730 --- .../src/com/android/documentsui/Events.java | 17 ++ .../dirlist/DirectoryFragment.java | 167 ++---------------- .../dirlist/ItemEventListener.java | 132 ++++++++++++++ .../dirlist/ListeningGestureDetector.java | 86 +++++++++ 4 files changed, 253 insertions(+), 149 deletions(-) create mode 100644 packages/DocumentsUI/src/com/android/documentsui/dirlist/ItemEventListener.java create mode 100644 packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java diff --git a/packages/DocumentsUI/src/com/android/documentsui/Events.java b/packages/DocumentsUI/src/com/android/documentsui/Events.java index 27293e16e69a3..116292365dfa5 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/Events.java +++ b/packages/DocumentsUI/src/com/android/documentsui/Events.java @@ -144,6 +144,10 @@ public final class Events { private static final Pools.SimplePool sPool = new Pools.SimplePool<>(1); private MotionEvent mEvent; + interface PositionProvider { + int get(MotionEvent e); + } + private int mPosition; private MotionInputEvent() { @@ -167,6 +171,19 @@ public final class Events { return instance; } + public static MotionInputEvent obtain( + MotionEvent event, PositionProvider positionProvider) { + Shared.checkMainLoop(); + + MotionInputEvent instance = sPool.acquire(); + instance = (instance != null ? instance : new MotionInputEvent()); + + instance.mEvent = event; + instance.mPosition = positionProvider.get(event); + + return instance; + } + public void recycle() { Shared.checkMainLoop(); diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java index 00f2885fb932c..6fcf24ce647e9 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -49,7 +49,6 @@ import android.support.v13.view.DragStartHelper; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager.SpanSizeLookup; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.OnItemTouchListener; import android.support.v7.widget.RecyclerView.RecyclerListener; import android.support.v7.widget.RecyclerView.ViewHolder; import android.text.BidiFormatter; @@ -61,14 +60,12 @@ import android.view.ContextMenu; import android.view.DragEvent; import android.view.GestureDetector; import android.view.HapticFeedbackConstants; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; -import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; @@ -143,7 +140,7 @@ public class DirectoryFragment extends Fragment private Model mModel; private MultiSelectManager mSelectionManager; private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener(); - private ItemEventListener mItemEventListener = new ItemEventListener(); + private ItemEventListener mItemEventListener; private SelectionModeListener mSelectionModeListener; private FocusManager mFocusManager; @@ -322,6 +319,13 @@ public class DirectoryFragment extends Fragment // Make sure this is done after the RecyclerView is set up. mFocusManager = new FocusManager(context, mRecView, mModel); + mItemEventListener = new ItemEventListener( + mSelectionManager, + mFocusManager, + this::handleViewItem, + this::deleteDocuments, + this::canSelect); + final BaseActivity activity = getBaseActivity(); mTuner = activity.createFragmentTuner(); mMenuManager = activity.getMenuManager(); @@ -1395,99 +1399,6 @@ public class DirectoryFragment extends Fragment return mSelectionManager.getSelection().contains(modelId); } - 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; - } - - @Override - public boolean onSelect(DocumentHolder doc) { - mSelectionManager.toggleSelection(doc.modelId); - 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; - } - - // Ignore tab key events. Those should be handled by the top-level key handler. - if (keyCode == KeyEvent.KEYCODE_TAB) { - return false; - } - - 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(doc, 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; - } - - // Handle enter key events - switch (keyCode) { - case KeyEvent.KEYCODE_ENTER: - if (event.isShiftPressed()) { - return onSelect(doc); - } - // For non-shifted enter keypresses, fall through. - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_BUTTON_A: - return onActivate(doc); - case KeyEvent.KEYCODE_FORWARD_DEL: - // This has to be handled here instead of in a keyboard shortcut, because - // keyboard shortcuts all have to be modified with the 'Ctrl' key. - if (mSelectionManager.hasSelection()) { - Selection selection = mSelectionManager.getSelection(new Selection()); - deleteDocuments(selection); - } - // Always handle the key, even if there was nothing to delete. This is a - // precaution to prevent other handlers from potentially picking up the event - // and triggering extra behaviours. - return true; - } - - return false; - } - - private boolean shouldExtendSelection(DocumentHolder doc, KeyEvent event) { - if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) { - return false; - } - - // TODO: Combine this method with onBeforeItemStateChange, as both of them are almost - // the same, and responsible for the same thing (whether to select or not). - final Cursor cursor = mModel.getItem(doc.modelId); - if (cursor == null) { - Log.w(TAG, "Couldn't obtain cursor for modelId: " + doc.modelId); - return false; - } - - final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); - final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); - return mTuner.canSelectType(docMimeType, docFlags); - } - } - private final class ModelUpdateListener implements Model.UpdateListener { @Override public void onModelUpdate(Model model) { @@ -1594,68 +1505,26 @@ public class DirectoryFragment extends Fragment } }; - // Previously we listened to events with one class, only to bounce them forward - // to GestureDetector. We're still doing that here, but with a single class - // that reduces overall complexity in our glue code. - private static final class ListeningGestureDetector extends GestureDetector - implements OnItemTouchListener, OnTouchListener { - - private DragStartHelper mDragHelper; - private GestureListener mGestureListener; - - public ListeningGestureDetector( - Context context, DragStartHelper dragHelper, GestureListener listener) { - super(context, listener); - mDragHelper = dragHelper; - mGestureListener = listener; - setOnDoubleTapListener(listener); - } - - @Override - public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - if (e.getAction() == MotionEvent.ACTION_DOWN && Events.isMouseEvent(e)) { - mGestureListener.setLastButtonState(e.getButtonState()); - } - - // Detect drag events. When a drag is detected, intercept the rest of the gesture. - View itemView = rv.findChildViewUnder(e.getX(), e.getY()); - if (itemView != null && mDragHelper.onTouch(itemView, e)) { - return true; - } - // Forward unhandled events to the GestureDetector. - onTouchEvent(e); + private boolean canSelect(String modelId) { + // TODO: Combine this method with onBeforeItemStateChange, as both of them are almost + // the same, and responsible for the same thing (whether to select or not). + final Cursor cursor = mModel.getItem(modelId); + if (cursor == null) { + Log.w(TAG, "Couldn't obtain cursor for modelId: " + modelId); return false; } - @Override - public void onTouchEvent(RecyclerView rv, MotionEvent e) { - View itemView = rv.findChildViewUnder(e.getX(), e.getY()); - mDragHelper.onTouch(itemView, e); - // Note: even though this event is being handled as part of a drag gesture, continue - // forwarding to the GestureDetector. The detector needs to see the entire cluster of - // events in order to properly interpret gestures. - onTouchEvent(e); - } - - @Override - public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {} - - // For mEmptyView right-click context menu - @Override - public boolean onTouch(View v, MotionEvent event) { - if (event.getButtonState() == MotionEvent.BUTTON_SECONDARY) { - return mGestureListener.onRightClick(event); - } - return false; - } + final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); + final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); + return mTuner.canSelectType(docMimeType, docFlags); } /** * The gesture listener for items in the list/grid view. Interprets gestures and sends the * events to the target DocumentHolder, whence they are routed to the appropriate listener. */ - private class GestureListener extends GestureDetector.SimpleOnGestureListener { + class GestureListener extends GestureDetector.SimpleOnGestureListener { // From the RecyclerView, we get two events sent to // ListeningGestureDetector#onInterceptTouchEvent on a mouse click; we first get an // ACTION_DOWN Event for clicking on the mouse, and then an ACTION_UP event from releasing diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ItemEventListener.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ItemEventListener.java new file mode 100644 index 0000000000000..cffba8d581d1a --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ItemEventListener.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2016 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.dirlist; + +import android.view.KeyEvent; + +import com.android.documentsui.Events; +import com.android.documentsui.dirlist.MultiSelectManager.Selection; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Handles click/tap/key events on individual DocumentHolders. + */ +class ItemEventListener implements DocumentHolder.EventListener { + private MultiSelectManager mSelectionManager; + private FocusManager mFocusManager; + + private Consumer mViewItemCallback; + private Consumer mDeleteDocumentsCallback; + private Predicate mCanSelectPredicate; + + public ItemEventListener( + MultiSelectManager selectionManager, + FocusManager focusManager, + Consumer viewItemCallback, + Consumer deleteDocumentsCallback, + Predicate canSelectPredicate) { + + mSelectionManager = selectionManager; + mFocusManager = focusManager; + mViewItemCallback = viewItemCallback; + mDeleteDocumentsCallback = deleteDocumentsCallback; + mCanSelectPredicate = canSelectPredicate; + } + + @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 { + mViewItemCallback.accept(doc.modelId); + } + return true; + } + + @Override + public boolean onSelect(DocumentHolder doc) { + mSelectionManager.toggleSelection(doc.modelId); + 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; + } + + // Ignore tab key events. Those should be handled by the top-level key handler. + if (keyCode == KeyEvent.KEYCODE_TAB) { + return false; + } + + 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(doc, 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; + } + + // Handle enter key events + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + if (event.isShiftPressed()) { + return onSelect(doc); + } + // For non-shifted enter keypresses, fall through. + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_BUTTON_A: + return onActivate(doc); + case KeyEvent.KEYCODE_FORWARD_DEL: + // This has to be handled here instead of in a keyboard shortcut, because + // keyboard shortcuts all have to be modified with the 'Ctrl' key. + if (mSelectionManager.hasSelection()) { + Selection selection = mSelectionManager.getSelection(new Selection()); + mDeleteDocumentsCallback.accept(selection); + } + // Always handle the key, even if there was nothing to delete. This is a + // precaution to prevent other handlers from potentially picking up the event + // and triggering extra behaviours. + return true; + } + + return false; + } + + private boolean shouldExtendSelection(DocumentHolder doc, KeyEvent event) { + if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) { + return false; + } + + return mCanSelectPredicate.test(doc.modelId); + } +} diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java new file mode 100644 index 0000000000000..96e15d97fa69e --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2016 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.dirlist; + +import android.content.Context; +import android.support.v13.view.DragStartHelper; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.OnItemTouchListener; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; + +import com.android.documentsui.Events; +import com.android.documentsui.dirlist.DirectoryFragment.GestureListener; + +// Previously we listened to events with one class, only to bounce them forward +// to GestureDetector. We're still doing that here, but with a single class +// that reduces overall complexity in our glue code. +final class ListeningGestureDetector extends GestureDetector + implements OnItemTouchListener, OnTouchListener { + + private DragStartHelper mDragHelper; + private GestureListener mGestureListener; + + public ListeningGestureDetector( + Context context, DragStartHelper dragHelper, GestureListener listener) { + super(context, listener); + mDragHelper = dragHelper; + mGestureListener = listener; + setOnDoubleTapListener(listener); + } + + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { + if (e.getAction() == MotionEvent.ACTION_DOWN && Events.isMouseEvent(e)) { + mGestureListener.setLastButtonState(e.getButtonState()); + } + + // Detect drag events. When a drag is detected, intercept the rest of the gesture. + View itemView = rv.findChildViewUnder(e.getX(), e.getY()); + if (itemView != null && mDragHelper.onTouch(itemView, e)) { + return true; + } + // Forward unhandled events to the GestureDetector. + onTouchEvent(e); + + return false; + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent e) { + View itemView = rv.findChildViewUnder(e.getX(), e.getY()); + mDragHelper.onTouch(itemView, e); + // Note: even though this event is being handled as part of a drag gesture, continue + // forwarding to the GestureDetector. The detector needs to see the entire cluster of + // events in order to properly interpret gestures. + onTouchEvent(e); + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {} + + // For mEmptyView right-click context menu + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getButtonState() == MotionEvent.BUTTON_SECONDARY) { + return mGestureListener.onRightClick(event); + } + return false; + } +} \ No newline at end of file