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