aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDocile-Alligator <25734209+Docile-Alligator@users.noreply.github.com>2024-10-29 18:33:32 +0000
committerDocile-Alligator <25734209+Docile-Alligator@users.noreply.github.com>2024-10-29 18:33:32 +0000
commit8663477f6a6d7f51eb1508879868004fce5bb26c (patch)
tree5bd36679e52afef7eafd6b0cfe6239e8592ef9a4
parent14bcbd5f58494efe044543e943976faea523d650 (diff)
downloadinfinity-for-reddit-8663477f6a6d7f51eb1508879868004fce5bb26c.tar
infinity-for-reddit-8663477f6a6d7f51eb1508879868004fce5bb26c.tar.gz
infinity-for-reddit-8663477f6a6d7f51eb1508879868004fce5bb26c.tar.bz2
infinity-for-reddit-8663477f6a6d7f51eb1508879868004fce5bb26c.tar.lz
infinity-for-reddit-8663477f6a6d7f51eb1508879868004fce5bb26c.tar.xz
infinity-for-reddit-8663477f6a6d7f51eb1508879868004fce5bb26c.tar.zst
infinity-for-reddit-8663477f6a6d7f51eb1508879868004fce5bb26c.zip
Reduce ItemTouchHelper sensitivity in ViewPostDetailFragment by using AdjustableTouchSlopItemTouchHelper. Refine ItemTouchHelper onChildDraw in CommentsListingFragment.
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsRecyclerViewAdapter.java8
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/customviews/AdjustableTouchSlopItemTouchHelper.java2533
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/fragments/CommentsListingFragment.java116
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/fragments/HistoryPostFragment.java2
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragment.java2
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragmentBase.java9
-rw-r--r--app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewPostDetailFragment.java7
7 files changed, 2610 insertions, 67 deletions
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsRecyclerViewAdapter.java
index 0021c8e3..6d25253b 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsRecyclerViewAdapter.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsRecyclerViewAdapter.java
@@ -9,7 +9,6 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.Spanned;
-import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -41,9 +40,6 @@ import io.noties.markwon.MarkwonPlugin;
import io.noties.markwon.core.MarkwonTheme;
import jp.wasabeef.glide.transformations.RoundedCornersTransformation;
import ml.docilealligator.infinityforreddit.R;
-import ml.docilealligator.infinityforreddit.thing.SaveThing;
-import ml.docilealligator.infinityforreddit.thing.SortType;
-import ml.docilealligator.infinityforreddit.thing.VoteThing;
import ml.docilealligator.infinityforreddit.account.Account;
import ml.docilealligator.infinityforreddit.activities.BaseActivity;
import ml.docilealligator.infinityforreddit.activities.CommentActivity;
@@ -78,6 +74,9 @@ import ml.docilealligator.infinityforreddit.markdown.ImageAndGifEntry;
import ml.docilealligator.infinityforreddit.markdown.ImageAndGifPlugin;
import ml.docilealligator.infinityforreddit.markdown.MarkdownUtils;
import ml.docilealligator.infinityforreddit.post.Post;
+import ml.docilealligator.infinityforreddit.thing.SaveThing;
+import ml.docilealligator.infinityforreddit.thing.SortType;
+import ml.docilealligator.infinityforreddit.thing.VoteThing;
import ml.docilealligator.infinityforreddit.utils.APIUtils;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import ml.docilealligator.infinityforreddit.utils.Utils;
@@ -1062,7 +1061,6 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
if (mIsSingleCommentThreadMode) {
notifyItemChanged(position + 1);
- Log.i("asdfasdf", "asdf");
} else {
notifyItemChanged(position);
}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/AdjustableTouchSlopItemTouchHelper.java b/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/AdjustableTouchSlopItemTouchHelper.java
new file mode 100644
index 00000000..15fecd39
--- /dev/null
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/AdjustableTouchSlopItemTouchHelper.java
@@ -0,0 +1,2533 @@
+package ml.docilealligator.infinityforreddit.customviews;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+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.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.ItemTouchUIUtil;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import ml.docilealligator.infinityforreddit.R;
+
+public class AdjustableTouchSlopItemTouchHelper 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 */
+ RecyclerView.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
+ */
+ @NonNull
+ Callback mCallback;
+
+ /**
+ * Current mode.
+ */
+ private int mActionState = ACTION_STATE_IDLE;
+
+ /**
+ * The direction flags obtained from unmasking
+ * {@link Callback#getAbsoluteMovementFlags(RecyclerView, 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<RecyclerView.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 AdjustableTouchSlopItemTouchHelperGestureListener mItemTouchHelperGestureListener;
+
+ private final RecyclerView.OnItemTouchListener mOnItemTouchListener = new RecyclerView.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);
+ }
+ RecyclerView.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 AdjustableTouchSlopItemTouchHelper(@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, float touchSlopCoefficient) {
+ 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(touchSlopCoefficient);
+ }
+ }
+
+ private void setupCallbacks(float touchSlopCoefficient) {
+ ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
+ mSlop = (int) (vc.getScaledTouchSlop() * touchSlopCoefficient);
+ 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 AdjustableTouchSlopItemTouchHelperGestureListener();
+ 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(
+ @NonNull Canvas c,
+ @NonNull RecyclerView parent,
+ @NonNull 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
+ @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
+ 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 RecyclerView.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 RecyclerView.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<RecyclerView.ViewHolder> findSwapTargets(RecyclerView.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 RecyclerView.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(RecyclerView.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<RecyclerView.ViewHolder> swapTargets = findSwapTargets(viewHolder);
+ if (swapTargets.size() == 0) {
+ return;
+ }
+ // may swap.
+ RecyclerView.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 RecyclerView.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(RecyclerView.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
+ @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
+ 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 RecyclerView.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 RecyclerView.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 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 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) {
+ * mstartDrag(viewHolder);
+ * }
+ * return false;
+ * }
+ * });
+ * </pre>
+ * <p>
+ *
+ * @param viewHolder The ViewHolder to start dragging. It must be a direct child of
+ * RecyclerView.
+ * @see Callback#isItemViewSwipeEnabled()
+ */
+ public void startDrag(@NonNull RecyclerView.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 ");
+ 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 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 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) {
+ * mstartSwipe(viewHolder);
+ * }
+ * return false;
+ * }
+ * });
+ * </pre>
+ *
+ * @param viewHolder The ViewHolder to start swiping. It must be a direct child of
+ * RecyclerView.
+ */
+ public void startSwipe(@NonNull RecyclerView.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 ");
+ 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(RecyclerView.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(RecyclerView.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(RecyclerView.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, 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, RecyclerView.ViewHolder, RecyclerView.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, RecyclerView.ViewHolder, RecyclerView.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(RecyclerView.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(RecyclerView.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 RecyclerView.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,
+ RecyclerView.ViewHolder viewHolder) {
+ final int flags = getMovementFlags(recyclerView, viewHolder);
+ return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView));
+ }
+
+ boolean hasDragFlag(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
+ final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder);
+ return (flags & ACTION_MODE_DRAG_MASK) != 0;
+ }
+
+ boolean hasSwipeFlag(RecyclerView 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(RecyclerView.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 RecyclerView.ViewHolder current,
+ @NonNull RecyclerView.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 RecyclerView.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, RecyclerView.ViewHolder, int, RecyclerView.ViewHolder, int, int, int)
+ */
+ public abstract boolean onMove(@NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.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(RecyclerView.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(RecyclerView.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(RecyclerView.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(RecyclerView.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 RecyclerView.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 RecyclerView.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
+ *
+ * @return The minimum swipe velocity. The default implementation returns the
+ * <code>defaultValue</code> parameter.
+ * @see #getSwipeVelocityThreshold(float)
+ * @see #getSwipeThreshold(RecyclerView.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
+ * @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")
+ @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
+ public RecyclerView.ViewHolder chooseDropTarget(@NonNull RecyclerView.ViewHolder selected,
+ @NonNull List<RecyclerView.ViewHolder> dropTargets, int curX, int curY) {
+ int right = curX + selected.itemView.getWidth();
+ int bottom = curY + selected.itemView.getHeight();
+ RecyclerView.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 RecyclerView.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, 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, 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, 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 RecyclerView.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 RecyclerView.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, RecyclerView.ViewHolder, RecyclerView.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
+ * @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 RecyclerView.ViewHolder viewHolder, int fromPos, @NonNull final RecyclerView.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, RecyclerView.ViewHolder selected,
+ List<RecoverAnimation> recoverAnimationList,
+ int actionState, float dX, float dY) {
+ final int recoverAnimSize = recoverAnimationList.size();
+ for (int i = 0; i < recoverAnimSize; i++) {
+ final 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, RecyclerView.ViewHolder selected,
+ List<RecoverAnimation> recoverAnimationList,
+ int actionState, float dX, float dY) {
+ final int recoverAnimSize = recoverAnimationList.size();
+ for (int i = 0; i < recoverAnimSize; i++) {
+ final 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, RecyclerView.ViewHolder, float, float, int,
+ * boolean)} or
+ * {@link #onChildDrawOver(Canvas, RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)}.
+ *
+ * @param recyclerView The RecyclerView which is controlled by the
+ * @param viewHolder The View that was interacted by the user.
+ */
+ public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.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, RecyclerView.ViewHolder, float, float, int,
+ * boolean)
+ */
+ public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.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, RecyclerView.ViewHolder, float, float, int,
+ * boolean)
+ */
+ public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
+ @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
+ 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 SimpleCallback(UP | DOWN,
+ * 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, RecyclerView.ViewHolder)}
+ * and / or {@link #getDragDirs(RecyclerView, 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") RecyclerView.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 RecyclerView.ViewHolder viewHolder) {
+ return mDefaultDragDirs;
+ }
+
+ @Override
+ public int getMovementFlags(@NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.ViewHolder viewHolder) {
+ return makeMovementFlags(getDragDirs(recyclerView, viewHolder),
+ getSwipeDirs(recyclerView, viewHolder));
+ }
+ }
+
+ private class AdjustableTouchSlopItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
+
+ /**
+ * Whether to execute code in response to the the invoking of
+ * {@link AdjustableTouchSlopItemTouchHelperGestureListener#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;
+
+ AdjustableTouchSlopItemTouchHelperGestureListener() {
+ }
+
+ /**
+ * Call to prevent executing code in response to
+ * {@link AdjustableTouchSlopItemTouchHelperGestureListener#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) {
+ RecyclerView.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 RecyclerView.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(RecyclerView.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) {
+
+ }
+ }
+
+ private static class ItemTouchUIUtilImpl implements ItemTouchUIUtil {
+ static final ItemTouchUIUtil INSTANCE = new ItemTouchUIUtilImpl();
+
+ @Override
+ public void onDraw(
+ @NonNull Canvas c,
+ @NonNull RecyclerView recyclerView,
+ @NonNull View view,
+ float dX,
+ float dY,
+ int actionState,
+ boolean isCurrentlyActive
+ ) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ if (isCurrentlyActive) {
+ Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
+ if (originalElevation == null) {
+ originalElevation = ViewCompat.getElevation(view);
+ float newElevation = 1f + findMaxElevation(recyclerView, view);
+ ViewCompat.setElevation(view, newElevation);
+ view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
+ }
+ }
+ }
+
+ view.setTranslationX(dX);
+ view.setTranslationY(dY);
+ }
+
+ private static float findMaxElevation(RecyclerView recyclerView, View itemView) {
+ final int childCount = recyclerView.getChildCount();
+ float max = 0;
+ for (int i = 0; i < childCount; i++) {
+ final View child = recyclerView.getChildAt(i);
+ if (child == itemView) {
+ continue;
+ }
+ final float elevation = ViewCompat.getElevation(child);
+ if (elevation > max) {
+ max = elevation;
+ }
+ }
+ return max;
+ }
+
+ @Override
+ public void onDrawOver(
+ @NonNull Canvas c,
+ @NonNull RecyclerView recyclerView,
+ @NonNull View view,
+ float dX,
+ float dY,
+ int actionState,
+ boolean isCurrentlyActive
+ ) {
+ }
+
+ @Override
+ public void clearView(@NonNull View view) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);
+ if (tag instanceof Float) {
+ ViewCompat.setElevation(view, (Float) tag);
+ }
+ view.setTag(R.id.item_touch_helper_previous_elevation, null);
+ }
+
+ view.setTranslationX(0f);
+ view.setTranslationY(0f);
+ }
+
+ @Override
+ public void onSelected(@NonNull View view) {
+ }
+ }
+}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/CommentsListingFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/CommentsListingFragment.java
index f8e5968b..f9b4ddaf 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/CommentsListingFragment.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/CommentsListingFragment.java
@@ -13,6 +13,7 @@ import android.os.Handler;
import android.os.Looper;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
@@ -40,6 +41,7 @@ import ml.docilealligator.infinityforreddit.NetworkState;
import ml.docilealligator.infinityforreddit.R;
import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface;
import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase;
+import ml.docilealligator.infinityforreddit.customviews.AdjustableTouchSlopItemTouchHelper;
import ml.docilealligator.infinityforreddit.thing.ReplyNotificationsToggle;
import ml.docilealligator.infinityforreddit.thing.SortType;
import ml.docilealligator.infinityforreddit.account.Account;
@@ -101,7 +103,8 @@ public class CommentsListingFragment extends Fragment implements FragmentCommuni
private int swipeLeftAction;
private int swipeRightAction;
private float swipeActionThreshold;
- private ItemTouchHelper touchHelper;
+ private AdjustableTouchSlopItemTouchHelper touchHelper;
+ private boolean shouldSwipeBack;
private FragmentCommentsListingBinding binding;
public CommentsListingFragment() {
@@ -140,7 +143,7 @@ public class CommentsListingFragment extends Fragment implements FragmentCommuni
swipeRightAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_RIGHT_ACTION, "1"));
swipeLeftAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_LEFT_ACTION, "0"));
initializeSwipeActionDrawable();
- touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
+ touchHelper = new AdjustableTouchSlopItemTouchHelper(new AdjustableTouchSlopItemTouchHelper.Callback() {
boolean exceedThreshold = false;
@Override
@@ -163,69 +166,71 @@ public class CommentsListingFragment extends Fragment implements FragmentCommuni
}
@Override
- public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
- if (touchHelper != null) {
- exceedThreshold = false;
- touchHelper.attachToRecyclerView(null);
- touchHelper.attachToRecyclerView(binding.recyclerViewCommentsListingFragment);
+ public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {}
+
+ @Override
+ public int convertToAbsoluteDirection(int flags, int layoutDirection) {
+ if (shouldSwipeBack) {
+ shouldSwipeBack = false;
+ return 0;
}
+ return super.convertToAbsoluteDirection(flags, layoutDirection);
}
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
- super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
-
- if (isCurrentlyActive) {
- View itemView = viewHolder.itemView;
- int horizontalOffset = (int) Utils.convertDpToPixel(16, mActivity);
- if (dX > 0) {
- if (dX > (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold) {
- if (!exceedThreshold) {
- exceedThreshold = true;
- if (vibrateWhenActionTriggered) {
- viewHolder.itemView.setHapticFeedbackEnabled(true);
- viewHolder.itemView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
- }
+ View itemView = viewHolder.itemView;
+ int horizontalOffset = (int) Utils.convertDpToPixel(16, mActivity);
+ if (dX > 0) {
+ if (dX > (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold) {
+ dX = (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold;
+ if (!exceedThreshold && isCurrentlyActive) {
+ exceedThreshold = true;
+ if (vibrateWhenActionTriggered) {
+ itemView.setHapticFeedbackEnabled(true);
+ itemView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
}
- backgroundSwipeRight.setBounds(0, itemView.getTop(), itemView.getRight(), itemView.getBottom());
- } else {
- exceedThreshold = false;
- backgroundSwipeRight.setBounds(0, 0, 0, 0);
}
+ backgroundSwipeRight.setBounds(0, itemView.getTop(), itemView.getRight(), itemView.getBottom());
+ } else {
+ exceedThreshold = false;
+ backgroundSwipeRight.setBounds(0, 0, 0, 0);
+ }
- drawableSwipeRight.setBounds(itemView.getLeft() + ((int) dX) - horizontalOffset - drawableSwipeRight.getIntrinsicWidth(),
- (itemView.getBottom() + itemView.getTop() - drawableSwipeRight.getIntrinsicHeight()) / 2,
- itemView.getLeft() + ((int) dX) - horizontalOffset,
- (itemView.getBottom() + itemView.getTop() + drawableSwipeRight.getIntrinsicHeight()) / 2);
- backgroundSwipeRight.draw(c);
- drawableSwipeRight.draw(c);
- } else if (dX < 0) {
- if (-dX > (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold) {
- if (!exceedThreshold) {
- exceedThreshold = true;
- if (vibrateWhenActionTriggered) {
- viewHolder.itemView.setHapticFeedbackEnabled(true);
- viewHolder.itemView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
- }
+ drawableSwipeRight.setBounds(itemView.getLeft() + ((int) dX) - horizontalOffset - drawableSwipeRight.getIntrinsicWidth(),
+ (itemView.getBottom() + itemView.getTop() - drawableSwipeRight.getIntrinsicHeight()) / 2,
+ itemView.getLeft() + ((int) dX) - horizontalOffset,
+ (itemView.getBottom() + itemView.getTop() + drawableSwipeRight.getIntrinsicHeight()) / 2);
+ backgroundSwipeRight.draw(c);
+ drawableSwipeRight.draw(c);
+ } else if (dX < 0) {
+ if (-dX > (itemView.getRight() - itemView.getLeft()) * swipeActionThreshold) {
+ dX = -(itemView.getRight() - itemView.getLeft()) * swipeActionThreshold;
+ if (!exceedThreshold && isCurrentlyActive) {
+ exceedThreshold = true;
+ if (vibrateWhenActionTriggered) {
+ itemView.setHapticFeedbackEnabled(true);
+ itemView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
}
- backgroundSwipeLeft.setBounds(0, itemView.getTop(), itemView.getRight(), itemView.getBottom());
- } else {
- exceedThreshold = false;
- backgroundSwipeLeft.setBounds(0, 0, 0, 0);
}
- drawableSwipeLeft.setBounds(itemView.getRight() + ((int) dX) + horizontalOffset,
- (itemView.getBottom() + itemView.getTop() - drawableSwipeLeft.getIntrinsicHeight()) / 2,
- itemView.getRight() + ((int) dX) + horizontalOffset + drawableSwipeLeft.getIntrinsicWidth(),
- (itemView.getBottom() + itemView.getTop() + drawableSwipeLeft.getIntrinsicHeight()) / 2);
- backgroundSwipeLeft.draw(c);
- drawableSwipeLeft.draw(c);
- }
- } else {
- if (exceedThreshold) {
- mAdapter.onItemSwipe(viewHolder, dX > 0 ? ItemTouchHelper.END : ItemTouchHelper.START, swipeLeftAction, swipeRightAction);
+ backgroundSwipeLeft.setBounds(0, itemView.getTop(), itemView.getRight(), itemView.getBottom());
+ } else {
exceedThreshold = false;
+ backgroundSwipeLeft.setBounds(0, 0, 0, 0);
}
+ drawableSwipeLeft.setBounds(itemView.getRight() + ((int) dX) + horizontalOffset,
+ (itemView.getBottom() + itemView.getTop() - drawableSwipeLeft.getIntrinsicHeight()) / 2,
+ itemView.getRight() + ((int) dX) + horizontalOffset + drawableSwipeLeft.getIntrinsicWidth(),
+ (itemView.getBottom() + itemView.getTop() + drawableSwipeLeft.getIntrinsicHeight()) / 2);
+ backgroundSwipeLeft.draw(c);
+ drawableSwipeLeft.draw(c);
+ }
+
+ if (!isCurrentlyActive && exceedThreshold) {
+ mAdapter.onItemSwipe(viewHolder, dX > 0 ? ItemTouchHelper.END : ItemTouchHelper.START, swipeLeftAction, swipeRightAction);
+ exceedThreshold = false;
}
+ super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
@Override
@@ -234,8 +239,13 @@ public class CommentsListingFragment extends Fragment implements FragmentCommuni
}
});
+ binding.recyclerViewCommentsListingFragment.setOnTouchListener((view, motionEvent) -> {
+ shouldSwipeBack = motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP;
+ return false;
+ });
+
if (enableSwipeAction) {
- touchHelper.attachToRecyclerView(binding.recyclerViewCommentsListingFragment);
+ touchHelper.attachToRecyclerView(binding.recyclerViewCommentsListingFragment, 1);
}
new Handler().postDelayed(() -> bindView(resources), 0);
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/HistoryPostFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/HistoryPostFragment.java
index c7f1b284..0667e4f4 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/HistoryPostFragment.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/HistoryPostFragment.java
@@ -269,7 +269,7 @@ public class HistoryPostFragment extends PostFragmentBase implements FragmentCom
if (nColumns == 1 && mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_SWIPE_ACTION, false)) {
swipeActionEnabled = true;
- touchHelper.attachToRecyclerView(binding.recyclerViewHistoryPostFragment);
+ touchHelper.attachToRecyclerView(binding.recyclerViewHistoryPostFragment, 1);
}
binding.recyclerViewHistoryPostFragment.setAdapter(mAdapter);
binding.recyclerViewHistoryPostFragment.setCacheManager(mAdapter);
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragment.java
index 8acbc099..40ba795f 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragment.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragment.java
@@ -817,7 +817,7 @@ public class PostFragment extends PostFragmentBase implements FragmentCommunicat
if (nColumns == 1 && mSharedPreferences.getBoolean(SharedPreferencesUtils.ENABLE_SWIPE_ACTION, false)) {
swipeActionEnabled = true;
- touchHelper.attachToRecyclerView(binding.recyclerViewPostFragment);
+ touchHelper.attachToRecyclerView(binding.recyclerViewPostFragment, 1);
}
binding.recyclerViewPostFragment.setAdapter(mAdapter);
binding.recyclerViewPostFragment.setCacheManager(mAdapter);
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragmentBase.java b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragmentBase.java
index 10d10558..ae3c6fb1 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragmentBase.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/PostFragmentBase.java
@@ -56,6 +56,7 @@ import ml.docilealligator.infinityforreddit.adapters.PostRecyclerViewAdapter;
import ml.docilealligator.infinityforreddit.asynctasks.LoadSubredditIcon;
import ml.docilealligator.infinityforreddit.asynctasks.LoadUserData;
import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper;
+import ml.docilealligator.infinityforreddit.customviews.AdjustableTouchSlopItemTouchHelper;
import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed;
import ml.docilealligator.infinityforreddit.events.ChangeAutoplayNsfwVideosEvent;
import ml.docilealligator.infinityforreddit.events.ChangeCompactLayoutToolbarHiddenByDefaultEvent;
@@ -143,7 +144,7 @@ public abstract class PostFragmentBase extends Fragment {
protected float swipeActionThreshold;
protected int swipeLeftAction;
protected int swipeRightAction;
- protected ItemTouchHelper touchHelper;
+ protected AdjustableTouchSlopItemTouchHelper touchHelper;
private boolean shouldSwipeBack;
protected final Map<String, String> subredditOrUserIcons = new HashMap<>();
@@ -216,7 +217,7 @@ public abstract class PostFragmentBase extends Fragment {
swipeLeftAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_LEFT_ACTION, "0"));
initializeSwipeActionDrawable();
- touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
+ touchHelper = new AdjustableTouchSlopItemTouchHelper(new AdjustableTouchSlopItemTouchHelper.Callback() {
boolean exceedThreshold = false;
@Override
@@ -767,9 +768,9 @@ public abstract class PostFragmentBase extends Fragment {
if (getNColumns(getResources()) == 1 && touchHelper != null) {
swipeActionEnabled = changeEnableSwipeActionSwitchEvent.enableSwipeAction;
if (changeEnableSwipeActionSwitchEvent.enableSwipeAction) {
- touchHelper.attachToRecyclerView(getPostRecyclerView());
+ touchHelper.attachToRecyclerView(getPostRecyclerView(), 1);
} else {
- touchHelper.attachToRecyclerView(null);
+ touchHelper.attachToRecyclerView(null, 1);
}
}
}
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewPostDetailFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewPostDetailFragment.java
index 28b0a377..87d8ec4f 100644
--- a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewPostDetailFragment.java
+++ b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewPostDetailFragment.java
@@ -85,6 +85,7 @@ import ml.docilealligator.infinityforreddit.comment.ParseComment;
import ml.docilealligator.infinityforreddit.commentfilter.CommentFilter;
import ml.docilealligator.infinityforreddit.commentfilter.FetchCommentFilter;
import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper;
+import ml.docilealligator.infinityforreddit.customviews.AdjustableTouchSlopItemTouchHelper;
import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed;
import ml.docilealligator.infinityforreddit.databinding.FragmentViewPostDetailBinding;
import ml.docilealligator.infinityforreddit.events.ChangeNSFWBlurEvent;
@@ -229,7 +230,7 @@ public class ViewPostDetailFragment extends Fragment implements FragmentCommunic
private int swipeLeftAction;
private int swipeRightAction;
private float swipeActionThreshold;
- private ItemTouchHelper touchHelper;
+ private AdjustableTouchSlopItemTouchHelper touchHelper;
private boolean shouldSwipeBack;
private int scrollPosition;
private FragmentViewPostDetailBinding binding;
@@ -411,7 +412,7 @@ public class ViewPostDetailFragment extends Fragment implements FragmentCommunic
swipeRightAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_RIGHT_ACTION, "1"));
swipeLeftAction = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.SWIPE_LEFT_ACTION, "0"));
initializeSwipeActionDrawable();
- touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
+ touchHelper = new AdjustableTouchSlopItemTouchHelper(new AdjustableTouchSlopItemTouchHelper.Callback() {
boolean exceedThreshold = false;
@Override
@@ -513,7 +514,7 @@ public class ViewPostDetailFragment extends Fragment implements FragmentCommunic
});
if (enableSwipeAction) {
- touchHelper.attachToRecyclerView((mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView));
+ touchHelper.attachToRecyclerView((mCommentsRecyclerView == null ? binding.postDetailRecyclerViewViewPostDetailFragment : mCommentsRecyclerView), 5);
}
binding.swipeRefreshLayoutViewPostDetailFragment.setOnRefreshListener(() -> refresh(true, true));