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