Loading res/layout/fragment_directory.xml +2 −2 Original line number Diff line number Diff line Loading @@ -39,7 +39,7 @@ android:background="@color/material_grey_50" android:visibility="gone"/> <com.android.documentsui.dirlist.TouchSwipeRefreshLayout <com.android.documentsui.dirlist.DocumentsSwipeRefreshLayout android:id="@+id/refresh_layout" android:layout_width="match_parent" android:layout_height="match_parent"> Loading Loading @@ -113,6 +113,6 @@ </LinearLayout> </FrameLayout> </com.android.documentsui.dirlist.TouchSwipeRefreshLayout> </com.android.documentsui.dirlist.DocumentsSwipeRefreshLayout> </com.android.documentsui.dirlist.AnimationView> src/com/android/documentsui/dirlist/DirectoryFragment.java +27 −13 Original line number Diff line number Diff line Loading @@ -155,7 +155,6 @@ public class DirectoryFragment extends Fragment private FocusManager mFocusManager; private IconHelper mIconHelper; private SwipeRefreshLayout mRefreshLayout; private View mEmptyView; private RecyclerView mRecView; Loading Loading @@ -322,10 +321,21 @@ public class DirectoryFragment extends Fragment this::onActivate, (DocumentDetails ignored) -> { return onDeleteSelectedDocuments(); }); }, this::onDragAndDrop, this::onGestureMultiSelect); final int edgeHeight = (int) getResources().getDimension(R.dimen.autoscroll_edge_height); mMultiSelectHelper = GestureMultiSelectHelper.create(mColumnCount, edgeHeight, mAdapter::getModelId, mSelectionMgr, mRecView); mMultiSelectHelper.setEnabled(state.derivedMode == MODE_GRID); mGestureDetector = new ListeningGestureDetector(this.getContext(), mDragHelper, mInputHandler); new ListeningGestureDetector(this.getContext(), mDragHelper, mInputHandler, mMultiSelectHelper); mRecView.addOnItemTouchListener(mGestureDetector); mEmptyView.setOnTouchListener(mGestureDetector); Loading Loading @@ -530,6 +540,7 @@ public class DirectoryFragment extends Fragment private void updateDisplayState() { State state = getDisplayState(); mMultiSelectHelper.setEnabled(state.derivedMode == MODE_GRID); updateLayout(state.derivedMode); mRecView.setAdapter(mAdapter); } Loading Loading @@ -1302,11 +1313,6 @@ public class DirectoryFragment extends Fragment // is handled at the list/grid view level. view.setOnDragListener(mOnDragListener); } if (mTuner.dragAndDropEnabled()) { // Make all items draggable. view.setOnLongClickListener(onLongClickListener); } } void dragStarted() { Loading Loading @@ -1555,13 +1561,21 @@ public class DirectoryFragment extends Fragment private DragStartHelper mDragHelper = new DragStartHelper(null, mOnDragStartListener); private GestureMultiSelectHelper mMultiSelectHelper; private View.OnLongClickListener onLongClickListener = new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { return mDragHelper.onLongClick(v); private boolean onDragAndDrop(InputEvent event) { if (mTuner.dragAndDropEnabled()) { View childView = mRecView.findChildViewUnder(event.getX(), event.getY()); return mDragHelper.onLongClick(childView); } return false; } private boolean onGestureMultiSelect(InputEvent event) { mMultiSelectHelper.start(); return true; } }; private boolean canSelect(DocumentDetails doc) { return canSelect(doc.getModelId()); Loading src/com/android/documentsui/dirlist/TouchSwipeRefreshLayout.java→src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java +6 −5 Original line number Diff line number Diff line Loading @@ -26,18 +26,19 @@ import android.view.MotionEvent; import com.android.documentsui.Events; /** * A {@link SwipeRefreshLayout} that only refresh on touch events. * A {@link SwipeRefreshLayout} that does not intercept any touch events. This relies on its nested * view to scroll in order to cause a refresh. */ public class TouchSwipeRefreshLayout extends SwipeRefreshLayout { public class DocumentsSwipeRefreshLayout extends SwipeRefreshLayout { private static final int[] COLOR_RES = new int[] { android.R.attr.colorAccent }; private static int COLOR_ACCENT_INDEX = 0; public TouchSwipeRefreshLayout(Context context) { public DocumentsSwipeRefreshLayout(Context context) { this(context, null); } public TouchSwipeRefreshLayout(Context context, AttributeSet attrs) { public DocumentsSwipeRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(COLOR_RES); Loading @@ -48,6 +49,6 @@ public class TouchSwipeRefreshLayout extends SwipeRefreshLayout { @Override public boolean onInterceptTouchEvent(MotionEvent e) { return Events.isMouseEvent(e) ? false : super.onInterceptTouchEvent(e); return false; } } src/com/android/documentsui/dirlist/GestureMultiSelectHelper.java 0 → 100644 +353 −0 Original line number Diff line number Diff line /* * 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.annotation.IntDef; import android.graphics.Point; import android.support.v7.widget.RecyclerView; import android.view.MotionEvent; import android.view.View; import com.android.documentsui.Events.InputEvent; import com.android.documentsui.Events.MotionInputEvent; import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate; import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.HashSet; import java.util.Set; import java.util.function.Function; import java.util.function.IntSupplier; /* * Helper class used to intercept events that could cause a gesture multi-select, and keeps * the interception going if necessary. */ class GestureMultiSelectHelper { // Gesture can be used to either select or erase file selections. These are used to define the // type of on-going gestures. @IntDef(flag = true, value = { TYPE_NONE, TYPE_SELECTION, TYPE_ERASE }) @Retention(RetentionPolicy.SOURCE) public @interface SelectType {} public static final int TYPE_NONE = 0; public static final int TYPE_SELECTION = 1; public static final int TYPE_ERASE = 2; // User intent. When intercepting an event, we can see if user intends to scroll, select, or // the intent is unknown. @IntDef(flag = true, value = { TYPE_UNKNOWN, TYPE_SELECT, TYPE_SCROLL }) @Retention(RetentionPolicy.SOURCE) public @interface GestureSelectIntent {} public static final int TYPE_UNKNOWN = 0; public static final int TYPE_SELECT = 1; public static final int TYPE_SCROLL = 2; private final MultiSelectManager mSelectionMgr; private final Runnable mDragScroller; private final Function<Integer, String> mModelIdFinder; private final int mAutoScrollEdgeHeight; private final int mColumnCount; private final IntSupplier mHeight; private final Set<String> mCurrentSelectedIds = new HashSet<>(); private int mLastGlidedItemPos = -1; private int mLastStartedItemPos = -1; private boolean mEnabled = false; private Point mLastStartedPoint; private Point mLastInterceptedPoint; private @SelectType int mType = TYPE_NONE; private @GestureSelectIntent int mUserIntent = TYPE_UNKNOWN; GestureMultiSelectHelper( int columnCount, int autoScrollEdgeHeight, Function<Integer, String> modelIdFinder, MultiSelectManager selectionMgr, IntSupplier heightSupplier, ScrollActionDelegate actionDelegate) { mColumnCount = columnCount; mAutoScrollEdgeHeight = autoScrollEdgeHeight; mModelIdFinder = modelIdFinder; mSelectionMgr = selectionMgr; mHeight = heightSupplier; ScrollDistanceDelegate distanceDelegate = new ScrollDistanceDelegate() { @Override public Point getCurrentPosition() { return mLastInterceptedPoint; } @Override public int getViewHeight() { return mHeight.getAsInt(); } @Override public boolean isActive() { return mSelectionMgr.hasSelection(); } }; mDragScroller = new ViewAutoScroller( mAutoScrollEdgeHeight, distanceDelegate, actionDelegate); } static GestureMultiSelectHelper create( int columnCount, int autoScrollEdgeHeight, Function<Integer, String> modelIdFinder, MultiSelectManager selectionMgr, View scrollView) { ScrollActionDelegate actionDelegate = new ScrollActionDelegate() { @Override public void scrollBy(int dy) { scrollView.scrollBy(0, dy); } @Override public void runAtNextFrame(Runnable r) { scrollView.postOnAnimation(r); } @Override public void removeCallback(Runnable r) { scrollView.removeCallbacks(r); } }; GestureMultiSelectHelper helper = new GestureMultiSelectHelper(columnCount, autoScrollEdgeHeight, modelIdFinder, selectionMgr, scrollView::getHeight, actionDelegate); return helper; } // Explicitly kick off a gesture multi-select without any second guessing void start() { mUserIntent = TYPE_SELECT; } public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { if (!mEnabled) { return false; } boolean handled = false; if (e.getAction() == MotionEvent.ACTION_DOWN) { handled = handleInterceptedDownEvent(rv, e); } if (e.getAction() == MotionEvent.ACTION_MOVE) { handled = handleInterceptedMoveEvent(rv, e); } if (e.getAction() == MotionEvent.ACTION_UP) { handled = handleUpEvent(rv, e); } return handled; } public void onTouchEvent(RecyclerView rv, MotionEvent e) { if (!mEnabled) { return; } if (e.getAction() == MotionEvent.ACTION_UP) { handleUpEvent(rv, e); } if (e.getAction() == MotionEvent.ACTION_MOVE) { handleOnTouchMoveEvent(rv, e); } } public void setEnabled(boolean enabled) { mEnabled = enabled; } // Called when an ACTION_DOWN event is intercepted. // Sets mode to ERASE if the item below the MotionEvent is already selected // Else, sets it to SELECTION mode. private boolean handleInterceptedDownEvent(RecyclerView rv, MotionEvent e) { View itemView = rv.findChildViewUnder(e.getX(), e.getY()); try (InputEvent event = MotionInputEvent.obtain(e, rv)) { mLastStartedPoint = event.getOrigin(); if (itemView != null) { mLastStartedItemPos = rv.getChildAdapterPosition(itemView); mLastGlidedItemPos = mLastStartedItemPos; String modelId = mModelIdFinder.apply(mLastStartedItemPos); if (mSelectionMgr.getSelection().contains(modelId)) { mType = TYPE_ERASE; } else { mType = TYPE_SELECTION; } } } return false; } // Called when an ACTION_MOVE event is intercepted. private boolean handleInterceptedMoveEvent(RecyclerView rv, MotionEvent e) { if (shouldInterceptMoveEvent(rv, e)) { View itemView = rv.findChildViewUnder(e.getX(), e.getY()); int pos = rv.getChildAdapterPosition(itemView); String modelId = mModelIdFinder.apply(pos); mCurrentSelectedIds.add(modelId); return true; } return false; } // Called when ACTION_UP event is intercepted. // Essentially, since this means all gesture movement is over, reset everything. private boolean handleUpEvent(RecyclerView rv, MotionEvent e) { mType = TYPE_NONE; mLastGlidedItemPos = -1; mLastStartedItemPos = -1; mLastStartedPoint = null; mUserIntent = TYPE_UNKNOWN; mCurrentSelectedIds.clear(); return false; } // Call when an intercepted ACTION_MOVE event is passed down. // At this point, we are sure user wants to gesture multi-select. private void handleOnTouchMoveEvent(RecyclerView rv, MotionEvent e) { try (InputEvent event = MotionInputEvent.obtain(e, rv)) { mLastInterceptedPoint = event.getOrigin(); // If user has moved his pointer to the bottom-right empty pane (ie. to the right of the // last item of the recycler view), we would want to set that as the currentItemPos View lastItem = rv.getLayoutManager() .getChildAt(rv.getLayoutManager().getChildCount() - 1); boolean bottomRight = e.getX() > lastItem.getRight() && e.getY() > lastItem.getTop(); // Since views get attached & detached from RecyclerView, // {@link LayoutManager#getChildCount} can return a different number from the actual // number // of items in the adapter. Using the adapter is the for sure way to get the actual last // item position. int lastGlidedItemPos = (bottomRight) ? rv.getAdapter().getItemCount() - 1 : rv.getChildAdapterPosition(rv.findChildViewUnder(e.getX(), e.getY())); if (lastGlidedItemPos != RecyclerView.NO_POSITION && mLastGlidedItemPos != lastGlidedItemPos) { doGestureMultiSelect(mLastStartedItemPos, lastGlidedItemPos); mLastGlidedItemPos = lastGlidedItemPos; } if (insideDragZone(rv)) { mDragScroller.run(); } } } /* Given the start position and the end position, select or erase everything in-between. * @param startPos The adapter position of the start item. * @param endPos The adapter position of the end item. */ private void doGestureMultiSelect(int startPos, int endPos) { boolean selectionMode = (mType == TYPE_SELECTION); // First, reset everything that's currently selected/erased except the start item mCurrentSelectedIds.remove(mModelIdFinder.apply(startPos)); mSelectionMgr.setItemsSelected(mCurrentSelectedIds, !selectionMode); // Then clear the set mCurrentSelectedIds.clear(); // Add everything to be selected/erased if (startPos > endPos) { addItemsToModelIds(endPos, startPos); } else { addItemsToModelIds(startPos, endPos); } mSelectionMgr.setItemsSelected(mCurrentSelectedIds, selectionMode); } // Helper for {@code doGestureMultiSelect (int, int)}. Add all items from startPos <= i <= // endPos into mModelIds. private void addItemsToModelIds(int startPos, int endPos) { for (int i = startPos; i <= endPos; i++) { String modelId = mModelIdFinder.apply(i); if (modelId != null) { mCurrentSelectedIds.add(modelId); } } } // Logic dictating whether a particular ACTION_MOVE event should be intercepted or not. // If user has already shown some clear intent to want to select, we will always return true. // If user has moved to an adjacent item, two possible cases: // 1. User moved left/right. Then it's explicit that they want to multi-select. // 2. User moved top/bottom. Then it's explicit that they want to scroll/natural behavior. private boolean shouldInterceptMoveEvent(RecyclerView rv, MotionEvent e) { try (InputEvent event = MotionInputEvent.obtain(e, rv)) { mLastInterceptedPoint = event.getOrigin(); if (mUserIntent == TYPE_SELECT) { return true; } int startItemPos = rv.getChildAdapterPosition(rv.findChildViewUnder(mLastStartedPoint.x, mLastStartedPoint.y)); int currentItemPos = rv .getChildAdapterPosition(rv.findChildViewUnder(e.getX(), e.getY())); if (startItemPos == RecyclerView.NO_POSITION || currentItemPos == RecyclerView.NO_POSITION) { // It's possible that user either started gesture from an empty space, or is so far // moving his finger to an empty space. Either way, we should not consume the event, // so // return false. return false; } if (mLastGlidedItemPos != currentItemPos) { int diff = Math.abs(startItemPos - currentItemPos); if (diff == 1 && mSelectionMgr.hasSelection()) { mUserIntent = TYPE_SELECT; return true; } else if (diff == mColumnCount) { mUserIntent = TYPE_SCROLL; } } } return false; } private boolean insideDragZone(View scrollView) { if (mLastInterceptedPoint == null) { return false; } boolean shouldScrollUp = mLastInterceptedPoint.y < mAutoScrollEdgeHeight && scrollView.canScrollVertically(-1); boolean shouldScrollDown = mLastInterceptedPoint.y > scrollView.getHeight() - mAutoScrollEdgeHeight && scrollView.canScrollVertically(1); return shouldScrollUp || shouldScrollDown; } } No newline at end of file src/com/android/documentsui/dirlist/ListeningGestureDetector.java +25 −15 Original line number Diff line number Diff line Loading @@ -25,41 +25,51 @@ import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import com.android.documentsui.Events.InputEvent; //Receives event meant for both directory and empty view, and either pass them to //{@link UserInputHandler} for simple gestures (Single Tap, Long-Press), or intercept them for //other types of gestures (drag n' drop) final class ListeningGestureDetector extends GestureDetector implements OnItemTouchListener, OnTouchListener { private DragStartHelper mDragHelper; private UserInputHandler mInputHandler; private final DragStartHelper mDragHelper; private final GestureMultiSelectHelper mGestureSelectHelper; public ListeningGestureDetector( Context context, DragStartHelper dragHelper, UserInputHandler handler) { Context context, DragStartHelper dragHelper, UserInputHandler<? extends InputEvent> handler, GestureMultiSelectHelper gestureMultiSelectHelper) { super(context, handler); mDragHelper = dragHelper; mInputHandler = handler; mGestureSelectHelper = gestureMultiSelectHelper; setOnDoubleTapListener(handler); } @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { // Detect drag events. When a drag is detected, intercept the rest of the gesture. // Detect drag events from mouse. 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; } if (mGestureSelectHelper.onInterceptTouchEvent(rv, e)) { return true; } // Forward unhandled events to UserInputHandler. return onTouchEvent(e); } @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. mGestureSelectHelper.onTouchEvent(rv, e); // Note: even though this event is being handled as part of gesture-multi select, continue // forwarding to the GestureDetector. The detector needs to see the entire cluster of events // in order to properly interpret other gestures, such as long press. onTouchEvent(e); } Loading Loading
res/layout/fragment_directory.xml +2 −2 Original line number Diff line number Diff line Loading @@ -39,7 +39,7 @@ android:background="@color/material_grey_50" android:visibility="gone"/> <com.android.documentsui.dirlist.TouchSwipeRefreshLayout <com.android.documentsui.dirlist.DocumentsSwipeRefreshLayout android:id="@+id/refresh_layout" android:layout_width="match_parent" android:layout_height="match_parent"> Loading Loading @@ -113,6 +113,6 @@ </LinearLayout> </FrameLayout> </com.android.documentsui.dirlist.TouchSwipeRefreshLayout> </com.android.documentsui.dirlist.DocumentsSwipeRefreshLayout> </com.android.documentsui.dirlist.AnimationView>
src/com/android/documentsui/dirlist/DirectoryFragment.java +27 −13 Original line number Diff line number Diff line Loading @@ -155,7 +155,6 @@ public class DirectoryFragment extends Fragment private FocusManager mFocusManager; private IconHelper mIconHelper; private SwipeRefreshLayout mRefreshLayout; private View mEmptyView; private RecyclerView mRecView; Loading Loading @@ -322,10 +321,21 @@ public class DirectoryFragment extends Fragment this::onActivate, (DocumentDetails ignored) -> { return onDeleteSelectedDocuments(); }); }, this::onDragAndDrop, this::onGestureMultiSelect); final int edgeHeight = (int) getResources().getDimension(R.dimen.autoscroll_edge_height); mMultiSelectHelper = GestureMultiSelectHelper.create(mColumnCount, edgeHeight, mAdapter::getModelId, mSelectionMgr, mRecView); mMultiSelectHelper.setEnabled(state.derivedMode == MODE_GRID); mGestureDetector = new ListeningGestureDetector(this.getContext(), mDragHelper, mInputHandler); new ListeningGestureDetector(this.getContext(), mDragHelper, mInputHandler, mMultiSelectHelper); mRecView.addOnItemTouchListener(mGestureDetector); mEmptyView.setOnTouchListener(mGestureDetector); Loading Loading @@ -530,6 +540,7 @@ public class DirectoryFragment extends Fragment private void updateDisplayState() { State state = getDisplayState(); mMultiSelectHelper.setEnabled(state.derivedMode == MODE_GRID); updateLayout(state.derivedMode); mRecView.setAdapter(mAdapter); } Loading Loading @@ -1302,11 +1313,6 @@ public class DirectoryFragment extends Fragment // is handled at the list/grid view level. view.setOnDragListener(mOnDragListener); } if (mTuner.dragAndDropEnabled()) { // Make all items draggable. view.setOnLongClickListener(onLongClickListener); } } void dragStarted() { Loading Loading @@ -1555,13 +1561,21 @@ public class DirectoryFragment extends Fragment private DragStartHelper mDragHelper = new DragStartHelper(null, mOnDragStartListener); private GestureMultiSelectHelper mMultiSelectHelper; private View.OnLongClickListener onLongClickListener = new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { return mDragHelper.onLongClick(v); private boolean onDragAndDrop(InputEvent event) { if (mTuner.dragAndDropEnabled()) { View childView = mRecView.findChildViewUnder(event.getX(), event.getY()); return mDragHelper.onLongClick(childView); } return false; } private boolean onGestureMultiSelect(InputEvent event) { mMultiSelectHelper.start(); return true; } }; private boolean canSelect(DocumentDetails doc) { return canSelect(doc.getModelId()); Loading
src/com/android/documentsui/dirlist/TouchSwipeRefreshLayout.java→src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java +6 −5 Original line number Diff line number Diff line Loading @@ -26,18 +26,19 @@ import android.view.MotionEvent; import com.android.documentsui.Events; /** * A {@link SwipeRefreshLayout} that only refresh on touch events. * A {@link SwipeRefreshLayout} that does not intercept any touch events. This relies on its nested * view to scroll in order to cause a refresh. */ public class TouchSwipeRefreshLayout extends SwipeRefreshLayout { public class DocumentsSwipeRefreshLayout extends SwipeRefreshLayout { private static final int[] COLOR_RES = new int[] { android.R.attr.colorAccent }; private static int COLOR_ACCENT_INDEX = 0; public TouchSwipeRefreshLayout(Context context) { public DocumentsSwipeRefreshLayout(Context context) { this(context, null); } public TouchSwipeRefreshLayout(Context context, AttributeSet attrs) { public DocumentsSwipeRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(COLOR_RES); Loading @@ -48,6 +49,6 @@ public class TouchSwipeRefreshLayout extends SwipeRefreshLayout { @Override public boolean onInterceptTouchEvent(MotionEvent e) { return Events.isMouseEvent(e) ? false : super.onInterceptTouchEvent(e); return false; } }
src/com/android/documentsui/dirlist/GestureMultiSelectHelper.java 0 → 100644 +353 −0 Original line number Diff line number Diff line /* * 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.annotation.IntDef; import android.graphics.Point; import android.support.v7.widget.RecyclerView; import android.view.MotionEvent; import android.view.View; import com.android.documentsui.Events.InputEvent; import com.android.documentsui.Events.MotionInputEvent; import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate; import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.HashSet; import java.util.Set; import java.util.function.Function; import java.util.function.IntSupplier; /* * Helper class used to intercept events that could cause a gesture multi-select, and keeps * the interception going if necessary. */ class GestureMultiSelectHelper { // Gesture can be used to either select or erase file selections. These are used to define the // type of on-going gestures. @IntDef(flag = true, value = { TYPE_NONE, TYPE_SELECTION, TYPE_ERASE }) @Retention(RetentionPolicy.SOURCE) public @interface SelectType {} public static final int TYPE_NONE = 0; public static final int TYPE_SELECTION = 1; public static final int TYPE_ERASE = 2; // User intent. When intercepting an event, we can see if user intends to scroll, select, or // the intent is unknown. @IntDef(flag = true, value = { TYPE_UNKNOWN, TYPE_SELECT, TYPE_SCROLL }) @Retention(RetentionPolicy.SOURCE) public @interface GestureSelectIntent {} public static final int TYPE_UNKNOWN = 0; public static final int TYPE_SELECT = 1; public static final int TYPE_SCROLL = 2; private final MultiSelectManager mSelectionMgr; private final Runnable mDragScroller; private final Function<Integer, String> mModelIdFinder; private final int mAutoScrollEdgeHeight; private final int mColumnCount; private final IntSupplier mHeight; private final Set<String> mCurrentSelectedIds = new HashSet<>(); private int mLastGlidedItemPos = -1; private int mLastStartedItemPos = -1; private boolean mEnabled = false; private Point mLastStartedPoint; private Point mLastInterceptedPoint; private @SelectType int mType = TYPE_NONE; private @GestureSelectIntent int mUserIntent = TYPE_UNKNOWN; GestureMultiSelectHelper( int columnCount, int autoScrollEdgeHeight, Function<Integer, String> modelIdFinder, MultiSelectManager selectionMgr, IntSupplier heightSupplier, ScrollActionDelegate actionDelegate) { mColumnCount = columnCount; mAutoScrollEdgeHeight = autoScrollEdgeHeight; mModelIdFinder = modelIdFinder; mSelectionMgr = selectionMgr; mHeight = heightSupplier; ScrollDistanceDelegate distanceDelegate = new ScrollDistanceDelegate() { @Override public Point getCurrentPosition() { return mLastInterceptedPoint; } @Override public int getViewHeight() { return mHeight.getAsInt(); } @Override public boolean isActive() { return mSelectionMgr.hasSelection(); } }; mDragScroller = new ViewAutoScroller( mAutoScrollEdgeHeight, distanceDelegate, actionDelegate); } static GestureMultiSelectHelper create( int columnCount, int autoScrollEdgeHeight, Function<Integer, String> modelIdFinder, MultiSelectManager selectionMgr, View scrollView) { ScrollActionDelegate actionDelegate = new ScrollActionDelegate() { @Override public void scrollBy(int dy) { scrollView.scrollBy(0, dy); } @Override public void runAtNextFrame(Runnable r) { scrollView.postOnAnimation(r); } @Override public void removeCallback(Runnable r) { scrollView.removeCallbacks(r); } }; GestureMultiSelectHelper helper = new GestureMultiSelectHelper(columnCount, autoScrollEdgeHeight, modelIdFinder, selectionMgr, scrollView::getHeight, actionDelegate); return helper; } // Explicitly kick off a gesture multi-select without any second guessing void start() { mUserIntent = TYPE_SELECT; } public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { if (!mEnabled) { return false; } boolean handled = false; if (e.getAction() == MotionEvent.ACTION_DOWN) { handled = handleInterceptedDownEvent(rv, e); } if (e.getAction() == MotionEvent.ACTION_MOVE) { handled = handleInterceptedMoveEvent(rv, e); } if (e.getAction() == MotionEvent.ACTION_UP) { handled = handleUpEvent(rv, e); } return handled; } public void onTouchEvent(RecyclerView rv, MotionEvent e) { if (!mEnabled) { return; } if (e.getAction() == MotionEvent.ACTION_UP) { handleUpEvent(rv, e); } if (e.getAction() == MotionEvent.ACTION_MOVE) { handleOnTouchMoveEvent(rv, e); } } public void setEnabled(boolean enabled) { mEnabled = enabled; } // Called when an ACTION_DOWN event is intercepted. // Sets mode to ERASE if the item below the MotionEvent is already selected // Else, sets it to SELECTION mode. private boolean handleInterceptedDownEvent(RecyclerView rv, MotionEvent e) { View itemView = rv.findChildViewUnder(e.getX(), e.getY()); try (InputEvent event = MotionInputEvent.obtain(e, rv)) { mLastStartedPoint = event.getOrigin(); if (itemView != null) { mLastStartedItemPos = rv.getChildAdapterPosition(itemView); mLastGlidedItemPos = mLastStartedItemPos; String modelId = mModelIdFinder.apply(mLastStartedItemPos); if (mSelectionMgr.getSelection().contains(modelId)) { mType = TYPE_ERASE; } else { mType = TYPE_SELECTION; } } } return false; } // Called when an ACTION_MOVE event is intercepted. private boolean handleInterceptedMoveEvent(RecyclerView rv, MotionEvent e) { if (shouldInterceptMoveEvent(rv, e)) { View itemView = rv.findChildViewUnder(e.getX(), e.getY()); int pos = rv.getChildAdapterPosition(itemView); String modelId = mModelIdFinder.apply(pos); mCurrentSelectedIds.add(modelId); return true; } return false; } // Called when ACTION_UP event is intercepted. // Essentially, since this means all gesture movement is over, reset everything. private boolean handleUpEvent(RecyclerView rv, MotionEvent e) { mType = TYPE_NONE; mLastGlidedItemPos = -1; mLastStartedItemPos = -1; mLastStartedPoint = null; mUserIntent = TYPE_UNKNOWN; mCurrentSelectedIds.clear(); return false; } // Call when an intercepted ACTION_MOVE event is passed down. // At this point, we are sure user wants to gesture multi-select. private void handleOnTouchMoveEvent(RecyclerView rv, MotionEvent e) { try (InputEvent event = MotionInputEvent.obtain(e, rv)) { mLastInterceptedPoint = event.getOrigin(); // If user has moved his pointer to the bottom-right empty pane (ie. to the right of the // last item of the recycler view), we would want to set that as the currentItemPos View lastItem = rv.getLayoutManager() .getChildAt(rv.getLayoutManager().getChildCount() - 1); boolean bottomRight = e.getX() > lastItem.getRight() && e.getY() > lastItem.getTop(); // Since views get attached & detached from RecyclerView, // {@link LayoutManager#getChildCount} can return a different number from the actual // number // of items in the adapter. Using the adapter is the for sure way to get the actual last // item position. int lastGlidedItemPos = (bottomRight) ? rv.getAdapter().getItemCount() - 1 : rv.getChildAdapterPosition(rv.findChildViewUnder(e.getX(), e.getY())); if (lastGlidedItemPos != RecyclerView.NO_POSITION && mLastGlidedItemPos != lastGlidedItemPos) { doGestureMultiSelect(mLastStartedItemPos, lastGlidedItemPos); mLastGlidedItemPos = lastGlidedItemPos; } if (insideDragZone(rv)) { mDragScroller.run(); } } } /* Given the start position and the end position, select or erase everything in-between. * @param startPos The adapter position of the start item. * @param endPos The adapter position of the end item. */ private void doGestureMultiSelect(int startPos, int endPos) { boolean selectionMode = (mType == TYPE_SELECTION); // First, reset everything that's currently selected/erased except the start item mCurrentSelectedIds.remove(mModelIdFinder.apply(startPos)); mSelectionMgr.setItemsSelected(mCurrentSelectedIds, !selectionMode); // Then clear the set mCurrentSelectedIds.clear(); // Add everything to be selected/erased if (startPos > endPos) { addItemsToModelIds(endPos, startPos); } else { addItemsToModelIds(startPos, endPos); } mSelectionMgr.setItemsSelected(mCurrentSelectedIds, selectionMode); } // Helper for {@code doGestureMultiSelect (int, int)}. Add all items from startPos <= i <= // endPos into mModelIds. private void addItemsToModelIds(int startPos, int endPos) { for (int i = startPos; i <= endPos; i++) { String modelId = mModelIdFinder.apply(i); if (modelId != null) { mCurrentSelectedIds.add(modelId); } } } // Logic dictating whether a particular ACTION_MOVE event should be intercepted or not. // If user has already shown some clear intent to want to select, we will always return true. // If user has moved to an adjacent item, two possible cases: // 1. User moved left/right. Then it's explicit that they want to multi-select. // 2. User moved top/bottom. Then it's explicit that they want to scroll/natural behavior. private boolean shouldInterceptMoveEvent(RecyclerView rv, MotionEvent e) { try (InputEvent event = MotionInputEvent.obtain(e, rv)) { mLastInterceptedPoint = event.getOrigin(); if (mUserIntent == TYPE_SELECT) { return true; } int startItemPos = rv.getChildAdapterPosition(rv.findChildViewUnder(mLastStartedPoint.x, mLastStartedPoint.y)); int currentItemPos = rv .getChildAdapterPosition(rv.findChildViewUnder(e.getX(), e.getY())); if (startItemPos == RecyclerView.NO_POSITION || currentItemPos == RecyclerView.NO_POSITION) { // It's possible that user either started gesture from an empty space, or is so far // moving his finger to an empty space. Either way, we should not consume the event, // so // return false. return false; } if (mLastGlidedItemPos != currentItemPos) { int diff = Math.abs(startItemPos - currentItemPos); if (diff == 1 && mSelectionMgr.hasSelection()) { mUserIntent = TYPE_SELECT; return true; } else if (diff == mColumnCount) { mUserIntent = TYPE_SCROLL; } } } return false; } private boolean insideDragZone(View scrollView) { if (mLastInterceptedPoint == null) { return false; } boolean shouldScrollUp = mLastInterceptedPoint.y < mAutoScrollEdgeHeight && scrollView.canScrollVertically(-1); boolean shouldScrollDown = mLastInterceptedPoint.y > scrollView.getHeight() - mAutoScrollEdgeHeight && scrollView.canScrollVertically(1); return shouldScrollUp || shouldScrollDown; } } No newline at end of file
src/com/android/documentsui/dirlist/ListeningGestureDetector.java +25 −15 Original line number Diff line number Diff line Loading @@ -25,41 +25,51 @@ import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import com.android.documentsui.Events.InputEvent; //Receives event meant for both directory and empty view, and either pass them to //{@link UserInputHandler} for simple gestures (Single Tap, Long-Press), or intercept them for //other types of gestures (drag n' drop) final class ListeningGestureDetector extends GestureDetector implements OnItemTouchListener, OnTouchListener { private DragStartHelper mDragHelper; private UserInputHandler mInputHandler; private final DragStartHelper mDragHelper; private final GestureMultiSelectHelper mGestureSelectHelper; public ListeningGestureDetector( Context context, DragStartHelper dragHelper, UserInputHandler handler) { Context context, DragStartHelper dragHelper, UserInputHandler<? extends InputEvent> handler, GestureMultiSelectHelper gestureMultiSelectHelper) { super(context, handler); mDragHelper = dragHelper; mInputHandler = handler; mGestureSelectHelper = gestureMultiSelectHelper; setOnDoubleTapListener(handler); } @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { // Detect drag events. When a drag is detected, intercept the rest of the gesture. // Detect drag events from mouse. 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; } if (mGestureSelectHelper.onInterceptTouchEvent(rv, e)) { return true; } // Forward unhandled events to UserInputHandler. return onTouchEvent(e); } @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. mGestureSelectHelper.onTouchEvent(rv, e); // Note: even though this event is being handled as part of gesture-multi select, continue // forwarding to the GestureDetector. The detector needs to see the entire cluster of events // in order to properly interpret other gestures, such as long press. onTouchEvent(e); } Loading