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