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