上次分析源码,我们知道,ItemTouchHelper对被选中的ViewHodler进行动画操作都是通过ItemTouchUIUtilImpl这个类,我们想要实现侧滑删除,必定需要对ViewHodler进行平移操作,ItemTouchHelper.Callback通过onChildDraw方法调用了ItemTouchUIUtilImpl中的方法,所以我们改写onChildDraw方法
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (dY != 0 && dX == 0) {//因为我们只关注侧滑,而侧滑条件是dX!=0&&dY ==0,所以其他的情况调用ItemTouchUIUtilImpl的方法
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
if (iMoveAndSwipeCallback != null) {
iMoveAndSwipeCallback.onChildDraw(viewHolder, dX, dY);
}
}
并实现iMoveAndSwipeCallback的onChildDraw方法
@Override
public void onChildDraw(RecyclerView.ViewHolder viewHolder, float dX, float dY) {
MyAdapter.MyViewHolder myViewHolder = (MyAdapter.MyViewHolder) viewHolder;
//最大偏移不超过删除布局宽度
if (dX < -myViewHolder.ll_remove.getWidth()) {
dX = -myViewHolder.ll_remove.getWidth();
}
myViewHolder.tv.setTranslationX(dX);
}
我们RecyclerView的item的布局文件是这样的
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/ll_remove"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="right"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_remove"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="@android:color/holo_red_light"
android:gravity="center"
android:text="remove"
android:textColor="@android:color/white" />
<TextView
android:id="@+id/tv_cancel"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_bright"
android:gravity="center"
android:text="cancel"
android:textColor="@android:color/white" />
</LinearLayout>
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
android:gravity="center"
android:textColor="@android:color/white" />
</FrameLayout>
其中删除和取消的布局在下层,好了,我们来看下效果
但是,我们的item并不能获取点击事件,因为ItemTouchHelper并没有把事件传递给子控件,解决方法:把ItemTouchHelper复制到自己的项目中!我们只需要改OnItemTouchListener这个对象就可以了,修改后如下:
private final RecyclerView.OnItemTouchListener mOnItemTouchListener = new RecyclerView.OnItemTouchListener() {
private boolean onClick;
@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, 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();
//记录ACTION_DOWN的位置
mDownX = event.getX();
mDownY = event.getY();
onClick = true;
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;
if (action == MotionEvent.ACTION_UP) {
//点击事件
click(event);
}
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);
}
}
//处理下move事件
if (action == MotionEvent.ACTION_MOVE) {
//移动了,就解除点击
final float dx = event.getX(mActivePointerId) - mDownX;
final float dy = event.getY(mActivePointerId) - mDownY;
final float absDx = Math.abs(dx);
final float absDy = Math.abs(dy);
if (absDx > mSlop || absDy > mSlop) {
onClick = false;
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return mSelected != null;
}
@Override
public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
if (DEBUG) {
Log.d(TAG,
"on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event);
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
return;
}
final int action = event.getActionMasked();
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
if (activePointerIndex >= 0) {
checkSelectForSwipe(action, event, activePointerIndex);
}
ViewHolder viewHolder = mSelected;
if (viewHolder == null) {
return;
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
if (activePointerIndex >= 0) {
updateDxDy(event, mSelectedFlags, activePointerIndex);
moveIfNecessary(viewHolder);
mRecyclerView.removeCallbacks(mScrollRunnable);
mScrollRunnable.run();
mRecyclerView.invalidate();
}
break;
}
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
// fall through
case MotionEvent.ACTION_UP:
//点击事件
click(event);
select(null, ACTION_STATE_IDLE);
mActivePointerId = ACTIVE_POINTER_ID_NONE;
break;
case MotionEvent.ACTION_POINTER_UP: {
onClick = false;
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;
}
default:
onClick = false;
break;
}
}
/**
* 点击事件处理
* @param event
*/
private void click(MotionEvent event) {
if (onClick && mSelected != null) {//抬起时,调用点击
View view = mSelected.itemView;
if (view instanceof ViewGroup) {
view = findView((ViewGroup) mSelected.itemView, event.getRawX(), event.getRawY());
}
if (view != null) {
view.performClick();
}
}
onClick = false;
}
/**
* 找到被点击的View
*
* @param parent
* @param x
* @param y
* @return
*/
private View findView(ViewGroup parent, float x, float y) {
int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
View view = parent.getChildAt(i);
//如果是组件,并可见的话
if (view instanceof ViewGroup && view.getVisibility() == View.VISIBLE) {
//递归调用
View child = findView((ViewGroup) view, x, y);
if (child != null) {
return child;
}
}
//如果在可点击区域内,又是控件的话
if (isInRegion(view, x, y) && view.getVisibility() == View.VISIBLE) {
return view;
}
}
//没有child的情况,判断下有没有在可点击区域内
if (isInRegion(parent, x, y) && parent.getVisibility() == View.VISIBLE) {
return parent;
}
return null;
}
/**
* 判断点击位置是否在区域内
* @param child
* @param x
* @param y
* @return
*/
private boolean isInRegion(View child, float x, float y) {
Rect rect = new Rect();
//获取在屏幕全局的区域
child.getGlobalVisibleRect(rect);
if (rect.contains((int) x, (int) y) && ViewCompat.hasOnClickListeners(child) &&
child.getVisibility() == View.VISIBLE) {
return true;
}
return false;
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (!disallowIntercept) {
return;
}
select(null, ACTION_STATE_IDLE);
}
};
我们自己处理并找到被点击的控件,并调用performClick方法,这时我们可以删除了
我们还需要解决的问题是上下滑动或者选中其他ViewHodler时的时候,把ViewHodler复原
在select方法中,记录上一个ViewHodler
/**
* 之前侧滑的ViewHolder
*/
ViewHolder mPreSelected = null;
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);
//记录
mPreSelected = prevSelected;
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);
}
}
};
定义恢复动画
/**
* 关闭动画
*/
private void closeOpenedPreItem() {
final View view = mCallback.getItemFrontView(mPreSelected);
if (mPreSelected == null || view == null) return;
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "translationX", view.getTranslationX(), 0f);
objectAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
if (mPreSelected != null) mCallback.clearView(mRecyclerView, mPreSelected);
if (mPreSelected != null) mPendingCleanup.remove(mPreSelected.itemView);
endRecoverAnimation(mPreSelected, true);
mPreSelected = mSelected;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}
});
objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
objectAnimator.start();
}
在recyclerView滑动和ViewHodler改变时调用
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != 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);
//添加滑动监听
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_DRAGGING && mPreSelected != null) {
closeOpenedPreItem();
}
}
});
setupCallbacks();
}
}
/**
* Checks whether we should select a View for swiping.
*/
boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
if (mSelected != null || action != MotionEvent.ACTION_MOVE
|| mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) {
return false;
}
if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
return false;
}
final ViewHolder vh = findSwipedView(motionEvent);
if (vh == null) {
return false;
}
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 false;
}
// 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 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;
}
}
mDx = mDy = 0f;
mActivePointerId = motionEvent.getPointerId(0);
select(vh, ACTION_STATE_SWIPE);
if (mPreSelected != null && mPreSelected != vh && mPreSelected != null) {
closeOpenedPreItem();
}
return true;
}
最终效果: