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 }