• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.documentsui.selection;
18 
19 import static com.android.documentsui.base.Shared.DEBUG;
20 
21 import android.graphics.Point;
22 import android.support.annotation.VisibleForTesting;
23 import android.support.v7.widget.RecyclerView;
24 import android.util.Log;
25 import android.view.View;
26 
27 import com.android.documentsui.DirectoryReloadLock;
28 import com.android.documentsui.base.Events.InputEvent;
29 import com.android.documentsui.ui.ViewAutoScroller;
30 import com.android.documentsui.ui.ViewAutoScroller.ScrollActionDelegate;
31 import com.android.documentsui.ui.ViewAutoScroller.ScrollDistanceDelegate;
32 
33 import java.util.function.IntSupplier;
34 
35 import javax.annotation.Nullable;
36 
37 /*
38  * Helper class used to intercept events that could cause a gesture multi-select, and keeps
39  * the interception going if necessary.
40  */
41 public final class GestureSelector {
42     private final String TAG = "GestureSelector";
43 
44     private final SelectionManager mSelectionMgr;
45     private final Runnable mDragScroller;
46     private final IntSupplier mHeight;
47     private final ViewFinder mViewFinder;
48     private final DirectoryReloadLock mLock;
49     private int mLastStartedItemPos = -1;
50     private boolean mStarted = false;
51     private Point mLastInterceptedPoint;
52 
GestureSelector( SelectionManager selectionMgr, IntSupplier heightSupplier, ViewFinder viewFinder, ScrollActionDelegate actionDelegate, DirectoryReloadLock lock)53     GestureSelector(
54             SelectionManager selectionMgr,
55             IntSupplier heightSupplier,
56             ViewFinder viewFinder,
57             ScrollActionDelegate actionDelegate,
58             DirectoryReloadLock lock) {
59         mSelectionMgr = selectionMgr;
60         mHeight = heightSupplier;
61         mViewFinder = viewFinder;
62         mLock = lock;
63 
64         ScrollDistanceDelegate distanceDelegate = new ScrollDistanceDelegate() {
65             @Override
66             public Point getCurrentPosition() {
67                 return mLastInterceptedPoint;
68             }
69 
70             @Override
71             public int getViewHeight() {
72                 return mHeight.getAsInt();
73             }
74 
75             @Override
76             public boolean isActive() {
77                 return mStarted && mSelectionMgr.hasSelection();
78             }
79         };
80 
81         mDragScroller = new ViewAutoScroller(distanceDelegate, actionDelegate);
82     }
83 
create( SelectionManager selectionMgr, RecyclerView scrollView, DirectoryReloadLock lock)84     public static GestureSelector create(
85             SelectionManager selectionMgr,
86             RecyclerView scrollView,
87             DirectoryReloadLock lock) {
88         ScrollActionDelegate actionDelegate = new ScrollActionDelegate() {
89             @Override
90             public void scrollBy(int dy) {
91                 scrollView.scrollBy(0, dy);
92             }
93 
94             @Override
95             public void runAtNextFrame(Runnable r) {
96                 scrollView.postOnAnimation(r);
97             }
98 
99             @Override
100             public void removeCallback(Runnable r) {
101                 scrollView.removeCallbacks(r);
102             }
103         };
104         GestureSelector helper =
105                 new GestureSelector(
106                         selectionMgr,
107                         scrollView::getHeight,
108                         scrollView::findChildViewUnder,
109                         actionDelegate,
110                         lock);
111 
112         return helper;
113     }
114 
115     // Explicitly kick off a gesture multi-select.
start(InputEvent event)116     public boolean start(InputEvent event) {
117         //the anchor must already be set before a multi-select event can be started
118         if (mLastStartedItemPos < 0) {
119             if (DEBUG) Log.d(TAG, "Tried to start multi-select without setting an anchor.");
120             return false;
121         }
122         if (mStarted) {
123             return false;
124         }
125         mStarted = true;
126         return true;
127     }
128 
onInterceptTouchEvent(InputEvent e)129     public boolean onInterceptTouchEvent(InputEvent e) {
130         if (e.isMouseEvent()) {
131             return false;
132         }
133 
134         boolean handled = false;
135 
136         if (e.isActionDown()) {
137             handled = handleInterceptedDownEvent(e);
138         }
139 
140         if (e.isActionMove()) {
141             handled = handleInterceptedMoveEvent(e);
142         }
143 
144         return handled;
145     }
146 
onTouchEvent(RecyclerView rv, InputEvent e)147     public void onTouchEvent(RecyclerView rv, InputEvent e) {
148         if (!mStarted) {
149             return;
150         }
151 
152         if (e.isActionUp()) {
153             handleUpEvent(e);
154         }
155 
156         if (e.isActionCancel()) {
157             handleCancelEvent(e);
158         }
159 
160         if (e.isActionMove()) {
161             handleOnTouchMoveEvent(rv, e);
162         }
163     }
164 
165     // Called when an ACTION_DOWN event is intercepted.
166     // If down event happens on a file/doc, we mark that item's position as last started.
handleInterceptedDownEvent(InputEvent e)167     private boolean handleInterceptedDownEvent(InputEvent e) {
168         View itemView = mViewFinder.findView(e.getX(), e.getY());
169         if (itemView != null) {
170             mLastStartedItemPos = e.getItemPosition();
171         }
172         return false;
173     }
174 
175     // Called when an ACTION_MOVE event is intercepted.
handleInterceptedMoveEvent(InputEvent e)176     private boolean handleInterceptedMoveEvent(InputEvent e) {
177         mLastInterceptedPoint = e.getOrigin();
178         if (mStarted) {
179             mSelectionMgr.startRangeSelection(mLastStartedItemPos);
180             // Gesture Selection about to start
181             mLock.block();
182             return true;
183         }
184         return false;
185     }
186 
187     // Called when ACTION_UP event is to be handled.
188     // Essentially, since this means all gesture movement is over, reset everything and apply
189     // provisional selection.
handleUpEvent(InputEvent e)190     private void handleUpEvent(InputEvent e) {
191         mSelectionMgr.getSelection().applyProvisionalSelection();
192         endSelection();
193     }
194 
195     // Called when ACTION_CANCEL event is to be handled.
196     // This means this gesture selection is aborted, so reset everything and abandon provisional
197     // selection.
handleCancelEvent(InputEvent e)198     private void handleCancelEvent(InputEvent e) {
199         mSelectionMgr.cancelProvisionalSelection();
200         endSelection();
201     }
202 
endSelection()203     private void endSelection() {
204         assert(mStarted);
205         mLastStartedItemPos = -1;
206         mStarted = false;
207         mLock.unblock();
208     }
209 
210     // Call when an intercepted ACTION_MOVE event is passed down.
211     // At this point, we are sure user wants to gesture multi-select.
handleOnTouchMoveEvent(RecyclerView rv, InputEvent e)212     private void handleOnTouchMoveEvent(RecyclerView rv, InputEvent e) {
213         mLastInterceptedPoint = e.getOrigin();
214 
215         // If user has moved his pointer to the bottom-right empty pane (ie. to the right of the
216         // last item of the recycler view), we would want to set that as the currentItemPos
217         View lastItem = rv.getLayoutManager()
218                 .getChildAt(rv.getLayoutManager().getChildCount() - 1);
219         int direction = rv.getContext().getResources().getConfiguration().getLayoutDirection();
220         final boolean pastLastItem = isPastLastItem(lastItem.getTop(),
221                 lastItem.getLeft(),
222                 lastItem.getRight(),
223                 e,
224                 direction);
225 
226         // Since views get attached & detached from RecyclerView,
227         // {@link LayoutManager#getChildCount} can return a different number from the actual
228         // number
229         // of items in the adapter. Using the adapter is the for sure way to get the actual last
230         // item position.
231         final float inboundY = getInboundY(rv.getHeight(), e.getY());
232         final int lastGlidedItemPos = (pastLastItem) ? rv.getAdapter().getItemCount() - 1
233                 : rv.getChildAdapterPosition(rv.findChildViewUnder(e.getX(), inboundY));
234         if (lastGlidedItemPos != RecyclerView.NO_POSITION) {
235             doGestureMultiSelect(lastGlidedItemPos);
236         }
237         scrollIfNecessary();
238     }
239 
240     // It's possible for events to go over the top/bottom of the RecyclerView.
241     // We want to get a Y-coordinate within the RecyclerView so we can find the childView underneath
242     // correctly.
getInboundY(float max, float y)243     private static float getInboundY(float max, float y) {
244         if (y < 0f) {
245             return 0f;
246         } else if (y > max) {
247             return max;
248         }
249         return y;
250     }
251 
252     /*
253      * Check to see an InputEvent if past a particular item, i.e. to the right or to the bottom
254      * of the item.
255      * For RTL, it would to be to the left or to the bottom of the item.
256      */
257     @VisibleForTesting
isPastLastItem(int top, int left, int right, InputEvent e, int direction)258     static boolean isPastLastItem(int top, int left, int right, InputEvent e, int direction) {
259         if (direction == View.LAYOUT_DIRECTION_LTR) {
260             return e.getX() > right && e.getY() > top;
261         } else {
262             return e.getX() < left && e.getY() > top;
263         }
264     }
265 
266     /* Given the end position, select everything in-between.
267      * @param endPos  The adapter position of the end item.
268      */
doGestureMultiSelect(int endPos)269     private void doGestureMultiSelect(int endPos) {
270         mSelectionMgr.snapProvisionalRangeSelection(endPos);
271     }
272 
scrollIfNecessary()273     private void scrollIfNecessary() {
274         mDragScroller.run();
275     }
276 
277     @FunctionalInterface
278     interface ViewFinder {
findView(float x, float y)279         @Nullable View findView(float x, float y);
280     }
281 }