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