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