Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 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 | |
Steve McKay | 0aa7207 | 2017-09-29 09:43:40 -0700 | [diff] [blame] | 17 | package com.android.documentsui.selection; |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 18 | |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 19 | import static android.support.v4.util.Preconditions.checkArgument; |
| 20 | import static android.support.v4.util.Preconditions.checkState; |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 21 | |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 22 | import android.graphics.Point; |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 23 | import android.os.Build; |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 24 | import android.support.annotation.NonNull; |
Ben Lin | abbb6e0 | 2016-08-24 14:12:37 -0700 | [diff] [blame] | 25 | import android.support.annotation.VisibleForTesting; |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 26 | import android.support.v7.widget.RecyclerView; |
Steve McKay | 07be125 | 2017-09-22 10:58:15 -0700 | [diff] [blame] | 27 | import android.support.v7.widget.RecyclerView.OnItemTouchListener; |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 28 | |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 29 | import android.util.Log; |
Steve McKay | 57c559c | 2017-08-25 12:46:57 -0700 | [diff] [blame] | 30 | import android.view.MotionEvent; |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 31 | import android.view.View; |
| 32 | |
Steve McKay | 0aa7207 | 2017-09-29 09:43:40 -0700 | [diff] [blame] | 33 | import com.android.documentsui.selection.ViewAutoScroller.ScrollHost; |
| 34 | import com.android.documentsui.selection.ViewAutoScroller.ScrollerCallbacks; |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 35 | |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 36 | /** |
| 37 | * GestureSelectionHelper provides logic that interprets a combination |
| 38 | * of motions and gestures in order to provide gesture driven selection support |
| 39 | * when used in conjunction with RecyclerView and other classes in the ReyclerView |
| 40 | * selection support package. |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 41 | */ |
Steve McKay | 6373bfd | 2017-09-27 12:37:00 -0700 | [diff] [blame] | 42 | public final class GestureSelectionHelper extends ScrollHost implements OnItemTouchListener { |
Steve McKay | 53c8e80 | 2017-08-21 12:27:10 -0700 | [diff] [blame] | 43 | |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 44 | private static final String TAG = "GestureSelectionHelper"; |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 45 | |
Steve McKay | 365e3cb | 2017-08-31 10:27:08 -0700 | [diff] [blame] | 46 | private final SelectionHelper mSelectionMgr; |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 47 | private final Runnable mScroller; |
Steve McKay | 57c559c | 2017-08-25 12:46:57 -0700 | [diff] [blame] | 48 | private final ViewDelegate mView; |
Steve McKay | 1239452 | 2017-08-24 14:14:10 -0700 | [diff] [blame] | 49 | private final ContentLock mLock; |
Steve McKay | b9373d0 | 2018-05-07 15:56:35 -0700 | [diff] [blame] | 50 | private final ItemDetailsLookup mItemLookup; |
Steve McKay | 57c559c | 2017-08-25 12:46:57 -0700 | [diff] [blame] | 51 | |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 52 | private int mLastTouchedItemPosition = RecyclerView.NO_POSITION; |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 53 | private boolean mStarted = false; |
| 54 | private Point mLastInterceptedPoint; |
| 55 | |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 56 | /** |
| 57 | * See {@link #create(SelectionHelper, RecyclerView, ContentLock)} for convenience |
| 58 | * method. |
| 59 | */ |
| 60 | @VisibleForTesting |
Steve McKay | 365e3cb | 2017-08-31 10:27:08 -0700 | [diff] [blame] | 61 | GestureSelectionHelper( |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 62 | SelectionHelper selectionHelper, |
Steve McKay | 57c559c | 2017-08-25 12:46:57 -0700 | [diff] [blame] | 63 | ViewDelegate view, |
Steve McKay | b9373d0 | 2018-05-07 15:56:35 -0700 | [diff] [blame] | 64 | ContentLock lock, |
| 65 | ItemDetailsLookup itemLookup) { |
Steve McKay | 1239452 | 2017-08-24 14:14:10 -0700 | [diff] [blame] | 66 | |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 67 | checkArgument(selectionHelper != null); |
| 68 | checkArgument(view != null); |
| 69 | checkArgument(lock != null); |
Steve McKay | b9373d0 | 2018-05-07 15:56:35 -0700 | [diff] [blame] | 70 | checkArgument(itemLookup != null); |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 71 | |
| 72 | mSelectionMgr = selectionHelper; |
Steve McKay | 57c559c | 2017-08-25 12:46:57 -0700 | [diff] [blame] | 73 | mView = view; |
Ben Lin | 9fea312 | 2016-10-10 18:32:26 -0700 | [diff] [blame] | 74 | mLock = lock; |
Steve McKay | b9373d0 | 2018-05-07 15:56:35 -0700 | [diff] [blame] | 75 | mItemLookup = itemLookup; |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 76 | |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 77 | mScroller = new ViewAutoScroller(this, mView); |
Steve McKay | 57c559c | 2017-08-25 12:46:57 -0700 | [diff] [blame] | 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Explicitly kicks off a gesture multi-select. |
| 82 | * |
| 83 | * @return true if started. |
| 84 | */ |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 85 | public void start() { |
| 86 | checkState(!mStarted); |
Steve McKay | 88336c6 | 2018-04-04 11:44:18 -0700 | [diff] [blame] | 87 | // See: b/70518185. It appears start() is being called via onLongPress |
| 88 | // even though we never received an intial handleInterceptedDownEvent |
| 89 | // where we would usually initialize mLastStartedItemPos. |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 90 | if (mLastTouchedItemPosition == RecyclerView.NO_POSITION) { |
| 91 | Log.w(TAG, "Illegal state. Can't start without valid mLastStartedItemPos."); |
| 92 | return; |
Steve McKay | 88336c6 | 2018-04-04 11:44:18 -0700 | [diff] [blame] | 93 | } |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 94 | |
| 95 | // Partner code in MotionInputHandler ensures items |
| 96 | // are selected and range established prior to |
| 97 | // start being called. |
| 98 | // Verify the truth of that statement here |
| 99 | // to make the implicit coupling less of a time bomb. |
| 100 | checkState(mSelectionMgr.isRangeActive()); |
| 101 | |
| 102 | mLock.checkUnlocked(); |
Steve McKay | 6373bfd | 2017-09-27 12:37:00 -0700 | [diff] [blame] | 103 | |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 104 | mStarted = true; |
Steve McKay | 6373bfd | 2017-09-27 12:37:00 -0700 | [diff] [blame] | 105 | mLock.block(); |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 106 | } |
| 107 | |
Steve McKay | 6373bfd | 2017-09-27 12:37:00 -0700 | [diff] [blame] | 108 | @Override |
| 109 | public boolean onInterceptTouchEvent(RecyclerView unused, MotionEvent e) { |
Steve McKay | 57c559c | 2017-08-25 12:46:57 -0700 | [diff] [blame] | 110 | if (MotionEvents.isMouseEvent(e)) { |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 111 | if (Shared.DEBUG) Log.w(TAG, "Unexpected Mouse event. Check configuration."); |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 112 | } |
| 113 | |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 114 | // TODO(b/109808552): It seems that mLastStartedItemPos should likely be set as a method |
| 115 | // parameter in start(). |
| 116 | if (e.getActionMasked() == MotionEvent.ACTION_DOWN) { |
| 117 | if (mItemLookup.getItemDetails(e) != null) { |
| 118 | mLastTouchedItemPosition = mView.getItemUnder(e); |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | // See handleTouch(MotionEvent) javadoc for explanation as to why this is correct. |
| 123 | return handleTouch(e); |
| 124 | } |
| 125 | |
| 126 | @Override |
| 127 | /** @hide */ |
| 128 | public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) { |
| 129 | // See handleTouch(MotionEvent) javadoc for explanation as to why this is correct. |
| 130 | handleTouch(e); |
| 131 | } |
| 132 | |
| 133 | /** |
| 134 | * If selection has started, will handle all appropriate types of MotionEvents and will return |
| 135 | * true if this OnItemTouchListener should start intercepting the rest of the MotionEvents. |
| 136 | * |
| 137 | * <p>This code, and the fact that this method is used by both OnInterceptTouchEvent and |
| 138 | * OnTouchEvent, is correct and valid because: |
| 139 | * <ol> |
| 140 | * <li>MotionEvents that aren't ACTION_DOWN are only ever passed to either onInterceptTouchEvent |
| 141 | * or onTouchEvent; never to both. The MotionEvents we are handling in this method are not |
| 142 | * ACTION_DOWN, and therefore, its appropriate that both the onInterceptTouchEvent and |
| 143 | * onTouchEvent code paths cross this method. |
| 144 | * <li>This method returns true when we want to intercept MotionEvents. OnInterceptTouchEvent |
| 145 | * uses that information to determine its own return, and OnMotionEvent doesn't have a return |
| 146 | * so this methods return value is irrelevant to it. |
| 147 | * </ol> |
| 148 | */ |
| 149 | private boolean handleTouch(MotionEvent e) { |
| 150 | if (!mStarted) { |
| 151 | return false; |
| 152 | } |
| 153 | |
Steve McKay | 6373bfd | 2017-09-27 12:37:00 -0700 | [diff] [blame] | 154 | switch (e.getActionMasked()) { |
Steve McKay | 6373bfd | 2017-09-27 12:37:00 -0700 | [diff] [blame] | 155 | case MotionEvent.ACTION_MOVE: |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 156 | handleMoveEvent(e); |
| 157 | return true; |
| 158 | case MotionEvent.ACTION_UP: |
| 159 | handleUpEvent(); |
| 160 | return true; |
| 161 | case MotionEvent.ACTION_CANCEL: |
| 162 | handleCancelEvent(); |
| 163 | return true; |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 164 | } |
| 165 | |
Steve McKay | 6373bfd | 2017-09-27 12:37:00 -0700 | [diff] [blame] | 166 | return false; |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 167 | } |
| 168 | |
Steve McKay | 6373bfd | 2017-09-27 12:37:00 -0700 | [diff] [blame] | 169 | @Override |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 170 | public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 171 | } |
| 172 | |
Garfield Tan | f7df715 | 2016-10-27 16:55:14 -0700 | [diff] [blame] | 173 | // Called when ACTION_UP event is to be handled. |
| 174 | // Essentially, since this means all gesture movement is over, reset everything and apply |
| 175 | // provisional selection. |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 176 | private void handleUpEvent() { |
Steve McKay | 8845659 | 2017-08-24 10:09:01 -0700 | [diff] [blame] | 177 | mSelectionMgr.mergeProvisionalSelection(); |
Garfield Tan | f7df715 | 2016-10-27 16:55:14 -0700 | [diff] [blame] | 178 | endSelection(); |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 179 | if (mLastTouchedItemPosition != RecyclerView.NO_POSITION) { |
Steve McKay | b9373d0 | 2018-05-07 15:56:35 -0700 | [diff] [blame] | 180 | mSelectionMgr.startRange(mLastTouchedItemPosition); |
Steve McKay | 6373bfd | 2017-09-27 12:37:00 -0700 | [diff] [blame] | 181 | } |
Garfield Tan | f7df715 | 2016-10-27 16:55:14 -0700 | [diff] [blame] | 182 | } |
| 183 | |
| 184 | // Called when ACTION_CANCEL event is to be handled. |
| 185 | // This means this gesture selection is aborted, so reset everything and abandon provisional |
| 186 | // selection. |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 187 | private void handleCancelEvent() { |
Steve McKay | 8845659 | 2017-08-24 10:09:01 -0700 | [diff] [blame] | 188 | mSelectionMgr.clearProvisionalSelection(); |
Garfield Tan | f7df715 | 2016-10-27 16:55:14 -0700 | [diff] [blame] | 189 | endSelection(); |
| 190 | } |
| 191 | |
| 192 | private void endSelection() { |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 193 | checkState(mStarted); |
| 194 | |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 195 | mLastTouchedItemPosition = RecyclerView.NO_POSITION; |
Garfield Tan | f7df715 | 2016-10-27 16:55:14 -0700 | [diff] [blame] | 196 | mStarted = false; |
| 197 | mLock.unblock(); |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 198 | } |
| 199 | |
| 200 | // Call when an intercepted ACTION_MOVE event is passed down. |
| 201 | // At this point, we are sure user wants to gesture multi-select. |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 202 | private void handleMoveEvent(MotionEvent e) { |
Steve McKay | 57c559c | 2017-08-25 12:46:57 -0700 | [diff] [blame] | 203 | mLastInterceptedPoint = MotionEvents.getOrigin(e); |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 204 | |
Steve McKay | 57c559c | 2017-08-25 12:46:57 -0700 | [diff] [blame] | 205 | int lastGlidedItemPos = mView.getLastGlidedItemPosition(e); |
Ben Lin | 35f99e0 | 2016-08-31 12:46:04 -0700 | [diff] [blame] | 206 | if (lastGlidedItemPos != RecyclerView.NO_POSITION) { |
| 207 | doGestureMultiSelect(lastGlidedItemPos); |
| 208 | } |
Ben Lin | 3dbd3b1 | 2016-09-27 14:03:04 -0700 | [diff] [blame] | 209 | scrollIfNecessary(); |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 210 | } |
| 211 | |
| 212 | // It's possible for events to go over the top/bottom of the RecyclerView. |
| 213 | // We want to get a Y-coordinate within the RecyclerView so we can find the childView underneath |
| 214 | // correctly. |
Ben Lin | abbb6e0 | 2016-08-24 14:12:37 -0700 | [diff] [blame] | 215 | private static float getInboundY(float max, float y) { |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 216 | if (y < 0f) { |
| 217 | return 0f; |
| 218 | } else if (y > max) { |
| 219 | return max; |
| 220 | } |
| 221 | return y; |
| 222 | } |
| 223 | |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 224 | /* Given the end position, select everything in-between. |
| 225 | * @param endPos The adapter position of the end item. |
| 226 | */ |
| 227 | private void doGestureMultiSelect(int endPos) { |
Steve McKay | 365e3cb | 2017-08-31 10:27:08 -0700 | [diff] [blame] | 228 | mSelectionMgr.extendProvisionalRange(endPos); |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 229 | } |
| 230 | |
Ben Lin | 3dbd3b1 | 2016-09-27 14:03:04 -0700 | [diff] [blame] | 231 | private void scrollIfNecessary() { |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 232 | mScroller.run(); |
Ben Lin | 4272880 | 2016-08-19 16:07:39 -0700 | [diff] [blame] | 233 | } |
Ben Lin | 35f99e0 | 2016-08-31 12:46:04 -0700 | [diff] [blame] | 234 | |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 235 | @Override |
| 236 | public Point getCurrentPosition() { |
| 237 | return mLastInterceptedPoint; |
| 238 | } |
| 239 | |
| 240 | @Override |
| 241 | public int getViewHeight() { |
| 242 | return mView.getHeight(); |
| 243 | } |
| 244 | |
| 245 | @Override |
| 246 | public boolean isActive() { |
| 247 | return mStarted && mSelectionMgr.hasSelection(); |
| 248 | } |
| 249 | |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 250 | /** |
Steve McKay | b9373d0 | 2018-05-07 15:56:35 -0700 | [diff] [blame] | 251 | * Returns a new instance of GestureSelectionHelper, wrapping the |
| 252 | * RecyclerView in a test friendly wrapper. |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 253 | */ |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 254 | public static GestureSelectionHelper create( |
Steve McKay | b9373d0 | 2018-05-07 15:56:35 -0700 | [diff] [blame] | 255 | SelectionHelper selectionMgr, |
| 256 | RecyclerView recycler, |
| 257 | ContentLock lock, |
| 258 | ItemDetailsLookup itemLookup) { |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 259 | |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 260 | return new GestureSelectionHelper( |
Steve McKay | b9373d0 | 2018-05-07 15:56:35 -0700 | [diff] [blame] | 261 | selectionMgr, new RecyclerViewDelegate(recycler), lock, itemLookup); |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 262 | } |
| 263 | |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 264 | @VisibleForTesting |
| 265 | static abstract class ViewDelegate extends ScrollerCallbacks { |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 266 | abstract int getHeight(); |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 267 | |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 268 | abstract int getItemUnder(MotionEvent e); |
Louis Chang | 3403252 | 2018-06-08 09:33:48 +0800 | [diff] [blame] | 269 | |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 270 | abstract int getLastGlidedItemPosition(MotionEvent e); |
| 271 | } |
| 272 | |
| 273 | @VisibleForTesting |
| 274 | static final class RecyclerViewDelegate extends ViewDelegate { |
| 275 | |
| 276 | private final RecyclerView mView; |
| 277 | |
| 278 | RecyclerViewDelegate(RecyclerView view) { |
| 279 | checkArgument(view != null); |
| 280 | mView = view; |
| 281 | } |
| 282 | |
| 283 | @Override |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 284 | int getHeight() { |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 285 | return mView.getHeight(); |
| 286 | } |
| 287 | |
| 288 | @Override |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 289 | int getItemUnder(MotionEvent e) { |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 290 | View child = mView.findChildViewUnder(e.getX(), e.getY()); |
| 291 | return child != null |
| 292 | ? mView.getChildAdapterPosition(child) |
| 293 | : RecyclerView.NO_POSITION; |
| 294 | } |
| 295 | |
| 296 | @Override |
Steve McKay | 5bf51ec | 2017-09-29 13:43:48 -0700 | [diff] [blame] | 297 | int getLastGlidedItemPosition(MotionEvent e) { |
Steve McKay | 5a62037 | 2017-09-11 12:18:56 -0700 | [diff] [blame] | 298 | // If user has moved his pointer to the bottom-right empty pane (ie. to the right of the |
| 299 | // last item of the recycler view), we would want to set that as the currentItemPos |
| 300 | View lastItem = mView.getLayoutManager() |
| 301 | .getChildAt(mView.getLayoutManager().getChildCount() - 1); |
| 302 | int direction = |
| 303 | mView.getContext().getResources().getConfiguration().getLayoutDirection(); |
| 304 | final boolean pastLastItem = isPastLastItem(lastItem.getTop(), |
| 305 | lastItem.getLeft(), |
| 306 | lastItem.getRight(), |
| 307 | e, |
| 308 | direction); |
| 309 | |
| 310 | // Since views get attached & detached from RecyclerView, |
| 311 | // {@link LayoutManager#getChildCount} can return a different number from the actual |
| 312 | // number |
| 313 | // of items in the adapter. Using the adapter is the for sure way to get the actual last |
| 314 | // item position. |
| 315 | final float inboundY = getInboundY(mView.getHeight(), e.getY()); |
| 316 | return (pastLastItem) ? mView.getAdapter().getItemCount() - 1 |
| 317 | : mView.getChildAdapterPosition(mView.findChildViewUnder(e.getX(), inboundY)); |
| 318 | } |
| 319 | |
| 320 | /* |
| 321 | * Check to see if MotionEvent if past a particular item, i.e. to the right or to the bottom |
| 322 | * of the item. |
| 323 | * For RTL, it would to be to the left or to the bottom of the item. |
| 324 | */ |
| 325 | @VisibleForTesting |
| 326 | static boolean isPastLastItem(int top, int left, int right, MotionEvent e, int direction) { |
| 327 | if (direction == View.LAYOUT_DIRECTION_LTR) { |
| 328 | return e.getX() > right && e.getY() > top; |
| 329 | } else { |
| 330 | return e.getX() < left && e.getY() > top; |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | @Override |
| 335 | public void scrollBy(int dy) { |
| 336 | mView.scrollBy(0, dy); |
| 337 | } |
| 338 | |
| 339 | @Override |
| 340 | public void runAtNextFrame(Runnable r) { |
| 341 | mView.postOnAnimation(r); |
| 342 | } |
| 343 | |
| 344 | @Override |
| 345 | public void removeCallback(Runnable r) { |
| 346 | mView.removeCallbacks(r); |
| 347 | } |
Ben Lin | 35f99e0 | 2016-08-31 12:46:04 -0700 | [diff] [blame] | 348 | } |
Steve McKay | 57c559c | 2017-08-25 12:46:57 -0700 | [diff] [blame] | 349 | } |