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