Java tutorial
package atownsend.swipeopenhelper; /* * Adapted from Google's android.support.v7.widget.helper.ItemTouchHelper * https://github.com/android/platform_frameworks_support/blob/master/v7/recyclerview/src/android/support/v7/widget/helper/ItemTouchHelper.java * * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Rect; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.v4.animation.AnimatorCompatHelper; import android.support.v4.animation.AnimatorListenerCompat; import android.support.v4.animation.AnimatorUpdateListenerCompat; import android.support.v4.animation.ValueAnimatorCompat; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; import android.util.Log; import android.util.SparseArray; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; import java.util.ArrayList; import java.util.List; /** * Helper class to allow for swiping open hidden views of a RecyclerView. * Adapted from and based off of Google's {@link android.support.v7.widget.helper.ItemTouchHelper} */ public class SwipeOpenItemTouchHelper extends RecyclerView.ItemDecoration implements RecyclerView.OnChildAttachStateChangeListener { private static final String OPENED_STATES = "opened_states"; /** * Up direction, used for swipe to open */ public static final int UP = 1; /** * Down direction, used for swipe to open */ public static final int DOWN = 1 << 1; /** * Left direction, used for swipe to open */ public static final int LEFT = 1 << 2; /** * Right direction, used for swipe to open */ 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 */ public static final int START = LEFT << 2; /** * Horizontal end direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout * direction */ public static final int END = RIGHT << 2; /** * SwipeOpenItemTouchHelper 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. */ public static final int ACTION_STATE_SWIPE = 1; /** * Animation type for views which are swiped and will animate back to an open or closed position */ public static final int ANIMATION_TYPE_SWIPE = 1 << 2; private static final String TAG = "SwipeOpenHelper"; private static final boolean DEBUG = false; private static final int ACTIVE_POINTER_ID_NONE = -1; private static final int DIRECTION_FLAG_COUNT = 8; private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1; private static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT; /** * Re-use array to calculate dx dy for a ViewHolder */ private final float[] tmpPosition = new float[2]; /** * Currently selected view holder */ private SwipeOpenViewHolder selected = null; /** * Initial touch point for swipe */ float initialTouchX; float initialTouchY; /** * The diff between the last event and initial touch. */ float dX; float dY; /** * 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. */ float selectedStartX; float selectedStartY; /** * The pointer we are tracking. */ int activePointerId = ACTIVE_POINTER_ID_NONE; /** * Developer callback which controls the behavior of ItemTouchHelper. */ Callback callback; /** * Current mode. */ int actionState = ACTION_STATE_IDLE; /** * The direction flags obtained from unmasking * {@link Callback#getAbsoluteMovementFlags(RecyclerView, RecyclerView.ViewHolder)} for the * current * action state. */ int selectedFlags; /** * When a View is swiped and needs to return to an open or closed position, 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. */ private List<RecoverAnimation> recoverAnimations = new ArrayList<>(); private int slop; private RecyclerView recyclerView; private boolean isRtl; /** * Flag for if any open SwipeOpenViewHolders should be close when the view is scrolled or if * a new view holder is swiped * DEFAULT: true */ private boolean closeOnAction = true; private SwipeOpenViewHolder prevSelected; /** * Used for detecting fling swipe */ private VelocityTracker velocityTracker; private SparseArray<SavedOpenState> openedPositions = new SparseArray<>(); /** * Data Observer that allow us to remove any opened positions when something is removed from the * adapter */ private final RecyclerView.AdapterDataObserver adapterDataObserver = new RecyclerView.AdapterDataObserver() { @Override public void onItemRangeRemoved(int positionStart, int itemCount) { // if an item is removed, we need to remove the opened position for (int i = positionStart; i < positionStart + itemCount; i++) { openedPositions.remove(i); } } }; private final RecyclerView.OnItemTouchListener mOnItemTouchListener = new RecyclerView.OnItemTouchListener() { @Override public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) { if (DEBUG) { Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event); } final int action = MotionEventCompat.getActionMasked(event); if (action == MotionEvent.ACTION_DOWN) { activePointerId = MotionEventCompat.getPointerId(event, 0); initialTouchX = event.getX(); initialTouchY = event.getY(); obtainVelocityTracker(); if (selected == null) { final RecoverAnimation animation = findAnimation(event); if (animation != null) { initialTouchX -= animation.mX; initialTouchY -= animation.mY; endRecoverAnimation(animation.mViewHolder, true); select(animation.mViewHolder, animation.mActionState); updateDxDy(event, selectedFlags, 0); } } } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { activePointerId = ACTIVE_POINTER_ID_NONE; select(null, ACTION_STATE_IDLE); } else if (activePointerId != ACTIVE_POINTER_ID_NONE) { // in a non scroll orientation, if distance change is above threshold, we // can select the item final int index = MotionEventCompat.findPointerIndex(event, activePointerId); if (DEBUG) { Log.d(TAG, "pointer index " + index); } if (index >= 0) { checkSelectForSwipe(action, event, index); } } if (velocityTracker != null) { velocityTracker.addMovement(event); } return selected != null; } @Override public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) { if (DEBUG) { Log.d(TAG, "on touch: x:" + initialTouchX + ",y:" + initialTouchY + ", :" + event); } if (velocityTracker != null) { velocityTracker.addMovement(event); } if (activePointerId == ACTIVE_POINTER_ID_NONE) { return; } final int action = MotionEventCompat.getActionMasked(event); final int activePointerIndex = MotionEventCompat.findPointerIndex(event, activePointerId); if (activePointerIndex >= 0) { checkSelectForSwipe(action, event, activePointerIndex); } if (selected == null) { return; } switch (action) { case MotionEvent.ACTION_MOVE: { // Find the index of the active pointer and fetch its position if (activePointerIndex >= 0) { updateDxDy(event, selectedFlags, activePointerIndex); SwipeOpenItemTouchHelper.this.recyclerView.invalidate(); } break; } case MotionEvent.ACTION_CANCEL: if (velocityTracker != null) { velocityTracker.clear(); } // fall through case MotionEvent.ACTION_UP: select(null, ACTION_STATE_IDLE); activePointerId = ACTIVE_POINTER_ID_NONE; break; case MotionEvent.ACTION_POINTER_UP: { final int pointerIndex = MotionEventCompat.getActionIndex(event); final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex); if (pointerId == activePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; activePointerId = MotionEventCompat.getPointerId(event, newPointerIndex); updateDxDy(event, selectedFlags, pointerIndex); } break; } } } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (!disallowIntercept) { return; } select(null, ACTION_STATE_IDLE); } }; private final RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (closeOnAction && (dx != 0 || dy != 0)) { if (prevSelected != null && (Math.abs(ViewCompat.getTranslationX(prevSelected.getSwipeView())) > 0 || Math.abs(ViewCompat.getTranslationY(prevSelected.getSwipeView())) > 0)) { closeOpenHolder(prevSelected); prevSelected = null; } // if we've got any open positions saved from a rotation, close those if (openedPositions.size() > 0) { for (int i = 0; i < openedPositions.size(); i++) { View child = recyclerView.getChildAt(openedPositions.keyAt(0)); // view needs to be attached, otherwise we can just mark it has removed since it's not visible if (child != null && child.getParent() != null) { RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(child); if (holder instanceof SwipeOpenViewHolder) { closeOpenHolder((SwipeOpenViewHolder) holder); } } openedPositions.removeAt(i); } } } } }; /** * Creates an SwipeOpenItemTouchHelper that will work with the given Callback. * <p> * You can attach SwipeOpenItemTouchHelper 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 SwipeOpenItemTouchHelper(Callback callback) { this.callback = 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 SwipeOpenItemTouchHelper to the provided RecyclerView. If the helper 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. * NOTE: RecyclerView must have an adapter set in order to allow adapter data observing to * correctly save opened positions state. * * @param recyclerView The RecyclerView instance to which you want to add this helper or * {@code null} if you want to remove SwipeOpenItemTouchHelper from the current * RecyclerView. */ public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { if (this.recyclerView == recyclerView) { return; // nothing to do } if (this.recyclerView != null) { destroyCallbacks(); } this.recyclerView = recyclerView; if (this.recyclerView != null) { setupCallbacks(); } } /** * Flag to determine if any open SwipeViewHolders are closed when the RecyclerView is scrolled, * or when a new view holder is swiped. * Default value is true. * * @param closeOnAction true to close on an action, false to keep them open */ public void setCloseOnAction(boolean closeOnAction) { this.closeOnAction = closeOnAction; } private void setupCallbacks() { ViewConfiguration vc = ViewConfiguration.get(recyclerView.getContext()); slop = vc.getScaledTouchSlop(); recyclerView.addItemDecoration(this); recyclerView.addOnItemTouchListener(mOnItemTouchListener); recyclerView.addOnChildAttachStateChangeListener(this); recyclerView.addOnScrollListener(scrollListener); Resources resources = recyclerView.getContext().getResources(); isRtl = resources.getBoolean(R.bool.rtl_enabled); if (recyclerView.getAdapter() == null) { throw new IllegalStateException("SwipeOpenItemTouchHelper.attachToRecyclerView must be called after " + "the RecyclerView's adapter has been set."); } else { recyclerView.getAdapter().registerAdapterDataObserver(adapterDataObserver); } } private void destroyCallbacks() { recyclerView.removeItemDecoration(this); recyclerView.removeOnItemTouchListener(mOnItemTouchListener); recyclerView.removeOnChildAttachStateChangeListener(this); if (recyclerView.getAdapter() != null) { recyclerView.getAdapter().unregisterAdapterDataObserver(adapterDataObserver); } // clean all attached final int recoverAnimSize = recoverAnimations.size(); for (int i = recoverAnimSize - 1; i >= 0; i--) { final RecoverAnimation recoverAnimation = recoverAnimations.get(0); callback.clearView(recyclerView, recoverAnimation.mViewHolder); } recoverAnimations.clear(); releaseVelocityTracker(); isRtl = false; } private void getSelectedDxDy(float[] outPosition) { if ((selectedFlags & (LEFT | RIGHT)) != 0) { outPosition[0] = selectedStartX + dX - selected.getSwipeView().getLeft(); } else { outPosition[0] = ViewCompat.getTranslationX(selected.getSwipeView()); } if ((selectedFlags & (UP | DOWN)) != 0) { outPosition[1] = selectedStartY + dY - selected.getSwipeView().getTop(); } else { outPosition[1] = ViewCompat.getTranslationY(selected.getSwipeView()); } } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { float dx = 0, dy = 0; if (selected != null) { getSelectedDxDy(tmpPosition); dx = tmpPosition[0]; dy = tmpPosition[1]; } callback.onDrawOver(c, parent, selected, recoverAnimations, actionState, dx, dy); } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { float dx = 0, dy = 0; if (selected != null) { getSelectedDxDy(tmpPosition); dx = tmpPosition[0]; dy = tmpPosition[1]; } callback.onDraw(c, parent, selected, recoverAnimations, actionState, dx, dy, isRtl); } /** * Starts dragging or swiping the given View. Call with null if you want to clear it. * * @param selected The ViewHolder to swipe. Can be null if you want to cancel the * current action * @param actionState The type of action */ private void select(SwipeOpenViewHolder selected, int actionState) { if (selected == this.selected && actionState == this.actionState) { return; } final int prevActionState = this.actionState; // prevent duplicate animations endRecoverAnimation(selected, true); this.actionState = actionState; int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState)) - 1; boolean preventLayout = false; // close the previously selected view holder if we're swiping a new one and the flag is true if (closeOnAction && selected != null && prevSelected != null && selected != prevSelected) { closeOpenHolder(prevSelected); prevSelected = null; preventLayout = true; } // if we've got any opened positions, and closeOnAction is true, close them // NOTE: only real way for this to happen is to have a view opened during configuration change // that then has its' state saved if (closeOnAction && openedPositions.size() > 0) { for (int i = 0; i < openedPositions.size(); i++) { View child = recyclerView.getChildAt(openedPositions.keyAt(0)); if (child.getParent() != null) { RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(child); // if our selected isn't the opened position, close it if (holder instanceof SwipeOpenViewHolder && (selected == null || holder.getAdapterPosition() != selected.getViewHolder().getAdapterPosition())) { closeOpenHolder((SwipeOpenViewHolder) holder); } } openedPositions.removeAt(i); } } if (this.selected != null) { prevSelected = this.selected; // we've changed selection, we need to animate it back if (prevSelected.getViewHolder().itemView.getParent() != null) { final int swipeDir = checkPreviousSwipeDirection(prevSelected.getViewHolder()); releaseVelocityTracker(); // find where we should animate to final float targetTranslateX, targetTranslateY; getSelectedDxDy(tmpPosition); final float currentTranslateX = tmpPosition[0]; final float currentTranslateY = tmpPosition[1]; final float absTranslateX = Math.abs(currentTranslateX); final float absTranslateY = Math.abs(currentTranslateY); final SavedOpenState state; switch (swipeDir) { case LEFT: case START: targetTranslateY = 0; // check if we need to close or go to the open position if (absTranslateX > prevSelected.getEndHiddenViewSize() / 2) { targetTranslateX = prevSelected.getEndHiddenViewSize() * Math.signum(dX); state = SavedOpenState.END_OPEN; } else { targetTranslateX = 0; state = null; } break; case RIGHT: case END: targetTranslateY = 0; if (absTranslateX > prevSelected.getStartHiddenViewSize() / 2) { targetTranslateX = prevSelected.getStartHiddenViewSize() * Math.signum(dX); state = SavedOpenState.START_OPEN; } else { targetTranslateX = 0; state = null; } break; case UP: targetTranslateX = 0; if (absTranslateY > prevSelected.getEndHiddenViewSize() / 2) { targetTranslateY = prevSelected.getEndHiddenViewSize() * Math.signum(dY); state = SavedOpenState.END_OPEN; } else { targetTranslateY = 0; state = null; } break; case DOWN: targetTranslateX = 0; if (absTranslateY > prevSelected.getStartHiddenViewSize() / 2) { targetTranslateY = prevSelected.getStartHiddenViewSize() * Math.signum(dY); state = SavedOpenState.START_OPEN; } else { targetTranslateY = 0; state = null; } break; default: state = null; targetTranslateX = 0; targetTranslateY = 0; } // if state == null, we're closing it if (state == null) { openedPositions.remove(prevSelected.getViewHolder().getAdapterPosition()); } else { openedPositions.put(prevSelected.getViewHolder().getAdapterPosition(), state); } final RecoverAnimation rv = new RecoverAnimation(prevSelected, prevActionState, currentTranslateX, currentTranslateY, targetTranslateX, targetTranslateY); final long duration = callback.getAnimationDuration(recyclerView, ANIMATION_TYPE_SWIPE, targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); rv.setDuration(duration); recoverAnimations.add(rv); rv.start(); preventLayout = true; } else { callback.clearView(recyclerView, prevSelected); } this.selected = null; } if (selected != null) { selectedFlags = (callback.getAbsoluteMovementFlags(recyclerView, selected.getViewHolder()) & actionStateMask) >> (this.actionState * DIRECTION_FLAG_COUNT); selectedStartX = selected.getViewHolder().itemView.getLeft() + ViewCompat.getTranslationX(selected.getSwipeView()); selectedStartY = selected.getViewHolder().itemView.getTop() + ViewCompat.getTranslationY(selected.getSwipeView()); this.selected = selected; } final ViewParent rvParent = recyclerView.getParent(); if (rvParent != null) { rvParent.requestDisallowInterceptTouchEvent(this.selected != null); } if (!preventLayout) { recyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout(); } callback.onSelectedChanged(this.selected, this.actionState); recyclerView.invalidate(); } @Override public void onChildViewAttachedToWindow(View view) { final RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(view); if (holder == null || !(holder instanceof SwipeOpenViewHolder)) { return; } // check if the view we are about to attach had previously saved open state, // and then open it based off that if (openedPositions.get(holder.getAdapterPosition(), null) != null) { final SwipeOpenViewHolder swipeHolder = (SwipeOpenViewHolder) holder; final SavedOpenState state = openedPositions.get(holder.getAdapterPosition()); if (recyclerView.getLayoutManager().canScrollVertically()) { int rtlFlipStart = isRtl ? -1 : 1; int rtlFlipEnd = isRtl ? 1 : -1; // if we're in an opened state and both view sizes are 0, then we're attempting // to restore the opened position before the view has measured, so we need to measure it if (swipeHolder.getStartHiddenViewSize() == 0 && swipeHolder.getEndHiddenViewSize() == 0) { final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); swipeHolder.getViewHolder().itemView.measure(widthSpec, heightSpec); } ViewCompat.setTranslationX(swipeHolder.getSwipeView(), state == SavedOpenState.START_OPEN ? swipeHolder.getStartHiddenViewSize() * rtlFlipStart : swipeHolder.getEndHiddenViewSize() * rtlFlipEnd); } else { ViewCompat.setTranslationY(swipeHolder.getSwipeView(), state == SavedOpenState.START_OPEN ? swipeHolder.getStartHiddenViewSize() : swipeHolder.getEndHiddenViewSize() * -1); } } } /** * When a View is detached from the RecyclerView it is either because the item has been deleted, * or the View is being detached/recycled because it is no longer visible (e.g. RecyclerView has * been scrolled) * * @param view the view being detached */ @Override public void onChildViewDetachedFromWindow(View view) { final RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(view); if (holder == null || !(holder instanceof SwipeOpenViewHolder)) { return; } final SwipeOpenViewHolder swipeHolder = (SwipeOpenViewHolder) holder; if (prevSelected == swipeHolder) { prevSelected = null; } if (selected != null && swipeHolder == selected) { select(null, ACTION_STATE_IDLE); } else { callback.clearView(recyclerView, swipeHolder); endRecoverAnimation(swipeHolder, false); // this may push it into pending cleanup list. } } private void endRecoverAnimation(SwipeOpenViewHolder viewHolder, boolean override) { final int recoverAnimSize = recoverAnimations.size(); for (int i = recoverAnimSize - 1; i >= 0; i--) { final RecoverAnimation anim = recoverAnimations.get(i); if (anim.mViewHolder == viewHolder) { anim.mOverridden |= override; if (!anim.mEnded) { anim.cancel(); } recoverAnimations.remove(i); } } } /** * Closes a SwipeOpenHolder that has been previously opened * * @param holder the holder */ private void closeOpenHolder(SwipeOpenViewHolder holder) { final View swipeView = holder.getSwipeView(); final float translationX = ViewCompat.getTranslationX(swipeView); final float translationY = ViewCompat.getTranslationY(swipeView); final RecoverAnimation rv = new RecoverAnimation(holder, 0, translationX, translationY, 0, 0); final long duration = callback.getAnimationDuration(recyclerView, ANIMATION_TYPE_SWIPE, translationX, translationY); rv.setDuration(duration); recoverAnimations.add(rv); rv.start(); // remove it from our open positions if we've got it openedPositions.remove(holder.getViewHolder().getAdapterPosition()); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { outRect.setEmpty(); } private void obtainVelocityTracker() { if (velocityTracker != null) { velocityTracker.recycle(); } velocityTracker = VelocityTracker.obtain(); } private void releaseVelocityTracker() { if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; } } private RecyclerView.ViewHolder findSwipedView(MotionEvent motionEvent) { final RecyclerView.LayoutManager lm = recyclerView.getLayoutManager(); if (activePointerId == ACTIVE_POINTER_ID_NONE) { return null; } final int pointerIndex = MotionEventCompat.findPointerIndex(motionEvent, activePointerId); final float dx = MotionEventCompat.getX(motionEvent, pointerIndex) - initialTouchX; final float dy = MotionEventCompat.getY(motionEvent, pointerIndex) - initialTouchY; final float absDx = Math.abs(dx); final float absDy = Math.abs(dy); if (absDx < slop && absDy < slop) { 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; } RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(child); if (holder instanceof SwipeOpenViewHolder) { return recyclerView.getChildViewHolder(child); } else { throw new IllegalStateException("Swiped View Holders must implement SwipeOpenViewHolder"); } } /** * Checks whether we should select a View for swiping. */ private boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) { if (selected != null || action != MotionEvent.ACTION_MOVE) { return false; } if (recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { return false; } final RecyclerView.ViewHolder vh = findSwipedView(motionEvent); if (vh == null || !(vh instanceof SwipeOpenViewHolder)) { return false; } final int movementFlags = callback.getAbsoluteMovementFlags(recyclerView, vh); final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE); if (swipeFlags == 0) { return false; } // dX and dY 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 = MotionEventCompat.getX(motionEvent, pointerIndex); final float y = MotionEventCompat.getY(motionEvent, pointerIndex); // Calculate the distance moved final float dx = x - initialTouchX; final float dy = y - initialTouchY; // 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 dX dY to check slope value again. final float absDx = Math.abs(dx); final float absDy = Math.abs(dy); if (absDx < slop && absDy < slop) { return false; } if (absDx > absDy) { if (dx < 0 && (swipeFlags & LEFT) == 0) { return false; } if (dx > 0 && (swipeFlags & RIGHT) == 0) { return false; } } else { if (dy < 0 && (swipeFlags & UP) == 0) { return false; } if (dy > 0 && (swipeFlags & DOWN) == 0) { return false; } } dX = dY = 0f; activePointerId = MotionEventCompat.getPointerId(motionEvent, 0); select((SwipeOpenViewHolder) vh, ACTION_STATE_SWIPE); return true; } private View findChildView(MotionEvent event) { // first check elevated views, if none, then call RV final float x = event.getX(); final float y = event.getY(); if (selected != null) { final View selectedView = selected.getViewHolder().itemView; if (hitTest(selectedView, x, y, selectedStartX + dX, selectedStartY + dY)) { return selectedView; } } for (int i = recoverAnimations.size() - 1; i >= 0; i--) { final RecoverAnimation anim = recoverAnimations.get(i); final View view = anim.mViewHolder.getViewHolder().itemView; if (hitTest(view, x, y, anim.mX, anim.mY)) { return view; } } return recyclerView.findChildViewUnder(x, y); } /** * Starts swiping the provided ViewHolder. * See {@link android.support.v7.widget.helper.ItemTouchHelper#startSwipe(RecyclerView.ViewHolder)} * * @param viewHolder The ViewHolder to start swiping. It must be a direct child of * RecyclerView. */ public void startSwipe(SwipeOpenViewHolder viewHolder) { if (viewHolder.getViewHolder().itemView.getParent() != recyclerView) { Log.e(TAG, "Start swipe has been called with a view holder which is not a child of " + "the RecyclerView controlled by this SwipeOpenItemTouchHelper."); return; } obtainVelocityTracker(); dX = dY = 0f; select(viewHolder, ACTION_STATE_SWIPE); } private RecoverAnimation findAnimation(MotionEvent event) { if (recoverAnimations.isEmpty()) { return null; } View target = findChildView(event); for (int i = recoverAnimations.size() - 1; i >= 0; i--) { final RecoverAnimation anim = recoverAnimations.get(i); if (anim.mViewHolder.getViewHolder().itemView == target) { return anim; } } return null; } private void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) { final float x = MotionEventCompat.getX(ev, pointerIndex); final float y = MotionEventCompat.getY(ev, pointerIndex); // Calculate the distance moved dX = x - initialTouchX; dY = y - initialTouchY; if ((directionFlags & LEFT) == 0) { dX = Math.max(0, dX); } if ((directionFlags & RIGHT) == 0) { dX = Math.min(0, dX); } if ((directionFlags & UP) == 0) { dY = Math.max(0, dY); } if ((directionFlags & DOWN) == 0) { dY = Math.min(0, dY); } } private int checkPreviousSwipeDirection(RecyclerView.ViewHolder viewHolder) { final int originalMovementFlags = callback.getMovementFlags(recyclerView, viewHolder); final int absoluteMovementFlags = callback.convertToAbsoluteDirection(originalMovementFlags, ViewCompat.getLayoutDirection(recyclerView)); 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(dX) > Math.abs(dY)) { if ((swipeDir = checkHorizontalSwipe(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(recyclerView)); } return swipeDir; } if ((swipeDir = checkVerticalSwipe(flags)) > 0) { return swipeDir; } } else { if ((swipeDir = checkVerticalSwipe(flags)) > 0) { return swipeDir; } if ((swipeDir = checkHorizontalSwipe(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(recyclerView)); } return swipeDir; } } return 0; } private int checkHorizontalSwipe(int flags) { if ((flags & (LEFT | RIGHT)) != 0) { return dX > 0 ? RIGHT : LEFT; } return 0; } private int checkVerticalSwipe(int flags) { if ((flags & (UP | DOWN)) != 0) { return dY > 0 ? DOWN : UP; } return 0; } public void onSaveInstanceState(Bundle outState) { outState.putSparseParcelableArray(OPENED_STATES, openedPositions); } public void restoreInstanceState(Bundle savedInstanceState) { openedPositions = savedInstanceState.getSparseParcelableArray(OPENED_STATES); if (openedPositions == null) { openedPositions = new SparseArray<>(); } } /** * Base Callback class that extends off of {@link ItemTouchHelper.Callback} */ @SuppressWarnings("UnusedParameters") public abstract static class Callback extends ItemTouchHelper.Callback { @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { // do not use return false; } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { // do not use } /** * 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 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 swipeFlags) { return makeFlag(ACTION_STATE_IDLE, swipeFlags) | makeFlag(ACTION_STATE_SWIPE, swipeFlags); } final int getAbsoluteMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { final int flags = getMovementFlags(recyclerView, viewHolder); return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView)); } /** * Called when the ViewHolder is changed. * <p/> * If you override this method, you should call super. * * @param viewHolder The new ViewHolder that is being swiped. Might be null if * it is cleared. * @param actionState One of {@link SwipeOpenItemTouchHelper#ACTION_STATE_IDLE}, * {@link SwipeOpenItemTouchHelper#ACTION_STATE_SWIPE} * @see #clearView(RecyclerView, SwipeOpenViewHolder) */ public void onSelectedChanged(SwipeOpenViewHolder viewHolder, int actionState) { if (viewHolder != null) { getDefaultUIUtil().onSelected(viewHolder.getSwipeView()); } } private void onDraw(Canvas c, RecyclerView parent, SwipeOpenViewHolder selected, List<RecoverAnimation> recoverAnimationList, int actionState, float dX, float dY, boolean isRtl) { final int recoverAnimSize = recoverAnimationList.size(); for (int i = 0; i < recoverAnimSize; i++) { final SwipeOpenItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); anim.update(); final int count = c.save(); onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, false); c.restoreToCount(count); } if (selected != null) { final int count = c.save(); notifySwipeDirections(selected, isRtl, dX, dY); onChildDraw(c, parent, selected, dX, dY, true); c.restoreToCount(count); } } /** * Notifies the SwipeOpenHolder when one of its hidden views has become visible. * * @param holder the holder * @param isRtl if the layout is RTL or not * @param dX the new dX of the swiped view * @param dY the new dY of the swiped view */ private void notifySwipeDirections(SwipeOpenViewHolder holder, boolean isRtl, float dX, float dY) { // check if we are about to start a swipe to open start or open end positions View swipeView = holder.getSwipeView(); // 0 or negative translationX, heading to positive translationX if (ViewCompat.getTranslationX(swipeView) <= 0 && dX > 0) { if (isRtl) { holder.notifyEndOpen(); } else { holder.notifyStartOpen(); } // 0 or positive translationX, heading to negative translationX } else if (ViewCompat.getTranslationX(swipeView) >= 0 && dX < 0) { if (isRtl) { holder.notifyStartOpen(); } else { holder.notifyEndOpen(); } // 0 or positive translationY, heading to negative translationY } else if (ViewCompat.getTranslationY(swipeView) >= 0 && dY < 0) { holder.notifyEndOpen(); } else if (ViewCompat.getTranslationY(swipeView) <= 0 && dY > 0) { holder.notifyStartOpen(); } } private void onDrawOver(Canvas c, RecyclerView parent, SwipeOpenViewHolder selected, List<RecoverAnimation> recoverAnimationList, int actionState, float dX, float dY) { final int recoverAnimSize = recoverAnimationList.size(); 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(); } } public void clearView(RecyclerView recyclerView, SwipeOpenViewHolder viewHolder) { getDefaultUIUtil().clearView(viewHolder.getSwipeView()); } public void onChildDraw(Canvas c, RecyclerView recyclerView, SwipeOpenViewHolder viewHolder, float dX, float dY, boolean isCurrentlyActive) { // handle the draw getDefaultUIUtil().onDraw(c, recyclerView, viewHolder.getSwipeView(), dX, dY, ACTION_STATE_SWIPE, isCurrentlyActive); } public long getAnimationDuration(RecyclerView recyclerView, int animationType, float animateDx, float animateDy) { final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); if (itemAnimator == null) { return DEFAULT_SWIPE_ANIMATION_DURATION; } else { return itemAnimator.getMoveDuration(); } } } /** * Simple callback class that defines the swipe directions allowed and delegates everything else * to the base class */ @SuppressWarnings("UnusedParameters") public static class SimpleCallback extends Callback { private int mDefaultSwipeDirs; public SimpleCallback(int swipeDirs) { mDefaultSwipeDirs = swipeDirs; } public void setDefaultSwipeDirs(int defaultSwipeDirs) { mDefaultSwipeDirs = defaultSwipeDirs; } public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { return mDefaultSwipeDirs; } @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { return makeMovementFlags(getSwipeDirs(recyclerView, viewHolder)); } } private class RecoverAnimation implements AnimatorListenerCompat { final float mStartDx; final float mStartDy; final float mTargetX; final float mTargetY; final SwipeOpenViewHolder mViewHolder; final int mActionState; private final ValueAnimatorCompat mValueAnimator; public boolean mIsPendingCleanup; float mX; float mY; // if user starts touching a recovering view, we put it into interaction mode again, // instantly. boolean mOverridden = false; private boolean mEnded = false; private float mFraction; public RecoverAnimation(SwipeOpenViewHolder viewHolder, int actionState, float startDx, float startDy, float targetX, float targetY) { mActionState = actionState; mViewHolder = viewHolder; mStartDx = startDx; mStartDy = startDy; mTargetX = targetX; mTargetY = targetY; mValueAnimator = AnimatorCompatHelper.emptyValueAnimator(); mValueAnimator.addUpdateListener(new AnimatorUpdateListenerCompat() { @Override public void onAnimationUpdate(ValueAnimatorCompat animation) { setFraction(animation.getAnimatedFraction()); } }); mValueAnimator.setTarget(viewHolder.getViewHolder().itemView); mValueAnimator.addListener(this); setFraction(0f); } public void setDuration(long duration) { mValueAnimator.setDuration(duration); } public void start() { mViewHolder.getViewHolder().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 = ViewCompat.getTranslationX(mViewHolder.getSwipeView()); } else { mX = mStartDx + mFraction * (mTargetX - mStartDx); } if (mStartDy == mTargetY) { mY = ViewCompat.getTranslationY(mViewHolder.getSwipeView()); } else { mY = mStartDy + mFraction * (mTargetY - mStartDy); } } @Override public void onAnimationStart(ValueAnimatorCompat animation) { } @Override public void onAnimationEnd(ValueAnimatorCompat animation) { if (!mEnded) { mViewHolder.getViewHolder().setIsRecyclable(true); } mEnded = true; } @Override public void onAnimationCancel(ValueAnimatorCompat animation) { setFraction(1f); //make sure we recover the view's state. } @Override public void onAnimationRepeat(ValueAnimatorCompat animation) { } } /** * Enum for saving the opened state of the view holders */ enum SavedOpenState implements Parcelable { START_OPEN, END_OPEN; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } public static final Parcelable.Creator<SavedOpenState> CREATOR = new Parcelable.Creator<SavedOpenState>() { @Override public SavedOpenState createFromParcel(Parcel source) { return SavedOpenState.values()[source.readInt()]; } @Override public SavedOpenState[] newArray(int size) { return new SavedOpenState[size]; } }; } }