实现拖拽和侧滑之前我门需要先了解一个recyclerview的辅助类ItemTouchHelper
ItemTouchHelper是一个工具类,可实现侧滑删除和拖拽移动,使用这个工具类需要RecyclerView和Callback。同时根据需要重写onMove和onSwiped方法。接下来就来讲述ItemTouchHelper的使用方法。
ItemTouchHelper 常用的函数列出如下:
/**
* 针对swipe和drag状态,设置不同状态(swipe、drag)下支持的方向
* (LEFT, RIGHT, START, END, UP, DOWN)
* idle:0~7位表示swipe和drag的方向
* swipe:8~15位表示滑动方向
* drag:16~23位表示拖动方向
*/
public abstract int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder);
/**
* 针对swipe和drag状态,当swipe或者drag对应的ViewHolder改变的时候调用
* 我们可以通过重写这个函数获取到swipe、drag开始和结束时机,viewHolder 不为空的时候是开始,空的时候是结束
*/
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
}
/**
* 针对swipe状态,是否允许swipe(滑动)操作
*/
public boolean isItemViewSwipeEnabled() {
return true;
}
/**
* 针对swipe状态,swipe滑动的位置超过了百分之多少就消失
*/
public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
return .5f;
}
/**
* 针对swipe状态,swipe的逃逸速度,换句话说就算没达到getSwipeThreshold设置的距离,达到了这个逃逸速度item也会被swipe消失掉
*/
public float getSwipeEscapeVelocity(float defaultValue) {
return defaultValue;
}
/**
* 针对swipe状态,swipe滑动的阻尼系数,设置最大滑动速度
*/
public float getSwipeVelocityThreshold(float defaultValue) {
return defaultValue;
}
/**
* 针对swipe状态,swipe 到达滑动消失的距离回调函数,一般在这个函数里面处理删除item的逻辑
* 确切的来讲是swipe item滑出屏幕动画结束的时候调用
*/
public abstract void onSwiped(RecyclerView.ViewHolder viewHolder, int direction);
/**
* 针对drag状态,当item长按的时候是否允许进入drag(拖动)状态
*/
public boolean isLongPressDragEnabled() {
return true;
}
/**
* 针对drag状态,当前target对应的item是否允许move
* 换句话说我们一般用drag来做一些换位置的操作,就是当前target对应的item是否可以换位置
*/
public boolean canDropOver(RecyclerView recyclerView, RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) {
return true;
}
/**
* 针对drag状态,在canDropOver()函数返回true的情况下,会调用该函数让我们去处理拖动换位置的逻辑(需要重写自己处理变换位置的逻辑)
* 如果有位置变换返回true,否则发挥false
*/
public abstract boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target);
/**
* 针对drag状态,当drag itemView和底下的itemView重叠的时候,可以给drag itemView设置额外的margin,让重叠更加容易发生。
* 相当于增大了drag itemView的区域
*/
public int getBoundingBoxMargin() {
return 0;
}
/**
* 针对drag状态,滑动超过百分之多少的距离可以可以调用onMove()函数(注意哦,这里指的是onMove()函数的调用,并不是随手指移动的那个view哦)
*/
public float getMoveThreshold(RecyclerView.ViewHolder viewHolder) {
return .5f;
}
/**
* 针对drag状态,在drag的过程中获取drag itemView底下对应的ViewHolder(一般不用我们处理直接super就好了)
*/
public RecyclerView.ViewHolder chooseDropTarget(RecyclerView.ViewHolder selected,
List<RecyclerView.ViewHolder> dropTargets,
int curX,
int curY) {
return super.chooseDropTarget(selected, dropTargets, curX, curY);
}
/**
* 当onMove return true的时候调用(一般不用我们自己处理,直接super就好)
*/
public void onMoved(final RecyclerView recyclerView,
final RecyclerView.ViewHolder viewHolder,
int fromPos,
final RecyclerView.ViewHolder target,
int toPos,
int x,
int y) {
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y);
}
/**
* 针对swipe和drag状态,当一个item view在swipe、drag状态结束的时候调用
* drag状态:当手指释放的时候会调用
* swipe状态:当item从RecyclerView中删除的时候调用,一般我们会在onSwiped()函数里面删除掉指定的item view
*/
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
}
/**
* 针对swipe和drag状态,整个过程中一直会调用这个函数,随手指移动的view就是在super里面做到的(和ItemDecoration里面的onDraw()函数对应)
*/
public void onChildDraw(Canvas c,
RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
float dX,
float dY,
int actionState,
boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
/**
* 针对swipe和drag状态,整个过程中一直会调用这个函数(和ItemDecoration里面的onDrawOver()函数对应)
* 这个函数提供给我们可以在RecyclerView的上面再绘制一层东西,比如绘制一层蒙层啥的
*/
public void onChildDrawOver(Canvas c,
RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
float dX,
float dY,
int actionState,
boolean isCurrentlyActive) {
super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
/**
* 针对swipe和drag状态,当手指离开之后,view回到指定位置动画的持续时间(swipe可能是回到原位,也有可能是swipe掉)
*/
public long getAnimationDuration(RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
return super.getAnimationDuration(recyclerView, animationType, animateDx, animateDy);
}
/**
* 针对drag状态,当itemView滑动到RecyclerView边界的时候(比如下面边界的时候),RecyclerView会scroll,
* 同时会调用该函数去获取scroller距离(不用我们处理 直接super)
*/
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView,
int viewSize,
int viewSizeOutOfBounds,
int totalSize,
long msSinceStartScroll) {
return super.interpolateOutOfBoundsScroll(recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
}
下面的代码是继承了ItemTouchHelper.Callback 实现一些基本的业务操作 数据交换等等
public class SwapCallBack extends ItemTouchHelper.Callback {
private List<ImgBean> datas;
private RecyclerView.Adapter adapter;
private Context mContext;
public SwapCallBack(List<ImgBean> datas, RecyclerView.Adapter adapter, Context mContext) {
this.datas = datas;
this.adapter = adapter;
this.mContext = mContext;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
int dragFrlg = 0;
if (recyclerView.getLayoutManager() instanceof GridLayoutManager){
dragFrlg = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
}else {
dragFrlg = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
}
return makeMovementFlags(dragFrlg,0);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
//滑动事件 下面注释的代码,滑动后数据和条目错乱,被舍弃
// Collections.swap(datas,viewHolder.getAdapterPosition(),target.getAdapterPosition());
// adapter.notifyItemMoved(viewHolder.getAdapterPosition(),target.getAdapterPosition());
//得到当前拖拽的viewHolder的position
int fromPosition = viewHolder.getAdapterPosition();
//得到当前要拖拽到的item的viewHolder
int toPosition = target.getAdapterPosition();
if (fromPosition < datas.size() && toPosition < datas.size()){
if (fromPosition < toPosition){
for (int i = fromPosition; i < toPosition; i++) {
Collections.swap(datas , i ,i + 1);
}
}else {
for (int i = fromPosition; i > toPosition; i--) {
Collections.swap(datas , i ,i - 1);
}
}
adapter.notifyItemMoved(fromPosition,toPosition);
}
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
//侧面滑动使用
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
/**
* 长按选中Item的时候开始调用
* 长按高亮
* @param viewHolder
* @param actionState
*/
@Override
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE){
// viewHolder.itemView.setBackgroundColor(Color.RED);
Vibrator vib = (Vibrator) mContext.getSystemService(Service.VIBRATOR_SERVICE);
vib.vibrate(50);
}
super.onSelectedChanged(viewHolder, actionState);
}
/**
* 手指松开时还原高亮
* @param recyclerView
* @param viewHolder
*/
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
// viewHolder.itemView.setBackgroundColor(0);
adapter.notifyDataSetChanged();
}
}
然后 将SwapCallBack对象传给ItemTouchHelper 去处理
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwapCallBack(list,otherAdapter,this));
然后将ItemTouchHelper 绑定一个我们要操作reyclerView
itemTouchHelper.attachToRecyclerView(recyclerView);
实现效果如下:
3.自定义侧滑动画
有时候我们对默认的动画效果可能不满意,需要自己实现想要的动画效果,ItemTouchHelper.Callback提供的onChildDraw方法可以让我们很方便地实现想要的效果。
该效果是比较常见的,用户向左滑动Item的时候,一开始提示的是“左滑删除”,滑动到一定距离后,显示删除的图标,并且随着滑动距离的增加该图标不断变大,达到最大后用户松开手指,该Item被删除。
接下来我们来分析一下怎样实现以上的效果:
首先,要想左滑出现一个删除的方块,可以在LinearLayout放一个这样的“方块”,让它与Item水平并排排列,以下是布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#fff"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.haocai.itemtouchhelper.view.CircleImageView
android:id="@+id/iv_logo"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginBottom="8dp"
android:layout_marginLeft="5dp"
android:layout_marginTop="8dp"
android:src="@drawable/logo1" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dip"
android:orientation="vertical"
android:paddingRight="5dp">
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:text="小王"
android:textColor="#000"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:text="14:31"
android:textColor="#a6a6a6"
android:textSize="12sp" />
</RelativeLayout>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dip"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_lastMsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:text="一起吃饭"
android:textColor="#808080"
android:textSize="12sp" />
<ImageView
android:id="@+id/iv_pop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true" />
</RelativeLayout>
</LinearLayout>
<FrameLayout
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="#f33213">
<ImageView
android:id="@+id/iv_detele"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:src="@drawable/delete"
android:visibility="invisible" />
<TextView
android:id="@+id/tv_detele"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="左滑删除"
android:textColor="#ffffff"
android:textSize="18sp" />
</FrameLayout>
</LinearLayout>
布局文件修改后,我们尝试来滑动一下,发现后面的删除方块并不会出现,这是因为默认的滑动方式是setTranslationX(int),即是对整个View的滑动,所以无论我们怎样滑动,都不会出现删除方块。因此,我们要改变一个种滑动方式,比如使用scrollTo(int,int),这种是对View的内容的滑动,所以随着左滑,item会向左滑去,而位于右方的方块自然也就出现了。
接着,我们考虑该“删除眼睛”的图标是怎样从小变大的,这个实现也比较简单,只要根据滑动的距离对该ImageView的LayoutParams.width进行改变就行了,不过要注意限制大小,否则过大会造成图片的失真。当滑动距离等于RecyclerView宽度的一半时,此时松开手会使Item删除,那么我们可以在该滑动距离达到该值时时“眼睛”变得最大,此时可以达到良好的交互效果,提示了用户无需继续滑动即可删除该Item了。
最后我们要考虑的是:在删除了Item或者没删除而滑回原来的位置后,我们要把所做的改变重置一下,否则,会由于RecyclerView的复用而导致其他位置的ViewHolder与当前的ViewHolder所做的改变一样,即造成显示的错误。我们可以在clearView()方法内重置改变,这样就能解决因复用而导致的显示问题了。
最后我们来看看SimpleItemTouchHelperCallback的代码:
public class MyItemTouchHelperCallback3 extends ItemTouchHelper.Callback {
//限制ImageView长度所能增加的最大值
private double ICON_MAX_SIZE = 40;
//ImageView的初始长宽
private int fixedWidth = 120;
private ItemTouchMoveListener moveListener;
public MyItemTouchHelperCallback3(ItemTouchMoveListener moveListener) {
this.moveListener = moveListener;
}
// /**
// * 设置滑动类型标记
// *
// * @param recyclerView
// * @param viewHolder
// * @return
// * 返回一个整数类型的标识,用于判断Item那种移动行为是允许的
// */
// @Override
// public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
// //START 右向左 END左向右 LEFT 向左 RIGHT向右 UP向上
// //如果某个值传0,表示不触发该操作
// return makeMovementFlags(ItemTouchHelper.UP|ItemTouchHelper.DOWN,ItemTouchHelper.END );
// }
/**
* Callback回调监听时先调用的,用来判断当前是什么动作,比如判断方向
* 作用:哪个方向的拖动
*
* @param recyclerView
* @param viewHolder
* @return
*/
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//方向:up,down,left,right
//常量
// ItemTouchHelper.UP 0x0001
// ItemTouchHelper.DOWN 0x0010
// ItemTouchHelper.LEFT
// ItemTouchHelper.RIGHT
//我要监听的拖拽方向是哪个方向
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
//我要监听的swipe侧滑方向是哪个方向
int swipeFlags = ItemTouchHelper.LEFT ;
int flags = makeMovementFlags(dragFlags, swipeFlags);
return flags;
}
/**
* 是否打开长按拖拽效果
*
* @return
*/
@Override
public boolean isLongPressDragEnabled() {
return true;
}
/**
* Item是否支持滑动
*
* @return
* true 支持滑动操作
* false 不支持滑动操作
*/
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
//当上下移动的时候回调的方法
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder srcHolder, RecyclerView.ViewHolder targetHolder) {
// 在拖拽过程中不断地调用adapter.notifyItemMoved(from,to);
if (srcHolder.getItemViewType() != targetHolder.getItemViewType()) {
return false;
}
//在拖拽的过程中不断调用adapter.notifyItemMoved(from,to);
boolean result = moveListener.onItemMove(srcHolder.getAdapterPosition(), targetHolder.getAdapterPosition());
return result;
}
//侧滑的时候回调的方法
@Override
public void onSwiped(RecyclerView.ViewHolder holder, int direction) {
//监听侧滑,1.删除数据 2.调用adapter.notifyItemRemove(position);
moveListener.onItemRemove(holder.getAdapterPosition());
}
//设置滑动item的背景
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
//判断选中状态
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext().getResources().getColor(R.color.colorC));
}
super.onSelectedChanged(viewHolder, actionState);
}
//清除滑动item的背景
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
// 恢复
viewHolder.itemView.setBackgroundColor(Color.WHITE);
//防止出现复用问题 而导致条目不显示 方式一
viewHolder.itemView.setAlpha(1);//1-0
//设置滑出大小
// viewHolder.itemView.setScaleX(1);
// viewHolder.itemView.setScaleY(1);
QQAdapter.MyViewHolder myViewHolder = (QQAdapter.MyViewHolder)viewHolder;
//重置改变,防止由于复用而导致的显示问题
viewHolder.itemView.setScrollX(0);
myViewHolder.tvDetele.setText("左滑删除");
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) myViewHolder.ivDetele.getLayoutParams();
params.width = 150;
params.height = 150;
myViewHolder.ivDetele.setLayoutParams(params);
myViewHolder.ivDetele.setVisibility(View.INVISIBLE);
super.clearView(recyclerView, viewHolder);
}
//设置滑动时item的背景透明度
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
QQAdapter.MyViewHolder myViewHolder = (QQAdapter.MyViewHolder)viewHolder;
//仅对侧滑状态下的效果做出改变
if (actionState ==ItemTouchHelper.ACTION_STATE_SWIPE){
Log.d("http","4444");
//如果dX小于等于删除方块的宽度,那么我们把该方块滑出来
if (Math.abs(dX) <= getSlideLimitation(viewHolder)){
viewHolder.itemView.scrollTo(-(int) dX,0);
}
//如果dX还未达到能删除的距离,此时慢慢增加“眼睛”的大小,增加的最大值为ICON_MAX_SIZE
else if (Math.abs(dX) <= recyclerView.getWidth() / 2){
double distance = (recyclerView.getWidth() / 2 -getSlideLimitation(viewHolder));
double factor = ICON_MAX_SIZE / distance;
double diff = (Math.abs(dX) - getSlideLimitation(viewHolder)) * factor;
if (diff >= ICON_MAX_SIZE)
diff = ICON_MAX_SIZE;
myViewHolder.tvDetele.setText(""); //把文字去掉
myViewHolder.ivDetele.setVisibility(View.VISIBLE); //显示眼睛
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) myViewHolder.ivDetele.getLayoutParams();
params.width = (int) (fixedWidth + diff);
params.height = (int) (fixedWidth + diff);
myViewHolder.ivDetele.setLayoutParams(params);
}
}else {
//拖拽状态下不做改变,需要调用父类的方法
super.onChildDraw(c,recyclerView,viewHolder,dX,dY,actionState,isCurrentlyActive);
}
}
/**
* 获取删除方块的宽度
*/
public int getSlideLimitation(RecyclerView.ViewHolder viewHolder){
ViewGroup viewGroup = (ViewGroup) viewHolder.itemView;
return viewGroup.getChildAt(2).getLayoutParams().width;
}
}
ItemTouchHelper源码走读
当我们在对ItemTouchHelper源码有了一个简单的了解之后,会让我们更好的理解ItemTouchHelper使用。
在ItemTouchHelper源码走读之前,我们先抛出几个疑问:
RecyclerView的触摸事件是怎么在ItemTouchHelper里面进行处理的。
ItemTouchHelper是里面怎么判断进入swipe或者drag状态。
进入swipe或者drag状态之后,选中的item是怎么随着手指移动的。
随手指移动的item在移动的时候感觉是在RecyclerView上面移动的,当和item有重叠的时候是怎么让选中的item在上层移动的。
现在开始进入正题了哈,开始对ItemTouchHelper代码进行走读。
ItemTouchHelper源码所有的的逻辑处理都是围绕RecyclerView使的四个类来进行,分别是:ItemDecoration、OnItemTouchListener、OnChildAttachStateChangeListener、GestureDetector。ItemTouchHelper里面所有的逻辑处理都是围绕着四个类来进行的。所以我们先对这四个帮助类做一个简单的介绍,关于这几个类更加详细的解释可以自行去google。
ItemDecoration:用来装饰RecyclerView中每个item的帮助类。ItemDecoration里面就三个函数:getItemOffsets()用来给每个item设置额外的offset、onDraw()可以通过这个函数给item绘制任何合适的decoration装饰、onDrawOver()也是用来给item绘制装饰用的但是它和onDraw()函数还是有区别的;onDrawOver()是在RecyclerView的draw()调用完之后在调用的,换句话说onDrawOver()是绘制在最上层的。ItemTouchHelper源码里面我们会在ItemDecoration的onDraw()里面让item随手指移动。之前的博客我们也使用ItemDecoration实现了一个简单的功能,有兴趣的可以瞧下RecyclerView分组悬浮列表
OnItemTouchListener:RecyclerView提供给我们处理item各种事件的一个类,一般会配合GestureDetector来处理item的各种手势事件。或者用来处理item里面一些事件的拦截。OnItemTouchListener里面有三个大家非常熟悉的函数:onInterceptTouchEvent()、onTouchEvent()、onRequestDisallowInterceptTouchEvent()。ItemTouchHelper源码里面我们会在OnItemTouchListener里面处理item的各种事件。
OnChildAttachStateChangeListener:用来监听RecyclerView里面item添加和删除。当我们上层逻辑有item删除的时候,ItemTouchHelper会在OnChildAttachStateChangeListener的onChildViewDetachedFromWindow()函数里面做一些回收工作的处理。
GestureDetector:GestureDetector用来获取触摸过程中的各种手势事件。ItemTouchHelper源码里面会用到GestureDetector来获取item长按的的手势,长按之后然后判断要不要进入drag状态。
ItemTouchHelper select()函数,进入退出swipe或者drag状态的时候会调用到select()函数。
我们先来看下ItemTouchHelper里面的select()函数,首先select()函数两个参数:ViewHolder代表当前swipe或者drag选中的item对应的ViewHolder(如果ViewHolder不为空代表选中了一个item,为空代表swipe或者drag释放了item)、actionState代表当前模式有三个值ACTION_STATE_IDLE空闲状态、ACTION_STATE_SWIPE状态对应 swipe模式、ACTION_STATE_DRAG状态对应 drag模式。select()函数里面做的主要工作有:
如果之前mSelected不会空的时候,会给该mSelected对应的item设置动画,这个动画主要用来处理这些情况的,比如是swipe模式对应的mSelected的时候,当手指释放的时候该mSelected对应的item要么是回到原始位置,要么是滑出屏幕之外。这些都是通过动画来完成的。如果是drag模式对应的mSelected的时候,同样当手指释放的时候该mSelected对应的item也要回到指定的位置上去。这些动画都会存放在mRecoverAnimations里面。
把当前参数的ViewHolder设置给mSelected,同时记录mSelectedStartX,mSelectedStartY等的一些位置,便于后面计算移动的距离。
调用Callback的onSelectedChanged()函数。可以通过参数ViewHolder是否为空来判断是进入还是退出swipe或者drag状态。
告诉RecyclerView重绘,强迫RecyclerView去调用onDraw()函数。(RecyclerView的onDraw()函数的调用会引起ItemDecoration里面onDraw()函数的调用)
简单的看了下select()函数,之后,我们再来梳理ItemTouchHelper逻辑的流程。
attachToRecyclerView()函数相关逻辑处理
第一步,从attachToRecyclerView()函数开始,该函数里面setupCallbacks()的调用给RecyclerView设置ItemDecoration、OnItemTouchListener、OnChildAttachStateChangeListener、GestureDetector的一些处理。(会在ItemDecoration里面onDraw()函数里面处理item随手指移动的逻辑,OnItemTouchListener里面处理item事件拦截和事件处理的逻辑,OnChildAttachStateChangeListener里面处理当上层逻辑删除item的时候一些回收机制的逻辑,GestureDetector里面处理item长按的逻辑)。
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.addOnChildAttachStateChangeListener(this);
initGestureDetector();
}
OnItemTouchListener相关逻辑处理
第二步,因为ItemTouchHelper里面所的逻辑都是围绕触摸事件来进行的,所以当MotionEvent 事件没有被item的子view处理的时候,该MotionEvent 事件会进入到RecyclerView的帮助类OnItemTouchListener里面去,所以这一步的重点就转到了OnItemTouchListener里面的逻辑处理部分了
private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
@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();
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(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:
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);
}
};
看到里面有三个我们非常熟悉的函数:onInterceptTouchEvent()、onTouchEvent()、onRequestDisallowInterceptTouchEvent()。其中一个用来处理拦截事件的逻辑,一个用来处理事件逻辑,最后一个用来给子view设置item是否可以拦截的设置。
onInterceptTouchEvent()函数里面的逻辑细节
总的来就是如果有mSelected的时候事件就会被拦截下来(mSelected是会随手指移动的item对应的ViewHolder)。onInterceptTouchEvent()函数里面更加具体的细节先把事件添加到GestureDetector里面去(便于GestureDetector里面获取不同的手势的处理),然后分别对MotionEvent.ACTION_DOWN、 MotionEvent.ACTION_UP做不同的逻辑处理;MotionEvent.ACTION_DOWN里面会先记录下初始按下的位置,接下来如果当前触摸位置对应的item有动画(不管是swipe还是drag模式,在手指离开的时候,当前选中的item都会有一个到指定位置的动画)还在执行动画中。这个时候这个item会当做选中的item来处理。MotionEvent.ACTION_UP里面就是清除之前mSelected的选择。
onTouchEvent()函数里面,
checkSelectForSwipe(action, event, activePointerIndex)的调用里面会先去判断是否支持swipe模式Callback.isItemViewSwipeEnabled(),然后去判断swipe支持的方向是否和滑动的方向是否一致Callback.getAbsoluteMovementFlags()。如果这两个条件都满足会在select(vh, ACTION_STATE_SWIPE)函数里面把当前手指下对应的item设置为mSelected,模式对应设置为ACTION_STATE_SWIPE swipe模式。
MotionEvent.ACTION_MOVE的时候先调用updateDxDy(event, mSelectedFlags, activePointerIndex)更新已经滑动的距离,接着调用moveIfNecessary(viewHolder)去设置是否要move,里面也会去回调Callback里面的chooseDropTarget()、onMoved()的函数。接着调用RecyclerView的invalidate()函数迫使RecyclerView去调用onDraw()函数。
MotionEvent.ACTION_UP的时候就是调用了select(null, ACTION_STATE_IDLE)做一些释放操作。
onRequestDisallowInterceptTouchEvent函数里面
如果子view设置disallow的时候会调用select(null, ACTION_STATE_IDLE)函数,其实也好理解,子view都告诉父view不能处理这个事件饿,所以要做一些释放操作。
GestureDetectorCompat类的使用
OnItemTouchListener的帮助类mOnItemTouchListener里面我们看到到了swipe模式的进入时机(onTouchEvent()函数里面checkSelectForSwipe()函数的调用)。但是没有看到drag模式是怎么进入的呀,别着急,另一个帮助类登场了;GestureDetectorCompat。GestureDetectorCompat里面用到的关键的东西都在ItemTouchHelperGestureListener类里面呢:
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
ItemTouchHelperGestureListener() {
}
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public void onLongPress(MotionEvent e) {
View child = findChildView(e);
if (child != null) {
ViewHolder vh = mRecyclerView.getChildViewHolder(child);
if (vh != null) {
if (!mCallback.hasDragFlag(mRecyclerView, vh)) {
return;
}
int pointerId = e.getPointerId(0);
// Long press is deferred.
// Check w/ active pointer id to avoid selecting after motion
// event is canceled.
if (pointerId == mActivePointerId) {
final int index = e.findPointerIndex(mActivePointerId);
final float x = e.getX(index);
final float y = e.getY(index);
mInitialTouchX = x;
mInitialTouchY = y;
mDx = mDy = 0f;
if (DEBUG) {
Log.d(TAG,
"onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY);
}
if (mCallback.isLongPressDragEnabled()) {
select(vh, ACTION_STATE_DRAG);
}
}
}
}
}
}
咦,就处理了一个onLongPress长按手势事件,里面逻辑就是先得到当前MotionEvent事件对应的ViewHolder,调用Callback的hasDragFlag()函数判断是否允许进入drag模式,在调用Callback的isLongPressDragEnabled()函数判断是否允许长按进入drag模式,最后调用select(vh, ACTION_STATE_DRAG)设置mSelected。之后MotionEvent移动事件的处理就都跑到OnItemTouchListener帮助类里面的onTouchEvent()函数里面去了。
通过上面对OnItemTouchListener和手势帮助类GestureDetectorCompat的分析我们可以知道:
swipe和drag模式进入的时机。swipe模式进入的判断是在OnItemTouchListener帮助类里面onTouchEvent()的函数的checkSelectForSwipe()的调用里面判断是否进入,drag模式进入的判断是在GestureDetectorCompat帮助里ItemTouchHelperGestureListener里面onLongPress()里面判断是否进入。
触摸事件在移动的过程中(信息信息请看OnItemTouchListener帮助类里面onTouchEvent()函数的MotionEvent.ACTION_MOVE逻辑处理)会一直去更新滑动的位置(updateDxDy函数)和一直让去重绘(mRecyclerView.invalidate的调用)。
ItemDecoration类的使用
分析到这个时候,咱们还没看到当进入swipe或者drag模式之后,mSelected对应的item是怎么随着手指移动的呀。这个时候就是ItemDecoration派上用场的时候了。上面触摸移动的过程中一直会调用mRecyclerView.invalidate()函数,迫使RecyclerView的onDraw()函数的调用。RecyclerView的onDraw()的调用又会引起ItemDecoration里面的onDraw()函数的调用。瞧一瞧
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
// we don't know if RV changed something so we should invalidate this index.
mOverdrawChildPosition = -1;
float dx = 0, dy = 0;
if (mSelected != null) {
getSelectedDxDy(mTmpPosition);
dx = mTmpPosition[0];
dy = mTmpPosition[1];
}
mCallback.onDraw(c, parent, mSelected,
mRecoverAnimations, mActionState, dx, dy);
}
又跑到Callback里面的onDraw()函数去了,在更进去瞧一瞧,
void onDraw(Canvas c, RecyclerView parent, ViewHolder selected,
List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
int actionState, float dX, float dY) {
final int recoverAnimSize = recoverAnimationList.size();
for (int i = 0; i < recoverAnimSize; i++) {
final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
anim.update();
final int count = c.save();
onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
false);
c.restoreToCount(count);
}
if (selected != null) {
final int count = c.save();
onChildDraw(c, parent, selected, dX, dY, actionState, true);
c.restoreToCount(count);
}
}
该函数里面分两部分,一部分是动画列表里面对应item的绘制,另一部分就是swipe或者drag模式选中的item的绘制。也好理解动画过程中的那些view要更新位置,随跟随手指移动的view也要更新位置。两个都是调用了onChildDraw()函数,最终到了ItemTouchUIUtilImpl里面BaseImpl类的
@Override
public void onDraw(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
view.setTranslationX(dX);
view.setTranslationY(dY);
}
到这里我们就分析完了item随手指移动的逻辑了。
总结下随手指移动的逻辑,在手指移动的过程中会一直调用mRecyclerView.invalidate(),迫使RecyclerView去调用onDraw(),接着调用到ItemDecoration里面的onDraw(),又调用到Callback里面的onDraw(),接着又到Callback里面的onChildDraw()函数。最终到了ItemTouchUIUtilImpl内部BaseImpl类的onDraw()函数里面最后会调用view.setTranslationX(),view.setTranslationY()来移动view。
这里你可能会提出一个疑问,不对呀。看效果的时候随手指移动的那个item感觉是绘制在RecyclerView之上的呀,因为我们手指滑动的时候选中的item是在RecyclerView的上层滑动的呀。这个是咋做到的呀。这里就要分:Build.VERSION.SDK_INT<21、Build.VERSION.SDK_INT>=21两种情况了。
Build.VERSION.SDK_INT<21:改变了RecyclerView里面item的绘制顺序,把选中的item放到最后一个绘制。详细的内容请参考select()函数里面addChildDrawingOrderCallback()里面具体细节。
Build.VERSION.SDK_INT>=21:通过给选中的item 调用View.setElevation()增加效果来实现的,详细的内容请参考ItemTouchUIUtilImpl内部Api21Impl类onDraw()函数里面具体细节。