ItemTouchHelper.java revision e71a1df9b3c0e1bd3c21a1b3dd20a41790d4a950
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 final int s = scrollX; 735 scrollX = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, 736 mSelected.itemView.getWidth(), scrollX, 737 mRecyclerView.getWidth(), scrollDuration); 738 } 739 if (scrollY != 0) { 740 final int s = scrollY; 741 scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, 742 mSelected.itemView.getHeight(), scrollY, 743 mRecyclerView.getHeight(), scrollDuration); 744 } 745 if (scrollX != 0 || scrollY != 0) { 746 if (mDragScrollStartTimeInMs == Long.MIN_VALUE) { 747 mDragScrollStartTimeInMs = now; 748 } 749 mRecyclerView.scrollBy(scrollX, scrollY); 750 return true; 751 } 752 mDragScrollStartTimeInMs = Long.MIN_VALUE; 753 return false; 754 } 755 756 private List<ViewHolder> findSwapTargets(ViewHolder viewHolder) { 757 if (mSwapTargets == null) { 758 mSwapTargets = new ArrayList<ViewHolder>(); 759 mDistances = new ArrayList<Integer>(); 760 } else { 761 mSwapTargets.clear(); 762 mDistances.clear(); 763 } 764 final int margin = mCallback.getBoundingBoxMargin(); 765 final int left = Math.round(mSelectedStartX + mDx) - margin; 766 final int top = Math.round(mSelectedStartY + mDy) - margin; 767 final int right = left + viewHolder.itemView.getWidth() + 2 * margin; 768 final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin; 769 final int centerX = (left + right) / 2; 770 final int centerY = (top + bottom) / 2; 771 final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); 772 final int childCount = lm.getChildCount(); 773 for (int i = 0; i < childCount; i++) { 774 View other = lm.getChildAt(i); 775 if (other == viewHolder.itemView) { 776 continue;//myself! 777 } 778 if (other.getBottom() < top || other.getTop() > bottom 779 || other.getRight() < left || other.getLeft() > right) { 780 continue; 781 } 782 final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other); 783 if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) { 784 // find the index to add 785 final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2); 786 final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2); 787 final int dist = dx * dx + dy * dy; 788 789 int pos = 0; 790 final int cnt = mSwapTargets.size(); 791 for (int j = 0; j < cnt; j++) { 792 if (dist > mDistances.get(j)) { 793 pos++; 794 } else { 795 break; 796 } 797 } 798 mSwapTargets.add(pos, otherVh); 799 mDistances.add(pos, dist); 800 } 801 } 802 return mSwapTargets; 803 } 804 805 /** 806 * Checks if we should swap w/ another view holder. 807 */ 808 private void moveIfNecessary(ViewHolder viewHolder) { 809 if (mRecyclerView.isLayoutRequested()) { 810 return; 811 } 812 if (mActionState != ACTION_STATE_DRAG) { 813 return; 814 } 815 816 final float threshold = mCallback.getMoveThreshold(viewHolder); 817 final int x = (int) (mSelectedStartX + mDx); 818 final int y = (int) (mSelectedStartY + mDy); 819 if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold 820 && Math.abs(x - viewHolder.itemView.getLeft()) 821 < viewHolder.itemView.getWidth() * threshold) { 822 return; 823 } 824 List<ViewHolder> swapTargets = findSwapTargets(viewHolder); 825 if (swapTargets.size() == 0) { 826 return; 827 } 828 // may swap. 829 ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y); 830 if (target == null) { 831 mSwapTargets.clear(); 832 mDistances.clear(); 833 return; 834 } 835 final int toPosition = target.getAdapterPosition(); 836 final int fromPosition = viewHolder.getAdapterPosition(); 837 if (mCallback.onMove(mRecyclerView, viewHolder, target)) { 838 // keep target visible 839 mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, 840 target, toPosition, x, y); 841 return; 842 } 843 } 844 845 @Override 846 public void onChildViewAttachedToWindow(View view) { 847 } 848 849 @Override 850 public void onChildViewDetachedFromWindow(View view) { 851 removeChildDrawingOrderCallbackIfNecessary(view); 852 final ViewHolder holder = mRecyclerView.getChildViewHolder(view); 853 if (holder == null) { 854 return; 855 } 856 if (mSelected != null && holder == mSelected) { 857 select(null, ACTION_STATE_IDLE); 858 } else { 859 endRecoverAnimation(holder, false); // this may push it into pending cleanup list. 860 if (mPendingCleanup.remove(holder.itemView)) { 861 mCallback.clearView(mRecyclerView, holder); 862 } 863 } 864 } 865 866 /** 867 * Returns the animation type or 0 if cannot be found. 868 */ 869 private int endRecoverAnimation(ViewHolder viewHolder, boolean override) { 870 final int recoverAnimSize = mRecoverAnimations.size(); 871 for (int i = recoverAnimSize - 1; i >= 0; i--) { 872 final RecoverAnimation anim = mRecoverAnimations.get(i); 873 if (anim.mViewHolder == viewHolder) { 874 anim.mOverridden |= override; 875 if (!anim.mEnded) { 876 anim.cancel(); 877 } 878 mRecoverAnimations.remove(i); 879 anim.mViewHolder.setIsRecyclable(true); 880 return anim.mAnimationType; 881 } 882 } 883 return 0; 884 } 885 886 @Override 887 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 888 RecyclerView.State state) { 889 outRect.setEmpty(); 890 } 891 892 private void obtainVelocityTracker() { 893 if (mVelocityTracker != null) { 894 mVelocityTracker.recycle(); 895 } 896 mVelocityTracker = VelocityTracker.obtain(); 897 } 898 899 private void releaseVelocityTracker() { 900 if (mVelocityTracker != null) { 901 mVelocityTracker.recycle(); 902 mVelocityTracker = null; 903 } 904 } 905 906 private ViewHolder findSwipedView(MotionEvent motionEvent) { 907 final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); 908 if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { 909 return null; 910 } 911 final int pointerIndex = MotionEventCompat.findPointerIndex(motionEvent, mActivePointerId); 912 final float dx = MotionEventCompat.getX(motionEvent, pointerIndex) - mInitialTouchX; 913 final float dy = MotionEventCompat.getY(motionEvent, pointerIndex) - mInitialTouchY; 914 final float absDx = Math.abs(dx); 915 final float absDy = Math.abs(dy); 916 917 if (absDx < mSlop && absDy < mSlop) { 918 return null; 919 } 920 if (absDx > absDy && lm.canScrollHorizontally()) { 921 return null; 922 } else if (absDy > absDx && lm.canScrollVertically()) { 923 return null; 924 } 925 View child = findChildView(motionEvent); 926 if (child == null) { 927 return null; 928 } 929 return mRecyclerView.getChildViewHolder(child); 930 } 931 932 /** 933 * Checks whether we should select a View for swiping. 934 */ 935 private boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) { 936 if (mSelected != null || action != MotionEvent.ACTION_MOVE 937 || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) { 938 return false; 939 } 940 if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { 941 return false; 942 } 943 final ViewHolder vh = findSwipedView(motionEvent); 944 if (vh == null) { 945 return false; 946 } 947 final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh); 948 949 final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) 950 >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE); 951 952 if (swipeFlags == 0) { 953 return false; 954 } 955 956 // mDx and mDy are only set in allowed directions. We use custom x/y here instead of 957 // updateDxDy to avoid swiping if user moves more in the other direction 958 final float x = MotionEventCompat.getX(motionEvent, pointerIndex); 959 final float y = MotionEventCompat.getY(motionEvent, pointerIndex); 960 961 // Calculate the distance moved 962 final float dx = x - mInitialTouchX; 963 final float dy = y - mInitialTouchY; 964 // swipe target is chose w/o applying flags so it does not really check if swiping in that 965 // direction is allowed. This why here, we use mDx mDy to check slope value again. 966 final float absDx = Math.abs(dx); 967 final float absDy = Math.abs(dy); 968 969 if (absDx < mSlop && absDy < mSlop) { 970 return false; 971 } 972 if (absDx > absDy) { 973 if (dx < 0 && (swipeFlags & LEFT) == 0) { 974 return false; 975 } 976 if (dx > 0 && (swipeFlags & RIGHT) == 0) { 977 return false; 978 } 979 } else { 980 if (dy < 0 && (swipeFlags & UP) == 0) { 981 return false; 982 } 983 if (dy > 0 && (swipeFlags & DOWN) == 0) { 984 return false; 985 } 986 } 987 mDx = mDy = 0f; 988 mActivePointerId = MotionEventCompat.getPointerId(motionEvent, 0); 989 select(vh, ACTION_STATE_SWIPE); 990 return true; 991 } 992 993 private View findChildView(MotionEvent event) { 994 // first check elevated views, if none, then call RV 995 final float x = event.getX(); 996 final float y = event.getY(); 997 if (mSelected != null) { 998 final View selectedView = mSelected.itemView; 999 if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) { 1000 return selectedView; 1001 } 1002 } 1003 for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { 1004 final RecoverAnimation anim = mRecoverAnimations.get(i); 1005 final View view = anim.mViewHolder.itemView; 1006 if (hitTest(view, x, y, anim.mX, anim.mY)) { 1007 return view; 1008 } 1009 } 1010 return mRecyclerView.findChildViewUnder(x, y); 1011 } 1012 1013 /** 1014 * Starts dragging the provided ViewHolder. By default, ItemTouchHelper starts a drag when a 1015 * View is long pressed. You can disable that behavior via 1016 * {@link ItemTouchHelper.Callback#isLongPressDragEnabled()}. 1017 * <p> 1018 * For this method to work: 1019 * <ul> 1020 * <li>The provided ViewHolder must be a child of the RecyclerView to which this 1021 * ItemTouchHelper 1022 * is attached.</li> 1023 * <li>{@link ItemTouchHelper.Callback} must have dragging enabled.</li> 1024 * <li>There must be a previous touch event that was reported to the ItemTouchHelper 1025 * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener 1026 * grabs previous events, this should work as expected.</li> 1027 * </ul> 1028 * 1029 * For example, if you would like to let your user to be able to drag an Item by touching one 1030 * of its descendants, you may implement it as follows: 1031 * <pre> 1032 * viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() { 1033 * public boolean onTouch(View v, MotionEvent event) { 1034 * if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) { 1035 * mItemTouchHelper.startDrag(viewHolder); 1036 * } 1037 * return false; 1038 * } 1039 * }); 1040 * </pre> 1041 * <p> 1042 * 1043 * @param viewHolder The ViewHolder to start dragging. It must be a direct child of 1044 * RecyclerView. 1045 * @see ItemTouchHelper.Callback#isItemViewSwipeEnabled() 1046 */ 1047 public void startDrag(ViewHolder viewHolder) { 1048 if (!mCallback.hasDragFlag(mRecyclerView, viewHolder)) { 1049 Log.e(TAG, "Start drag has been called but swiping is not enabled"); 1050 return; 1051 } 1052 if (viewHolder.itemView.getParent() != mRecyclerView) { 1053 Log.e(TAG, "Start drag has been called with a view holder which is not a child of " 1054 + "the RecyclerView which is controlled by this ItemTouchHelper."); 1055 return; 1056 } 1057 obtainVelocityTracker(); 1058 mDx = mDy = 0f; 1059 select(viewHolder, ACTION_STATE_DRAG); 1060 } 1061 1062 /** 1063 * Starts swiping the provided ViewHolder. By default, ItemTouchHelper starts swiping a View 1064 * when user swipes their finger (or mouse pointer) over the View. You can disable this 1065 * behavior 1066 * by overriding {@link ItemTouchHelper.Callback} 1067 * <p> 1068 * For this method to work: 1069 * <ul> 1070 * <li>The provided ViewHolder must be a child of the RecyclerView to which this 1071 * ItemTouchHelper is attached.</li> 1072 * <li>{@link ItemTouchHelper.Callback} must have swiping enabled.</li> 1073 * <li>There must be a previous touch event that was reported to the ItemTouchHelper 1074 * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener 1075 * grabs previous events, this should work as expected.</li> 1076 * </ul> 1077 * 1078 * For example, if you would like to let your user to be able to swipe an Item by touching one 1079 * of its descendants, you may implement it as follows: 1080 * <pre> 1081 * viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() { 1082 * public boolean onTouch(View v, MotionEvent event) { 1083 * if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) { 1084 * mItemTouchHelper.startSwipe(viewHolder); 1085 * } 1086 * return false; 1087 * } 1088 * }); 1089 * </pre> 1090 * 1091 * @param viewHolder The ViewHolder to start swiping. It must be a direct child of 1092 * RecyclerView. 1093 */ 1094 public void startSwipe(ViewHolder viewHolder) { 1095 if (!mCallback.hasSwipeFlag(mRecyclerView, viewHolder)) { 1096 Log.e(TAG, "Start swipe has been called but dragging is not enabled"); 1097 return; 1098 } 1099 if (viewHolder.itemView.getParent() != mRecyclerView) { 1100 Log.e(TAG, "Start swipe has been called with a view holder which is not a child of " 1101 + "the RecyclerView controlled by this ItemTouchHelper."); 1102 return; 1103 } 1104 obtainVelocityTracker(); 1105 mDx = mDy = 0f; 1106 select(viewHolder, ACTION_STATE_SWIPE); 1107 } 1108 1109 private RecoverAnimation findAnimation(MotionEvent event) { 1110 if (mRecoverAnimations.isEmpty()) { 1111 return null; 1112 } 1113 View target = findChildView(event); 1114 for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { 1115 final RecoverAnimation anim = mRecoverAnimations.get(i); 1116 if (anim.mViewHolder.itemView == target) { 1117 return anim; 1118 } 1119 } 1120 return null; 1121 } 1122 1123 private void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) { 1124 final float x = MotionEventCompat.getX(ev, pointerIndex); 1125 final float y = MotionEventCompat.getY(ev, pointerIndex); 1126 1127 // Calculate the distance moved 1128 mDx = x - mInitialTouchX; 1129 mDy = y - mInitialTouchY; 1130 if ((directionFlags & LEFT) == 0) { 1131 mDx = Math.max(0, mDx); 1132 } 1133 if ((directionFlags & RIGHT) == 0) { 1134 mDx = Math.min(0, mDx); 1135 } 1136 if ((directionFlags & UP) == 0) { 1137 mDy = Math.max(0, mDy); 1138 } 1139 if ((directionFlags & DOWN) == 0) { 1140 mDy = Math.min(0, mDy); 1141 } 1142 } 1143 1144 private int swipeIfNecessary(ViewHolder viewHolder) { 1145 if (mActionState == ACTION_STATE_DRAG) { 1146 return 0; 1147 } 1148 final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder); 1149 final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection( 1150 originalMovementFlags, 1151 ViewCompat.getLayoutDirection(mRecyclerView)); 1152 final int flags = (absoluteMovementFlags 1153 & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); 1154 if (flags == 0) { 1155 return 0; 1156 } 1157 final int originalFlags = (originalMovementFlags 1158 & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); 1159 int swipeDir; 1160 if (Math.abs(mDx) > Math.abs(mDy)) { 1161 if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { 1162 // if swipe dir is not in original flags, it should be the relative direction 1163 if ((originalFlags & swipeDir) == 0) { 1164 // convert to relative 1165 return Callback.convertToRelativeDirection(swipeDir, 1166 ViewCompat.getLayoutDirection(mRecyclerView)); 1167 } 1168 return swipeDir; 1169 } 1170 if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { 1171 return swipeDir; 1172 } 1173 } else { 1174 if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { 1175 return swipeDir; 1176 } 1177 if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { 1178 // if swipe dir is not in original flags, it should be the relative direction 1179 if ((originalFlags & swipeDir) == 0) { 1180 // convert to relative 1181 return Callback.convertToRelativeDirection(swipeDir, 1182 ViewCompat.getLayoutDirection(mRecyclerView)); 1183 } 1184 return swipeDir; 1185 } 1186 } 1187 return 0; 1188 } 1189 1190 private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) { 1191 if ((flags & (LEFT | RIGHT)) != 0) { 1192 final int dirFlag = mDx > 0 ? RIGHT : LEFT; 1193 if (mVelocityTracker != null && mActivePointerId > -1) { 1194 final float xVelocity = VelocityTrackerCompat 1195 .getXVelocity(mVelocityTracker, mActivePointerId); 1196 final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT; 1197 if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag && 1198 Math.abs(xVelocity) >= mRecyclerView.getMinFlingVelocity()) { 1199 return velDirFlag; 1200 } 1201 } 1202 1203 final float threshold = mRecyclerView.getWidth() * mCallback 1204 .getSwipeThreshold(viewHolder); 1205 1206 if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) { 1207 return dirFlag; 1208 } 1209 } 1210 return 0; 1211 } 1212 1213 private int checkVerticalSwipe(ViewHolder viewHolder, int flags) { 1214 if ((flags & (UP | DOWN)) != 0) { 1215 final int dirFlag = mDy > 0 ? DOWN : UP; 1216 if (mVelocityTracker != null && mActivePointerId > -1) { 1217 final float yVelocity = VelocityTrackerCompat 1218 .getYVelocity(mVelocityTracker, mActivePointerId); 1219 final int velDirFlag = yVelocity > 0f ? DOWN : UP; 1220 if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag && 1221 Math.abs(yVelocity) >= mRecyclerView.getMinFlingVelocity()) { 1222 return velDirFlag; 1223 } 1224 } 1225 1226 final float threshold = mRecyclerView.getHeight() * mCallback 1227 .getSwipeThreshold(viewHolder); 1228 if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) { 1229 return dirFlag; 1230 } 1231 } 1232 return 0; 1233 } 1234 1235 private void addChildDrawingOrderCallback() { 1236 if (Build.VERSION.SDK_INT >= 21) { 1237 return;// we use elevation on Lollipop 1238 } 1239 if (mChildDrawingOrderCallback == null) { 1240 mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() { 1241 @Override 1242 public int onGetChildDrawingOrder(int childCount, int i) { 1243 if (mOverdrawChild == null) { 1244 return i; 1245 } 1246 int childPosition = mOverdrawChildPosition; 1247 if (childPosition == -1) { 1248 childPosition = mRecyclerView.indexOfChild(mOverdrawChild); 1249 mOverdrawChildPosition = childPosition; 1250 } 1251 if (i == childCount - 1) { 1252 return childPosition; 1253 } 1254 return i < childPosition ? i : i + 1; 1255 } 1256 }; 1257 } 1258 mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback); 1259 } 1260 1261 private void removeChildDrawingOrderCallbackIfNecessary(View view) { 1262 if (view == mOverdrawChild) { 1263 mOverdrawChild = null; 1264 // only remove if we've added 1265 if (mChildDrawingOrderCallback != null) { 1266 mRecyclerView.setChildDrawingOrderCallback(null); 1267 } 1268 } 1269 } 1270 1271 /** 1272 * An interface which can be implemented by LayoutManager for better integration with 1273 * {@link ItemTouchHelper}. 1274 */ 1275 public static interface ViewDropHandler { 1276 1277 /** 1278 * Called by the {@link ItemTouchHelper} after a View is dropped over another View. 1279 * <p> 1280 * A LayoutManager should implement this interface to get ready for the upcoming move 1281 * operation. 1282 * <p> 1283 * For example, LinearLayoutManager sets up a "scrollToPositionWithOffset" calls so that 1284 * the View under drag will be used as an anchor View while calculating the next layout, 1285 * making layout stay consistent. 1286 * 1287 * @param view The View which is being dragged. It is very likely that user is still 1288 * dragging this View so there might be other 1289 * {@link #prepareForDrop(View, View, int, int)} after this one. 1290 * @param target The target view which is being dropped on. 1291 * @param x The <code>left</code> offset of the View that is being dragged. This value 1292 * includes the movement caused by the user. 1293 * @param y The <code>top</code> offset of the View that is being dragged. This value 1294 * includes the movement caused by the user. 1295 */ 1296 public void prepareForDrop(View view, View target, int x, int y); 1297 } 1298 1299 /** 1300 * This class is the contract between ItemTouchHelper and your application. It lets you control 1301 * which touch behaviors are enabled per each ViewHolder and also receive callbacks when user 1302 * performs these actions. 1303 * <p> 1304 * To control which actions user can take on each view, you should override 1305 * {@link #getMovementFlags(RecyclerView, ViewHolder)} and return appropriate set 1306 * of direction flags. ({@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link #END}, 1307 * {@link #UP}, {@link #DOWN}). You can use 1308 * {@link #makeMovementFlags(int, int)} to easily construct it. Alternatively, you can use 1309 * {@link SimpleCallback}. 1310 * <p> 1311 * If user drags an item, ItemTouchHelper will call 1312 * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder) 1313 * onMove(recyclerView, dragged, target)}. 1314 * Upon receiving this callback, you should move the item from the old position 1315 * ({@code dragged.getAdapterPosition()}) to new position ({@code target.getAdapterPosition()}) 1316 * in your adapter and also call {@link RecyclerView.Adapter#notifyItemMoved(int, int)}. 1317 * To control where a View can be dropped, you can override 1318 * {@link #canDropOver(RecyclerView, ViewHolder, ViewHolder)}. When a 1319 * dragging View overlaps multiple other views, Callback chooses the closest View with which 1320 * dragged View might have changed positions. Although this approach works for many use cases, 1321 * if you have a custom LayoutManager, you can override 1322 * {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)} to select a 1323 * custom drop target. 1324 * <p> 1325 * When a View is swiped, ItemTouchHelper animates it until it goes out of bounds, then calls 1326 * {@link #onSwiped(ViewHolder, int)}. At this point, you should update your 1327 * adapter (e.g. remove the item) and call related Adapter#notify event. 1328 */ 1329 @SuppressWarnings("UnusedParameters") 1330 public abstract static class Callback { 1331 1332 public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200; 1333 1334 public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250; 1335 1336 static final int RELATIVE_DIR_FLAGS = START | END | 1337 ((START | END) << DIRECTION_FLAG_COUNT) | 1338 ((START | END) << (2 * DIRECTION_FLAG_COUNT)); 1339 1340 private static final ItemTouchUICompat mUICallback; 1341 1342 private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT | 1343 ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT) | 1344 ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT)); 1345 1346 private static final Interpolator sDragScrollInterpolator = new Interpolator() { 1347 public float getInterpolation(float t) { 1348 return t * t * t * t * t; 1349 } 1350 }; 1351 1352 private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() { 1353 public float getInterpolation(float t) { 1354 t -= 1.0f; 1355 return t * t * t * t * t + 1.0f; 1356 } 1357 }; 1358 1359 /** 1360 * Drag scroll speed keeps accelerating until this many milliseconds before being capped. 1361 */ 1362 private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; 1363 1364 private int mCachedMaxScrollSpeed = -1; 1365 1366 static { 1367 if (Build.VERSION.SDK_INT >= 21) { 1368 mUICallback = new ItemTouchUICompat.LollipopImpl(); 1369 } else if (Build.VERSION.SDK_INT >= 11) { 1370 mUICallback = new ItemTouchUICompat.HoneycombImpl(); 1371 } else { 1372 mUICallback = new ItemTouchUICompat.GingerbreadImpl(); 1373 } 1374 } 1375 1376 /** 1377 * Replaces a movement direction with its relative version by taking layout direction into 1378 * account. 1379 * 1380 * @param flags The flag value that include any number of movement flags. 1381 * @param layoutDirection The layout direction of the View. Can be obtained from 1382 * {@link ViewCompat#getLayoutDirection(android.view.View)}. 1383 * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead 1384 * of {@link #LEFT}, {@link #RIGHT}. 1385 * @see #convertToAbsoluteDirection(int, int) 1386 */ 1387 public static int convertToRelativeDirection(int flags, int layoutDirection) { 1388 int masked = flags & ABS_HORIZONTAL_DIR_FLAGS; 1389 if (masked == 0) { 1390 return flags;// does not have any abs flags, good. 1391 } 1392 flags &= ~masked; //remove left / right. 1393 if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { 1394 // no change. just OR with 2 bits shifted mask and return 1395 flags |= masked << 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. 1396 return flags; 1397 } else { 1398 // add RIGHT flag as START 1399 flags |= ((masked << 1) & ~ABS_HORIZONTAL_DIR_FLAGS); 1400 // first clean RIGHT bit then add LEFT flag as END 1401 flags |= ((masked << 1) & ABS_HORIZONTAL_DIR_FLAGS) << 2; 1402 } 1403 return flags; 1404 } 1405 1406 /** 1407 * Convenience method to create movement flags. 1408 * <p> 1409 * For instance, if you want to let your items be drag & dropped vertically and swiped 1410 * left to be dismissed, you can call this method with: 1411 * <code>makeMovementFlags(UP | DOWN, LEFT);</code> 1412 * 1413 * @param dragFlags The directions in which the item can be dragged. 1414 * @param swipeFlags The directions in which the item can be swiped. 1415 * @return Returns an integer composed of the given drag and swipe flags. 1416 */ 1417 public static int makeMovementFlags(int dragFlags, int swipeFlags) { 1418 return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags) | 1419 makeFlag(ACTION_STATE_SWIPE, swipeFlags) | makeFlag(ACTION_STATE_DRAG, 1420 dragFlags); 1421 } 1422 1423 /** 1424 * Shifts the given direction flags to the offset of the given action state. 1425 * 1426 * @param actionState The action state you want to get flags in. Should be one of 1427 * {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or 1428 * {@link #ACTION_STATE_DRAG}. 1429 * @param directions The direction flags. Can be composed from {@link #UP}, {@link #DOWN}, 1430 * {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}. 1431 * @return And integer that represents the given directions in the provided actionState. 1432 */ 1433 public static int makeFlag(int actionState, int directions) { 1434 return directions << (actionState * DIRECTION_FLAG_COUNT); 1435 } 1436 1437 /** 1438 * Should return a composite flag which defines the enabled move directions in each state 1439 * (idle, swiping, dragging). 1440 * <p> 1441 * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int, 1442 * int)} 1443 * or {@link #makeFlag(int, int)}. 1444 * <p> 1445 * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next 1446 * 8 bits are for SWIPE state and third 8 bits are for DRAG state. 1447 * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in 1448 * {@link ItemTouchHelper}. 1449 * <p> 1450 * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to 1451 * swipe by swiping RIGHT, you can return: 1452 * <pre> 1453 * makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT); 1454 * </pre> 1455 * This means, allow right movement while IDLE and allow right and left movement while 1456 * swiping. 1457 * 1458 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached. 1459 * @param viewHolder The ViewHolder for which the movement information is necessary. 1460 * @return flags specifying which movements are allowed on this ViewHolder. 1461 * @see #makeMovementFlags(int, int) 1462 * @see #makeFlag(int, int) 1463 */ 1464 public abstract int getMovementFlags(RecyclerView recyclerView, 1465 ViewHolder viewHolder); 1466 1467 /** 1468 * Converts a given set of flags to absolution direction which means {@link #START} and 1469 * {@link #END} are replaced with {@link #LEFT} and {@link #RIGHT} depending on the layout 1470 * direction. 1471 * 1472 * @param flags The flag value that include any number of movement flags. 1473 * @param layoutDirection The layout direction of the RecyclerView. 1474 * @return Updated flags which includes only absolute direction values. 1475 */ 1476 public int convertToAbsoluteDirection(int flags, int layoutDirection) { 1477 int masked = flags & RELATIVE_DIR_FLAGS; 1478 if (masked == 0) { 1479 return flags;// does not have any relative flags, good. 1480 } 1481 flags &= ~masked; //remove start / end 1482 if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { 1483 // no change. just OR with 2 bits shifted mask and return 1484 flags |= masked >> 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. 1485 return flags; 1486 } else { 1487 // add START flag as RIGHT 1488 flags |= ((masked >> 1) & ~RELATIVE_DIR_FLAGS); 1489 // first clean start bit then add END flag as LEFT 1490 flags |= ((masked >> 1) & RELATIVE_DIR_FLAGS) >> 2; 1491 } 1492 return flags; 1493 } 1494 1495 final int getAbsoluteMovementFlags(RecyclerView recyclerView, 1496 ViewHolder viewHolder) { 1497 final int flags = getMovementFlags(recyclerView, viewHolder); 1498 return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView)); 1499 } 1500 1501 private boolean hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder) { 1502 final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); 1503 return (flags & ACTION_MODE_DRAG_MASK) != 0; 1504 } 1505 1506 private boolean hasSwipeFlag(RecyclerView recyclerView, 1507 ViewHolder viewHolder) { 1508 final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); 1509 return (flags & ACTION_MODE_SWIPE_MASK) != 0; 1510 } 1511 1512 /** 1513 * Return true if the current ViewHolder can be dropped over the the target ViewHolder. 1514 * <p> 1515 * This method is used when selecting drop target for the dragged View. After Views are 1516 * eliminated either via bounds check or via this method, resulting set of views will be 1517 * passed to {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)}. 1518 * <p> 1519 * Default implementation returns true. 1520 * 1521 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. 1522 * @param current The ViewHolder that user is dragging. 1523 * @param target The ViewHolder which is below the dragged ViewHolder. 1524 * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false 1525 * otherwise. 1526 */ 1527 public boolean canDropOver(RecyclerView recyclerView, ViewHolder current, 1528 ViewHolder target) { 1529 return true; 1530 } 1531 1532 /** 1533 * Called when ItemTouchHelper wants to move the dragged item from its old position to 1534 * the new position. 1535 * <p> 1536 * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved 1537 * to the adapter position of {@code target} ViewHolder 1538 * ({@link ViewHolder#getAdapterPosition() 1539 * ViewHolder#getAdapterPosition()}). 1540 * <p> 1541 * If you don't support drag & drop, this method will never be called. 1542 * 1543 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. 1544 * @param viewHolder The ViewHolder which is being dragged by the user. 1545 * @param target The ViewHolder over which the currently active item is being 1546 * dragged. 1547 * @return True if the {@code viewHolder} has been moved to the adapter position of 1548 * {@code target}. 1549 * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int) 1550 */ 1551 public abstract boolean onMove(RecyclerView recyclerView, 1552 ViewHolder viewHolder, ViewHolder target); 1553 1554 /** 1555 * Returns whether ItemTouchHelper should start a drag and drop operation if an item is 1556 * long pressed. 1557 * <p> 1558 * Default value returns true but you may want to disable this if you want to start 1559 * dragging on a custom view touch using {@link #startDrag(ViewHolder)}. 1560 * 1561 * @return True if ItemTouchHelper should start dragging an item when it is long pressed, 1562 * false otherwise. Default value is <code>true</code>. 1563 * @see #startDrag(ViewHolder) 1564 */ 1565 public boolean isLongPressDragEnabled() { 1566 return true; 1567 } 1568 1569 /** 1570 * Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped 1571 * over the View. 1572 * <p> 1573 * Default value returns true but you may want to disable this if you want to start 1574 * swiping on a custom view touch using {@link #startSwipe(ViewHolder)}. 1575 * 1576 * @return True if ItemTouchHelper should start swiping an item when user swipes a pointer 1577 * over the View, false otherwise. Default value is <code>true</code>. 1578 * @see #startSwipe(ViewHolder) 1579 */ 1580 public boolean isItemViewSwipeEnabled() { 1581 return true; 1582 } 1583 1584 /** 1585 * When finding views under a dragged view, by default, ItemTouchHelper searches for views 1586 * that overlap with the dragged View. By overriding this method, you can extend or shrink 1587 * the search box. 1588 * 1589 * @return The extra margin to be added to the hit box of the dragged View. 1590 */ 1591 public int getBoundingBoxMargin() { 1592 return 0; 1593 } 1594 1595 /** 1596 * Returns the fraction that the user should move the View to be considered as swiped. 1597 * The fraction is calculated with respect to RecyclerView's bounds. 1598 * <p> 1599 * Default value is .5f, which means, to swipe a View, user must move the View at least 1600 * half of RecyclerView's width or height, depending on the swipe direction. 1601 * 1602 * @param viewHolder The ViewHolder that is being dragged. 1603 * @return A float value that denotes the fraction of the View size. Default value 1604 * is .5f . 1605 */ 1606 public float getSwipeThreshold(ViewHolder viewHolder) { 1607 return .5f; 1608 } 1609 1610 /** 1611 * Returns the fraction that the user should move the View to be considered as it is 1612 * dragged. After a view is moved this amount, ItemTouchHelper starts checking for Views 1613 * below it for a possible drop. 1614 * 1615 * @param viewHolder The ViewHolder that is being dragged. 1616 * @return A float value that denotes the fraction of the View size. Default value is 1617 * .5f . 1618 */ 1619 public float getMoveThreshold(ViewHolder viewHolder) { 1620 return .5f; 1621 } 1622 1623 /** 1624 * Called by ItemTouchHelper to select a drop target from the list of ViewHolders that 1625 * are under the dragged View. 1626 * <p> 1627 * Default implementation filters the View with which dragged item have changed position 1628 * in the drag direction. For instance, if the view is dragged UP, it compares the 1629 * <code>view.getTop()</code> of the two views before and after drag started. If that value 1630 * is different, the target view passes the filter. 1631 * <p> 1632 * Among these Views which pass the test, the one closest to the dragged view is chosen. 1633 * <p> 1634 * This method is called on the main thread every time user moves the View. If you want to 1635 * override it, make sure it does not do any expensive operations. 1636 * 1637 * @param selected The ViewHolder being dragged by the user. 1638 * @param dropTargets The list of ViewHolder that are under the dragged View and 1639 * candidate as a drop. 1640 * @param curX The updated left value of the dragged View after drag translations 1641 * are applied. This value does not include margins added by 1642 * {@link RecyclerView.ItemDecoration}s. 1643 * @param curY The updated top value of the dragged View after drag translations 1644 * are applied. This value does not include margins added by 1645 * {@link RecyclerView.ItemDecoration}s. 1646 * @return A ViewHolder to whose position the dragged ViewHolder should be 1647 * moved to. 1648 */ 1649 public ViewHolder chooseDropTarget(ViewHolder selected, 1650 List<ViewHolder> dropTargets, int curX, int curY) { 1651 int right = curX + selected.itemView.getWidth(); 1652 int bottom = curY + selected.itemView.getHeight(); 1653 ViewHolder winner = null; 1654 int winnerScore = -1; 1655 final int dx = curX - selected.itemView.getLeft(); 1656 final int dy = curY - selected.itemView.getTop(); 1657 final int targetsSize = dropTargets.size(); 1658 for (int i = 0; i < targetsSize; i++) { 1659 final ViewHolder target = dropTargets.get(i); 1660 if (dx > 0) { 1661 int diff = target.itemView.getRight() - right; 1662 if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) { 1663 final int score = Math.abs(diff); 1664 if (score > winnerScore) { 1665 winnerScore = score; 1666 winner = target; 1667 } 1668 } 1669 } 1670 if (dx < 0) { 1671 int diff = target.itemView.getLeft() - curX; 1672 if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) { 1673 final int score = Math.abs(diff); 1674 if (score > winnerScore) { 1675 winnerScore = score; 1676 winner = target; 1677 } 1678 } 1679 } 1680 if (dy < 0) { 1681 int diff = target.itemView.getTop() - curY; 1682 if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) { 1683 final int score = Math.abs(diff); 1684 if (score > winnerScore) { 1685 winnerScore = score; 1686 winner = target; 1687 } 1688 } 1689 } 1690 1691 if (dy > 0) { 1692 int diff = target.itemView.getBottom() - bottom; 1693 if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) { 1694 final int score = Math.abs(diff); 1695 if (score > winnerScore) { 1696 winnerScore = score; 1697 winner = target; 1698 } 1699 } 1700 } 1701 } 1702 return winner; 1703 } 1704 1705 /** 1706 * Called when a ViewHolder is swiped by the user. 1707 * <p> 1708 * If you are returning relative directions ({@link #START} , {@link #END}) from the 1709 * {@link #getMovementFlags(RecyclerView, ViewHolder)} method, this method 1710 * will also use relative directions. Otherwise, it will use absolute directions. 1711 * <p> 1712 * If you don't support swiping, this method will never be called. 1713 * <p> 1714 * ItemTouchHelper will keep a reference to the View until it is detached from 1715 * RecyclerView. 1716 * As soon as it is detached, ItemTouchHelper will call 1717 * {@link #clearView(RecyclerView, ViewHolder)}. 1718 * 1719 * @param viewHolder The ViewHolder which has been swiped by the user. 1720 * @param direction The direction to which the ViewHolder is swiped. It is one of 1721 * {@link #UP}, {@link #DOWN}, 1722 * {@link #LEFT} or {@link #RIGHT}. If your 1723 * {@link #getMovementFlags(RecyclerView, ViewHolder)} 1724 * method 1725 * returned relative flags instead of {@link #LEFT} / {@link #RIGHT}; 1726 * `direction` will be relative as well. ({@link #START} or {@link 1727 * #END}). 1728 */ 1729 public abstract void onSwiped(ViewHolder viewHolder, int direction); 1730 1731 /** 1732 * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed. 1733 * <p/> 1734 * If you override this method, you should call super. 1735 * 1736 * @param viewHolder The new ViewHolder that is being swiped or dragged. Might be null if 1737 * it is cleared. 1738 * @param actionState One of {@link ItemTouchHelper#ACTION_STATE_IDLE}, 1739 * {@link ItemTouchHelper#ACTION_STATE_SWIPE} or 1740 * {@link ItemTouchHelper#ACTION_STATE_DRAG}. 1741 * 1742 * @see #clearView(RecyclerView, RecyclerView.ViewHolder) 1743 */ 1744 public void onSelectedChanged(ViewHolder viewHolder, int actionState) { 1745 if (viewHolder != null) { 1746 mUICallback.onSelected(viewHolder); 1747 } 1748 } 1749 1750 private int getMaxDragScroll(RecyclerView recyclerView) { 1751 if (mCachedMaxScrollSpeed == -1) { 1752 mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize( 1753 R.dimen.item_touch_helper_max_drag_scroll_per_frame); 1754 } 1755 return mCachedMaxScrollSpeed; 1756 } 1757 1758 /** 1759 * Called when {@link #onMove(RecyclerView, ViewHolder, ViewHolder)} returns true. 1760 * <p> 1761 * ItemTouchHelper does not create an extra Bitmap or View while dragging, instead, it 1762 * modifies the existing View. Because of this reason, it is important that the View is 1763 * still part of the layout after it is moved. This may not work as intended when swapped 1764 * Views are close to RecyclerView bounds or there are gaps between them (e.g. other Views 1765 * which were not eligible for dropping over). 1766 * <p> 1767 * This method is responsible to give necessary hint to the LayoutManager so that it will 1768 * keep the View in visible area. For example, for LinearLayoutManager, this is as simple 1769 * as calling {@link LinearLayoutManager#scrollToPositionWithOffset(int, int)}. 1770 * 1771 * Default implementation calls {@link RecyclerView#scrollToPosition(int)} if the View's 1772 * new position is likely to be out of bounds. 1773 * <p> 1774 * It is important to ensure the ViewHolder will stay visible as otherwise, it might be 1775 * removed by the LayoutManager if the move causes the View to go out of bounds. In that 1776 * case, drag will end prematurely. 1777 * 1778 * @param recyclerView The RecyclerView controlled by the ItemTouchHelper. 1779 * @param viewHolder The ViewHolder under user's control. 1780 * @param fromPos The previous adapter position of the dragged item (before it was 1781 * moved). 1782 * @param target The ViewHolder on which the currently active item has been dropped. 1783 * @param toPos The new adapter position of the dragged item. 1784 * @param x The updated left value of the dragged View after drag translations 1785 * are applied. This value does not include margins added by 1786 * {@link RecyclerView.ItemDecoration}s. 1787 * @param y The updated top value of the dragged View after drag translations 1788 * are applied. This value does not include margins added by 1789 * {@link RecyclerView.ItemDecoration}s. 1790 */ 1791 public void onMoved(final RecyclerView recyclerView, 1792 final ViewHolder viewHolder, int fromPos, final ViewHolder target, int toPos, int x, 1793 int y) { 1794 final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); 1795 if (layoutManager instanceof ViewDropHandler) { 1796 ((ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView, 1797 target.itemView, x, y); 1798 return; 1799 } 1800 1801 // if layout manager cannot handle it, do some guesswork 1802 if (layoutManager.canScrollHorizontally()) { 1803 final int minLeft = layoutManager.getDecoratedLeft(target.itemView); 1804 if (minLeft <= recyclerView.getPaddingLeft()) { 1805 recyclerView.scrollToPosition(toPos); 1806 } 1807 final int maxRight = layoutManager.getDecoratedRight(target.itemView); 1808 if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) { 1809 recyclerView.scrollToPosition(toPos); 1810 } 1811 } 1812 1813 if (layoutManager.canScrollVertically()) { 1814 final int minTop = layoutManager.getDecoratedTop(target.itemView); 1815 if (minTop <= recyclerView.getPaddingTop()) { 1816 recyclerView.scrollToPosition(toPos); 1817 } 1818 final int maxBottom = layoutManager.getDecoratedBottom(target.itemView); 1819 if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) { 1820 recyclerView.scrollToPosition(toPos); 1821 } 1822 } 1823 } 1824 1825 private void onDraw(Canvas c, RecyclerView parent, ViewHolder selected, 1826 List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, 1827 int actionState, float dX, float dY) { 1828 final int recoverAnimSize = recoverAnimationList.size(); 1829 for (int i = 0; i < recoverAnimSize; i++) { 1830 final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); 1831 anim.update(); 1832 final int count = c.save(); 1833 onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, 1834 false); 1835 c.restoreToCount(count); 1836 } 1837 if (selected != null) { 1838 final int count = c.save(); 1839 onChildDraw(c, parent, selected, dX, dY, actionState, true); 1840 c.restoreToCount(count); 1841 } 1842 } 1843 1844 private void onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected, 1845 List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, 1846 int actionState, float dX, float dY) { 1847 final int recoverAnimSize = recoverAnimationList.size(); 1848 for (int i = 0; i < recoverAnimSize; i++) { 1849 final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); 1850 final int count = c.save(); 1851 onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, 1852 false); 1853 c.restoreToCount(count); 1854 } 1855 if (selected != null) { 1856 final int count = c.save(); 1857 onChildDrawOver(c, parent, selected, dX, dY, actionState, true); 1858 c.restoreToCount(count); 1859 } 1860 boolean hasRunningAnimation = false; 1861 for (int i = recoverAnimSize - 1; i >= 0; i--) { 1862 final RecoverAnimation anim = recoverAnimationList.get(i); 1863 if (anim.mEnded && !anim.mIsPendingCleanup) { 1864 recoverAnimationList.remove(i); 1865 anim.mViewHolder.setIsRecyclable(true); 1866 } else if (!anim.mEnded) { 1867 hasRunningAnimation = true; 1868 } 1869 } 1870 if (hasRunningAnimation) { 1871 parent.invalidate(); 1872 } 1873 } 1874 1875 /** 1876 * Called by the ItemTouchHelper when the user interaction with an element is over and it 1877 * also completed its animation. 1878 * <p> 1879 * This is a good place to clear all changes on the View that was done in 1880 * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)}, 1881 * {@link #onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int, 1882 * boolean)} or 1883 * {@link #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)}. 1884 * 1885 * @param recyclerView The RecyclerView which is controlled by the ItemTouchHelper. 1886 * @param viewHolder The View that was interacted by the user. 1887 */ 1888 public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { 1889 mUICallback.clearView(viewHolder); 1890 } 1891 1892 /** 1893 * Called by ItemTouchHelper on RecyclerView's onDraw callback. 1894 * <p> 1895 * If you would like to customize how your View's respond to user interactions, this is 1896 * a good place to override. 1897 * <p> 1898 * Default implementation translates the child by the given <code>dX</code>, 1899 * <code>dY</code>. 1900 * ItemTouchHelper also takes care of drawing the child after other children if it is being 1901 * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this 1902 * is 1903 * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L 1904 * and after, it changes View's elevation value to be greater than all other children.) 1905 * 1906 * @param c The canvas which RecyclerView is drawing its children 1907 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to 1908 * @param viewHolder The ViewHolder which is being interacted by the User or it was 1909 * interacted and simply animating to its original position 1910 * @param dX The amount of horizontal displacement caused by user's action 1911 * @param dY The amount of vertical displacement caused by user's action 1912 * @param actionState The type of interaction on the View. Is either {@link 1913 * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. 1914 * @param isCurrentlyActive True if this view is currently being controlled by the user or 1915 * false it is simply animating back to its original state. 1916 * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, 1917 * boolean) 1918 */ 1919 public void onChildDraw(Canvas c, RecyclerView recyclerView, 1920 ViewHolder viewHolder, 1921 float dX, float dY, int actionState, boolean isCurrentlyActive) { 1922 mUICallback.onDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); 1923 } 1924 1925 /** 1926 * Called by ItemTouchHelper on RecyclerView's onDraw callback. 1927 * <p> 1928 * If you would like to customize how your View's respond to user interactions, this is 1929 * a good place to override. 1930 * <p> 1931 * Default implementation translates the child by the given <code>dX</code>, 1932 * <code>dY</code>. 1933 * ItemTouchHelper also takes care of drawing the child after other children if it is being 1934 * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this 1935 * is 1936 * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L 1937 * and after, it changes View's elevation value to be greater than all other children.) 1938 * 1939 * @param c The canvas which RecyclerView is drawing its children 1940 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to 1941 * @param viewHolder The ViewHolder which is being interacted by the User or it was 1942 * interacted and simply animating to its original position 1943 * @param dX The amount of horizontal displacement caused by user's action 1944 * @param dY The amount of vertical displacement caused by user's action 1945 * @param actionState The type of interaction on the View. Is either {@link 1946 * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. 1947 * @param isCurrentlyActive True if this view is currently being controlled by the user or 1948 * false it is simply animating back to its original state. 1949 * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, 1950 * boolean) 1951 */ 1952 public void onChildDrawOver(Canvas c, RecyclerView recyclerView, 1953 ViewHolder viewHolder, 1954 float dX, float dY, int actionState, boolean isCurrentlyActive) { 1955 mUICallback.onDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, 1956 isCurrentlyActive); 1957 } 1958 1959 /** 1960 * Called by the ItemTouchHelper when user action finished on a ViewHolder and now the View 1961 * will be animated to its final position. 1962 * <p> 1963 * Default implementation uses ItemAnimator's duration values. If 1964 * <code>animationType</code> is {@link #ANIMATION_TYPE_DRAG}, it returns 1965 * {@link RecyclerView.ItemAnimator#getMoveDuration()}, otherwise, it returns 1966 * {@link RecyclerView.ItemAnimator#getRemoveDuration()}. If RecyclerView does not have 1967 * any {@link RecyclerView.ItemAnimator} attached, this method returns 1968 * {@code DEFAULT_DRAG_ANIMATION_DURATION} or {@code DEFAULT_SWIPE_ANIMATION_DURATION} 1969 * depending on the animation type. 1970 * 1971 * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. 1972 * @param animationType The type of animation. Is one of {@link #ANIMATION_TYPE_DRAG}, 1973 * {@link #ANIMATION_TYPE_SWIPE_CANCEL} or 1974 * {@link #ANIMATION_TYPE_SWIPE_SUCCESS}. 1975 * @param animateDx The horizontal distance that the animation will offset 1976 * @param animateDy The vertical distance that the animation will offset 1977 * @return The duration for the animation 1978 */ 1979 public long getAnimationDuration(RecyclerView recyclerView, int animationType, 1980 float animateDx, float animateDy) { 1981 final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); 1982 if (itemAnimator == null) { 1983 return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION 1984 : DEFAULT_SWIPE_ANIMATION_DURATION; 1985 } else { 1986 return animationType == ANIMATION_TYPE_DRAG ? itemAnimator.getMoveDuration() 1987 : itemAnimator.getRemoveDuration(); 1988 } 1989 } 1990 1991 /** 1992 * Called by the ItemTouchHelper when user is dragging a view out of bounds. 1993 * <p> 1994 * You can override this method to decide how much RecyclerView should scroll in response 1995 * to this action. Default implementation calculates a value based on the amount of View 1996 * out of bounds and the time it spent there. The longer user keeps the View out of bounds, 1997 * the faster the list will scroll. Similarly, the larger portion of the View is out of 1998 * bounds, the faster the RecyclerView will scroll. 1999 * 2000 * @param recyclerView The RecyclerView instance to which ItemTouchHelper is attached 2001 * to. 2002 * @param viewSize The total size of the View in scroll direction, excluding 2003 * item decorations. 2004 * @param viewSizeOutOfBounds The total size of the View that is out of bounds. This value 2005 * is negative if the View is dragged towards left or top edge. 2006 * @param totalSize The total size of RecyclerView in the scroll direction. 2007 * @param msSinceStartScroll The time passed since View is kept out of bounds. 2008 * 2009 * @return The amount that RecyclerView should scroll. Keep in mind that this value will 2010 * be passed to {@link RecyclerView#scrollBy(int, int)} method. 2011 */ 2012 public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, 2013 int viewSize, int viewSizeOutOfBounds, 2014 int totalSize, long msSinceStartScroll) { 2015 final int maxScroll = getMaxDragScroll(recyclerView); 2016 final int absOutOfBounds = Math.abs(viewSizeOutOfBounds); 2017 final int direction = (int) Math.signum(viewSizeOutOfBounds); 2018 // might be negative if other direction 2019 float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize); 2020 final int cappedScroll = (int) (direction * maxScroll * 2021 sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio)); 2022 final float timeRatio; 2023 if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) { 2024 timeRatio = 1f; 2025 } else { 2026 timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS; 2027 } 2028 final int value = (int) (cappedScroll * sDragScrollInterpolator 2029 .getInterpolation(timeRatio)); 2030 if (value == 0) { 2031 return viewSizeOutOfBounds > 0 ? 1 : -1; 2032 } 2033 return value; 2034 } 2035 } 2036 2037 /** 2038 * A simple wrapper to the default Callback which you can construct with drag and swipe 2039 * directions and this class will handle the flag callbacks. You should still override onMove 2040 * or 2041 * onSwiped depending on your use case. 2042 * 2043 * <pre> 2044 * ItemTouchHelper mIth = new ItemTouchHelper( 2045 * new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 2046 * ItemTouchHelper.LEFT) { 2047 * public abstract boolean onMove(RecyclerView recyclerView, 2048 * ViewHolder viewHolder, ViewHolder target) { 2049 * final int fromPos = viewHolder.getAdapterPosition(); 2050 * final int toPos = viewHolder.getAdapterPosition(); 2051 * // move item in `fromPos` to `toPos` in adapter. 2052 * return true;// true if moved, false otherwise 2053 * } 2054 * public void onSwiped(ViewHolder viewHolder, int direction) { 2055 * // remove from adapter 2056 * } 2057 * }); 2058 * </pre> 2059 */ 2060 public abstract static class SimpleCallback extends Callback { 2061 2062 private int mDefaultSwipeDirs; 2063 2064 private int mDefaultDragDirs; 2065 2066 /** 2067 * Creates a Callback for the given drag and swipe allowance. These values serve as 2068 * defaults 2069 * and if you want to customize behavior per ViewHolder, you can override 2070 * {@link #getSwipeDirs(RecyclerView, ViewHolder)} 2071 * and / or {@link #getDragDirs(RecyclerView, ViewHolder)}. 2072 * 2073 * @param dragDirs Binary OR of direction flags in which the Views can be dragged. Must be 2074 * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link 2075 * #END}, 2076 * {@link #UP} and {@link #DOWN}. 2077 * @param swipeDirs Binary OR of direction flags in which the Views can be swiped. Must be 2078 * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link 2079 * #END}, 2080 * {@link #UP} and {@link #DOWN}. 2081 */ 2082 public SimpleCallback(int dragDirs, int swipeDirs) { 2083 mDefaultSwipeDirs = swipeDirs; 2084 mDefaultDragDirs = dragDirs; 2085 } 2086 2087 /** 2088 * Updates the default swipe directions. For example, you can use this method to toggle 2089 * certain directions depending on your use case. 2090 * 2091 * @param defaultSwipeDirs Binary OR of directions in which the ViewHolders can be swiped. 2092 */ 2093 public void setDefaultSwipeDirs(int defaultSwipeDirs) { 2094 mDefaultSwipeDirs = defaultSwipeDirs; 2095 } 2096 2097 /** 2098 * Updates the default drag directions. For example, you can use this method to toggle 2099 * certain directions depending on your use case. 2100 * 2101 * @param defaultDragDirs Binary OR of directions in which the ViewHolders can be dragged. 2102 */ 2103 public void setDefaultDragDirs(int defaultDragDirs) { 2104 mDefaultDragDirs = defaultDragDirs; 2105 } 2106 2107 /** 2108 * Returns the swipe directions for the provided ViewHolder. 2109 * Default implementation returns the swipe directions that was set via constructor or 2110 * {@link #setDefaultSwipeDirs(int)}. 2111 * 2112 * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. 2113 * @param viewHolder The RecyclerView for which the swipe drection is queried. 2114 * @return A binary OR of direction flags. 2115 */ 2116 public int getSwipeDirs(RecyclerView recyclerView, ViewHolder viewHolder) { 2117 return mDefaultSwipeDirs; 2118 } 2119 2120 /** 2121 * Returns the drag directions for the provided ViewHolder. 2122 * Default implementation returns the drag directions that was set via constructor or 2123 * {@link #setDefaultDragDirs(int)}. 2124 * 2125 * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. 2126 * @param viewHolder The RecyclerView for which the swipe drection is queried. 2127 * @return A binary OR of direction flags. 2128 */ 2129 public int getDragDirs(RecyclerView recyclerView, ViewHolder viewHolder) { 2130 return mDefaultDragDirs; 2131 } 2132 2133 @Override 2134 public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) { 2135 return makeMovementFlags(getDragDirs(recyclerView, viewHolder), 2136 getSwipeDirs(recyclerView, viewHolder)); 2137 } 2138 } 2139 2140 private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { 2141 2142 @Override 2143 public boolean onDown(MotionEvent e) { 2144 return true; 2145 } 2146 2147 @Override 2148 public void onLongPress(MotionEvent e) { 2149 View child = findChildView(e); 2150 if (child != null) { 2151 ViewHolder vh = mRecyclerView.getChildViewHolder(child); 2152 if (vh != null) { 2153 if (!mCallback.hasDragFlag(mRecyclerView, vh)) { 2154 return; 2155 } 2156 int pointerId = MotionEventCompat.getPointerId(e, 0); 2157 // Long press is deferred. 2158 // Check w/ active pointer id to avoid selecting after motion 2159 // event is canceled. 2160 if (pointerId == mActivePointerId) { 2161 final int index = MotionEventCompat 2162 .findPointerIndex(e, mActivePointerId); 2163 final float x = MotionEventCompat.getX(e, index); 2164 final float y = MotionEventCompat.getY(e, index); 2165 mInitialTouchX = x; 2166 mInitialTouchY = y; 2167 mDx = mDy = 0f; 2168 if (DEBUG) { 2169 Log.d(TAG, 2170 "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY); 2171 } 2172 if (mCallback.isLongPressDragEnabled()) { 2173 select(vh, ACTION_STATE_DRAG); 2174 } 2175 } 2176 } 2177 } 2178 } 2179 } 2180 2181 private class RecoverAnimation implements AnimatorListenerCompat { 2182 2183 final float mStartDx; 2184 2185 final float mStartDy; 2186 2187 final float mTargetX; 2188 2189 final float mTargetY; 2190 2191 final ViewHolder mViewHolder; 2192 2193 final int mActionState; 2194 2195 private final ValueAnimatorCompat mValueAnimator; 2196 2197 private final int mAnimationType; 2198 2199 public boolean mIsPendingCleanup; 2200 2201 float mX; 2202 2203 float mY; 2204 2205 // if user starts touching a recovering view, we put it into interaction mode again, 2206 // instantly. 2207 boolean mOverridden = false; 2208 2209 private boolean mEnded = false; 2210 2211 private float mFraction; 2212 2213 public RecoverAnimation(ViewHolder viewHolder, int animationType, 2214 int actionState, float startDx, float startDy, float targetX, float targetY) { 2215 mActionState = actionState; 2216 mAnimationType = animationType; 2217 mViewHolder = viewHolder; 2218 mStartDx = startDx; 2219 mStartDy = startDy; 2220 mTargetX = targetX; 2221 mTargetY = targetY; 2222 mValueAnimator = AnimatorCompatHelper.emptyValueAnimator(); 2223 mValueAnimator.addUpdateListener( 2224 new AnimatorUpdateListenerCompat() { 2225 @Override 2226 public void onAnimationUpdate(ValueAnimatorCompat animation) { 2227 setFraction(animation.getAnimatedFraction()); 2228 } 2229 }); 2230 mValueAnimator.setTarget(viewHolder.itemView); 2231 mValueAnimator.addListener(this); 2232 setFraction(0f); 2233 } 2234 2235 public void setDuration(long duration) { 2236 mValueAnimator.setDuration(duration); 2237 } 2238 2239 public void start() { 2240 mViewHolder.setIsRecyclable(false); 2241 mValueAnimator.start(); 2242 } 2243 2244 public void cancel() { 2245 mValueAnimator.cancel(); 2246 } 2247 2248 public void setFraction(float fraction) { 2249 mFraction = fraction; 2250 } 2251 2252 /** 2253 * We run updates on onDraw method but use the fraction from animator callback. 2254 * This way, we can sync translate x/y values w/ the animators to avoid one-off frames. 2255 */ 2256 public void update() { 2257 if (mStartDx == mTargetX) { 2258 mX = ViewCompat.getTranslationX(mViewHolder.itemView); 2259 } else { 2260 mX = mStartDx + mFraction * (mTargetX - mStartDx); 2261 } 2262 if (mStartDy == mTargetY) { 2263 mY = ViewCompat.getTranslationY(mViewHolder.itemView); 2264 } else { 2265 mY = mStartDy + mFraction * (mTargetY - mStartDy); 2266 } 2267 } 2268 2269 @Override 2270 public void onAnimationStart(ValueAnimatorCompat animation) { 2271 2272 } 2273 2274 @Override 2275 public void onAnimationEnd(ValueAnimatorCompat animation) { 2276 mEnded = true; 2277 } 2278 2279 @Override 2280 public void onAnimationCancel(ValueAnimatorCompat animation) { 2281 setFraction(1f); //make sure we recover the view's state. 2282 } 2283 2284 @Override 2285 public void onAnimationRepeat(ValueAnimatorCompat animation) { 2286 2287 } 2288 } 2289}