1 /* 2 * Copyright 2018 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 androidx.recyclerview.widget; 18 19 import android.animation.Animator; 20 import android.animation.ValueAnimator; 21 import android.annotation.SuppressLint; 22 import android.content.res.Resources; 23 import android.graphics.Canvas; 24 import android.graphics.Rect; 25 import android.os.Build; 26 import android.util.Log; 27 import android.view.GestureDetector; 28 import android.view.HapticFeedbackConstants; 29 import android.view.MotionEvent; 30 import android.view.VelocityTracker; 31 import android.view.View; 32 import android.view.ViewConfiguration; 33 import android.view.ViewParent; 34 import android.view.animation.Interpolator; 35 36 import androidx.annotation.VisibleForTesting; 37 import androidx.core.view.ViewCompat; 38 import androidx.recyclerview.R; 39 import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; 40 import androidx.recyclerview.widget.RecyclerView.ViewHolder; 41 42 import org.jspecify.annotations.NonNull; 43 import org.jspecify.annotations.Nullable; 44 45 import java.util.ArrayList; 46 import java.util.List; 47 48 /** 49 * This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView. 50 * <p> 51 * It works with a RecyclerView and a Callback class, which configures what type of interactions 52 * are enabled and also receives events when user performs these actions. 53 * <p> 54 * Depending on which functionality you support, you should override 55 * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)} and / or 56 * {@link Callback#onSwiped(ViewHolder, int)}. 57 * <p> 58 * This class is designed to work with any LayoutManager but for certain situations, it can be 59 * optimized for your custom LayoutManager by extending methods in the 60 * {@link ItemTouchHelper.Callback} class or implementing {@link ItemTouchHelper.ViewDropHandler} 61 * interface in your LayoutManager. 62 * <p> 63 * By default, ItemTouchHelper moves the items' translateX/Y properties to reposition them. You can 64 * customize these behaviors by overriding {@link Callback#onChildDraw(Canvas, RecyclerView, 65 * ViewHolder, float, float, int, boolean)} 66 * or {@link Callback#onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, 67 * boolean)}. 68 * <p/> 69 * Most of the time you only need to override <code>onChildDraw</code>. 70 */ 71 public class ItemTouchHelper extends RecyclerView.ItemDecoration 72 implements RecyclerView.OnChildAttachStateChangeListener { 73 74 /** 75 * Up direction, used for swipe & drag control. 76 */ 77 public static final int UP = 1; 78 79 /** 80 * Down direction, used for swipe & drag control. 81 */ 82 public static final int DOWN = 1 << 1; 83 84 /** 85 * Left direction, used for swipe & drag control. 86 */ 87 public static final int LEFT = 1 << 2; 88 89 /** 90 * Right direction, used for swipe & drag control. 91 */ 92 public static final int RIGHT = 1 << 3; 93 94 // If you change these relative direction values, update Callback#convertToAbsoluteDirection, 95 // Callback#convertToRelativeDirection. 96 /** 97 * Horizontal start direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout 98 * direction. Used for swipe & drag control. 99 */ 100 public static final int START = LEFT << 2; 101 102 /** 103 * Horizontal end direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout 104 * direction. Used for swipe & drag control. 105 */ 106 public static final int END = RIGHT << 2; 107 108 /** 109 * ItemTouchHelper is in idle state. At this state, either there is no related motion event by 110 * the user or latest motion events have not yet triggered a swipe or drag. 111 */ 112 public static final int ACTION_STATE_IDLE = 0; 113 114 /** 115 * A View is currently being swiped. 116 */ 117 @SuppressWarnings("WeakerAccess") 118 public static final int ACTION_STATE_SWIPE = 1; 119 120 /** 121 * A View is currently being dragged. 122 */ 123 @SuppressWarnings("WeakerAccess") 124 public static final int ACTION_STATE_DRAG = 2; 125 126 /** 127 * Animation type for views which are swiped successfully. 128 */ 129 @SuppressWarnings("WeakerAccess") 130 public static final int ANIMATION_TYPE_SWIPE_SUCCESS = 1 << 1; 131 132 /** 133 * Animation type for views which are not completely swiped thus will animate back to their 134 * original position. 135 */ 136 @SuppressWarnings("WeakerAccess") 137 public static final int ANIMATION_TYPE_SWIPE_CANCEL = 1 << 2; 138 139 /** 140 * Animation type for views that were dragged and now will animate to their final position. 141 */ 142 @SuppressWarnings("WeakerAccess") 143 public static final int ANIMATION_TYPE_DRAG = 1 << 3; 144 145 private static final String TAG = "ItemTouchHelper"; 146 147 private static final boolean DEBUG = false; 148 149 private static final int ACTIVE_POINTER_ID_NONE = -1; 150 151 static final int DIRECTION_FLAG_COUNT = 8; 152 153 private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1; 154 155 static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT; 156 157 static final int ACTION_MODE_DRAG_MASK = ACTION_MODE_SWIPE_MASK << DIRECTION_FLAG_COUNT; 158 159 /** 160 * The unit we are using to track velocity 161 */ 162 private static final int PIXELS_PER_SECOND = 1000; 163 164 /** 165 * Views, whose state should be cleared after they are detached from RecyclerView. 166 * This is necessary after swipe dismissing an item. We wait until animator finishes its job 167 * to clean these views. 168 */ 169 final List<View> mPendingCleanup = new ArrayList<>(); 170 171 /** 172 * Re-use array to calculate dx dy for a ViewHolder 173 */ 174 private final float[] mTmpPosition = new float[2]; 175 176 /** 177 * Currently selected view holder 178 */ 179 @SuppressWarnings("WeakerAccess") /* synthetic access */ 180 ViewHolder mSelected = null; 181 182 /** 183 * The reference coordinates for the action start. For drag & drop, this is the time long 184 * press is completed vs for swipe, this is the initial touch point. 185 */ 186 float mInitialTouchX; 187 188 float mInitialTouchY; 189 190 /** 191 * Set when ItemTouchHelper is assigned to a RecyclerView. 192 */ 193 private float mSwipeEscapeVelocity; 194 195 /** 196 * Set when ItemTouchHelper is assigned to a RecyclerView. 197 */ 198 private float mMaxSwipeVelocity; 199 200 /** 201 * The diff between the last event and initial touch. 202 */ 203 float mDx; 204 205 float mDy; 206 207 /** 208 * The coordinates of the selected view at the time it is selected. We record these values 209 * when action starts so that we can consistently position it even if LayoutManager moves the 210 * View. 211 */ 212 private float mSelectedStartX; 213 214 private float mSelectedStartY; 215 216 /** 217 * The pointer we are tracking. 218 */ 219 @SuppressWarnings("WeakerAccess") /* synthetic access */ 220 int mActivePointerId = ACTIVE_POINTER_ID_NONE; 221 222 /** 223 * Developer callback which controls the behavior of ItemTouchHelper. 224 */ 225 @NonNull Callback mCallback; 226 227 /** 228 * Current mode. 229 */ 230 private int mActionState = ACTION_STATE_IDLE; 231 232 /** 233 * The direction flags obtained from unmasking 234 * {@link Callback#getAbsoluteMovementFlags(RecyclerView, ViewHolder)} for the current 235 * action state. 236 */ 237 @SuppressWarnings("WeakerAccess") /* synthetic access */ 238 int mSelectedFlags; 239 240 /** 241 * When a View is dragged or swiped and needs to go back to where it was, we create a Recover 242 * Animation and animate it to its location using this custom Animator, instead of using 243 * framework Animators. 244 * Using framework animators has the side effect of clashing with ItemAnimator, creating 245 * jumpy UIs. 246 */ 247 @VisibleForTesting 248 List<RecoverAnimation> mRecoverAnimations = new ArrayList<>(); 249 250 private int mSlop; 251 252 RecyclerView mRecyclerView; 253 254 /** 255 * When user drags a view to the edge, we start scrolling the LayoutManager as long as View 256 * is partially out of bounds. 257 */ 258 @SuppressWarnings("WeakerAccess") /* synthetic access */ 259 final Runnable mScrollRunnable = new Runnable() { 260 @Override 261 public void run() { 262 if (mSelected != null && scrollIfNecessary()) { 263 if (mSelected != null) { //it might be lost during scrolling 264 moveIfNecessary(mSelected); 265 } 266 mRecyclerView.removeCallbacks(mScrollRunnable); 267 ViewCompat.postOnAnimation(mRecyclerView, this); 268 } 269 } 270 }; 271 272 /** 273 * Used for detecting fling swipe 274 */ 275 VelocityTracker mVelocityTracker; 276 277 //re-used list for selecting a swap target 278 private List<ViewHolder> mSwapTargets; 279 280 //re used for for sorting swap targets 281 private List<Integer> mDistances; 282 283 /** 284 * If drag & drop is supported, we use child drawing order to bring them to front. 285 */ 286 private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback = null; 287 288 /** 289 * This keeps a reference to the child dragged by the user. Even after user stops dragging, 290 * until view reaches its final position (end of recover animation), we keep a reference so 291 * that it can be drawn above other children. 292 */ 293 @SuppressWarnings("WeakerAccess") /* synthetic access */ 294 View mOverdrawChild = null; 295 296 /** 297 * We cache the position of the overdraw child to avoid recalculating it each time child 298 * position callback is called. This value is invalidated whenever a child is attached or 299 * detached. 300 */ 301 @SuppressWarnings("WeakerAccess") /* synthetic access */ 302 int mOverdrawChildPosition = -1; 303 304 /** 305 * Used to detect long press. 306 */ 307 @SuppressWarnings("WeakerAccess") /* synthetic access */ 308 GestureDetector mGestureDetector; 309 310 /** 311 * Callback for when long press occurs. 312 */ 313 private ItemTouchHelperGestureListener mItemTouchHelperGestureListener; 314 315 private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() { 316 @Override 317 public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, 318 @NonNull MotionEvent event) { 319 mGestureDetector.onTouchEvent(event); 320 if (DEBUG) { 321 Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event); 322 } 323 final int action = event.getActionMasked(); 324 if (action == MotionEvent.ACTION_DOWN) { 325 mActivePointerId = event.getPointerId(0); 326 mInitialTouchX = event.getX(); 327 mInitialTouchY = event.getY(); 328 obtainVelocityTracker(); 329 if (mSelected == null) { 330 final RecoverAnimation animation = findAnimation(event); 331 if (animation != null) { 332 mInitialTouchX -= animation.mX; 333 mInitialTouchY -= animation.mY; 334 endRecoverAnimation(animation.mViewHolder, true); 335 if (mPendingCleanup.remove(animation.mViewHolder.itemView)) { 336 mCallback.clearView(mRecyclerView, animation.mViewHolder); 337 } 338 select(animation.mViewHolder, animation.mActionState); 339 updateDxDy(event, mSelectedFlags, 0); 340 } 341 } 342 } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 343 mActivePointerId = ACTIVE_POINTER_ID_NONE; 344 select(null, ACTION_STATE_IDLE); 345 } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) { 346 // in a non scroll orientation, if distance change is above threshold, we 347 // can select the item 348 final int index = event.findPointerIndex(mActivePointerId); 349 if (DEBUG) { 350 Log.d(TAG, "pointer index " + index); 351 } 352 if (index >= 0) { 353 checkSelectForSwipe(action, event, index); 354 } 355 } 356 if (mVelocityTracker != null) { 357 mVelocityTracker.addMovement(event); 358 } 359 return mSelected != null; 360 } 361 362 @Override 363 public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) { 364 mGestureDetector.onTouchEvent(event); 365 if (DEBUG) { 366 Log.d(TAG, 367 "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event); 368 } 369 if (mVelocityTracker != null) { 370 mVelocityTracker.addMovement(event); 371 } 372 if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { 373 return; 374 } 375 final int action = event.getActionMasked(); 376 final int activePointerIndex = event.findPointerIndex(mActivePointerId); 377 if (activePointerIndex >= 0) { 378 checkSelectForSwipe(action, event, activePointerIndex); 379 } 380 ViewHolder viewHolder = mSelected; 381 if (viewHolder == null) { 382 return; 383 } 384 switch (action) { 385 case MotionEvent.ACTION_MOVE: { 386 // Find the index of the active pointer and fetch its position 387 if (activePointerIndex >= 0) { 388 updateDxDy(event, mSelectedFlags, activePointerIndex); 389 moveIfNecessary(viewHolder); 390 mRecyclerView.removeCallbacks(mScrollRunnable); 391 mScrollRunnable.run(); 392 mRecyclerView.invalidate(); 393 } 394 break; 395 } 396 case MotionEvent.ACTION_CANCEL: 397 if (mVelocityTracker != null) { 398 mVelocityTracker.clear(); 399 } 400 // fall through 401 case MotionEvent.ACTION_UP: 402 select(null, ACTION_STATE_IDLE); 403 mActivePointerId = ACTIVE_POINTER_ID_NONE; 404 break; 405 case MotionEvent.ACTION_POINTER_UP: { 406 final int pointerIndex = event.getActionIndex(); 407 final int pointerId = event.getPointerId(pointerIndex); 408 if (pointerId == mActivePointerId) { 409 // This was our active pointer going up. Choose a new 410 // active pointer and adjust accordingly. 411 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 412 mActivePointerId = event.getPointerId(newPointerIndex); 413 updateDxDy(event, mSelectedFlags, pointerIndex); 414 } 415 break; 416 } 417 } 418 } 419 420 @Override 421 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 422 if (!disallowIntercept) { 423 return; 424 } 425 select(null, ACTION_STATE_IDLE); 426 } 427 }; 428 429 /** 430 * Temporary rect instance that is used when we need to lookup Item decorations. 431 */ 432 private Rect mTmpRect; 433 434 /** 435 * When user started to drag scroll. Reset when we don't scroll 436 */ 437 private long mDragScrollStartTimeInMs; 438 439 /** 440 * Creates an ItemTouchHelper that will work with the given Callback. 441 * <p> 442 * You can attach ItemTouchHelper to a RecyclerView via 443 * {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration, 444 * an onItemTouchListener and a Child attach / detach listener to the RecyclerView. 445 * 446 * @param callback The Callback which controls the behavior of this touch helper. 447 */ ItemTouchHelper(@onNull Callback callback)448 public ItemTouchHelper(@NonNull Callback callback) { 449 mCallback = callback; 450 } 451 hitTest(View child, float x, float y, float left, float top)452 private static boolean hitTest(View child, float x, float y, float left, float top) { 453 return x >= left 454 && x <= left + child.getWidth() 455 && y >= top 456 && y <= top + child.getHeight(); 457 } 458 459 /** 460 * Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already 461 * attached to a RecyclerView, it will first detach from the previous one. You can call this 462 * method with {@code null} to detach it from the current RecyclerView. 463 * 464 * @param recyclerView The RecyclerView instance to which you want to add this helper or 465 * {@code null} if you want to remove ItemTouchHelper from the current 466 * RecyclerView. 467 */ attachToRecyclerView(@ullable RecyclerView recyclerView)468 public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { 469 if (mRecyclerView == recyclerView) { 470 return; // nothing to do 471 } 472 if (mRecyclerView != null) { 473 destroyCallbacks(); 474 } 475 mRecyclerView = recyclerView; 476 if (recyclerView != null) { 477 final Resources resources = recyclerView.getResources(); 478 mSwipeEscapeVelocity = resources 479 .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity); 480 mMaxSwipeVelocity = resources 481 .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity); 482 setupCallbacks(); 483 } 484 } 485 setupCallbacks()486 private void setupCallbacks() { 487 ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext()); 488 mSlop = vc.getScaledTouchSlop(); 489 mRecyclerView.addItemDecoration(this); 490 mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); 491 mRecyclerView.addOnChildAttachStateChangeListener(this); 492 startGestureDetection(); 493 } 494 destroyCallbacks()495 private void destroyCallbacks() { 496 mRecyclerView.removeItemDecoration(this); 497 mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); 498 mRecyclerView.removeOnChildAttachStateChangeListener(this); 499 // clean all attached 500 final int recoverAnimSize = mRecoverAnimations.size(); 501 for (int i = recoverAnimSize - 1; i >= 0; i--) { 502 final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0); 503 recoverAnimation.cancel(); 504 mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder); 505 } 506 mRecoverAnimations.clear(); 507 mOverdrawChild = null; 508 mOverdrawChildPosition = -1; 509 releaseVelocityTracker(); 510 stopGestureDetection(); 511 } 512 startGestureDetection()513 private void startGestureDetection() { 514 mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener(); 515 mGestureDetector = new GestureDetector(mRecyclerView.getContext(), 516 mItemTouchHelperGestureListener); 517 } 518 stopGestureDetection()519 private void stopGestureDetection() { 520 if (mItemTouchHelperGestureListener != null) { 521 mItemTouchHelperGestureListener.doNotReactToLongPress(); 522 mItemTouchHelperGestureListener = null; 523 } 524 if (mGestureDetector != null) { 525 mGestureDetector = null; 526 } 527 } 528 getSelectedDxDy(float[] outPosition)529 private void getSelectedDxDy(float[] outPosition) { 530 if ((mSelectedFlags & (LEFT | RIGHT)) != 0) { 531 outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft(); 532 } else { 533 outPosition[0] = mSelected.itemView.getTranslationX(); 534 } 535 if ((mSelectedFlags & (UP | DOWN)) != 0) { 536 outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop(); 537 } else { 538 outPosition[1] = mSelected.itemView.getTranslationY(); 539 } 540 } 541 542 @Override onDrawOver( @onNull Canvas c, @NonNull RecyclerView parent, RecyclerView.@NonNull State state )543 public void onDrawOver( 544 @NonNull Canvas c, 545 @NonNull RecyclerView parent, 546 RecyclerView.@NonNull State state 547 ) { 548 float dx = 0, dy = 0; 549 if (mSelected != null) { 550 getSelectedDxDy(mTmpPosition); 551 dx = mTmpPosition[0]; 552 dy = mTmpPosition[1]; 553 } 554 mCallback.onDrawOver(c, parent, mSelected, 555 mRecoverAnimations, mActionState, dx, dy); 556 } 557 558 @Override 559 @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)560 public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { 561 // we don't know if RV changed something so we should invalidate this index. 562 mOverdrawChildPosition = -1; 563 float dx = 0, dy = 0; 564 if (mSelected != null) { 565 getSelectedDxDy(mTmpPosition); 566 dx = mTmpPosition[0]; 567 dy = mTmpPosition[1]; 568 } 569 mCallback.onDraw(c, parent, mSelected, 570 mRecoverAnimations, mActionState, dx, dy); 571 } 572 573 /** 574 * Starts dragging or swiping the given View. Call with null if you want to clear it. 575 * 576 * @param selected The ViewHolder to drag or swipe. Can be null if you want to cancel the 577 * current action, but may not be null if actionState is ACTION_STATE_DRAG. 578 * @param actionState The type of action 579 */ 580 @SuppressWarnings("WeakerAccess") /* synthetic access */ select(@ullable ViewHolder selected, int actionState)581 void select(@Nullable ViewHolder selected, int actionState) { 582 if (selected == mSelected && actionState == mActionState) { 583 return; 584 } 585 mDragScrollStartTimeInMs = Long.MIN_VALUE; 586 final int prevActionState = mActionState; 587 // prevent duplicate animations 588 endRecoverAnimation(selected, true); 589 mActionState = actionState; 590 if (actionState == ACTION_STATE_DRAG) { 591 if (selected == null) { 592 throw new IllegalArgumentException("Must pass a ViewHolder when dragging"); 593 } 594 595 // we remove after animation is complete. this means we only elevate the last drag 596 // child but that should perform good enough as it is very hard to start dragging a 597 // new child before the previous one settles. 598 mOverdrawChild = selected.itemView; 599 addChildDrawingOrderCallback(); 600 } 601 int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState)) 602 - 1; 603 boolean preventLayout = false; 604 605 if (mSelected != null) { 606 final ViewHolder prevSelected = mSelected; 607 if (prevSelected.itemView.getParent() != null) { 608 final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 609 : swipeIfNecessary(prevSelected); 610 releaseVelocityTracker(); 611 // find where we should animate to 612 final float targetTranslateX, targetTranslateY; 613 int animationType; 614 switch (swipeDir) { 615 case LEFT: 616 case RIGHT: 617 case START: 618 case END: 619 targetTranslateY = 0; 620 targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth(); 621 break; 622 case UP: 623 case DOWN: 624 targetTranslateX = 0; 625 targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight(); 626 break; 627 default: 628 targetTranslateX = 0; 629 targetTranslateY = 0; 630 } 631 if (prevActionState == ACTION_STATE_DRAG) { 632 animationType = ANIMATION_TYPE_DRAG; 633 } else if (swipeDir > 0) { 634 animationType = ANIMATION_TYPE_SWIPE_SUCCESS; 635 } else { 636 animationType = ANIMATION_TYPE_SWIPE_CANCEL; 637 } 638 getSelectedDxDy(mTmpPosition); 639 final float currentTranslateX = mTmpPosition[0]; 640 final float currentTranslateY = mTmpPosition[1]; 641 final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType, 642 prevActionState, currentTranslateX, currentTranslateY, 643 targetTranslateX, targetTranslateY) { 644 @Override 645 public void onAnimationEnd(Animator animation) { 646 super.onAnimationEnd(animation); 647 if (this.mOverridden) { 648 return; 649 } 650 if (swipeDir <= 0) { 651 // this is a drag or failed swipe. recover immediately 652 mCallback.clearView(mRecyclerView, prevSelected); 653 // full cleanup will happen on onDrawOver 654 } else { 655 // wait until remove animation is complete. 656 mPendingCleanup.add(prevSelected.itemView); 657 mIsPendingCleanup = true; 658 if (swipeDir > 0) { 659 // Animation might be ended by other animators during a layout. 660 // We defer callback to avoid editing adapter during a layout. 661 postDispatchSwipe(this, swipeDir); 662 } 663 } 664 // removed from the list after it is drawn for the last time 665 if (mOverdrawChild == prevSelected.itemView) { 666 removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); 667 } 668 } 669 }; 670 final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, 671 targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); 672 rv.setDuration(duration); 673 mRecoverAnimations.add(rv); 674 rv.start(); 675 preventLayout = true; 676 } else { 677 removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); 678 mCallback.clearView(mRecyclerView, prevSelected); 679 } 680 mSelected = null; 681 } 682 if (selected != null) { 683 mSelectedFlags = 684 (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask) 685 >> (mActionState * DIRECTION_FLAG_COUNT); 686 mSelectedStartX = selected.itemView.getLeft(); 687 mSelectedStartY = selected.itemView.getTop(); 688 mSelected = selected; 689 690 if (actionState == ACTION_STATE_DRAG) { 691 mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 692 } 693 } 694 final ViewParent rvParent = mRecyclerView.getParent(); 695 if (rvParent != null) { 696 rvParent.requestDisallowInterceptTouchEvent(mSelected != null); 697 } 698 if (!preventLayout) { 699 mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout(); 700 } 701 mCallback.onSelectedChanged(mSelected, mActionState); 702 mRecyclerView.invalidate(); 703 } 704 705 @SuppressWarnings("WeakerAccess") /* synthetic access */ postDispatchSwipe(final RecoverAnimation anim, final int swipeDir)706 void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) { 707 // wait until animations are complete. 708 mRecyclerView.post(new Runnable() { 709 @Override 710 public void run() { 711 if (mRecyclerView != null && mRecyclerView.isAttachedToWindow() 712 && !anim.mOverridden 713 && anim.mViewHolder.getAbsoluteAdapterPosition() 714 != RecyclerView.NO_POSITION) { 715 final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator(); 716 // if animator is running or we have other active recover animations, we try 717 // not to call onSwiped because DefaultItemAnimator is not good at merging 718 // animations. Instead, we wait and batch. 719 if ((animator == null || !animator.isRunning(null)) 720 && !hasRunningRecoverAnim()) { 721 mCallback.onSwiped(anim.mViewHolder, swipeDir); 722 } else { 723 mRecyclerView.post(this); 724 } 725 } 726 } 727 }); 728 } 729 730 @SuppressWarnings("WeakerAccess") /* synthetic access */ hasRunningRecoverAnim()731 boolean hasRunningRecoverAnim() { 732 final int size = mRecoverAnimations.size(); 733 for (int i = 0; i < size; i++) { 734 if (!mRecoverAnimations.get(i).mEnded) { 735 return true; 736 } 737 } 738 return false; 739 } 740 741 /** 742 * If user drags the view to the edge, trigger a scroll if necessary. 743 */ 744 @SuppressWarnings("WeakerAccess") /* synthetic access */ scrollIfNecessary()745 boolean scrollIfNecessary() { 746 if (mSelected == null) { 747 mDragScrollStartTimeInMs = Long.MIN_VALUE; 748 return false; 749 } 750 final long now = System.currentTimeMillis(); 751 final long scrollDuration = mDragScrollStartTimeInMs 752 == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs; 753 RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); 754 if (mTmpRect == null) { 755 mTmpRect = new Rect(); 756 } 757 int scrollX = 0; 758 int scrollY = 0; 759 lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect); 760 if (lm.canScrollHorizontally()) { 761 int curX = (int) (mSelectedStartX + mDx); 762 final int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft(); 763 if (mDx < 0 && leftDiff < 0) { 764 scrollX = leftDiff; 765 } else if (mDx > 0) { 766 final int rightDiff = 767 curX + mSelected.itemView.getWidth() + mTmpRect.right 768 - (mRecyclerView.getWidth() - mRecyclerView.getPaddingRight()); 769 if (rightDiff > 0) { 770 scrollX = rightDiff; 771 } 772 } 773 } 774 if (lm.canScrollVertically()) { 775 int curY = (int) (mSelectedStartY + mDy); 776 final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop(); 777 if (mDy < 0 && topDiff < 0) { 778 scrollY = topDiff; 779 } else if (mDy > 0) { 780 final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom 781 - (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom()); 782 if (bottomDiff > 0) { 783 scrollY = bottomDiff; 784 } 785 } 786 } 787 if (scrollX != 0) { 788 scrollX = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, 789 mSelected.itemView.getWidth(), scrollX, 790 mRecyclerView.getWidth(), scrollDuration); 791 } 792 if (scrollY != 0) { 793 scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, 794 mSelected.itemView.getHeight(), scrollY, 795 mRecyclerView.getHeight(), scrollDuration); 796 } 797 if (scrollX != 0 || scrollY != 0) { 798 if (mDragScrollStartTimeInMs == Long.MIN_VALUE) { 799 mDragScrollStartTimeInMs = now; 800 } 801 mRecyclerView.scrollBy(scrollX, scrollY); 802 return true; 803 } 804 mDragScrollStartTimeInMs = Long.MIN_VALUE; 805 return false; 806 } 807 findSwapTargets(ViewHolder viewHolder)808 private List<ViewHolder> findSwapTargets(ViewHolder viewHolder) { 809 if (mSwapTargets == null) { 810 mSwapTargets = new ArrayList<>(); 811 mDistances = new ArrayList<>(); 812 } else { 813 mSwapTargets.clear(); 814 mDistances.clear(); 815 } 816 final int margin = mCallback.getBoundingBoxMargin(); 817 final int left = Math.round(mSelectedStartX + mDx) - margin; 818 final int top = Math.round(mSelectedStartY + mDy) - margin; 819 final int right = left + viewHolder.itemView.getWidth() + 2 * margin; 820 final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin; 821 final int centerX = (left + right) / 2; 822 final int centerY = (top + bottom) / 2; 823 final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); 824 final int childCount = lm.getChildCount(); 825 for (int i = 0; i < childCount; i++) { 826 View other = lm.getChildAt(i); 827 if (other == viewHolder.itemView) { 828 continue; //myself! 829 } 830 if (other.getBottom() < top || other.getTop() > bottom 831 || other.getRight() < left || other.getLeft() > right) { 832 continue; 833 } 834 final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other); 835 if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) { 836 // find the index to add 837 final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2); 838 final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2); 839 final int dist = dx * dx + dy * dy; 840 841 int pos = 0; 842 final int cnt = mSwapTargets.size(); 843 for (int j = 0; j < cnt; j++) { 844 if (dist > mDistances.get(j)) { 845 pos++; 846 } else { 847 break; 848 } 849 } 850 mSwapTargets.add(pos, otherVh); 851 mDistances.add(pos, dist); 852 } 853 } 854 return mSwapTargets; 855 } 856 857 /** 858 * Checks if we should swap w/ another view holder. 859 */ 860 @SuppressWarnings("WeakerAccess") /* synthetic access */ moveIfNecessary(ViewHolder viewHolder)861 void moveIfNecessary(ViewHolder viewHolder) { 862 if (mRecyclerView.isLayoutRequested()) { 863 return; 864 } 865 if (mActionState != ACTION_STATE_DRAG) { 866 return; 867 } 868 869 final float threshold = mCallback.getMoveThreshold(viewHolder); 870 final int x = (int) (mSelectedStartX + mDx); 871 final int y = (int) (mSelectedStartY + mDy); 872 if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold 873 && Math.abs(x - viewHolder.itemView.getLeft()) 874 < viewHolder.itemView.getWidth() * threshold) { 875 return; 876 } 877 List<ViewHolder> swapTargets = findSwapTargets(viewHolder); 878 if (swapTargets.size() == 0) { 879 return; 880 } 881 // may swap. 882 ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y); 883 if (target == null) { 884 mSwapTargets.clear(); 885 mDistances.clear(); 886 return; 887 } 888 final int toPosition = target.getAbsoluteAdapterPosition(); 889 final int fromPosition = viewHolder.getAbsoluteAdapterPosition(); 890 if (mCallback.onMove(mRecyclerView, viewHolder, target)) { 891 // keep target visible 892 mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, 893 target, toPosition, x, y); 894 } 895 } 896 897 @Override onChildViewAttachedToWindow(@onNull View view)898 public void onChildViewAttachedToWindow(@NonNull View view) { 899 } 900 901 @Override onChildViewDetachedFromWindow(@onNull View view)902 public void onChildViewDetachedFromWindow(@NonNull View view) { 903 removeChildDrawingOrderCallbackIfNecessary(view); 904 final ViewHolder holder = mRecyclerView.getChildViewHolder(view); 905 if (holder == null) { 906 return; 907 } 908 if (mSelected != null && holder == mSelected) { 909 select(null, ACTION_STATE_IDLE); 910 } else { 911 endRecoverAnimation(holder, false); // this may push it into pending cleanup list. 912 if (mPendingCleanup.remove(holder.itemView)) { 913 mCallback.clearView(mRecyclerView, holder); 914 } 915 } 916 } 917 918 /** 919 * Returns the animation type or 0 if cannot be found. 920 */ 921 @SuppressWarnings("WeakerAccess") /* synthetic access */ endRecoverAnimation(ViewHolder viewHolder, boolean override)922 void endRecoverAnimation(ViewHolder viewHolder, boolean override) { 923 final int recoverAnimSize = mRecoverAnimations.size(); 924 for (int i = recoverAnimSize - 1; i >= 0; i--) { 925 final RecoverAnimation anim = mRecoverAnimations.get(i); 926 if (anim.mViewHolder == viewHolder) { 927 anim.mOverridden |= override; 928 if (!anim.mEnded) { 929 anim.cancel(); 930 } 931 mRecoverAnimations.remove(i); 932 return; 933 } 934 } 935 } 936 937 @Override 938 @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)939 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 940 RecyclerView.State state) { 941 outRect.setEmpty(); 942 } 943 944 @SuppressWarnings("WeakerAccess") /* synthetic access */ obtainVelocityTracker()945 void obtainVelocityTracker() { 946 if (mVelocityTracker != null) { 947 mVelocityTracker.recycle(); 948 } 949 mVelocityTracker = VelocityTracker.obtain(); 950 } 951 releaseVelocityTracker()952 private void releaseVelocityTracker() { 953 if (mVelocityTracker != null) { 954 mVelocityTracker.recycle(); 955 mVelocityTracker = null; 956 } 957 } 958 findSwipedView(MotionEvent motionEvent)959 private ViewHolder findSwipedView(MotionEvent motionEvent) { 960 final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); 961 if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { 962 return null; 963 } 964 final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); 965 final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX; 966 final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY; 967 final float absDx = Math.abs(dx); 968 final float absDy = Math.abs(dy); 969 970 if (absDx < mSlop && absDy < mSlop) { 971 return null; 972 } 973 if (absDx > absDy && lm.canScrollHorizontally()) { 974 return null; 975 } else if (absDy > absDx && lm.canScrollVertically()) { 976 return null; 977 } 978 View child = findChildView(motionEvent); 979 if (child == null) { 980 return null; 981 } 982 return mRecyclerView.getChildViewHolder(child); 983 } 984 985 /** 986 * Checks whether we should select a View for swiping. 987 */ 988 @SuppressWarnings("WeakerAccess") /* synthetic access */ checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex)989 void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) { 990 if (mSelected != null || action != MotionEvent.ACTION_MOVE 991 || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) { 992 return; 993 } 994 if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { 995 return; 996 } 997 final ViewHolder vh = findSwipedView(motionEvent); 998 if (vh == null) { 999 return; 1000 } 1001 final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh); 1002 1003 final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) 1004 >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE); 1005 1006 if (swipeFlags == 0) { 1007 return; 1008 } 1009 1010 // mDx and mDy are only set in allowed directions. We use custom x/y here instead of 1011 // updateDxDy to avoid swiping if user moves more in the other direction 1012 final float x = motionEvent.getX(pointerIndex); 1013 final float y = motionEvent.getY(pointerIndex); 1014 1015 // Calculate the distance moved 1016 final float dx = x - mInitialTouchX; 1017 final float dy = y - mInitialTouchY; 1018 // swipe target is chose w/o applying flags so it does not really check if swiping in that 1019 // direction is allowed. This why here, we use mDx mDy to check slope value again. 1020 final float absDx = Math.abs(dx); 1021 final float absDy = Math.abs(dy); 1022 1023 if (absDx < mSlop && absDy < mSlop) { 1024 return; 1025 } 1026 if (absDx > absDy) { 1027 if (dx < 0 && (swipeFlags & LEFT) == 0) { 1028 return; 1029 } 1030 if (dx > 0 && (swipeFlags & RIGHT) == 0) { 1031 return; 1032 } 1033 } else { 1034 if (dy < 0 && (swipeFlags & UP) == 0) { 1035 return; 1036 } 1037 if (dy > 0 && (swipeFlags & DOWN) == 0) { 1038 return; 1039 } 1040 } 1041 mDx = mDy = 0f; 1042 mActivePointerId = motionEvent.getPointerId(0); 1043 select(vh, ACTION_STATE_SWIPE); 1044 } 1045 1046 @SuppressWarnings("WeakerAccess") /* synthetic access */ findChildView(MotionEvent event)1047 View findChildView(MotionEvent event) { 1048 // first check elevated views, if none, then call RV 1049 final float x = event.getX(); 1050 final float y = event.getY(); 1051 if (mSelected != null) { 1052 final View selectedView = mSelected.itemView; 1053 if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) { 1054 return selectedView; 1055 } 1056 } 1057 for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { 1058 final RecoverAnimation anim = mRecoverAnimations.get(i); 1059 final View view = anim.mViewHolder.itemView; 1060 if (hitTest(view, x, y, anim.mX, anim.mY)) { 1061 return view; 1062 } 1063 } 1064 return mRecyclerView.findChildViewUnder(x, y); 1065 } 1066 1067 /** 1068 * Starts dragging the provided ViewHolder. By default, ItemTouchHelper starts a drag when a 1069 * View is long pressed. You can disable that behavior by overriding 1070 * {@link ItemTouchHelper.Callback#isLongPressDragEnabled()}. 1071 * <p> 1072 * For this method to work: 1073 * <ul> 1074 * <li>The provided ViewHolder must be a child of the RecyclerView to which this 1075 * ItemTouchHelper 1076 * is attached.</li> 1077 * <li>{@link ItemTouchHelper.Callback} must have dragging enabled.</li> 1078 * <li>There must be a previous touch event that was reported to the ItemTouchHelper 1079 * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener 1080 * grabs previous events, this should work as expected.</li> 1081 * </ul> 1082 * 1083 * For example, if you would like to let your user to be able to drag an Item by touching one 1084 * of its descendants, you may implement it as follows: 1085 * <pre> 1086 * viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() { 1087 * public boolean onTouch(View v, MotionEvent event) { 1088 * if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) { 1089 * mItemTouchHelper.startDrag(viewHolder); 1090 * } 1091 * return false; 1092 * } 1093 * }); 1094 * </pre> 1095 * <p> 1096 * 1097 * @param viewHolder The ViewHolder to start dragging. It must be a direct child of 1098 * RecyclerView. 1099 * @see ItemTouchHelper.Callback#isItemViewSwipeEnabled() 1100 */ startDrag(@onNull ViewHolder viewHolder)1101 public void startDrag(@NonNull ViewHolder viewHolder) { 1102 if (!mCallback.hasDragFlag(mRecyclerView, viewHolder)) { 1103 Log.e(TAG, "Start drag has been called but dragging is not enabled"); 1104 return; 1105 } 1106 if (viewHolder.itemView.getParent() != mRecyclerView) { 1107 Log.e(TAG, "Start drag has been called with a view holder which is not a child of " 1108 + "the RecyclerView which is controlled by this ItemTouchHelper."); 1109 return; 1110 } 1111 obtainVelocityTracker(); 1112 mDx = mDy = 0f; 1113 select(viewHolder, ACTION_STATE_DRAG); 1114 } 1115 1116 /** 1117 * Starts swiping the provided ViewHolder. By default, ItemTouchHelper starts swiping a View 1118 * when user swipes their finger (or mouse pointer) over the View. You can disable this 1119 * behavior 1120 * by overriding {@link ItemTouchHelper.Callback} 1121 * <p> 1122 * For this method to work: 1123 * <ul> 1124 * <li>The provided ViewHolder must be a child of the RecyclerView to which this 1125 * ItemTouchHelper is attached.</li> 1126 * <li>{@link ItemTouchHelper.Callback} must have swiping enabled.</li> 1127 * <li>There must be a previous touch event that was reported to the ItemTouchHelper 1128 * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener 1129 * grabs previous events, this should work as expected.</li> 1130 * </ul> 1131 * 1132 * For example, if you would like to let your user to be able to swipe an Item by touching one 1133 * of its descendants, you may implement it as follows: 1134 * <pre> 1135 * viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() { 1136 * public boolean onTouch(View v, MotionEvent event) { 1137 * if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) { 1138 * mItemTouchHelper.startSwipe(viewHolder); 1139 * } 1140 * return false; 1141 * } 1142 * }); 1143 * </pre> 1144 * 1145 * @param viewHolder The ViewHolder to start swiping. It must be a direct child of 1146 * RecyclerView. 1147 */ startSwipe(@onNull ViewHolder viewHolder)1148 public void startSwipe(@NonNull ViewHolder viewHolder) { 1149 if (!mCallback.hasSwipeFlag(mRecyclerView, viewHolder)) { 1150 Log.e(TAG, "Start swipe has been called but swiping is not enabled"); 1151 return; 1152 } 1153 if (viewHolder.itemView.getParent() != mRecyclerView) { 1154 Log.e(TAG, "Start swipe has been called with a view holder which is not a child of " 1155 + "the RecyclerView controlled by this ItemTouchHelper."); 1156 return; 1157 } 1158 obtainVelocityTracker(); 1159 mDx = mDy = 0f; 1160 select(viewHolder, ACTION_STATE_SWIPE); 1161 } 1162 1163 @SuppressWarnings("WeakerAccess") /* synthetic access */ findAnimation(MotionEvent event)1164 RecoverAnimation findAnimation(MotionEvent event) { 1165 if (mRecoverAnimations.isEmpty()) { 1166 return null; 1167 } 1168 View target = findChildView(event); 1169 for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { 1170 final RecoverAnimation anim = mRecoverAnimations.get(i); 1171 if (anim.mViewHolder.itemView == target) { 1172 return anim; 1173 } 1174 } 1175 return null; 1176 } 1177 1178 @SuppressWarnings("WeakerAccess") /* synthetic access */ updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex)1179 void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) { 1180 final float x = ev.getX(pointerIndex); 1181 final float y = ev.getY(pointerIndex); 1182 1183 // Calculate the distance moved 1184 mDx = x - mInitialTouchX; 1185 mDy = y - mInitialTouchY; 1186 if ((directionFlags & LEFT) == 0) { 1187 mDx = Math.max(0, mDx); 1188 } 1189 if ((directionFlags & RIGHT) == 0) { 1190 mDx = Math.min(0, mDx); 1191 } 1192 if ((directionFlags & UP) == 0) { 1193 mDy = Math.max(0, mDy); 1194 } 1195 if ((directionFlags & DOWN) == 0) { 1196 mDy = Math.min(0, mDy); 1197 } 1198 } 1199 swipeIfNecessary(ViewHolder viewHolder)1200 private int swipeIfNecessary(ViewHolder viewHolder) { 1201 if (mActionState == ACTION_STATE_DRAG) { 1202 return 0; 1203 } 1204 final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder); 1205 final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection( 1206 originalMovementFlags, 1207 mRecyclerView.getLayoutDirection()); 1208 final int flags = (absoluteMovementFlags 1209 & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); 1210 if (flags == 0) { 1211 return 0; 1212 } 1213 final int originalFlags = (originalMovementFlags 1214 & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); 1215 int swipeDir; 1216 if (Math.abs(mDx) > Math.abs(mDy)) { 1217 if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { 1218 // if swipe dir is not in original flags, it should be the relative direction 1219 if ((originalFlags & swipeDir) == 0) { 1220 // convert to relative 1221 return Callback.convertToRelativeDirection(swipeDir, 1222 mRecyclerView.getLayoutDirection()); 1223 } 1224 return swipeDir; 1225 } 1226 if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { 1227 return swipeDir; 1228 } 1229 } else { 1230 if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { 1231 return swipeDir; 1232 } 1233 if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { 1234 // if swipe dir is not in original flags, it should be the relative direction 1235 if ((originalFlags & swipeDir) == 0) { 1236 // convert to relative 1237 return Callback.convertToRelativeDirection(swipeDir, 1238 mRecyclerView.getLayoutDirection()); 1239 } 1240 return swipeDir; 1241 } 1242 } 1243 return 0; 1244 } 1245 checkHorizontalSwipe(ViewHolder viewHolder, int flags)1246 private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) { 1247 if ((flags & (LEFT | RIGHT)) != 0) { 1248 final int dirFlag = mDx > 0 ? RIGHT : LEFT; 1249 if (mVelocityTracker != null && mActivePointerId > -1) { 1250 mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, 1251 mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); 1252 final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); 1253 final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); 1254 final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT; 1255 final float absXVelocity = Math.abs(xVelocity); 1256 if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag 1257 && absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) 1258 && absXVelocity > Math.abs(yVelocity)) { 1259 return velDirFlag; 1260 } 1261 } 1262 1263 final float threshold = mRecyclerView.getWidth() * mCallback 1264 .getSwipeThreshold(viewHolder); 1265 1266 if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) { 1267 return dirFlag; 1268 } 1269 } 1270 return 0; 1271 } 1272 checkVerticalSwipe(ViewHolder viewHolder, int flags)1273 private int checkVerticalSwipe(ViewHolder viewHolder, int flags) { 1274 if ((flags & (UP | DOWN)) != 0) { 1275 final int dirFlag = mDy > 0 ? DOWN : UP; 1276 if (mVelocityTracker != null && mActivePointerId > -1) { 1277 mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, 1278 mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); 1279 final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); 1280 final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); 1281 final int velDirFlag = yVelocity > 0f ? DOWN : UP; 1282 final float absYVelocity = Math.abs(yVelocity); 1283 if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag 1284 && absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) 1285 && absYVelocity > Math.abs(xVelocity)) { 1286 return velDirFlag; 1287 } 1288 } 1289 1290 final float threshold = mRecyclerView.getHeight() * mCallback 1291 .getSwipeThreshold(viewHolder); 1292 if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) { 1293 return dirFlag; 1294 } 1295 } 1296 return 0; 1297 } 1298 addChildDrawingOrderCallback()1299 private void addChildDrawingOrderCallback() { 1300 if (Build.VERSION.SDK_INT >= 21) { 1301 return; // we use elevation on Lollipop 1302 } 1303 if (mChildDrawingOrderCallback == null) { 1304 mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() { 1305 @Override 1306 public int onGetChildDrawingOrder(int childCount, int i) { 1307 if (mOverdrawChild == null) { 1308 return i; 1309 } 1310 int childPosition = mOverdrawChildPosition; 1311 if (childPosition == -1) { 1312 childPosition = mRecyclerView.indexOfChild(mOverdrawChild); 1313 mOverdrawChildPosition = childPosition; 1314 } 1315 if (i == childCount - 1) { 1316 return childPosition; 1317 } 1318 return i < childPosition ? i : i + 1; 1319 } 1320 }; 1321 } 1322 mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback); 1323 } 1324 1325 @SuppressWarnings("WeakerAccess") /* synthetic access */ removeChildDrawingOrderCallbackIfNecessary(View view)1326 void removeChildDrawingOrderCallbackIfNecessary(View view) { 1327 if (view == mOverdrawChild) { 1328 mOverdrawChild = null; 1329 // only remove if we've added 1330 if (mChildDrawingOrderCallback != null) { 1331 mRecyclerView.setChildDrawingOrderCallback(null); 1332 } 1333 } 1334 } 1335 1336 /** 1337 * An interface which can be implemented by LayoutManager for better integration with 1338 * {@link ItemTouchHelper}. 1339 */ 1340 public interface ViewDropHandler { 1341 1342 /** 1343 * Called by the {@link ItemTouchHelper} after a View is dropped over another View. 1344 * <p> 1345 * A LayoutManager should implement this interface to get ready for the upcoming move 1346 * operation. 1347 * <p> 1348 * For example, LinearLayoutManager sets up a "scrollToPositionWithOffset" calls so that 1349 * the View under drag will be used as an anchor View while calculating the next layout, 1350 * making layout stay consistent. 1351 * 1352 * @param view The View which is being dragged. It is very likely that user is still 1353 * dragging this View so there might be other calls to 1354 * {@code prepareForDrop()} after this one. 1355 * @param target The target view which is being dropped on. 1356 * @param x The <code>left</code> offset of the View that is being dragged. This value 1357 * includes the movement caused by the user. 1358 * @param y The <code>top</code> offset of the View that is being dragged. This value 1359 * includes the movement caused by the user. 1360 */ prepareForDrop(@onNull View view, @NonNull View target, int x, int y)1361 void prepareForDrop(@NonNull View view, @NonNull View target, int x, int y); 1362 } 1363 1364 /** 1365 * This class is the contract between ItemTouchHelper and your application. It lets you control 1366 * which touch behaviors are enabled per each ViewHolder and also receive callbacks when user 1367 * performs these actions. 1368 * <p> 1369 * To control which actions user can take on each view, you should override 1370 * {@link #getMovementFlags(RecyclerView, ViewHolder)} and return appropriate set 1371 * of direction flags. ({@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link #END}, 1372 * {@link #UP}, {@link #DOWN}). You can use 1373 * {@link #makeMovementFlags(int, int)} to easily construct it. Alternatively, you can use 1374 * {@link SimpleCallback}. 1375 * <p> 1376 * If user drags an item, ItemTouchHelper will call 1377 * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder) 1378 * onMove(recyclerView, dragged, target)}. 1379 * Upon receiving this callback, you should move the item from the old position 1380 * ({@code dragged.getAdapterPosition()}) to new position ({@code target.getAdapterPosition()}) 1381 * in your adapter and also call {@link RecyclerView.Adapter#notifyItemMoved(int, int)}. 1382 * To control where a View can be dropped, you can override 1383 * {@link #canDropOver(RecyclerView, ViewHolder, ViewHolder)}. When a 1384 * dragging View overlaps multiple other views, Callback chooses the closest View with which 1385 * dragged View might have changed positions. Although this approach works for many use cases, 1386 * if you have a custom LayoutManager, you can override 1387 * {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)} to select a 1388 * custom drop target. 1389 * <p> 1390 * When a View is swiped, ItemTouchHelper animates it until it goes out of bounds, then calls 1391 * {@link #onSwiped(ViewHolder, int)}. At this point, you should update your 1392 * adapter (e.g. remove the item) and call related Adapter#notify event. 1393 */ 1394 @SuppressWarnings("UnusedParameters") 1395 public abstract static class Callback { 1396 1397 @SuppressWarnings("WeakerAccess") 1398 public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200; 1399 1400 @SuppressWarnings("WeakerAccess") 1401 public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250; 1402 1403 static final int RELATIVE_DIR_FLAGS = START | END 1404 | ((START | END) << DIRECTION_FLAG_COUNT) 1405 | ((START | END) << (2 * DIRECTION_FLAG_COUNT)); 1406 1407 private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT 1408 | ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT) 1409 | ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT)); 1410 1411 private static final Interpolator sDragScrollInterpolator = new Interpolator() { 1412 @Override 1413 public float getInterpolation(float t) { 1414 return t * t * t * t * t; 1415 } 1416 }; 1417 1418 private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() { 1419 @Override 1420 public float getInterpolation(float t) { 1421 t -= 1.0f; 1422 return t * t * t * t * t + 1.0f; 1423 } 1424 }; 1425 1426 /** 1427 * Drag scroll speed keeps accelerating until this many milliseconds before being capped. 1428 */ 1429 private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; 1430 1431 private int mCachedMaxScrollSpeed = -1; 1432 1433 /** 1434 * Returns the {@link ItemTouchUIUtil} that is used by the {@link Callback} class for 1435 * visual 1436 * changes on Views in response to user interactions. {@link ItemTouchUIUtil} has different 1437 * implementations for different platform versions. 1438 * <p> 1439 * By default, {@link Callback} applies these changes on 1440 * {@link RecyclerView.ViewHolder#itemView}. 1441 * <p> 1442 * For example, if you have a use case where you only want the text to move when user 1443 * swipes over the view, you can do the following: 1444 * <pre> 1445 * public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder){ 1446 * getDefaultUIUtil().clearView(((ItemTouchViewHolder) viewHolder).textView); 1447 * } 1448 * public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { 1449 * if (viewHolder != null){ 1450 * getDefaultUIUtil().onSelected(((ItemTouchViewHolder) viewHolder).textView); 1451 * } 1452 * } 1453 * public void onChildDraw(Canvas c, RecyclerView recyclerView, 1454 * RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, 1455 * boolean isCurrentlyActive) { 1456 * getDefaultUIUtil().onDraw(c, recyclerView, 1457 * ((ItemTouchViewHolder) viewHolder).textView, dX, dY, 1458 * actionState, isCurrentlyActive); 1459 * return true; 1460 * } 1461 * public void onChildDrawOver(Canvas c, RecyclerView recyclerView, 1462 * RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, 1463 * boolean isCurrentlyActive) { 1464 * getDefaultUIUtil().onDrawOver(c, recyclerView, 1465 * ((ItemTouchViewHolder) viewHolder).textView, dX, dY, 1466 * actionState, isCurrentlyActive); 1467 * return true; 1468 * } 1469 * </pre> 1470 * 1471 * @return The {@link ItemTouchUIUtil} instance that is used by the {@link Callback} 1472 */ 1473 @SuppressWarnings("WeakerAccess") getDefaultUIUtil()1474 public static @NonNull ItemTouchUIUtil getDefaultUIUtil() { 1475 return ItemTouchUIUtilImpl.INSTANCE; 1476 } 1477 1478 /** 1479 * Replaces a movement direction with its relative version by taking layout direction into 1480 * account. 1481 * 1482 * @param flags The flag value that include any number of movement flags. 1483 * @param layoutDirection The layout direction of the View. Can be obtained from 1484 * {@link ViewCompat#getLayoutDirection(android.view.View)}. 1485 * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead 1486 * of {@link #LEFT}, {@link #RIGHT}. 1487 * @see #convertToAbsoluteDirection(int, int) 1488 */ 1489 @SuppressWarnings("WeakerAccess") convertToRelativeDirection(int flags, int layoutDirection)1490 public static int convertToRelativeDirection(int flags, int layoutDirection) { 1491 int masked = flags & ABS_HORIZONTAL_DIR_FLAGS; 1492 if (masked == 0) { 1493 return flags; // does not have any abs flags, good. 1494 } 1495 flags &= ~masked; //remove left / right. 1496 if (layoutDirection == View.LAYOUT_DIRECTION_LTR) { 1497 // no change. just OR with 2 bits shifted mask and return 1498 flags |= masked << 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. 1499 return flags; 1500 } else { 1501 // add RIGHT flag as START 1502 flags |= ((masked << 1) & ~ABS_HORIZONTAL_DIR_FLAGS); 1503 // first clean RIGHT bit then add LEFT flag as END 1504 flags |= ((masked << 1) & ABS_HORIZONTAL_DIR_FLAGS) << 2; 1505 } 1506 return flags; 1507 } 1508 1509 /** 1510 * Convenience method to create movement flags. 1511 * <p> 1512 * For instance, if you want to let your items be drag & dropped vertically and swiped 1513 * left to be dismissed, you can call this method with: 1514 * <code>makeMovementFlags(UP | DOWN, LEFT);</code> 1515 * 1516 * @param dragFlags The directions in which the item can be dragged. 1517 * @param swipeFlags The directions in which the item can be swiped. 1518 * @return Returns an integer composed of the given drag and swipe flags. 1519 */ makeMovementFlags(int dragFlags, int swipeFlags)1520 public static int makeMovementFlags(int dragFlags, int swipeFlags) { 1521 return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags) 1522 | makeFlag(ACTION_STATE_SWIPE, swipeFlags) 1523 | makeFlag(ACTION_STATE_DRAG, dragFlags); 1524 } 1525 1526 /** 1527 * Shifts the given direction flags to the offset of the given action state. 1528 * 1529 * @param actionState The action state you want to get flags in. Should be one of 1530 * {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or 1531 * {@link #ACTION_STATE_DRAG}. 1532 * @param directions The direction flags. Can be composed from {@link #UP}, {@link #DOWN}, 1533 * {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}. 1534 * @return And integer that represents the given directions in the provided actionState. 1535 */ 1536 @SuppressWarnings("WeakerAccess") makeFlag(int actionState, int directions)1537 public static int makeFlag(int actionState, int directions) { 1538 return directions << (actionState * DIRECTION_FLAG_COUNT); 1539 } 1540 1541 /** 1542 * Should return a composite flag which defines the enabled move directions in each state 1543 * (idle, swiping, dragging). 1544 * <p> 1545 * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int, 1546 * int)} 1547 * or {@link #makeFlag(int, int)}. 1548 * <p> 1549 * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next 1550 * 8 bits are for SWIPE state and third 8 bits are for DRAG state. 1551 * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in 1552 * {@link ItemTouchHelper}. 1553 * <p> 1554 * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to 1555 * swipe by swiping RIGHT, you can return: 1556 * <pre> 1557 * makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT); 1558 * </pre> 1559 * This means, allow right movement while IDLE and allow right and left movement while 1560 * swiping. 1561 * 1562 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached. 1563 * @param viewHolder The ViewHolder for which the movement information is necessary. 1564 * @return flags specifying which movements are allowed on this ViewHolder. 1565 * @see #makeMovementFlags(int, int) 1566 * @see #makeFlag(int, int) 1567 */ getMovementFlags(@onNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder)1568 public abstract int getMovementFlags(@NonNull RecyclerView recyclerView, 1569 @NonNull ViewHolder viewHolder); 1570 1571 /** 1572 * Converts a given set of flags to absolution direction which means {@link #START} and 1573 * {@link #END} are replaced with {@link #LEFT} and {@link #RIGHT} depending on the layout 1574 * direction. 1575 * 1576 * @param flags The flag value that include any number of movement flags. 1577 * @param layoutDirection The layout direction of the RecyclerView. 1578 * @return Updated flags which includes only absolute direction values. 1579 */ 1580 @SuppressWarnings("WeakerAccess") convertToAbsoluteDirection(int flags, int layoutDirection)1581 public int convertToAbsoluteDirection(int flags, int layoutDirection) { 1582 int masked = flags & RELATIVE_DIR_FLAGS; 1583 if (masked == 0) { 1584 return flags; // does not have any relative flags, good. 1585 } 1586 flags &= ~masked; //remove start / end 1587 if (layoutDirection == View.LAYOUT_DIRECTION_LTR) { 1588 // no change. just OR with 2 bits shifted mask and return 1589 flags |= masked >> 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. 1590 return flags; 1591 } else { 1592 // add START flag as RIGHT 1593 flags |= ((masked >> 1) & ~RELATIVE_DIR_FLAGS); 1594 // first clean start bit then add END flag as LEFT 1595 flags |= ((masked >> 1) & RELATIVE_DIR_FLAGS) >> 2; 1596 } 1597 return flags; 1598 } 1599 getAbsoluteMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder)1600 final int getAbsoluteMovementFlags(RecyclerView recyclerView, 1601 ViewHolder viewHolder) { 1602 final int flags = getMovementFlags(recyclerView, viewHolder); 1603 return convertToAbsoluteDirection(flags, recyclerView.getLayoutDirection()); 1604 } 1605 hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder)1606 boolean hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder) { 1607 final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); 1608 return (flags & ACTION_MODE_DRAG_MASK) != 0; 1609 } 1610 hasSwipeFlag(RecyclerView recyclerView, ViewHolder viewHolder)1611 boolean hasSwipeFlag(RecyclerView recyclerView, 1612 ViewHolder viewHolder) { 1613 final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); 1614 return (flags & ACTION_MODE_SWIPE_MASK) != 0; 1615 } 1616 1617 /** 1618 * Return true if the current ViewHolder can be dropped over the the target ViewHolder. 1619 * <p> 1620 * This method is used when selecting drop target for the dragged View. After Views are 1621 * eliminated either via bounds check or via this method, resulting set of views will be 1622 * passed to {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)}. 1623 * <p> 1624 * Default implementation returns true. 1625 * 1626 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. 1627 * @param current The ViewHolder that user is dragging. 1628 * @param target The ViewHolder which is below the dragged ViewHolder. 1629 * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false 1630 * otherwise. 1631 */ 1632 @SuppressWarnings("WeakerAccess") canDropOver(@onNull RecyclerView recyclerView, @NonNull ViewHolder current, @NonNull ViewHolder target)1633 public boolean canDropOver(@NonNull RecyclerView recyclerView, @NonNull ViewHolder current, 1634 @NonNull ViewHolder target) { 1635 return true; 1636 } 1637 1638 /** 1639 * Called when ItemTouchHelper wants to move the dragged item from its old position to 1640 * the new position. 1641 * <p> 1642 * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved 1643 * to the adapter position of {@code target} ViewHolder 1644 * ({@link ViewHolder#getAbsoluteAdapterPosition() 1645 * ViewHolder#getAdapterPositionInRecyclerView()}). 1646 * <p> 1647 * If you don't support drag & drop, this method will never be called. 1648 * 1649 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. 1650 * @param viewHolder The ViewHolder which is being dragged by the user. 1651 * @param target The ViewHolder over which the currently active item is being 1652 * dragged. 1653 * @return True if the {@code viewHolder} has been moved to the adapter position of 1654 * {@code target}. 1655 * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int) 1656 */ onMove(@onNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder, @NonNull ViewHolder target)1657 public abstract boolean onMove(@NonNull RecyclerView recyclerView, 1658 @NonNull ViewHolder viewHolder, @NonNull ViewHolder target); 1659 1660 /** 1661 * Returns whether ItemTouchHelper should start a drag and drop operation if an item is 1662 * long pressed. 1663 * <p> 1664 * Default value returns true but you may want to disable this if you want to start 1665 * dragging on a custom view touch using {@link #startDrag(ViewHolder)}. 1666 * 1667 * @return True if ItemTouchHelper should start dragging an item when it is long pressed, 1668 * false otherwise. Default value is <code>true</code>. 1669 * @see #startDrag(ViewHolder) 1670 */ isLongPressDragEnabled()1671 public boolean isLongPressDragEnabled() { 1672 return true; 1673 } 1674 1675 /** 1676 * Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped 1677 * over the View. 1678 * <p> 1679 * Default value returns true but you may want to disable this if you want to start 1680 * swiping on a custom view touch using {@link #startSwipe(ViewHolder)}. 1681 * 1682 * @return True if ItemTouchHelper should start swiping an item when user swipes a pointer 1683 * over the View, false otherwise. Default value is <code>true</code>. 1684 * @see #startSwipe(ViewHolder) 1685 */ isItemViewSwipeEnabled()1686 public boolean isItemViewSwipeEnabled() { 1687 return true; 1688 } 1689 1690 /** 1691 * When finding views under a dragged view, by default, ItemTouchHelper searches for views 1692 * that overlap with the dragged View. By overriding this method, you can extend or shrink 1693 * the search box. 1694 * 1695 * @return The extra margin to be added to the hit box of the dragged View. 1696 */ 1697 @SuppressWarnings("WeakerAccess") getBoundingBoxMargin()1698 public int getBoundingBoxMargin() { 1699 return 0; 1700 } 1701 1702 /** 1703 * Returns the fraction that the user should move the View to be considered as swiped. 1704 * The fraction is calculated with respect to RecyclerView's bounds. 1705 * <p> 1706 * Default value is .5f, which means, to swipe a View, user must move the View at least 1707 * half of RecyclerView's width or height, depending on the swipe direction. 1708 * 1709 * @param viewHolder The ViewHolder that is being dragged. 1710 * @return A float value that denotes the fraction of the View size. Default value 1711 * is .5f . 1712 */ 1713 @SuppressWarnings("WeakerAccess") getSwipeThreshold(@onNull ViewHolder viewHolder)1714 public float getSwipeThreshold(@NonNull ViewHolder viewHolder) { 1715 return .5f; 1716 } 1717 1718 /** 1719 * Returns the fraction that the user should move the View to be considered as it is 1720 * dragged. After a view is moved this amount, ItemTouchHelper starts checking for Views 1721 * below it for a possible drop. 1722 * 1723 * @param viewHolder The ViewHolder that is being dragged. 1724 * @return A float value that denotes the fraction of the View size. Default value is 1725 * .5f . 1726 */ 1727 @SuppressWarnings("WeakerAccess") getMoveThreshold(@onNull ViewHolder viewHolder)1728 public float getMoveThreshold(@NonNull ViewHolder viewHolder) { 1729 return .5f; 1730 } 1731 1732 /** 1733 * Defines the minimum velocity which will be considered as a swipe action by the user. 1734 * <p> 1735 * You can increase this value to make it harder to swipe or decrease it to make it easier. 1736 * Keep in mind that ItemTouchHelper also checks the perpendicular velocity and makes sure 1737 * current direction velocity is larger then the perpendicular one. Otherwise, user's 1738 * movement is ambiguous. You can change the threshold by overriding 1739 * {@link #getSwipeVelocityThreshold(float)}. 1740 * <p> 1741 * The velocity is calculated in pixels per second. 1742 * <p> 1743 * The default framework value is passed as a parameter so that you can modify it with a 1744 * multiplier. 1745 * 1746 * @param defaultValue The default value (in pixels per second) used by the 1747 * ItemTouchHelper. 1748 * @return The minimum swipe velocity. The default implementation returns the 1749 * <code>defaultValue</code> parameter. 1750 * @see #getSwipeVelocityThreshold(float) 1751 * @see #getSwipeThreshold(ViewHolder) 1752 */ 1753 @SuppressWarnings("WeakerAccess") getSwipeEscapeVelocity(float defaultValue)1754 public float getSwipeEscapeVelocity(float defaultValue) { 1755 return defaultValue; 1756 } 1757 1758 /** 1759 * Defines the maximum velocity ItemTouchHelper will ever calculate for pointer movements. 1760 * <p> 1761 * To consider a movement as swipe, ItemTouchHelper requires it to be larger than the 1762 * perpendicular movement. If both directions reach to the max threshold, none of them will 1763 * be considered as a swipe because it is usually an indication that user rather tried to 1764 * scroll then swipe. 1765 * <p> 1766 * The velocity is calculated in pixels per second. 1767 * <p> 1768 * You can customize this behavior by changing this method. If you increase the value, it 1769 * will be easier for the user to swipe diagonally and if you decrease the value, user will 1770 * need to make a rather straight finger movement to trigger a swipe. 1771 * 1772 * @param defaultValue The default value(in pixels per second) used by the ItemTouchHelper. 1773 * @return The velocity cap for pointer movements. The default implementation returns the 1774 * <code>defaultValue</code> parameter. 1775 * @see #getSwipeEscapeVelocity(float) 1776 */ 1777 @SuppressWarnings("WeakerAccess") getSwipeVelocityThreshold(float defaultValue)1778 public float getSwipeVelocityThreshold(float defaultValue) { 1779 return defaultValue; 1780 } 1781 1782 /** 1783 * Called by ItemTouchHelper to select a drop target from the list of ViewHolders that 1784 * are under the dragged View. 1785 * <p> 1786 * Default implementation filters the View with which dragged item have changed position 1787 * in the drag direction. For instance, if the view is dragged UP, it compares the 1788 * <code>view.getTop()</code> of the two views before and after drag started. If that value 1789 * is different, the target view passes the filter. 1790 * <p> 1791 * Among these Views which pass the test, the one closest to the dragged view is chosen. 1792 * <p> 1793 * This method is called on the main thread every time user moves the View. If you want to 1794 * override it, make sure it does not do any expensive operations. 1795 * 1796 * @param selected The ViewHolder being dragged by the user. 1797 * @param dropTargets The list of ViewHolder that are under the dragged View and 1798 * candidate as a drop. 1799 * @param curX The updated left value of the dragged View after drag translations 1800 * are applied. This value does not include margins added by 1801 * {@link RecyclerView.ItemDecoration}s. 1802 * @param curY The updated top value of the dragged View after drag translations 1803 * are applied. This value does not include margins added by 1804 * {@link RecyclerView.ItemDecoration}s. 1805 * @return A ViewHolder to whose position the dragged ViewHolder should be 1806 * moved to. 1807 */ 1808 @SuppressWarnings("WeakerAccess") 1809 @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly chooseDropTarget(@onNull ViewHolder selected, @NonNull List<ViewHolder> dropTargets, int curX, int curY)1810 public ViewHolder chooseDropTarget(@NonNull ViewHolder selected, 1811 @NonNull List<ViewHolder> dropTargets, int curX, int curY) { 1812 int right = curX + selected.itemView.getWidth(); 1813 int bottom = curY + selected.itemView.getHeight(); 1814 ViewHolder winner = null; 1815 int winnerScore = -1; 1816 final int dx = curX - selected.itemView.getLeft(); 1817 final int dy = curY - selected.itemView.getTop(); 1818 final int targetsSize = dropTargets.size(); 1819 for (int i = 0; i < targetsSize; i++) { 1820 final ViewHolder target = dropTargets.get(i); 1821 if (dx > 0) { 1822 int diff = target.itemView.getRight() - right; 1823 if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) { 1824 final int score = Math.abs(diff); 1825 if (score > winnerScore) { 1826 winnerScore = score; 1827 winner = target; 1828 } 1829 } 1830 } 1831 if (dx < 0) { 1832 int diff = target.itemView.getLeft() - curX; 1833 if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) { 1834 final int score = Math.abs(diff); 1835 if (score > winnerScore) { 1836 winnerScore = score; 1837 winner = target; 1838 } 1839 } 1840 } 1841 if (dy < 0) { 1842 int diff = target.itemView.getTop() - curY; 1843 if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) { 1844 final int score = Math.abs(diff); 1845 if (score > winnerScore) { 1846 winnerScore = score; 1847 winner = target; 1848 } 1849 } 1850 } 1851 1852 if (dy > 0) { 1853 int diff = target.itemView.getBottom() - bottom; 1854 if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) { 1855 final int score = Math.abs(diff); 1856 if (score > winnerScore) { 1857 winnerScore = score; 1858 winner = target; 1859 } 1860 } 1861 } 1862 } 1863 return winner; 1864 } 1865 1866 /** 1867 * Called when a ViewHolder is swiped by the user. 1868 * <p> 1869 * If you are returning relative directions ({@link #START} , {@link #END}) from the 1870 * {@link #getMovementFlags(RecyclerView, ViewHolder)} method, this method 1871 * will also use relative directions. Otherwise, it will use absolute directions. 1872 * <p> 1873 * If you don't support swiping, this method will never be called. 1874 * <p> 1875 * ItemTouchHelper will keep a reference to the View until it is detached from 1876 * RecyclerView. 1877 * As soon as it is detached, ItemTouchHelper will call 1878 * {@link #clearView(RecyclerView, ViewHolder)}. 1879 * 1880 * @param viewHolder The ViewHolder which has been swiped by the user. 1881 * @param direction The direction to which the ViewHolder is swiped. It is one of 1882 * {@link #UP}, {@link #DOWN}, 1883 * {@link #LEFT} or {@link #RIGHT}. If your 1884 * {@link #getMovementFlags(RecyclerView, ViewHolder)} 1885 * method 1886 * returned relative flags instead of {@link #LEFT} / {@link #RIGHT}; 1887 * `direction` will be relative as well. ({@link #START} or {@link 1888 * #END}). 1889 */ onSwiped(@onNull ViewHolder viewHolder, int direction)1890 public abstract void onSwiped(@NonNull ViewHolder viewHolder, int direction); 1891 1892 /** 1893 * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed. 1894 * <p/> 1895 * If you override this method, you should call super. 1896 * 1897 * @param viewHolder The new ViewHolder that is being swiped or dragged. Might be null if 1898 * it is cleared. 1899 * @param actionState One of {@link ItemTouchHelper#ACTION_STATE_IDLE}, 1900 * {@link ItemTouchHelper#ACTION_STATE_SWIPE} or 1901 * {@link ItemTouchHelper#ACTION_STATE_DRAG}. 1902 * @see #clearView(RecyclerView, RecyclerView.ViewHolder) 1903 */ onSelectedChanged(@ullable ViewHolder viewHolder, int actionState)1904 public void onSelectedChanged(@Nullable ViewHolder viewHolder, int actionState) { 1905 if (viewHolder != null) { 1906 ItemTouchUIUtilImpl.INSTANCE.onSelected(viewHolder.itemView); 1907 } 1908 } 1909 getMaxDragScroll(RecyclerView recyclerView)1910 private int getMaxDragScroll(RecyclerView recyclerView) { 1911 if (mCachedMaxScrollSpeed == -1) { 1912 mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize( 1913 R.dimen.item_touch_helper_max_drag_scroll_per_frame); 1914 } 1915 return mCachedMaxScrollSpeed; 1916 } 1917 1918 /** 1919 * Called when {@link #onMove(RecyclerView, ViewHolder, ViewHolder)} returns true. 1920 * <p> 1921 * ItemTouchHelper does not create an extra Bitmap or View while dragging, instead, it 1922 * modifies the existing View. Because of this reason, it is important that the View is 1923 * still part of the layout after it is moved. This may not work as intended when swapped 1924 * Views are close to RecyclerView bounds or there are gaps between them (e.g. other Views 1925 * which were not eligible for dropping over). 1926 * <p> 1927 * This method is responsible to give necessary hint to the LayoutManager so that it will 1928 * keep the View in visible area. For example, for LinearLayoutManager, this is as simple 1929 * as calling {@link LinearLayoutManager#scrollToPositionWithOffset(int, int)}. 1930 * 1931 * Default implementation calls {@link RecyclerView#scrollToPosition(int)} if the View's 1932 * new position is likely to be out of bounds. 1933 * <p> 1934 * It is important to ensure the ViewHolder will stay visible as otherwise, it might be 1935 * removed by the LayoutManager if the move causes the View to go out of bounds. In that 1936 * case, drag will end prematurely. 1937 * 1938 * @param recyclerView The RecyclerView controlled by the ItemTouchHelper. 1939 * @param viewHolder The ViewHolder under user's control. 1940 * @param fromPos The previous adapter position of the dragged item (before it was 1941 * moved). 1942 * @param target The ViewHolder on which the currently active item has been dropped. 1943 * @param toPos The new adapter position of the dragged item. 1944 * @param x The updated left value of the dragged View after drag translations 1945 * are applied. This value does not include margins added by 1946 * {@link RecyclerView.ItemDecoration}s. 1947 * @param y The updated top value of the dragged View after drag translations 1948 * are applied. This value does not include margins added by 1949 * {@link RecyclerView.ItemDecoration}s. 1950 */ onMoved(final @NonNull RecyclerView recyclerView, final @NonNull ViewHolder viewHolder, int fromPos, final @NonNull ViewHolder target, int toPos, int x, int y)1951 public void onMoved(final @NonNull RecyclerView recyclerView, 1952 final @NonNull ViewHolder viewHolder, int fromPos, final @NonNull ViewHolder target, 1953 int toPos, int x, int y) { 1954 final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); 1955 if (layoutManager instanceof ViewDropHandler) { 1956 ((ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView, 1957 target.itemView, x, y); 1958 return; 1959 } 1960 1961 // if layout manager cannot handle it, do some guesswork 1962 if (layoutManager.canScrollHorizontally()) { 1963 final int minLeft = layoutManager.getDecoratedLeft(target.itemView); 1964 if (minLeft <= recyclerView.getPaddingLeft()) { 1965 recyclerView.scrollToPosition(toPos); 1966 } 1967 final int maxRight = layoutManager.getDecoratedRight(target.itemView); 1968 if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) { 1969 recyclerView.scrollToPosition(toPos); 1970 } 1971 } 1972 1973 if (layoutManager.canScrollVertically()) { 1974 final int minTop = layoutManager.getDecoratedTop(target.itemView); 1975 if (minTop <= recyclerView.getPaddingTop()) { 1976 recyclerView.scrollToPosition(toPos); 1977 } 1978 final int maxBottom = layoutManager.getDecoratedBottom(target.itemView); 1979 if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) { 1980 recyclerView.scrollToPosition(toPos); 1981 } 1982 } 1983 } 1984 onDraw(Canvas c, RecyclerView parent, ViewHolder selected, List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, int actionState, float dX, float dY)1985 void onDraw(Canvas c, RecyclerView parent, ViewHolder selected, 1986 List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, 1987 int actionState, float dX, float dY) { 1988 final int recoverAnimSize = recoverAnimationList.size(); 1989 for (int i = 0; i < recoverAnimSize; i++) { 1990 final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); 1991 anim.update(); 1992 final int count = c.save(); 1993 onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, 1994 false); 1995 c.restoreToCount(count); 1996 } 1997 if (selected != null) { 1998 final int count = c.save(); 1999 onChildDraw(c, parent, selected, dX, dY, actionState, true); 2000 c.restoreToCount(count); 2001 } 2002 } 2003 onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected, List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, int actionState, float dX, float dY)2004 void onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected, 2005 List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, 2006 int actionState, float dX, float dY) { 2007 final int recoverAnimSize = recoverAnimationList.size(); 2008 for (int i = 0; i < recoverAnimSize; i++) { 2009 final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); 2010 final int count = c.save(); 2011 onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, 2012 false); 2013 c.restoreToCount(count); 2014 } 2015 if (selected != null) { 2016 final int count = c.save(); 2017 onChildDrawOver(c, parent, selected, dX, dY, actionState, true); 2018 c.restoreToCount(count); 2019 } 2020 boolean hasRunningAnimation = false; 2021 for (int i = recoverAnimSize - 1; i >= 0; i--) { 2022 final RecoverAnimation anim = recoverAnimationList.get(i); 2023 if (anim.mEnded && !anim.mIsPendingCleanup) { 2024 recoverAnimationList.remove(i); 2025 } else if (!anim.mEnded) { 2026 hasRunningAnimation = true; 2027 } 2028 } 2029 if (hasRunningAnimation) { 2030 parent.invalidate(); 2031 } 2032 } 2033 2034 /** 2035 * Called by the ItemTouchHelper when the user interaction with an element is over and it 2036 * also completed its animation. 2037 * <p> 2038 * This is a good place to clear all changes on the View that was done in 2039 * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)}, 2040 * {@link #onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int, 2041 * boolean)} or 2042 * {@link #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)}. 2043 * 2044 * @param recyclerView The RecyclerView which is controlled by the ItemTouchHelper. 2045 * @param viewHolder The View that was interacted by the user. 2046 */ clearView(@onNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder)2047 public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) { 2048 ItemTouchUIUtilImpl.INSTANCE.clearView(viewHolder.itemView); 2049 } 2050 2051 /** 2052 * Called by ItemTouchHelper on RecyclerView's onDraw callback. 2053 * <p> 2054 * If you would like to customize how your View's respond to user interactions, this is 2055 * a good place to override. 2056 * <p> 2057 * Default implementation translates the child by the given <code>dX</code>, 2058 * <code>dY</code>. 2059 * ItemTouchHelper also takes care of drawing the child after other children if it is being 2060 * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this 2061 * is 2062 * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L 2063 * and after, it changes View's elevation value to be greater than all other children.) 2064 * 2065 * @param c The canvas which RecyclerView is drawing its children 2066 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to 2067 * @param viewHolder The ViewHolder which is being interacted by the User or it was 2068 * interacted and simply animating to its original position 2069 * @param dX The amount of horizontal displacement caused by user's action 2070 * @param dY The amount of vertical displacement caused by user's action 2071 * @param actionState The type of interaction on the View. Is either {@link 2072 * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. 2073 * @param isCurrentlyActive True if this view is currently being controlled by the user or 2074 * false it is simply animating back to its original state. 2075 * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, 2076 * boolean) 2077 */ onChildDraw(@onNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive)2078 public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, 2079 @NonNull ViewHolder viewHolder, 2080 float dX, float dY, int actionState, boolean isCurrentlyActive) { 2081 ItemTouchUIUtilImpl.INSTANCE.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, 2082 actionState, isCurrentlyActive); 2083 } 2084 2085 /** 2086 * Called by ItemTouchHelper on RecyclerView's onDraw callback. 2087 * <p> 2088 * If you would like to customize how your View's respond to user interactions, this is 2089 * a good place to override. 2090 * <p> 2091 * Default implementation translates the child by the given <code>dX</code>, 2092 * <code>dY</code>. 2093 * ItemTouchHelper also takes care of drawing the child after other children if it is being 2094 * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this 2095 * is 2096 * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L 2097 * and after, it changes View's elevation value to be greater than all other children.) 2098 * 2099 * @param c The canvas which RecyclerView is drawing its children 2100 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to 2101 * @param viewHolder The ViewHolder which is being interacted by the User or it was 2102 * interacted and simply animating to its original position 2103 * @param dX The amount of horizontal displacement caused by user's action 2104 * @param dY The amount of vertical displacement caused by user's action 2105 * @param actionState The type of interaction on the View. Is either {@link 2106 * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. 2107 * @param isCurrentlyActive True if this view is currently being controlled by the user or 2108 * false it is simply animating back to its original state. 2109 * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, 2110 * boolean) 2111 */ onChildDrawOver(@onNull Canvas c, @NonNull RecyclerView recyclerView, @SuppressLint("UnknownNullness") ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive)2112 public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, 2113 @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly 2114 ViewHolder viewHolder, 2115 float dX, float dY, int actionState, boolean isCurrentlyActive) { 2116 ItemTouchUIUtilImpl.INSTANCE.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, 2117 actionState, isCurrentlyActive); 2118 } 2119 2120 /** 2121 * Called by the ItemTouchHelper when user action finished on a ViewHolder and now the View 2122 * will be animated to its final position. 2123 * <p> 2124 * Default implementation uses ItemAnimator's duration values. If 2125 * <code>animationType</code> is {@link #ANIMATION_TYPE_DRAG}, it returns 2126 * {@link RecyclerView.ItemAnimator#getMoveDuration()}, otherwise, it returns 2127 * {@link RecyclerView.ItemAnimator#getRemoveDuration()}. If RecyclerView does not have 2128 * any {@link RecyclerView.ItemAnimator} attached, this method returns 2129 * {@code DEFAULT_DRAG_ANIMATION_DURATION} or {@code DEFAULT_SWIPE_ANIMATION_DURATION} 2130 * depending on the animation type. 2131 * 2132 * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. 2133 * @param animationType The type of animation. Is one of {@link #ANIMATION_TYPE_DRAG}, 2134 * {@link #ANIMATION_TYPE_SWIPE_CANCEL} or 2135 * {@link #ANIMATION_TYPE_SWIPE_SUCCESS}. 2136 * @param animateDx The horizontal distance that the animation will offset 2137 * @param animateDy The vertical distance that the animation will offset 2138 * @return The duration for the animation 2139 */ 2140 @SuppressWarnings("WeakerAccess") getAnimationDuration(@onNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy)2141 public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, 2142 float animateDx, float animateDy) { 2143 final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); 2144 if (itemAnimator == null) { 2145 return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION 2146 : DEFAULT_SWIPE_ANIMATION_DURATION; 2147 } else { 2148 return animationType == ANIMATION_TYPE_DRAG ? itemAnimator.getMoveDuration() 2149 : itemAnimator.getRemoveDuration(); 2150 } 2151 } 2152 2153 /** 2154 * Called by the ItemTouchHelper when user is dragging a view out of bounds. 2155 * <p> 2156 * You can override this method to decide how much RecyclerView should scroll in response 2157 * to this action. Default implementation calculates a value based on the amount of View 2158 * out of bounds and the time it spent there. The longer user keeps the View out of bounds, 2159 * the faster the list will scroll. Similarly, the larger portion of the View is out of 2160 * bounds, the faster the RecyclerView will scroll. 2161 * 2162 * @param recyclerView The RecyclerView instance to which ItemTouchHelper is 2163 * attached to. 2164 * @param viewSize The total size of the View in scroll direction, excluding 2165 * item decorations. 2166 * @param viewSizeOutOfBounds The total size of the View that is out of bounds. This value 2167 * is negative if the View is dragged towards left or top edge. 2168 * @param totalSize The total size of RecyclerView in the scroll direction. 2169 * @param msSinceStartScroll The time passed since View is kept out of bounds. 2170 * @return The amount that RecyclerView should scroll. Keep in mind that this value will 2171 * be passed to {@link RecyclerView#scrollBy(int, int)} method. 2172 */ 2173 @SuppressWarnings("WeakerAccess") interpolateOutOfBoundsScroll(@onNull RecyclerView recyclerView, int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll)2174 public int interpolateOutOfBoundsScroll(@NonNull RecyclerView recyclerView, 2175 int viewSize, int viewSizeOutOfBounds, 2176 int totalSize, long msSinceStartScroll) { 2177 final int maxScroll = getMaxDragScroll(recyclerView); 2178 final int absOutOfBounds = Math.abs(viewSizeOutOfBounds); 2179 final int direction = (int) Math.signum(viewSizeOutOfBounds); 2180 // might be negative if other direction 2181 float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize); 2182 final int cappedScroll = (int) (direction * maxScroll 2183 * sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio)); 2184 final float timeRatio; 2185 if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) { 2186 timeRatio = 1f; 2187 } else { 2188 timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS; 2189 } 2190 final int value = (int) (cappedScroll * sDragScrollInterpolator 2191 .getInterpolation(timeRatio)); 2192 if (value == 0) { 2193 return viewSizeOutOfBounds > 0 ? 1 : -1; 2194 } 2195 return value; 2196 } 2197 } 2198 2199 /** 2200 * A simple wrapper to the default Callback which you can construct with drag and swipe 2201 * directions and this class will handle the flag callbacks. You should still override onMove 2202 * or 2203 * onSwiped depending on your use case. 2204 * 2205 * <pre> 2206 * ItemTouchHelper mIth = new ItemTouchHelper( 2207 * new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 2208 * ItemTouchHelper.LEFT) { 2209 * public boolean onMove(RecyclerView recyclerView, 2210 * ViewHolder viewHolder, ViewHolder target) { 2211 * final int fromPos = viewHolder.getAdapterPosition(); 2212 * final int toPos = target.getAdapterPosition(); 2213 * // move item in `fromPos` to `toPos` in adapter. 2214 * return true;// true if moved, false otherwise 2215 * } 2216 * public void onSwiped(ViewHolder viewHolder, int direction) { 2217 * // remove from adapter 2218 * } 2219 * }); 2220 * </pre> 2221 */ 2222 public abstract static class SimpleCallback extends Callback { 2223 2224 private int mDefaultSwipeDirs; 2225 2226 private int mDefaultDragDirs; 2227 2228 /** 2229 * Creates a Callback for the given drag and swipe allowance. These values serve as 2230 * defaults 2231 * and if you want to customize behavior per ViewHolder, you can override 2232 * {@link #getSwipeDirs(RecyclerView, ViewHolder)} 2233 * and / or {@link #getDragDirs(RecyclerView, ViewHolder)}. 2234 * 2235 * @param dragDirs Binary OR of direction flags in which the Views can be dragged. Must be 2236 * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link 2237 * #END}, 2238 * {@link #UP} and {@link #DOWN}. 2239 * @param swipeDirs Binary OR of direction flags in which the Views can be swiped. Must be 2240 * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link 2241 * #END}, 2242 * {@link #UP} and {@link #DOWN}. 2243 */ SimpleCallback(int dragDirs, int swipeDirs)2244 public SimpleCallback(int dragDirs, int swipeDirs) { 2245 mDefaultSwipeDirs = swipeDirs; 2246 mDefaultDragDirs = dragDirs; 2247 } 2248 2249 /** 2250 * Updates the default swipe directions. For example, you can use this method to toggle 2251 * certain directions depending on your use case. 2252 * 2253 * @param defaultSwipeDirs Binary OR of directions in which the ViewHolders can be swiped. 2254 */ 2255 @SuppressWarnings({"WeakerAccess", "unused"}) setDefaultSwipeDirs(@uppressWarnings"unused") int defaultSwipeDirs)2256 public void setDefaultSwipeDirs(@SuppressWarnings("unused") int defaultSwipeDirs) { 2257 mDefaultSwipeDirs = defaultSwipeDirs; 2258 } 2259 2260 /** 2261 * Updates the default drag directions. For example, you can use this method to toggle 2262 * certain directions depending on your use case. 2263 * 2264 * @param defaultDragDirs Binary OR of directions in which the ViewHolders can be dragged. 2265 */ 2266 @SuppressWarnings({"WeakerAccess", "unused"}) setDefaultDragDirs(@uppressWarnings"unused") int defaultDragDirs)2267 public void setDefaultDragDirs(@SuppressWarnings("unused") int defaultDragDirs) { 2268 mDefaultDragDirs = defaultDragDirs; 2269 } 2270 2271 /** 2272 * Returns the swipe directions for the provided ViewHolder. 2273 * Default implementation returns the swipe directions that was set via constructor or 2274 * {@link #setDefaultSwipeDirs(int)}. 2275 * 2276 * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. 2277 * @param viewHolder The ViewHolder for which the swipe direction is queried. 2278 * @return A binary OR of direction flags. 2279 */ 2280 @SuppressWarnings("WeakerAccess") getSwipeDirs(@uppressWarnings"unused") @onNull RecyclerView recyclerView, @SuppressWarnings("unused") @NonNull ViewHolder viewHolder)2281 public int getSwipeDirs(@SuppressWarnings("unused") @NonNull RecyclerView recyclerView, 2282 @SuppressWarnings("unused") @NonNull ViewHolder viewHolder) { 2283 return mDefaultSwipeDirs; 2284 } 2285 2286 /** 2287 * Returns the drag directions for the provided ViewHolder. 2288 * Default implementation returns the drag directions that was set via constructor or 2289 * {@link #setDefaultDragDirs(int)}. 2290 * 2291 * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. 2292 * @param viewHolder The ViewHolder for which the swipe direction is queried. 2293 * @return A binary OR of direction flags. 2294 */ 2295 @SuppressWarnings("WeakerAccess") getDragDirs(@uppressWarnings"unused") @onNull RecyclerView recyclerView, @SuppressWarnings("unused") @NonNull ViewHolder viewHolder)2296 public int getDragDirs(@SuppressWarnings("unused") @NonNull RecyclerView recyclerView, 2297 @SuppressWarnings("unused") @NonNull ViewHolder viewHolder) { 2298 return mDefaultDragDirs; 2299 } 2300 2301 @Override getMovementFlags(@onNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder)2302 public int getMovementFlags(@NonNull RecyclerView recyclerView, 2303 @NonNull ViewHolder viewHolder) { 2304 return makeMovementFlags(getDragDirs(recyclerView, viewHolder), 2305 getSwipeDirs(recyclerView, viewHolder)); 2306 } 2307 } 2308 2309 private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { 2310 2311 /** 2312 * Whether to execute code in response to the the invoking of 2313 * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)}. 2314 * 2315 * It is necessary to control this here because 2316 * {@link GestureDetector.SimpleOnGestureListener} can only be set on a 2317 * {@link GestureDetector} in a GestureDetector's constructor, a GestureDetector will call 2318 * onLongPress if an {@link MotionEvent#ACTION_DOWN} event is not followed by another event 2319 * that would cancel it (like {@link MotionEvent#ACTION_UP} or 2320 * {@link MotionEvent#ACTION_CANCEL}), the long press responding to the long press event 2321 * needs to be cancellable to prevent unexpected behavior. 2322 * 2323 * @see #doNotReactToLongPress() 2324 */ 2325 private boolean mShouldReactToLongPress = true; 2326 ItemTouchHelperGestureListener()2327 ItemTouchHelperGestureListener() { 2328 } 2329 2330 /** 2331 * Call to prevent executing code in response to 2332 * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)} being called. 2333 */ doNotReactToLongPress()2334 void doNotReactToLongPress() { 2335 mShouldReactToLongPress = false; 2336 } 2337 2338 @Override onDown(MotionEvent e)2339 public boolean onDown(MotionEvent e) { 2340 return true; 2341 } 2342 2343 @Override onLongPress(MotionEvent e)2344 public void onLongPress(MotionEvent e) { 2345 if (!mShouldReactToLongPress) { 2346 return; 2347 } 2348 View child = findChildView(e); 2349 if (child != null) { 2350 ViewHolder vh = mRecyclerView.getChildViewHolder(child); 2351 if (vh != null) { 2352 if (!mCallback.hasDragFlag(mRecyclerView, vh)) { 2353 return; 2354 } 2355 int pointerId = e.getPointerId(0); 2356 // Long press is deferred. 2357 // Check w/ active pointer id to avoid selecting after motion 2358 // event is canceled. 2359 if (pointerId == mActivePointerId) { 2360 final int index = e.findPointerIndex(mActivePointerId); 2361 final float x = e.getX(index); 2362 final float y = e.getY(index); 2363 mInitialTouchX = x; 2364 mInitialTouchY = y; 2365 mDx = mDy = 0f; 2366 if (DEBUG) { 2367 Log.d(TAG, 2368 "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY); 2369 } 2370 if (mCallback.isLongPressDragEnabled()) { 2371 select(vh, ACTION_STATE_DRAG); 2372 } 2373 } 2374 } 2375 } 2376 } 2377 } 2378 2379 @VisibleForTesting 2380 static class RecoverAnimation implements Animator.AnimatorListener { 2381 2382 final float mStartDx; 2383 2384 final float mStartDy; 2385 2386 final float mTargetX; 2387 2388 final float mTargetY; 2389 2390 final ViewHolder mViewHolder; 2391 2392 final int mActionState; 2393 2394 @VisibleForTesting 2395 final ValueAnimator mValueAnimator; 2396 2397 final int mAnimationType; 2398 2399 boolean mIsPendingCleanup; 2400 2401 float mX; 2402 2403 float mY; 2404 2405 // if user starts touching a recovering view, we put it into interaction mode again, 2406 // instantly. 2407 boolean mOverridden = false; 2408 2409 boolean mEnded = false; 2410 2411 private float mFraction; 2412 RecoverAnimation(ViewHolder viewHolder, int animationType, int actionState, float startDx, float startDy, float targetX, float targetY)2413 RecoverAnimation(ViewHolder viewHolder, int animationType, 2414 int actionState, float startDx, float startDy, float targetX, float targetY) { 2415 mActionState = actionState; 2416 mAnimationType = animationType; 2417 mViewHolder = viewHolder; 2418 mStartDx = startDx; 2419 mStartDy = startDy; 2420 mTargetX = targetX; 2421 mTargetY = targetY; 2422 mValueAnimator = ValueAnimator.ofFloat(0f, 1f); 2423 mValueAnimator.addUpdateListener( 2424 new ValueAnimator.AnimatorUpdateListener() { 2425 @Override 2426 public void onAnimationUpdate(ValueAnimator animation) { 2427 setFraction(animation.getAnimatedFraction()); 2428 } 2429 }); 2430 mValueAnimator.setTarget(viewHolder.itemView); 2431 mValueAnimator.addListener(this); 2432 setFraction(0f); 2433 } 2434 setDuration(long duration)2435 public void setDuration(long duration) { 2436 mValueAnimator.setDuration(duration); 2437 } 2438 start()2439 public void start() { 2440 mViewHolder.setIsRecyclable(false); 2441 mValueAnimator.start(); 2442 } 2443 cancel()2444 public void cancel() { 2445 mValueAnimator.cancel(); 2446 } 2447 setFraction(float fraction)2448 public void setFraction(float fraction) { 2449 mFraction = fraction; 2450 } 2451 2452 /** 2453 * We run updates on onDraw method but use the fraction from animator callback. 2454 * This way, we can sync translate x/y values w/ the animators to avoid one-off frames. 2455 */ update()2456 public void update() { 2457 if (mStartDx == mTargetX) { 2458 mX = mViewHolder.itemView.getTranslationX(); 2459 } else { 2460 mX = mStartDx + mFraction * (mTargetX - mStartDx); 2461 } 2462 if (mStartDy == mTargetY) { 2463 mY = mViewHolder.itemView.getTranslationY(); 2464 } else { 2465 mY = mStartDy + mFraction * (mTargetY - mStartDy); 2466 } 2467 } 2468 2469 @Override onAnimationStart(Animator animation)2470 public void onAnimationStart(Animator animation) { 2471 2472 } 2473 2474 @Override onAnimationEnd(Animator animation)2475 public void onAnimationEnd(Animator animation) { 2476 if (!mEnded) { 2477 mViewHolder.setIsRecyclable(true); 2478 } 2479 mEnded = true; 2480 } 2481 2482 @Override onAnimationCancel(Animator animation)2483 public void onAnimationCancel(Animator animation) { 2484 setFraction(1f); //make sure we recover the view's state. 2485 } 2486 2487 @Override onAnimationRepeat(Animator animation)2488 public void onAnimationRepeat(Animator animation) { 2489 2490 } 2491 } 2492 } 2493