| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.recyclerview.widget; |
| |
| import android.animation.Animator; |
| import android.animation.ValueAnimator; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Rect; |
| import android.os.Build; |
| import android.util.Log; |
| import android.view.GestureDetector; |
| import android.view.HapticFeedbackConstants; |
| import android.view.MotionEvent; |
| import android.view.VelocityTracker; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewParent; |
| import android.view.animation.Interpolator; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.core.view.GestureDetectorCompat; |
| import androidx.core.view.ViewCompat; |
| import androidx.recyclerview.R; |
| import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; |
| import androidx.recyclerview.widget.RecyclerView.ViewHolder; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView. |
| * <p> |
| * It works with a RecyclerView and a Callback class, which configures what type of interactions |
| * are enabled and also receives events when user performs these actions. |
| * <p> |
| * Depending on which functionality you support, you should override |
| * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)} and / or |
| * {@link Callback#onSwiped(ViewHolder, int)}. |
| * <p> |
| * This class is designed to work with any LayoutManager but for certain situations, it can be |
| * optimized for your custom LayoutManager by extending methods in the |
| * {@link ItemTouchHelper.Callback} class or implementing {@link ItemTouchHelper.ViewDropHandler} |
| * interface in your LayoutManager. |
| * <p> |
| * By default, ItemTouchHelper moves the items' translateX/Y properties to reposition them. You can |
| * customize these behaviors by overriding {@link Callback#onChildDraw(Canvas, RecyclerView, |
| * ViewHolder, float, float, int, boolean)} |
| * or {@link Callback#onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, |
| * boolean)}. |
| * <p/> |
| * Most of the time you only need to override <code>onChildDraw</code>. |
| */ |
| public class ItemTouchHelper extends RecyclerView.ItemDecoration |
| implements RecyclerView.OnChildAttachStateChangeListener { |
| |
| /** |
| * Up direction, used for swipe & drag control. |
| */ |
| public static final int UP = 1; |
| |
| /** |
| * Down direction, used for swipe & drag control. |
| */ |
| public static final int DOWN = 1 << 1; |
| |
| /** |
| * Left direction, used for swipe & drag control. |
| */ |
| public static final int LEFT = 1 << 2; |
| |
| /** |
| * Right direction, used for swipe & drag control. |
| */ |
| public static final int RIGHT = 1 << 3; |
| |
| // If you change these relative direction values, update Callback#convertToAbsoluteDirection, |
| // Callback#convertToRelativeDirection. |
| /** |
| * Horizontal start direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout |
| * direction. Used for swipe & drag control. |
| */ |
| public static final int START = LEFT << 2; |
| |
| /** |
| * Horizontal end direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout |
| * direction. Used for swipe & drag control. |
| */ |
| public static final int END = RIGHT << 2; |
| |
| /** |
| * ItemTouchHelper is in idle state. At this state, either there is no related motion event by |
| * the user or latest motion events have not yet triggered a swipe or drag. |
| */ |
| public static final int ACTION_STATE_IDLE = 0; |
| |
| /** |
| * A View is currently being swiped. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public static final int ACTION_STATE_SWIPE = 1; |
| |
| /** |
| * A View is currently being dragged. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public static final int ACTION_STATE_DRAG = 2; |
| |
| /** |
| * Animation type for views which are swiped successfully. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public static final int ANIMATION_TYPE_SWIPE_SUCCESS = 1 << 1; |
| |
| /** |
| * Animation type for views which are not completely swiped thus will animate back to their |
| * original position. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public static final int ANIMATION_TYPE_SWIPE_CANCEL = 1 << 2; |
| |
| /** |
| * Animation type for views that were dragged and now will animate to their final position. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public static final int ANIMATION_TYPE_DRAG = 1 << 3; |
| |
| private static final String TAG = "ItemTouchHelper"; |
| |
| private static final boolean DEBUG = false; |
| |
| private static final int ACTIVE_POINTER_ID_NONE = -1; |
| |
| static final int DIRECTION_FLAG_COUNT = 8; |
| |
| private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1; |
| |
| static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT; |
| |
| static final int ACTION_MODE_DRAG_MASK = ACTION_MODE_SWIPE_MASK << DIRECTION_FLAG_COUNT; |
| |
| /** |
| * The unit we are using to track velocity |
| */ |
| private static final int PIXELS_PER_SECOND = 1000; |
| |
| /** |
| * Views, whose state should be cleared after they are detached from RecyclerView. |
| * This is necessary after swipe dismissing an item. We wait until animator finishes its job |
| * to clean these views. |
| */ |
| final List<View> mPendingCleanup = new ArrayList<>(); |
| |
| /** |
| * Re-use array to calculate dx dy for a ViewHolder |
| */ |
| private final float[] mTmpPosition = new float[2]; |
| |
| /** |
| * Currently selected view holder |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| ViewHolder mSelected = null; |
| |
| /** |
| * The reference coordinates for the action start. For drag & drop, this is the time long |
| * press is completed vs for swipe, this is the initial touch point. |
| */ |
| float mInitialTouchX; |
| |
| float mInitialTouchY; |
| |
| /** |
| * Set when ItemTouchHelper is assigned to a RecyclerView. |
| */ |
| private float mSwipeEscapeVelocity; |
| |
| /** |
| * Set when ItemTouchHelper is assigned to a RecyclerView. |
| */ |
| private float mMaxSwipeVelocity; |
| |
| /** |
| * The diff between the last event and initial touch. |
| */ |
| float mDx; |
| |
| float mDy; |
| |
| /** |
| * The coordinates of the selected view at the time it is selected. We record these values |
| * when action starts so that we can consistently position it even if LayoutManager moves the |
| * View. |
| */ |
| private float mSelectedStartX; |
| |
| private float mSelectedStartY; |
| |
| /** |
| * The pointer we are tracking. |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| int mActivePointerId = ACTIVE_POINTER_ID_NONE; |
| |
| /** |
| * Developer callback which controls the behavior of ItemTouchHelper. |
| */ |
| @NonNull |
| Callback mCallback; |
| |
| /** |
| * Current mode. |
| */ |
| private int mActionState = ACTION_STATE_IDLE; |
| |
| /** |
| * The direction flags obtained from unmasking |
| * {@link Callback#getAbsoluteMovementFlags(RecyclerView, ViewHolder)} for the current |
| * action state. |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| int mSelectedFlags; |
| |
| /** |
| * When a View is dragged or swiped and needs to go back to where it was, we create a Recover |
| * Animation and animate it to its location using this custom Animator, instead of using |
| * framework Animators. |
| * Using framework animators has the side effect of clashing with ItemAnimator, creating |
| * jumpy UIs. |
| */ |
| @VisibleForTesting |
| List<RecoverAnimation> mRecoverAnimations = new ArrayList<>(); |
| |
| private int mSlop; |
| |
| RecyclerView mRecyclerView; |
| |
| /** |
| * When user drags a view to the edge, we start scrolling the LayoutManager as long as View |
| * is partially out of bounds. |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| final Runnable mScrollRunnable = new Runnable() { |
| @Override |
| public void run() { |
| if (mSelected != null && scrollIfNecessary()) { |
| if (mSelected != null) { //it might be lost during scrolling |
| moveIfNecessary(mSelected); |
| } |
| mRecyclerView.removeCallbacks(mScrollRunnable); |
| ViewCompat.postOnAnimation(mRecyclerView, this); |
| } |
| } |
| }; |
| |
| /** |
| * Used for detecting fling swipe |
| */ |
| VelocityTracker mVelocityTracker; |
| |
| //re-used list for selecting a swap target |
| private List<ViewHolder> mSwapTargets; |
| |
| //re used for for sorting swap targets |
| private List<Integer> mDistances; |
| |
| /** |
| * If drag & drop is supported, we use child drawing order to bring them to front. |
| */ |
| private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback = null; |
| |
| /** |
| * This keeps a reference to the child dragged by the user. Even after user stops dragging, |
| * until view reaches its final position (end of recover animation), we keep a reference so |
| * that it can be drawn above other children. |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| View mOverdrawChild = null; |
| |
| /** |
| * We cache the position of the overdraw child to avoid recalculating it each time child |
| * position callback is called. This value is invalidated whenever a child is attached or |
| * detached. |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| int mOverdrawChildPosition = -1; |
| |
| /** |
| * Used to detect long press. |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| GestureDetectorCompat mGestureDetector; |
| |
| /** |
| * Callback for when long press occurs. |
| */ |
| private ItemTouchHelperGestureListener mItemTouchHelperGestureListener; |
| |
| private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() { |
| @Override |
| public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, |
| @NonNull MotionEvent event) { |
| mGestureDetector.onTouchEvent(event); |
| if (DEBUG) { |
| Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event); |
| } |
| final int action = event.getActionMasked(); |
| if (action == MotionEvent.ACTION_DOWN) { |
| mActivePointerId = event.getPointerId(0); |
| mInitialTouchX = event.getX(); |
| mInitialTouchY = event.getY(); |
| obtainVelocityTracker(); |
| if (mSelected == null) { |
| final RecoverAnimation animation = findAnimation(event); |
| if (animation != null) { |
| mInitialTouchX -= animation.mX; |
| mInitialTouchY -= animation.mY; |
| endRecoverAnimation(animation.mViewHolder, true); |
| if (mPendingCleanup.remove(animation.mViewHolder.itemView)) { |
| mCallback.clearView(mRecyclerView, animation.mViewHolder); |
| } |
| select(animation.mViewHolder, animation.mActionState); |
| updateDxDy(event, mSelectedFlags, 0); |
| } |
| } |
| } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { |
| mActivePointerId = ACTIVE_POINTER_ID_NONE; |
| select(null, ACTION_STATE_IDLE); |
| } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) { |
| // in a non scroll orientation, if distance change is above threshold, we |
| // can select the item |
| final int index = event.findPointerIndex(mActivePointerId); |
| if (DEBUG) { |
| Log.d(TAG, "pointer index " + index); |
| } |
| if (index >= 0) { |
| checkSelectForSwipe(action, event, index); |
| } |
| } |
| if (mVelocityTracker != null) { |
| mVelocityTracker.addMovement(event); |
| } |
| return mSelected != null; |
| } |
| |
| @Override |
| public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) { |
| mGestureDetector.onTouchEvent(event); |
| if (DEBUG) { |
| Log.d(TAG, |
| "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event); |
| } |
| if (mVelocityTracker != null) { |
| mVelocityTracker.addMovement(event); |
| } |
| if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { |
| return; |
| } |
| final int action = event.getActionMasked(); |
| final int activePointerIndex = event.findPointerIndex(mActivePointerId); |
| if (activePointerIndex >= 0) { |
| checkSelectForSwipe(action, event, activePointerIndex); |
| } |
| ViewHolder viewHolder = mSelected; |
| if (viewHolder == null) { |
| return; |
| } |
| switch (action) { |
| case MotionEvent.ACTION_MOVE: { |
| // Find the index of the active pointer and fetch its position |
| if (activePointerIndex >= 0) { |
| updateDxDy(event, mSelectedFlags, activePointerIndex); |
| moveIfNecessary(viewHolder); |
| mRecyclerView.removeCallbacks(mScrollRunnable); |
| mScrollRunnable.run(); |
| mRecyclerView.invalidate(); |
| } |
| break; |
| } |
| case MotionEvent.ACTION_CANCEL: |
| if (mVelocityTracker != null) { |
| mVelocityTracker.clear(); |
| } |
| // fall through |
| case MotionEvent.ACTION_UP: |
| select(null, ACTION_STATE_IDLE); |
| mActivePointerId = ACTIVE_POINTER_ID_NONE; |
| break; |
| case MotionEvent.ACTION_POINTER_UP: { |
| final int pointerIndex = event.getActionIndex(); |
| final int pointerId = event.getPointerId(pointerIndex); |
| if (pointerId == mActivePointerId) { |
| // This was our active pointer going up. Choose a new |
| // active pointer and adjust accordingly. |
| final int newPointerIndex = pointerIndex == 0 ? 1 : 0; |
| mActivePointerId = event.getPointerId(newPointerIndex); |
| updateDxDy(event, mSelectedFlags, pointerIndex); |
| } |
| break; |
| } |
| } |
| } |
| |
| @Override |
| public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { |
| if (!disallowIntercept) { |
| return; |
| } |
| select(null, ACTION_STATE_IDLE); |
| } |
| }; |
| |
| /** |
| * Temporary rect instance that is used when we need to lookup Item decorations. |
| */ |
| private Rect mTmpRect; |
| |
| /** |
| * When user started to drag scroll. Reset when we don't scroll |
| */ |
| private long mDragScrollStartTimeInMs; |
| |
| /** |
| * Creates an ItemTouchHelper that will work with the given Callback. |
| * <p> |
| * You can attach ItemTouchHelper to a RecyclerView via |
| * {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration, |
| * an onItemTouchListener and a Child attach / detach listener to the RecyclerView. |
| * |
| * @param callback The Callback which controls the behavior of this touch helper. |
| */ |
| public ItemTouchHelper(@NonNull Callback callback) { |
| mCallback = callback; |
| } |
| |
| private static boolean hitTest(View child, float x, float y, float left, float top) { |
| return x >= left |
| && x <= left + child.getWidth() |
| && y >= top |
| && y <= top + child.getHeight(); |
| } |
| |
| /** |
| * Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already |
| * attached to a RecyclerView, it will first detach from the previous one. You can call this |
| * method with {@code null} to detach it from the current RecyclerView. |
| * |
| * @param recyclerView The RecyclerView instance to which you want to add this helper or |
| * {@code null} if you want to remove ItemTouchHelper from the current |
| * RecyclerView. |
| */ |
| public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { |
| if (mRecyclerView == recyclerView) { |
| return; // nothing to do |
| } |
| if (mRecyclerView != null) { |
| destroyCallbacks(); |
| } |
| mRecyclerView = recyclerView; |
| if (recyclerView != null) { |
| final Resources resources = recyclerView.getResources(); |
| mSwipeEscapeVelocity = resources |
| .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity); |
| mMaxSwipeVelocity = resources |
| .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity); |
| setupCallbacks(); |
| } |
| } |
| |
| private void setupCallbacks() { |
| ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext()); |
| mSlop = vc.getScaledTouchSlop(); |
| mRecyclerView.addItemDecoration(this); |
| mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); |
| mRecyclerView.addOnChildAttachStateChangeListener(this); |
| startGestureDetection(); |
| } |
| |
| private void destroyCallbacks() { |
| mRecyclerView.removeItemDecoration(this); |
| mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); |
| mRecyclerView.removeOnChildAttachStateChangeListener(this); |
| // clean all attached |
| final int recoverAnimSize = mRecoverAnimations.size(); |
| for (int i = recoverAnimSize - 1; i >= 0; i--) { |
| final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0); |
| recoverAnimation.cancel(); |
| mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder); |
| } |
| mRecoverAnimations.clear(); |
| mOverdrawChild = null; |
| mOverdrawChildPosition = -1; |
| releaseVelocityTracker(); |
| stopGestureDetection(); |
| } |
| |
| private void startGestureDetection() { |
| mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener(); |
| mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(), |
| mItemTouchHelperGestureListener); |
| } |
| |
| private void stopGestureDetection() { |
| if (mItemTouchHelperGestureListener != null) { |
| mItemTouchHelperGestureListener.doNotReactToLongPress(); |
| mItemTouchHelperGestureListener = null; |
| } |
| if (mGestureDetector != null) { |
| mGestureDetector = null; |
| } |
| } |
| |
| private void getSelectedDxDy(float[] outPosition) { |
| if ((mSelectedFlags & (LEFT | RIGHT)) != 0) { |
| outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft(); |
| } else { |
| outPosition[0] = mSelected.itemView.getTranslationX(); |
| } |
| if ((mSelectedFlags & (UP | DOWN)) != 0) { |
| outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop(); |
| } else { |
| outPosition[1] = mSelected.itemView.getTranslationY(); |
| } |
| } |
| |
| @Override |
| public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { |
| float dx = 0, dy = 0; |
| if (mSelected != null) { |
| getSelectedDxDy(mTmpPosition); |
| dx = mTmpPosition[0]; |
| dy = mTmpPosition[1]; |
| } |
| mCallback.onDrawOver(c, parent, mSelected, |
| mRecoverAnimations, mActionState, dx, dy); |
| } |
| |
| @Override |
| public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { |
| // we don't know if RV changed something so we should invalidate this index. |
| mOverdrawChildPosition = -1; |
| float dx = 0, dy = 0; |
| if (mSelected != null) { |
| getSelectedDxDy(mTmpPosition); |
| dx = mTmpPosition[0]; |
| dy = mTmpPosition[1]; |
| } |
| mCallback.onDraw(c, parent, mSelected, |
| mRecoverAnimations, mActionState, dx, dy); |
| } |
| |
| /** |
| * Starts dragging or swiping the given View. Call with null if you want to clear it. |
| * |
| * @param selected The ViewHolder to drag or swipe. Can be null if you want to cancel the |
| * current action, but may not be null if actionState is ACTION_STATE_DRAG. |
| * @param actionState The type of action |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void select(@Nullable ViewHolder selected, int actionState) { |
| if (selected == mSelected && actionState == mActionState) { |
| return; |
| } |
| mDragScrollStartTimeInMs = Long.MIN_VALUE; |
| final int prevActionState = mActionState; |
| // prevent duplicate animations |
| endRecoverAnimation(selected, true); |
| mActionState = actionState; |
| if (actionState == ACTION_STATE_DRAG) { |
| if (selected == null) { |
| throw new IllegalArgumentException("Must pass a ViewHolder when dragging"); |
| } |
| |
| // we remove after animation is complete. this means we only elevate the last drag |
| // child but that should perform good enough as it is very hard to start dragging a |
| // new child before the previous one settles. |
| mOverdrawChild = selected.itemView; |
| addChildDrawingOrderCallback(); |
| } |
| int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState)) |
| - 1; |
| boolean preventLayout = false; |
| |
| if (mSelected != null) { |
| final ViewHolder prevSelected = mSelected; |
| if (prevSelected.itemView.getParent() != null) { |
| final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 |
| : swipeIfNecessary(prevSelected); |
| releaseVelocityTracker(); |
| // find where we should animate to |
| final float targetTranslateX, targetTranslateY; |
| int animationType; |
| switch (swipeDir) { |
| case LEFT: |
| case RIGHT: |
| case START: |
| case END: |
| targetTranslateY = 0; |
| targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth(); |
| break; |
| case UP: |
| case DOWN: |
| targetTranslateX = 0; |
| targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight(); |
| break; |
| default: |
| targetTranslateX = 0; |
| targetTranslateY = 0; |
| } |
| if (prevActionState == ACTION_STATE_DRAG) { |
| animationType = ANIMATION_TYPE_DRAG; |
| } else if (swipeDir > 0) { |
| animationType = ANIMATION_TYPE_SWIPE_SUCCESS; |
| } else { |
| animationType = ANIMATION_TYPE_SWIPE_CANCEL; |
| } |
| getSelectedDxDy(mTmpPosition); |
| final float currentTranslateX = mTmpPosition[0]; |
| final float currentTranslateY = mTmpPosition[1]; |
| final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType, |
| prevActionState, currentTranslateX, currentTranslateY, |
| targetTranslateX, targetTranslateY) { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| super.onAnimationEnd(animation); |
| if (this.mOverridden) { |
| return; |
| } |
| if (swipeDir <= 0) { |
| // this is a drag or failed swipe. recover immediately |
| mCallback.clearView(mRecyclerView, prevSelected); |
| // full cleanup will happen on onDrawOver |
| } else { |
| // wait until remove animation is complete. |
| mPendingCleanup.add(prevSelected.itemView); |
| mIsPendingCleanup = true; |
| if (swipeDir > 0) { |
| // Animation might be ended by other animators during a layout. |
| // We defer callback to avoid editing adapter during a layout. |
| postDispatchSwipe(this, swipeDir); |
| } |
| } |
| // removed from the list after it is drawn for the last time |
| if (mOverdrawChild == prevSelected.itemView) { |
| removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); |
| } |
| } |
| }; |
| final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, |
| targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); |
| rv.setDuration(duration); |
| mRecoverAnimations.add(rv); |
| rv.start(); |
| preventLayout = true; |
| } else { |
| removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); |
| mCallback.clearView(mRecyclerView, prevSelected); |
| } |
| mSelected = null; |
| } |
| if (selected != null) { |
| mSelectedFlags = |
| (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask) |
| >> (mActionState * DIRECTION_FLAG_COUNT); |
| mSelectedStartX = selected.itemView.getLeft(); |
| mSelectedStartY = selected.itemView.getTop(); |
| mSelected = selected; |
| |
| if (actionState == ACTION_STATE_DRAG) { |
| mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); |
| } |
| } |
| final ViewParent rvParent = mRecyclerView.getParent(); |
| if (rvParent != null) { |
| rvParent.requestDisallowInterceptTouchEvent(mSelected != null); |
| } |
| if (!preventLayout) { |
| mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout(); |
| } |
| mCallback.onSelectedChanged(mSelected, mActionState); |
| mRecyclerView.invalidate(); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) { |
| // wait until animations are complete. |
| mRecyclerView.post(new Runnable() { |
| @Override |
| public void run() { |
| if (mRecyclerView != null && mRecyclerView.isAttachedToWindow() |
| && !anim.mOverridden |
| && anim.mViewHolder.getAbsoluteAdapterPosition() |
| != RecyclerView.NO_POSITION) { |
| final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator(); |
| // if animator is running or we have other active recover animations, we try |
| // not to call onSwiped because DefaultItemAnimator is not good at merging |
| // animations. Instead, we wait and batch. |
| if ((animator == null || !animator.isRunning(null)) |
| && !hasRunningRecoverAnim()) { |
| mCallback.onSwiped(anim.mViewHolder, swipeDir); |
| } else { |
| mRecyclerView.post(this); |
| } |
| } |
| } |
| }); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| boolean hasRunningRecoverAnim() { |
| final int size = mRecoverAnimations.size(); |
| for (int i = 0; i < size; i++) { |
| if (!mRecoverAnimations.get(i).mEnded) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * If user drags the view to the edge, trigger a scroll if necessary. |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| boolean scrollIfNecessary() { |
| if (mSelected == null) { |
| mDragScrollStartTimeInMs = Long.MIN_VALUE; |
| return false; |
| } |
| final long now = System.currentTimeMillis(); |
| final long scrollDuration = mDragScrollStartTimeInMs |
| == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs; |
| RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); |
| if (mTmpRect == null) { |
| mTmpRect = new Rect(); |
| } |
| int scrollX = 0; |
| int scrollY = 0; |
| lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect); |
| if (lm.canScrollHorizontally()) { |
| int curX = (int) (mSelectedStartX + mDx); |
| final int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft(); |
| if (mDx < 0 && leftDiff < 0) { |
| scrollX = leftDiff; |
| } else if (mDx > 0) { |
| final int rightDiff = |
| curX + mSelected.itemView.getWidth() + mTmpRect.right |
| - (mRecyclerView.getWidth() - mRecyclerView.getPaddingRight()); |
| if (rightDiff > 0) { |
| scrollX = rightDiff; |
| } |
| } |
| } |
| if (lm.canScrollVertically()) { |
| int curY = (int) (mSelectedStartY + mDy); |
| final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop(); |
| if (mDy < 0 && topDiff < 0) { |
| scrollY = topDiff; |
| } else if (mDy > 0) { |
| final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom |
| - (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom()); |
| if (bottomDiff > 0) { |
| scrollY = bottomDiff; |
| } |
| } |
| } |
| if (scrollX != 0) { |
| scrollX = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, |
| mSelected.itemView.getWidth(), scrollX, |
| mRecyclerView.getWidth(), scrollDuration); |
| } |
| if (scrollY != 0) { |
| scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, |
| mSelected.itemView.getHeight(), scrollY, |
| mRecyclerView.getHeight(), scrollDuration); |
| } |
| if (scrollX != 0 || scrollY != 0) { |
| if (mDragScrollStartTimeInMs == Long.MIN_VALUE) { |
| mDragScrollStartTimeInMs = now; |
| } |
| mRecyclerView.scrollBy(scrollX, scrollY); |
| return true; |
| } |
| mDragScrollStartTimeInMs = Long.MIN_VALUE; |
| return false; |
| } |
| |
| private List<ViewHolder> findSwapTargets(ViewHolder viewHolder) { |
| if (mSwapTargets == null) { |
| mSwapTargets = new ArrayList<>(); |
| mDistances = new ArrayList<>(); |
| } else { |
| mSwapTargets.clear(); |
| mDistances.clear(); |
| } |
| final int margin = mCallback.getBoundingBoxMargin(); |
| final int left = Math.round(mSelectedStartX + mDx) - margin; |
| final int top = Math.round(mSelectedStartY + mDy) - margin; |
| final int right = left + viewHolder.itemView.getWidth() + 2 * margin; |
| final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin; |
| final int centerX = (left + right) / 2; |
| final int centerY = (top + bottom) / 2; |
| final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); |
| final int childCount = lm.getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| View other = lm.getChildAt(i); |
| if (other == viewHolder.itemView) { |
| continue; //myself! |
| } |
| if (other.getBottom() < top || other.getTop() > bottom |
| || other.getRight() < left || other.getLeft() > right) { |
| continue; |
| } |
| final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other); |
| if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) { |
| // find the index to add |
| final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2); |
| final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2); |
| final int dist = dx * dx + dy * dy; |
| |
| int pos = 0; |
| final int cnt = mSwapTargets.size(); |
| for (int j = 0; j < cnt; j++) { |
| if (dist > mDistances.get(j)) { |
| pos++; |
| } else { |
| break; |
| } |
| } |
| mSwapTargets.add(pos, otherVh); |
| mDistances.add(pos, dist); |
| } |
| } |
| return mSwapTargets; |
| } |
| |
| /** |
| * Checks if we should swap w/ another view holder. |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void moveIfNecessary(ViewHolder viewHolder) { |
| if (mRecyclerView.isLayoutRequested()) { |
| return; |
| } |
| if (mActionState != ACTION_STATE_DRAG) { |
| return; |
| } |
| |
| final float threshold = mCallback.getMoveThreshold(viewHolder); |
| final int x = (int) (mSelectedStartX + mDx); |
| final int y = (int) (mSelectedStartY + mDy); |
| if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold |
| && Math.abs(x - viewHolder.itemView.getLeft()) |
| < viewHolder.itemView.getWidth() * threshold) { |
| return; |
| } |
| List<ViewHolder> swapTargets = findSwapTargets(viewHolder); |
| if (swapTargets.size() == 0) { |
| return; |
| } |
| // may swap. |
| ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y); |
| if (target == null) { |
| mSwapTargets.clear(); |
| mDistances.clear(); |
| return; |
| } |
| final int toPosition = target.getAbsoluteAdapterPosition(); |
| final int fromPosition = viewHolder.getAbsoluteAdapterPosition(); |
| if (mCallback.onMove(mRecyclerView, viewHolder, target)) { |
| // keep target visible |
| mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, |
| target, toPosition, x, y); |
| } |
| } |
| |
| @Override |
| public void onChildViewAttachedToWindow(@NonNull View view) { |
| } |
| |
| @Override |
| public void onChildViewDetachedFromWindow(@NonNull View view) { |
| removeChildDrawingOrderCallbackIfNecessary(view); |
| final ViewHolder holder = mRecyclerView.getChildViewHolder(view); |
| if (holder == null) { |
| return; |
| } |
| if (mSelected != null && holder == mSelected) { |
| select(null, ACTION_STATE_IDLE); |
| } else { |
| endRecoverAnimation(holder, false); // this may push it into pending cleanup list. |
| if (mPendingCleanup.remove(holder.itemView)) { |
| mCallback.clearView(mRecyclerView, holder); |
| } |
| } |
| } |
| |
| /** |
| * Returns the animation type or 0 if cannot be found. |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void endRecoverAnimation(ViewHolder viewHolder, boolean override) { |
| final int recoverAnimSize = mRecoverAnimations.size(); |
| for (int i = recoverAnimSize - 1; i >= 0; i--) { |
| final RecoverAnimation anim = mRecoverAnimations.get(i); |
| if (anim.mViewHolder == viewHolder) { |
| anim.mOverridden |= override; |
| if (!anim.mEnded) { |
| anim.cancel(); |
| } |
| mRecoverAnimations.remove(i); |
| return; |
| } |
| } |
| } |
| |
| @Override |
| public void getItemOffsets(Rect outRect, View view, RecyclerView parent, |
| RecyclerView.State state) { |
| outRect.setEmpty(); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void obtainVelocityTracker() { |
| if (mVelocityTracker != null) { |
| mVelocityTracker.recycle(); |
| } |
| mVelocityTracker = VelocityTracker.obtain(); |
| } |
| |
| private void releaseVelocityTracker() { |
| if (mVelocityTracker != null) { |
| mVelocityTracker.recycle(); |
| mVelocityTracker = null; |
| } |
| } |
| |
| private ViewHolder findSwipedView(MotionEvent motionEvent) { |
| final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); |
| if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { |
| return null; |
| } |
| final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); |
| final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX; |
| final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY; |
| final float absDx = Math.abs(dx); |
| final float absDy = Math.abs(dy); |
| |
| if (absDx < mSlop && absDy < mSlop) { |
| return null; |
| } |
| if (absDx > absDy && lm.canScrollHorizontally()) { |
| return null; |
| } else if (absDy > absDx && lm.canScrollVertically()) { |
| return null; |
| } |
| View child = findChildView(motionEvent); |
| if (child == null) { |
| return null; |
| } |
| return mRecyclerView.getChildViewHolder(child); |
| } |
| |
| /** |
| * Checks whether we should select a View for swiping. |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) { |
| if (mSelected != null || action != MotionEvent.ACTION_MOVE |
| || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) { |
| return; |
| } |
| if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { |
| return; |
| } |
| final ViewHolder vh = findSwipedView(motionEvent); |
| if (vh == null) { |
| return; |
| } |
| final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh); |
| |
| final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) |
| >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE); |
| |
| if (swipeFlags == 0) { |
| return; |
| } |
| |
| // mDx and mDy are only set in allowed directions. We use custom x/y here instead of |
| // updateDxDy to avoid swiping if user moves more in the other direction |
| final float x = motionEvent.getX(pointerIndex); |
| final float y = motionEvent.getY(pointerIndex); |
| |
| // Calculate the distance moved |
| final float dx = x - mInitialTouchX; |
| final float dy = y - mInitialTouchY; |
| // swipe target is chose w/o applying flags so it does not really check if swiping in that |
| // direction is allowed. This why here, we use mDx mDy to check slope value again. |
| final float absDx = Math.abs(dx); |
| final float absDy = Math.abs(dy); |
| |
| if (absDx < mSlop && absDy < mSlop) { |
| return; |
| } |
| if (absDx > absDy) { |
| if (dx < 0 && (swipeFlags & LEFT) == 0) { |
| return; |
| } |
| if (dx > 0 && (swipeFlags & RIGHT) == 0) { |
| return; |
| } |
| } else { |
| if (dy < 0 && (swipeFlags & UP) == 0) { |
| return; |
| } |
| if (dy > 0 && (swipeFlags & DOWN) == 0) { |
| return; |
| } |
| } |
| mDx = mDy = 0f; |
| mActivePointerId = motionEvent.getPointerId(0); |
| select(vh, ACTION_STATE_SWIPE); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| View findChildView(MotionEvent event) { |
| // first check elevated views, if none, then call RV |
| final float x = event.getX(); |
| final float y = event.getY(); |
| if (mSelected != null) { |
| final View selectedView = mSelected.itemView; |
| if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) { |
| return selectedView; |
| } |
| } |
| for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { |
| final RecoverAnimation anim = mRecoverAnimations.get(i); |
| final View view = anim.mViewHolder.itemView; |
| if (hitTest(view, x, y, anim.mX, anim.mY)) { |
| return view; |
| } |
| } |
| return mRecyclerView.findChildViewUnder(x, y); |
| } |
| |
| /** |
| * Starts dragging the provided ViewHolder. By default, ItemTouchHelper starts a drag when a |
| * View is long pressed. You can disable that behavior by overriding |
| * {@link ItemTouchHelper.Callback#isLongPressDragEnabled()}. |
| * <p> |
| * For this method to work: |
| * <ul> |
| * <li>The provided ViewHolder must be a child of the RecyclerView to which this |
| * ItemTouchHelper |
| * is attached.</li> |
| * <li>{@link ItemTouchHelper.Callback} must have dragging enabled.</li> |
| * <li>There must be a previous touch event that was reported to the ItemTouchHelper |
| * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener |
| * grabs previous events, this should work as expected.</li> |
| * </ul> |
| * |
| * For example, if you would like to let your user to be able to drag an Item by touching one |
| * of its descendants, you may implement it as follows: |
| * <pre> |
| * viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() { |
| * public boolean onTouch(View v, MotionEvent event) { |
| * if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) { |
| * mItemTouchHelper.startDrag(viewHolder); |
| * } |
| * return false; |
| * } |
| * }); |
| * </pre> |
| * <p> |
| * |
| * @param viewHolder The ViewHolder to start dragging. It must be a direct child of |
| * RecyclerView. |
| * @see ItemTouchHelper.Callback#isItemViewSwipeEnabled() |
| */ |
| public void startDrag(@NonNull ViewHolder viewHolder) { |
| if (!mCallback.hasDragFlag(mRecyclerView, viewHolder)) { |
| Log.e(TAG, "Start drag has been called but dragging is not enabled"); |
| return; |
| } |
| if (viewHolder.itemView.getParent() != mRecyclerView) { |
| Log.e(TAG, "Start drag has been called with a view holder which is not a child of " |
| + "the RecyclerView which is controlled by this ItemTouchHelper."); |
| return; |
| } |
| obtainVelocityTracker(); |
| mDx = mDy = 0f; |
| select(viewHolder, ACTION_STATE_DRAG); |
| } |
| |
| /** |
| * Starts swiping the provided ViewHolder. By default, ItemTouchHelper starts swiping a View |
| * when user swipes their finger (or mouse pointer) over the View. You can disable this |
| * behavior |
| * by overriding {@link ItemTouchHelper.Callback} |
| * <p> |
| * For this method to work: |
| * <ul> |
| * <li>The provided ViewHolder must be a child of the RecyclerView to which this |
| * ItemTouchHelper is attached.</li> |
| * <li>{@link ItemTouchHelper.Callback} must have swiping enabled.</li> |
| * <li>There must be a previous touch event that was reported to the ItemTouchHelper |
| * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener |
| * grabs previous events, this should work as expected.</li> |
| * </ul> |
| * |
| * For example, if you would like to let your user to be able to swipe an Item by touching one |
| * of its descendants, you may implement it as follows: |
| * <pre> |
| * viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() { |
| * public boolean onTouch(View v, MotionEvent event) { |
| * if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) { |
| * mItemTouchHelper.startSwipe(viewHolder); |
| * } |
| * return false; |
| * } |
| * }); |
| * </pre> |
| * |
| * @param viewHolder The ViewHolder to start swiping. It must be a direct child of |
| * RecyclerView. |
| */ |
| public void startSwipe(@NonNull ViewHolder viewHolder) { |
| if (!mCallback.hasSwipeFlag(mRecyclerView, viewHolder)) { |
| Log.e(TAG, "Start swipe has been called but swiping is not enabled"); |
| return; |
| } |
| if (viewHolder.itemView.getParent() != mRecyclerView) { |
| Log.e(TAG, "Start swipe has been called with a view holder which is not a child of " |
| + "the RecyclerView controlled by this ItemTouchHelper."); |
| return; |
| } |
| obtainVelocityTracker(); |
| mDx = mDy = 0f; |
| select(viewHolder, ACTION_STATE_SWIPE); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| RecoverAnimation findAnimation(MotionEvent event) { |
| if (mRecoverAnimations.isEmpty()) { |
| return null; |
| } |
| View target = findChildView(event); |
| for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { |
| final RecoverAnimation anim = mRecoverAnimations.get(i); |
| if (anim.mViewHolder.itemView == target) { |
| return anim; |
| } |
| } |
| return null; |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) { |
| final float x = ev.getX(pointerIndex); |
| final float y = ev.getY(pointerIndex); |
| |
| // Calculate the distance moved |
| mDx = x - mInitialTouchX; |
| mDy = y - mInitialTouchY; |
| if ((directionFlags & LEFT) == 0) { |
| mDx = Math.max(0, mDx); |
| } |
| if ((directionFlags & RIGHT) == 0) { |
| mDx = Math.min(0, mDx); |
| } |
| if ((directionFlags & UP) == 0) { |
| mDy = Math.max(0, mDy); |
| } |
| if ((directionFlags & DOWN) == 0) { |
| mDy = Math.min(0, mDy); |
| } |
| } |
| |
| private int swipeIfNecessary(ViewHolder viewHolder) { |
| if (mActionState == ACTION_STATE_DRAG) { |
| return 0; |
| } |
| final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder); |
| final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection( |
| originalMovementFlags, |
| ViewCompat.getLayoutDirection(mRecyclerView)); |
| final int flags = (absoluteMovementFlags |
| & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); |
| if (flags == 0) { |
| return 0; |
| } |
| final int originalFlags = (originalMovementFlags |
| & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); |
| int swipeDir; |
| if (Math.abs(mDx) > Math.abs(mDy)) { |
| if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { |
| // if swipe dir is not in original flags, it should be the relative direction |
| if ((originalFlags & swipeDir) == 0) { |
| // convert to relative |
| return Callback.convertToRelativeDirection(swipeDir, |
| ViewCompat.getLayoutDirection(mRecyclerView)); |
| } |
| return swipeDir; |
| } |
| if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { |
| return swipeDir; |
| } |
| } else { |
| if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { |
| return swipeDir; |
| } |
| if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { |
| // if swipe dir is not in original flags, it should be the relative direction |
| if ((originalFlags & swipeDir) == 0) { |
| // convert to relative |
| return Callback.convertToRelativeDirection(swipeDir, |
| ViewCompat.getLayoutDirection(mRecyclerView)); |
| } |
| return swipeDir; |
| } |
| } |
| return 0; |
| } |
| |
| private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) { |
| if ((flags & (LEFT | RIGHT)) != 0) { |
| final int dirFlag = mDx > 0 ? RIGHT : LEFT; |
| if (mVelocityTracker != null && mActivePointerId > -1) { |
| mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, |
| mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); |
| final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); |
| final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); |
| final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT; |
| final float absXVelocity = Math.abs(xVelocity); |
| if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag |
| && absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) |
| && absXVelocity > Math.abs(yVelocity)) { |
| return velDirFlag; |
| } |
| } |
| |
| final float threshold = mRecyclerView.getWidth() * mCallback |
| .getSwipeThreshold(viewHolder); |
| |
| if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) { |
| return dirFlag; |
| } |
| } |
| return 0; |
| } |
| |
| private int checkVerticalSwipe(ViewHolder viewHolder, int flags) { |
| if ((flags & (UP | DOWN)) != 0) { |
| final int dirFlag = mDy > 0 ? DOWN : UP; |
| if (mVelocityTracker != null && mActivePointerId > -1) { |
| mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, |
| mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); |
| final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); |
| final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); |
| final int velDirFlag = yVelocity > 0f ? DOWN : UP; |
| final float absYVelocity = Math.abs(yVelocity); |
| if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag |
| && absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) |
| && absYVelocity > Math.abs(xVelocity)) { |
| return velDirFlag; |
| } |
| } |
| |
| final float threshold = mRecyclerView.getHeight() * mCallback |
| .getSwipeThreshold(viewHolder); |
| if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) { |
| return dirFlag; |
| } |
| } |
| return 0; |
| } |
| |
| private void addChildDrawingOrderCallback() { |
| if (Build.VERSION.SDK_INT >= 21) { |
| return; // we use elevation on Lollipop |
| } |
| if (mChildDrawingOrderCallback == null) { |
| mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() { |
| @Override |
| public int onGetChildDrawingOrder(int childCount, int i) { |
| if (mOverdrawChild == null) { |
| return i; |
| } |
| int childPosition = mOverdrawChildPosition; |
| if (childPosition == -1) { |
| childPosition = mRecyclerView.indexOfChild(mOverdrawChild); |
| mOverdrawChildPosition = childPosition; |
| } |
| if (i == childCount - 1) { |
| return childPosition; |
| } |
| return i < childPosition ? i : i + 1; |
| } |
| }; |
| } |
| mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void removeChildDrawingOrderCallbackIfNecessary(View view) { |
| if (view == mOverdrawChild) { |
| mOverdrawChild = null; |
| // only remove if we've added |
| if (mChildDrawingOrderCallback != null) { |
| mRecyclerView.setChildDrawingOrderCallback(null); |
| } |
| } |
| } |
| |
| /** |
| * An interface which can be implemented by LayoutManager for better integration with |
| * {@link ItemTouchHelper}. |
| */ |
| public interface ViewDropHandler { |
| |
| /** |
| * Called by the {@link ItemTouchHelper} after a View is dropped over another View. |
| * <p> |
| * A LayoutManager should implement this interface to get ready for the upcoming move |
| * operation. |
| * <p> |
| * For example, LinearLayoutManager sets up a "scrollToPositionWithOffset" calls so that |
| * the View under drag will be used as an anchor View while calculating the next layout, |
| * making layout stay consistent. |
| * |
| * @param view The View which is being dragged. It is very likely that user is still |
| * dragging this View so there might be other calls to |
| * {@code prepareForDrop()} after this one. |
| * @param target The target view which is being dropped on. |
| * @param x The <code>left</code> offset of the View that is being dragged. This value |
| * includes the movement caused by the user. |
| * @param y The <code>top</code> offset of the View that is being dragged. This value |
| * includes the movement caused by the user. |
| */ |
| void prepareForDrop(@NonNull View view, @NonNull View target, int x, int y); |
| } |
| |
| /** |
| * This class is the contract between ItemTouchHelper and your application. It lets you control |
| * which touch behaviors are enabled per each ViewHolder and also receive callbacks when user |
| * performs these actions. |
| * <p> |
| * To control which actions user can take on each view, you should override |
| * {@link #getMovementFlags(RecyclerView, ViewHolder)} and return appropriate set |
| * of direction flags. ({@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link #END}, |
| * {@link #UP}, {@link #DOWN}). You can use |
| * {@link #makeMovementFlags(int, int)} to easily construct it. Alternatively, you can use |
| * {@link SimpleCallback}. |
| * <p> |
| * If user drags an item, ItemTouchHelper will call |
| * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder) |
| * onMove(recyclerView, dragged, target)}. |
| * Upon receiving this callback, you should move the item from the old position |
| * ({@code dragged.getAdapterPosition()}) to new position ({@code target.getAdapterPosition()}) |
| * in your adapter and also call {@link RecyclerView.Adapter#notifyItemMoved(int, int)}. |
| * To control where a View can be dropped, you can override |
| * {@link #canDropOver(RecyclerView, ViewHolder, ViewHolder)}. When a |
| * dragging View overlaps multiple other views, Callback chooses the closest View with which |
| * dragged View might have changed positions. Although this approach works for many use cases, |
| * if you have a custom LayoutManager, you can override |
| * {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)} to select a |
| * custom drop target. |
| * <p> |
| * When a View is swiped, ItemTouchHelper animates it until it goes out of bounds, then calls |
| * {@link #onSwiped(ViewHolder, int)}. At this point, you should update your |
| * adapter (e.g. remove the item) and call related Adapter#notify event. |
| */ |
| @SuppressWarnings("UnusedParameters") |
| public abstract static class Callback { |
| |
| @SuppressWarnings("WeakerAccess") |
| public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200; |
| |
| @SuppressWarnings("WeakerAccess") |
| public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250; |
| |
| static final int RELATIVE_DIR_FLAGS = START | END |
| | ((START | END) << DIRECTION_FLAG_COUNT) |
| | ((START | END) << (2 * DIRECTION_FLAG_COUNT)); |
| |
| private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT |
| | ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT) |
| | ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT)); |
| |
| private static final Interpolator sDragScrollInterpolator = new Interpolator() { |
| @Override |
| public float getInterpolation(float t) { |
| return t * t * t * t * t; |
| } |
| }; |
| |
| private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() { |
| @Override |
| public float getInterpolation(float t) { |
| t -= 1.0f; |
| return t * t * t * t * t + 1.0f; |
| } |
| }; |
| |
| /** |
| * Drag scroll speed keeps accelerating until this many milliseconds before being capped. |
| */ |
| private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; |
| |
| private int mCachedMaxScrollSpeed = -1; |
| |
| /** |
| * Returns the {@link ItemTouchUIUtil} that is used by the {@link Callback} class for |
| * visual |
| * changes on Views in response to user interactions. {@link ItemTouchUIUtil} has different |
| * implementations for different platform versions. |
| * <p> |
| * By default, {@link Callback} applies these changes on |
| * {@link RecyclerView.ViewHolder#itemView}. |
| * <p> |
| * For example, if you have a use case where you only want the text to move when user |
| * swipes over the view, you can do the following: |
| * <pre> |
| * public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder){ |
| * getDefaultUIUtil().clearView(((ItemTouchViewHolder) viewHolder).textView); |
| * } |
| * public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { |
| * if (viewHolder != null){ |
| * getDefaultUIUtil().onSelected(((ItemTouchViewHolder) viewHolder).textView); |
| * } |
| * } |
| * public void onChildDraw(Canvas c, RecyclerView recyclerView, |
| * RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, |
| * boolean isCurrentlyActive) { |
| * getDefaultUIUtil().onDraw(c, recyclerView, |
| * ((ItemTouchViewHolder) viewHolder).textView, dX, dY, |
| * actionState, isCurrentlyActive); |
| * return true; |
| * } |
| * public void onChildDrawOver(Canvas c, RecyclerView recyclerView, |
| * RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, |
| * boolean isCurrentlyActive) { |
| * getDefaultUIUtil().onDrawOver(c, recyclerView, |
| * ((ItemTouchViewHolder) viewHolder).textView, dX, dY, |
| * actionState, isCurrentlyActive); |
| * return true; |
| * } |
| * </pre> |
| * |
| * @return The {@link ItemTouchUIUtil} instance that is used by the {@link Callback} |
| */ |
| @SuppressWarnings("WeakerAccess") |
| @NonNull |
| public static ItemTouchUIUtil getDefaultUIUtil() { |
| return ItemTouchUIUtilImpl.INSTANCE; |
| } |
| |
| /** |
| * Replaces a movement direction with its relative version by taking layout direction into |
| * account. |
| * |
| * @param flags The flag value that include any number of movement flags. |
| * @param layoutDirection The layout direction of the View. Can be obtained from |
| * {@link ViewCompat#getLayoutDirection(android.view.View)}. |
| * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead |
| * of {@link #LEFT}, {@link #RIGHT}. |
| * @see #convertToAbsoluteDirection(int, int) |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public static int convertToRelativeDirection(int flags, int layoutDirection) { |
| int masked = flags & ABS_HORIZONTAL_DIR_FLAGS; |
| if (masked == 0) { |
| return flags; // does not have any abs flags, good. |
| } |
| flags &= ~masked; //remove left / right. |
| if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { |
| // no change. just OR with 2 bits shifted mask and return |
| flags |= masked << 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. |
| return flags; |
| } else { |
| // add RIGHT flag as START |
| flags |= ((masked << 1) & ~ABS_HORIZONTAL_DIR_FLAGS); |
| // first clean RIGHT bit then add LEFT flag as END |
| flags |= ((masked << 1) & ABS_HORIZONTAL_DIR_FLAGS) << 2; |
| } |
| return flags; |
| } |
| |
| /** |
| * Convenience method to create movement flags. |
| * <p> |
| * For instance, if you want to let your items be drag & dropped vertically and swiped |
| * left to be dismissed, you can call this method with: |
| * <code>makeMovementFlags(UP | DOWN, LEFT);</code> |
| * |
| * @param dragFlags The directions in which the item can be dragged. |
| * @param swipeFlags The directions in which the item can be swiped. |
| * @return Returns an integer composed of the given drag and swipe flags. |
| */ |
| public static int makeMovementFlags(int dragFlags, int swipeFlags) { |
| return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags) |
| | makeFlag(ACTION_STATE_SWIPE, swipeFlags) |
| | makeFlag(ACTION_STATE_DRAG, dragFlags); |
| } |
| |
| /** |
| * Shifts the given direction flags to the offset of the given action state. |
| * |
| * @param actionState The action state you want to get flags in. Should be one of |
| * {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or |
| * {@link #ACTION_STATE_DRAG}. |
| * @param directions The direction flags. Can be composed from {@link #UP}, {@link #DOWN}, |
| * {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}. |
| * @return And integer that represents the given directions in the provided actionState. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public static int makeFlag(int actionState, int directions) { |
| return directions << (actionState * DIRECTION_FLAG_COUNT); |
| } |
| |
| /** |
| * Should return a composite flag which defines the enabled move directions in each state |
| * (idle, swiping, dragging). |
| * <p> |
| * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int, |
| * int)} |
| * or {@link #makeFlag(int, int)}. |
| * <p> |
| * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next |
| * 8 bits are for SWIPE state and third 8 bits are for DRAG state. |
| * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in |
| * {@link ItemTouchHelper}. |
| * <p> |
| * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to |
| * swipe by swiping RIGHT, you can return: |
| * <pre> |
| * makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT); |
| * </pre> |
| * This means, allow right movement while IDLE and allow right and left movement while |
| * swiping. |
| * |
| * @param recyclerView The RecyclerView to which ItemTouchHelper is attached. |
| * @param viewHolder The ViewHolder for which the movement information is necessary. |
| * @return flags specifying which movements are allowed on this ViewHolder. |
| * @see #makeMovementFlags(int, int) |
| * @see #makeFlag(int, int) |
| */ |
| public abstract int getMovementFlags(@NonNull RecyclerView recyclerView, |
| @NonNull ViewHolder viewHolder); |
| |
| /** |
| * Converts a given set of flags to absolution direction which means {@link #START} and |
| * {@link #END} are replaced with {@link #LEFT} and {@link #RIGHT} depending on the layout |
| * direction. |
| * |
| * @param flags The flag value that include any number of movement flags. |
| * @param layoutDirection The layout direction of the RecyclerView. |
| * @return Updated flags which includes only absolute direction values. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public int convertToAbsoluteDirection(int flags, int layoutDirection) { |
| int masked = flags & RELATIVE_DIR_FLAGS; |
| if (masked == 0) { |
| return flags; // does not have any relative flags, good. |
| } |
| flags &= ~masked; //remove start / end |
| if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { |
| // no change. just OR with 2 bits shifted mask and return |
| flags |= masked >> 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. |
| return flags; |
| } else { |
| // add START flag as RIGHT |
| flags |= ((masked >> 1) & ~RELATIVE_DIR_FLAGS); |
| // first clean start bit then add END flag as LEFT |
| flags |= ((masked >> 1) & RELATIVE_DIR_FLAGS) >> 2; |
| } |
| return flags; |
| } |
| |
| final int getAbsoluteMovementFlags(RecyclerView recyclerView, |
| ViewHolder viewHolder) { |
| final int flags = getMovementFlags(recyclerView, viewHolder); |
| return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView)); |
| } |
| |
| boolean hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder) { |
| final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); |
| return (flags & ACTION_MODE_DRAG_MASK) != 0; |
| } |
| |
| boolean hasSwipeFlag(RecyclerView recyclerView, |
| ViewHolder viewHolder) { |
| final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); |
| return (flags & ACTION_MODE_SWIPE_MASK) != 0; |
| } |
| |
| /** |
| * Return true if the current ViewHolder can be dropped over the the target ViewHolder. |
| * <p> |
| * This method is used when selecting drop target for the dragged View. After Views are |
| * eliminated either via bounds check or via this method, resulting set of views will be |
| * passed to {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)}. |
| * <p> |
| * Default implementation returns true. |
| * |
| * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. |
| * @param current The ViewHolder that user is dragging. |
| * @param target The ViewHolder which is below the dragged ViewHolder. |
| * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false |
| * otherwise. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public boolean canDropOver(@NonNull RecyclerView recyclerView, @NonNull ViewHolder current, |
| @NonNull ViewHolder target) { |
| return true; |
| } |
| |
| /** |
| * Called when ItemTouchHelper wants to move the dragged item from its old position to |
| * the new position. |
| * <p> |
| * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved |
| * to the adapter position of {@code target} ViewHolder |
| * ({@link ViewHolder#getAbsoluteAdapterPosition() |
| * ViewHolder#getAdapterPositionInRecyclerView()}). |
| * <p> |
| * If you don't support drag & drop, this method will never be called. |
| * |
| * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. |
| * @param viewHolder The ViewHolder which is being dragged by the user. |
| * @param target The ViewHolder over which the currently active item is being |
| * dragged. |
| * @return True if the {@code viewHolder} has been moved to the adapter position of |
| * {@code target}. |
| * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int) |
| */ |
| public abstract boolean onMove(@NonNull RecyclerView recyclerView, |
| @NonNull ViewHolder viewHolder, @NonNull ViewHolder target); |
| |
| /** |
| * Returns whether ItemTouchHelper should start a drag and drop operation if an item is |
| * long pressed. |
| * <p> |
| * Default value returns true but you may want to disable this if you want to start |
| * dragging on a custom view touch using {@link #startDrag(ViewHolder)}. |
| * |
| * @return True if ItemTouchHelper should start dragging an item when it is long pressed, |
| * false otherwise. Default value is <code>true</code>. |
| * @see #startDrag(ViewHolder) |
| */ |
| public boolean isLongPressDragEnabled() { |
| return true; |
| } |
| |
| /** |
| * Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped |
| * over the View. |
| * <p> |
| * Default value returns true but you may want to disable this if you want to start |
| * swiping on a custom view touch using {@link #startSwipe(ViewHolder)}. |
| * |
| * @return True if ItemTouchHelper should start swiping an item when user swipes a pointer |
| * over the View, false otherwise. Default value is <code>true</code>. |
| * @see #startSwipe(ViewHolder) |
| */ |
| public boolean isItemViewSwipeEnabled() { |
| return true; |
| } |
| |
| /** |
| * When finding views under a dragged view, by default, ItemTouchHelper searches for views |
| * that overlap with the dragged View. By overriding this method, you can extend or shrink |
| * the search box. |
| * |
| * @return The extra margin to be added to the hit box of the dragged View. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public int getBoundingBoxMargin() { |
| return 0; |
| } |
| |
| /** |
| * Returns the fraction that the user should move the View to be considered as swiped. |
| * The fraction is calculated with respect to RecyclerView's bounds. |
| * <p> |
| * Default value is .5f, which means, to swipe a View, user must move the View at least |
| * half of RecyclerView's width or height, depending on the swipe direction. |
| * |
| * @param viewHolder The ViewHolder that is being dragged. |
| * @return A float value that denotes the fraction of the View size. Default value |
| * is .5f . |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public float getSwipeThreshold(@NonNull ViewHolder viewHolder) { |
| return .5f; |
| } |
| |
| /** |
| * Returns the fraction that the user should move the View to be considered as it is |
| * dragged. After a view is moved this amount, ItemTouchHelper starts checking for Views |
| * below it for a possible drop. |
| * |
| * @param viewHolder The ViewHolder that is being dragged. |
| * @return A float value that denotes the fraction of the View size. Default value is |
| * .5f . |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public float getMoveThreshold(@NonNull ViewHolder viewHolder) { |
| return .5f; |
| } |
| |
| /** |
| * Defines the minimum velocity which will be considered as a swipe action by the user. |
| * <p> |
| * You can increase this value to make it harder to swipe or decrease it to make it easier. |
| * Keep in mind that ItemTouchHelper also checks the perpendicular velocity and makes sure |
| * current direction velocity is larger then the perpendicular one. Otherwise, user's |
| * movement is ambiguous. You can change the threshold by overriding |
| * {@link #getSwipeVelocityThreshold(float)}. |
| * <p> |
| * The velocity is calculated in pixels per second. |
| * <p> |
| * The default framework value is passed as a parameter so that you can modify it with a |
| * multiplier. |
| * |
| * @param defaultValue The default value (in pixels per second) used by the |
| * ItemTouchHelper. |
| * @return The minimum swipe velocity. The default implementation returns the |
| * <code>defaultValue</code> parameter. |
| * @see #getSwipeVelocityThreshold(float) |
| * @see #getSwipeThreshold(ViewHolder) |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public float getSwipeEscapeVelocity(float defaultValue) { |
| return defaultValue; |
| } |
| |
| /** |
| * Defines the maximum velocity ItemTouchHelper will ever calculate for pointer movements. |
| * <p> |
| * To consider a movement as swipe, ItemTouchHelper requires it to be larger than the |
| * perpendicular movement. If both directions reach to the max threshold, none of them will |
| * be considered as a swipe because it is usually an indication that user rather tried to |
| * scroll then swipe. |
| * <p> |
| * The velocity is calculated in pixels per second. |
| * <p> |
| * You can customize this behavior by changing this method. If you increase the value, it |
| * will be easier for the user to swipe diagonally and if you decrease the value, user will |
| * need to make a rather straight finger movement to trigger a swipe. |
| * |
| * @param defaultValue The default value(in pixels per second) used by the ItemTouchHelper. |
| * @return The velocity cap for pointer movements. The default implementation returns the |
| * <code>defaultValue</code> parameter. |
| * @see #getSwipeEscapeVelocity(float) |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public float getSwipeVelocityThreshold(float defaultValue) { |
| return defaultValue; |
| } |
| |
| /** |
| * Called by ItemTouchHelper to select a drop target from the list of ViewHolders that |
| * are under the dragged View. |
| * <p> |
| * Default implementation filters the View with which dragged item have changed position |
| * in the drag direction. For instance, if the view is dragged UP, it compares the |
| * <code>view.getTop()</code> of the two views before and after drag started. If that value |
| * is different, the target view passes the filter. |
| * <p> |
| * Among these Views which pass the test, the one closest to the dragged view is chosen. |
| * <p> |
| * This method is called on the main thread every time user moves the View. If you want to |
| * override it, make sure it does not do any expensive operations. |
| * |
| * @param selected The ViewHolder being dragged by the user. |
| * @param dropTargets The list of ViewHolder that are under the dragged View and |
| * candidate as a drop. |
| * @param curX The updated left value of the dragged View after drag translations |
| * are applied. This value does not include margins added by |
| * {@link RecyclerView.ItemDecoration}s. |
| * @param curY The updated top value of the dragged View after drag translations |
| * are applied. This value does not include margins added by |
| * {@link RecyclerView.ItemDecoration}s. |
| * @return A ViewHolder to whose position the dragged ViewHolder should be |
| * moved to. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public ViewHolder chooseDropTarget(@NonNull ViewHolder selected, |
| @NonNull List<ViewHolder> dropTargets, int curX, int curY) { |
| int right = curX + selected.itemView.getWidth(); |
| int bottom = curY + selected.itemView.getHeight(); |
| ViewHolder winner = null; |
| int winnerScore = -1; |
| final int dx = curX - selected.itemView.getLeft(); |
| final int dy = curY - selected.itemView.getTop(); |
| final int targetsSize = dropTargets.size(); |
| for (int i = 0; i < targetsSize; i++) { |
| final ViewHolder target = dropTargets.get(i); |
| if (dx > 0) { |
| int diff = target.itemView.getRight() - right; |
| if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) { |
| final int score = Math.abs(diff); |
| if (score > winnerScore) { |
| winnerScore = score; |
| winner = target; |
| } |
| } |
| } |
| if (dx < 0) { |
| int diff = target.itemView.getLeft() - curX; |
| if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) { |
| final int score = Math.abs(diff); |
| if (score > winnerScore) { |
| winnerScore = score; |
| winner = target; |
| } |
| } |
| } |
| if (dy < 0) { |
| int diff = target.itemView.getTop() - curY; |
| if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) { |
| final int score = Math.abs(diff); |
| if (score > winnerScore) { |
| winnerScore = score; |
| winner = target; |
| } |
| } |
| } |
| |
| if (dy > 0) { |
| int diff = target.itemView.getBottom() - bottom; |
| if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) { |
| final int score = Math.abs(diff); |
| if (score > winnerScore) { |
| winnerScore = score; |
| winner = target; |
| } |
| } |
| } |
| } |
| return winner; |
| } |
| |
| /** |
| * Called when a ViewHolder is swiped by the user. |
| * <p> |
| * If you are returning relative directions ({@link #START} , {@link #END}) from the |
| * {@link #getMovementFlags(RecyclerView, ViewHolder)} method, this method |
| * will also use relative directions. Otherwise, it will use absolute directions. |
| * <p> |
| * If you don't support swiping, this method will never be called. |
| * <p> |
| * ItemTouchHelper will keep a reference to the View until it is detached from |
| * RecyclerView. |
| * As soon as it is detached, ItemTouchHelper will call |
| * {@link #clearView(RecyclerView, ViewHolder)}. |
| * |
| * @param viewHolder The ViewHolder which has been swiped by the user. |
| * @param direction The direction to which the ViewHolder is swiped. It is one of |
| * {@link #UP}, {@link #DOWN}, |
| * {@link #LEFT} or {@link #RIGHT}. If your |
| * {@link #getMovementFlags(RecyclerView, ViewHolder)} |
| * method |
| * returned relative flags instead of {@link #LEFT} / {@link #RIGHT}; |
| * `direction` will be relative as well. ({@link #START} or {@link |
| * #END}). |
| */ |
| public abstract void onSwiped(@NonNull ViewHolder viewHolder, int direction); |
| |
| /** |
| * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed. |
| * <p/> |
| * If you override this method, you should call super. |
| * |
| * @param viewHolder The new ViewHolder that is being swiped or dragged. Might be null if |
| * it is cleared. |
| * @param actionState One of {@link ItemTouchHelper#ACTION_STATE_IDLE}, |
| * {@link ItemTouchHelper#ACTION_STATE_SWIPE} or |
| * {@link ItemTouchHelper#ACTION_STATE_DRAG}. |
| * @see #clearView(RecyclerView, RecyclerView.ViewHolder) |
| */ |
| public void onSelectedChanged(@Nullable ViewHolder viewHolder, int actionState) { |
| if (viewHolder != null) { |
| ItemTouchUIUtilImpl.INSTANCE.onSelected(viewHolder.itemView); |
| } |
| } |
| |
| private int getMaxDragScroll(RecyclerView recyclerView) { |
| if (mCachedMaxScrollSpeed == -1) { |
| mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize( |
| R.dimen.item_touch_helper_max_drag_scroll_per_frame); |
| } |
| return mCachedMaxScrollSpeed; |
| } |
| |
| /** |
| * Called when {@link #onMove(RecyclerView, ViewHolder, ViewHolder)} returns true. |
| * <p> |
| * ItemTouchHelper does not create an extra Bitmap or View while dragging, instead, it |
| * modifies the existing View. Because of this reason, it is important that the View is |
| * still part of the layout after it is moved. This may not work as intended when swapped |
| * Views are close to RecyclerView bounds or there are gaps between them (e.g. other Views |
| * which were not eligible for dropping over). |
| * <p> |
| * This method is responsible to give necessary hint to the LayoutManager so that it will |
| * keep the View in visible area. For example, for LinearLayoutManager, this is as simple |
| * as calling {@link LinearLayoutManager#scrollToPositionWithOffset(int, int)}. |
| * |
| * Default implementation calls {@link RecyclerView#scrollToPosition(int)} if the View's |
| * new position is likely to be out of bounds. |
| * <p> |
| * It is important to ensure the ViewHolder will stay visible as otherwise, it might be |
| * removed by the LayoutManager if the move causes the View to go out of bounds. In that |
| * case, drag will end prematurely. |
| * |
| * @param recyclerView The RecyclerView controlled by the ItemTouchHelper. |
| * @param viewHolder The ViewHolder under user's control. |
| * @param fromPos The previous adapter position of the dragged item (before it was |
| * moved). |
| * @param target The ViewHolder on which the currently active item has been dropped. |
| * @param toPos The new adapter position of the dragged item. |
| * @param x The updated left value of the dragged View after drag translations |
| * are applied. This value does not include margins added by |
| * {@link RecyclerView.ItemDecoration}s. |
| * @param y The updated top value of the dragged View after drag translations |
| * are applied. This value does not include margins added by |
| * {@link RecyclerView.ItemDecoration}s. |
| */ |
| public void onMoved(@NonNull final RecyclerView recyclerView, |
| @NonNull final ViewHolder viewHolder, int fromPos, @NonNull final ViewHolder target, |
| int toPos, int x, int y) { |
| final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); |
| if (layoutManager instanceof ViewDropHandler) { |
| ((ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView, |
| target.itemView, x, y); |
| return; |
| } |
| |
| // if layout manager cannot handle it, do some guesswork |
| if (layoutManager.canScrollHorizontally()) { |
| final int minLeft = layoutManager.getDecoratedLeft(target.itemView); |
| if (minLeft <= recyclerView.getPaddingLeft()) { |
| recyclerView.scrollToPosition(toPos); |
| } |
| final int maxRight = layoutManager.getDecoratedRight(target.itemView); |
| if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) { |
| recyclerView.scrollToPosition(toPos); |
| } |
| } |
| |
| if (layoutManager.canScrollVertically()) { |
| final int minTop = layoutManager.getDecoratedTop(target.itemView); |
| if (minTop <= recyclerView.getPaddingTop()) { |
| recyclerView.scrollToPosition(toPos); |
| } |
| final int maxBottom = layoutManager.getDecoratedBottom(target.itemView); |
| if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) { |
| recyclerView.scrollToPosition(toPos); |
| } |
| } |
| } |
| |
| void onDraw(Canvas c, RecyclerView parent, ViewHolder selected, |
| List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, |
| int actionState, float dX, float dY) { |
| final int recoverAnimSize = recoverAnimationList.size(); |
| for (int i = 0; i < recoverAnimSize; i++) { |
| final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); |
| anim.update(); |
| final int count = c.save(); |
| onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, |
| false); |
| c.restoreToCount(count); |
| } |
| if (selected != null) { |
| final int count = c.save(); |
| onChildDraw(c, parent, selected, dX, dY, actionState, true); |
| c.restoreToCount(count); |
| } |
| } |
| |
| void onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected, |
| List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, |
| int actionState, float dX, float dY) { |
| final int recoverAnimSize = recoverAnimationList.size(); |
| for (int i = 0; i < recoverAnimSize; i++) { |
| final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); |
| final int count = c.save(); |
| onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, |
| false); |
| c.restoreToCount(count); |
| } |
| if (selected != null) { |
| final int count = c.save(); |
| onChildDrawOver(c, parent, selected, dX, dY, actionState, true); |
| c.restoreToCount(count); |
| } |
| boolean hasRunningAnimation = false; |
| for (int i = recoverAnimSize - 1; i >= 0; i--) { |
| final RecoverAnimation anim = recoverAnimationList.get(i); |
| if (anim.mEnded && !anim.mIsPendingCleanup) { |
| recoverAnimationList.remove(i); |
| } else if (!anim.mEnded) { |
| hasRunningAnimation = true; |
| } |
| } |
| if (hasRunningAnimation) { |
| parent.invalidate(); |
| } |
| } |
| |
| /** |
| * Called by the ItemTouchHelper when the user interaction with an element is over and it |
| * also completed its animation. |
| * <p> |
| * This is a good place to clear all changes on the View that was done in |
| * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)}, |
| * {@link #onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int, |
| * boolean)} or |
| * {@link #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)}. |
| * |
| * @param recyclerView The RecyclerView which is controlled by the ItemTouchHelper. |
| * @param viewHolder The View that was interacted by the user. |
| */ |
| public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) { |
| ItemTouchUIUtilImpl.INSTANCE.clearView(viewHolder.itemView); |
| } |
| |
| /** |
| * Called by ItemTouchHelper on RecyclerView's onDraw callback. |
| * <p> |
| * If you would like to customize how your View's respond to user interactions, this is |
| * a good place to override. |
| * <p> |
| * Default implementation translates the child by the given <code>dX</code>, |
| * <code>dY</code>. |
| * ItemTouchHelper also takes care of drawing the child after other children if it is being |
| * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this |
| * is |
| * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L |
| * and after, it changes View's elevation value to be greater than all other children.) |
| * |
| * @param c The canvas which RecyclerView is drawing its children |
| * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to |
| * @param viewHolder The ViewHolder which is being interacted by the User or it was |
| * interacted and simply animating to its original position |
| * @param dX The amount of horizontal displacement caused by user's action |
| * @param dY The amount of vertical displacement caused by user's action |
| * @param actionState The type of interaction on the View. Is either {@link |
| * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. |
| * @param isCurrentlyActive True if this view is currently being controlled by the user or |
| * false it is simply animating back to its original state. |
| * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, |
| * boolean) |
| */ |
| public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, |
| @NonNull ViewHolder viewHolder, |
| float dX, float dY, int actionState, boolean isCurrentlyActive) { |
| ItemTouchUIUtilImpl.INSTANCE.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, |
| actionState, isCurrentlyActive); |
| } |
| |
| /** |
| * Called by ItemTouchHelper on RecyclerView's onDraw callback. |
| * <p> |
| * If you would like to customize how your View's respond to user interactions, this is |
| * a good place to override. |
| * <p> |
| * Default implementation translates the child by the given <code>dX</code>, |
| * <code>dY</code>. |
| * ItemTouchHelper also takes care of drawing the child after other children if it is being |
| * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this |
| * is |
| * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L |
| * and after, it changes View's elevation value to be greater than all other children.) |
| * |
| * @param c The canvas which RecyclerView is drawing its children |
| * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to |
| * @param viewHolder The ViewHolder which is being interacted by the User or it was |
| * interacted and simply animating to its original position |
| * @param dX The amount of horizontal displacement caused by user's action |
| * @param dY The amount of vertical displacement caused by user's action |
| * @param actionState The type of interaction on the View. Is either {@link |
| * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. |
| * @param isCurrentlyActive True if this view is currently being controlled by the user or |
| * false it is simply animating back to its original state. |
| * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, |
| * boolean) |
| */ |
| public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, |
| ViewHolder viewHolder, |
| float dX, float dY, int actionState, boolean isCurrentlyActive) { |
| ItemTouchUIUtilImpl.INSTANCE.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, |
| actionState, isCurrentlyActive); |
| } |
| |
| /** |
| * Called by the ItemTouchHelper when user action finished on a ViewHolder and now the View |
| * will be animated to its final position. |
| * <p> |
| * Default implementation uses ItemAnimator's duration values. If |
| * <code>animationType</code> is {@link #ANIMATION_TYPE_DRAG}, it returns |
| * {@link RecyclerView.ItemAnimator#getMoveDuration()}, otherwise, it returns |
| * {@link RecyclerView.ItemAnimator#getRemoveDuration()}. If RecyclerView does not have |
| * any {@link RecyclerView.ItemAnimator} attached, this method returns |
| * {@code DEFAULT_DRAG_ANIMATION_DURATION} or {@code DEFAULT_SWIPE_ANIMATION_DURATION} |
| * depending on the animation type. |
| * |
| * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. |
| * @param animationType The type of animation. Is one of {@link #ANIMATION_TYPE_DRAG}, |
| * {@link #ANIMATION_TYPE_SWIPE_CANCEL} or |
| * {@link #ANIMATION_TYPE_SWIPE_SUCCESS}. |
| * @param animateDx The horizontal distance that the animation will offset |
| * @param animateDy The vertical distance that the animation will offset |
| * @return The duration for the animation |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, |
| float animateDx, float animateDy) { |
| final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); |
| if (itemAnimator == null) { |
| return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION |
| : DEFAULT_SWIPE_ANIMATION_DURATION; |
| } else { |
| return animationType == ANIMATION_TYPE_DRAG ? itemAnimator.getMoveDuration() |
| : itemAnimator.getRemoveDuration(); |
| } |
| } |
| |
| /** |
| * Called by the ItemTouchHelper when user is dragging a view out of bounds. |
| * <p> |
| * You can override this method to decide how much RecyclerView should scroll in response |
| * to this action. Default implementation calculates a value based on the amount of View |
| * out of bounds and the time it spent there. The longer user keeps the View out of bounds, |
| * the faster the list will scroll. Similarly, the larger portion of the View is out of |
| * bounds, the faster the RecyclerView will scroll. |
| * |
| * @param recyclerView The RecyclerView instance to which ItemTouchHelper is |
| * attached to. |
| * @param viewSize The total size of the View in scroll direction, excluding |
| * item decorations. |
| * @param viewSizeOutOfBounds The total size of the View that is out of bounds. This value |
| * is negative if the View is dragged towards left or top edge. |
| * @param totalSize The total size of RecyclerView in the scroll direction. |
| * @param msSinceStartScroll The time passed since View is kept out of bounds. |
| * @return The amount that RecyclerView should scroll. Keep in mind that this value will |
| * be passed to {@link RecyclerView#scrollBy(int, int)} method. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public int interpolateOutOfBoundsScroll(@NonNull RecyclerView recyclerView, |
| int viewSize, int viewSizeOutOfBounds, |
| int totalSize, long msSinceStartScroll) { |
| final int maxScroll = getMaxDragScroll(recyclerView); |
| final int absOutOfBounds = Math.abs(viewSizeOutOfBounds); |
| final int direction = (int) Math.signum(viewSizeOutOfBounds); |
| // might be negative if other direction |
| float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize); |
| final int cappedScroll = (int) (direction * maxScroll |
| * sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio)); |
| final float timeRatio; |
| if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) { |
| timeRatio = 1f; |
| } else { |
| timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS; |
| } |
| final int value = (int) (cappedScroll * sDragScrollInterpolator |
| .getInterpolation(timeRatio)); |
| if (value == 0) { |
| return viewSizeOutOfBounds > 0 ? 1 : -1; |
| } |
| return value; |
| } |
| } |
| |
| /** |
| * A simple wrapper to the default Callback which you can construct with drag and swipe |
| * directions and this class will handle the flag callbacks. You should still override onMove |
| * or |
| * onSwiped depending on your use case. |
| * |
| * <pre> |
| * ItemTouchHelper mIth = new ItemTouchHelper( |
| * new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, |
| * ItemTouchHelper.LEFT) { |
| * public boolean onMove(RecyclerView recyclerView, |
| * ViewHolder viewHolder, ViewHolder target) { |
| * final int fromPos = viewHolder.getAdapterPosition(); |
| * final int toPos = target.getAdapterPosition(); |
| * // move item in `fromPos` to `toPos` in adapter. |
| * return true;// true if moved, false otherwise |
| * } |
| * public void onSwiped(ViewHolder viewHolder, int direction) { |
| * // remove from adapter |
| * } |
| * }); |
| * </pre> |
| */ |
| public abstract static class SimpleCallback extends Callback { |
| |
| private int mDefaultSwipeDirs; |
| |
| private int mDefaultDragDirs; |
| |
| /** |
| * Creates a Callback for the given drag and swipe allowance. These values serve as |
| * defaults |
| * and if you want to customize behavior per ViewHolder, you can override |
| * {@link #getSwipeDirs(RecyclerView, ViewHolder)} |
| * and / or {@link #getDragDirs(RecyclerView, ViewHolder)}. |
| * |
| * @param dragDirs Binary OR of direction flags in which the Views can be dragged. Must be |
| * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link |
| * #END}, |
| * {@link #UP} and {@link #DOWN}. |
| * @param swipeDirs Binary OR of direction flags in which the Views can be swiped. Must be |
| * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link |
| * #END}, |
| * {@link #UP} and {@link #DOWN}. |
| */ |
| public SimpleCallback(int dragDirs, int swipeDirs) { |
| mDefaultSwipeDirs = swipeDirs; |
| mDefaultDragDirs = dragDirs; |
| } |
| |
| /** |
| * Updates the default swipe directions. For example, you can use this method to toggle |
| * certain directions depending on your use case. |
| * |
| * @param defaultSwipeDirs Binary OR of directions in which the ViewHolders can be swiped. |
| */ |
| @SuppressWarnings({"WeakerAccess", "unused"}) |
| public void setDefaultSwipeDirs(@SuppressWarnings("unused") int defaultSwipeDirs) { |
| mDefaultSwipeDirs = defaultSwipeDirs; |
| } |
| |
| /** |
| * Updates the default drag directions. For example, you can use this method to toggle |
| * certain directions depending on your use case. |
| * |
| * @param defaultDragDirs Binary OR of directions in which the ViewHolders can be dragged. |
| */ |
| @SuppressWarnings({"WeakerAccess", "unused"}) |
| public void setDefaultDragDirs(@SuppressWarnings("unused") int defaultDragDirs) { |
| mDefaultDragDirs = defaultDragDirs; |
| } |
| |
| /** |
| * Returns the swipe directions for the provided ViewHolder. |
| * Default implementation returns the swipe directions that was set via constructor or |
| * {@link #setDefaultSwipeDirs(int)}. |
| * |
| * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. |
| * @param viewHolder The ViewHolder for which the swipe direction is queried. |
| * @return A binary OR of direction flags. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public int getSwipeDirs(@SuppressWarnings("unused") @NonNull RecyclerView recyclerView, |
| @NonNull @SuppressWarnings("unused") ViewHolder viewHolder) { |
| return mDefaultSwipeDirs; |
| } |
| |
| /** |
| * Returns the drag directions for the provided ViewHolder. |
| * Default implementation returns the drag directions that was set via constructor or |
| * {@link #setDefaultDragDirs(int)}. |
| * |
| * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. |
| * @param viewHolder The ViewHolder for which the swipe direction is queried. |
| * @return A binary OR of direction flags. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public int getDragDirs(@SuppressWarnings("unused") @NonNull RecyclerView recyclerView, |
| @SuppressWarnings("unused") @NonNull ViewHolder viewHolder) { |
| return mDefaultDragDirs; |
| } |
| |
| @Override |
| public int getMovementFlags(@NonNull RecyclerView recyclerView, |
| @NonNull ViewHolder viewHolder) { |
| return makeMovementFlags(getDragDirs(recyclerView, viewHolder), |
| getSwipeDirs(recyclerView, viewHolder)); |
| } |
| } |
| |
| private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { |
| |
| /** |
| * Whether to execute code in response to the the invoking of |
| * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)}. |
| * |
| * It is necessary to control this here because |
| * {@link GestureDetector.SimpleOnGestureListener} can only be set on a |
| * {@link GestureDetector} in a GestureDetector's constructor, a GestureDetector will call |
| * onLongPress if an {@link MotionEvent#ACTION_DOWN} event is not followed by another event |
| * that would cancel it (like {@link MotionEvent#ACTION_UP} or |
| * {@link MotionEvent#ACTION_CANCEL}), the long press responding to the long press event |
| * needs to be cancellable to prevent unexpected behavior. |
| * |
| * @see #doNotReactToLongPress() |
| */ |
| private boolean mShouldReactToLongPress = true; |
| |
| ItemTouchHelperGestureListener() { |
| } |
| |
| /** |
| * Call to prevent executing code in response to |
| * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)} being called. |
| */ |
| void doNotReactToLongPress() { |
| mShouldReactToLongPress = false; |
| } |
| |
| @Override |
| public boolean onDown(MotionEvent e) { |
| return true; |
| } |
| |
| @Override |
| public void onLongPress(MotionEvent e) { |
| if (!mShouldReactToLongPress) { |
| return; |
| } |
| View child = findChildView(e); |
| if (child != null) { |
| ViewHolder vh = mRecyclerView.getChildViewHolder(child); |
| if (vh != null) { |
| if (!mCallback.hasDragFlag(mRecyclerView, vh)) { |
| return; |
| } |
| int pointerId = e.getPointerId(0); |
| // Long press is deferred. |
| // Check w/ active pointer id to avoid selecting after motion |
| // event is canceled. |
| if (pointerId == mActivePointerId) { |
| final int index = e.findPointerIndex(mActivePointerId); |
| final float x = e.getX(index); |
| final float y = e.getY(index); |
| mInitialTouchX = x; |
| mInitialTouchY = y; |
| mDx = mDy = 0f; |
| if (DEBUG) { |
| Log.d(TAG, |
| "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY); |
| } |
| if (mCallback.isLongPressDragEnabled()) { |
| select(vh, ACTION_STATE_DRAG); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| static class RecoverAnimation implements Animator.AnimatorListener { |
| |
| final float mStartDx; |
| |
| final float mStartDy; |
| |
| final float mTargetX; |
| |
| final float mTargetY; |
| |
| final ViewHolder mViewHolder; |
| |
| final int mActionState; |
| |
| @VisibleForTesting |
| final ValueAnimator mValueAnimator; |
| |
| final int mAnimationType; |
| |
| boolean mIsPendingCleanup; |
| |
| float mX; |
| |
| float mY; |
| |
| // if user starts touching a recovering view, we put it into interaction mode again, |
| // instantly. |
| boolean mOverridden = false; |
| |
| boolean mEnded = false; |
| |
| private float mFraction; |
| |
| RecoverAnimation(ViewHolder viewHolder, int animationType, |
| int actionState, float startDx, float startDy, float targetX, float targetY) { |
| mActionState = actionState; |
| mAnimationType = animationType; |
| mViewHolder = viewHolder; |
| mStartDx = startDx; |
| mStartDy = startDy; |
| mTargetX = targetX; |
| mTargetY = targetY; |
| mValueAnimator = ValueAnimator.ofFloat(0f, 1f); |
| mValueAnimator.addUpdateListener( |
| new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| setFraction(animation.getAnimatedFraction()); |
| } |
| }); |
| mValueAnimator.setTarget(viewHolder.itemView); |
| mValueAnimator.addListener(this); |
| setFraction(0f); |
| } |
| |
| public void setDuration(long duration) { |
| mValueAnimator.setDuration(duration); |
| } |
| |
| public void start() { |
| mViewHolder.setIsRecyclable(false); |
| mValueAnimator.start(); |
| } |
| |
| public void cancel() { |
| mValueAnimator.cancel(); |
| } |
| |
| public void setFraction(float fraction) { |
| mFraction = fraction; |
| } |
| |
| /** |
| * We run updates on onDraw method but use the fraction from animator callback. |
| * This way, we can sync translate x/y values w/ the animators to avoid one-off frames. |
| */ |
| public void update() { |
| if (mStartDx == mTargetX) { |
| mX = mViewHolder.itemView.getTranslationX(); |
| } else { |
| mX = mStartDx + mFraction * (mTargetX - mStartDx); |
| } |
| if (mStartDy == mTargetY) { |
| mY = mViewHolder.itemView.getTranslationY(); |
| } else { |
| mY = mStartDy + mFraction * (mTargetY - mStartDy); |
| } |
| } |
| |
| @Override |
| public void onAnimationStart(Animator animation) { |
| |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (!mEnded) { |
| mViewHolder.setIsRecyclable(true); |
| } |
| mEnded = true; |
| } |
| |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| setFraction(1f); //make sure we recover the view's state. |
| } |
| |
| @Override |
| public void onAnimationRepeat(Animator animation) { |
| |
| } |
| } |
| } |