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