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