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