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