一.简介
本文主角是ItemTouchHelper
,它是RecyclerView对于item交互处理的一个辅助类,主要用于拖拽以及滑动处理。关于RecyclerView的分析可参考文章RecyclerView显示及缓存机制
以接口实现的方式,起到了配置简单、逻辑解耦、职责分明的效果,并且支持所有的布局方式。
功能包括如下:
二.功能实现
2.1.实现接口
自定义一个类,实现ItemTouchHelper.Callback
接口,然后在实现方法中根据需求简单配置即可,代码如下:
public class TaskItemTouchHelper extends ItemTouchHelper.Callback {
}
ItemTouchHelper.Callback必须实现的3个方法:
• getMovementFlags
• onMove
• onSwiped
其他方法还有onSelectedChanged、clearView、getSwipeThreshold等
接下来对上述方法进行一一描述分析:
2.1.1.getMovementFlags
用于创建交互方式,交互方式分为两种:
a.拖拽:网格布局支持上下左右,列表只支持上下(UP、DOWN)
b.滑动:支持上下左右滑动(LEFT、UP、RIGHT、DOWN)
方法实现如下:
/**
* Called first when Callback, it is used to determine action and direction for current
* function: drag and swipe action
*/
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//direction:up,down,left,right
//constants
// ItemTouchHelper.UP 0x0001
// ItemTouchHelper.DOWN 0x0010
// ItemTouchHelper.LEFT 0x0100
// ItemTouchHelper.RIGHT 0x1000
//listen for direction of drag
int dragFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
//listen for direction of swipe
int swipeFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
return makeMovementFlags(dragFlags, swipeFlags);
}
通过makeMovementFlags
把结果返回回去,makeMovementFlags接收两个参数,dragFlags
和swipeFlags
,即拖拽和滑动组合的标志位。
2.1.2.onMove
拖拽时回调,这里我们主要对起始位置和目标位置的item做一个数据交换,然后刷新视图显示。
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder srcHolder,RecyclerView.ViewHolder targetHolder) {
// call adapter.notifyItemMoved(from,to) continuously when drag process
if (srcHolder.getItemViewType() != targetHolder.getItemViewType()) {
return false;
}
// call adapter.notifyItemMoved(from,to) continuously when drag process,
// callback onItemMove to implements class
return moveListener.onItemMove(srcHolder.getAdapterPosition(),
targetHolder.getAdapterPosition());
}
通过获取起始位置,不断调用adapter的notifyItemMoved()对UI进行刷新。
2.1.3.onSwiped
滑动时回调,这个回调方法里主要是做数据和视图的更新操作。
@Override
public void onSwiped(RecyclerView.ViewHolder holder, int direction) {
//listen for swipe up:1.delete data;2.call adapter.notifyItemRemove(position);
moveListener.onItemRemove(holder.getAdapterPosition());
}}
比如:滑动删除某个item,监听到满足swipe()的阈值后进行删除操作;
2.2.关联RecyclerView
上面接口实现部分已经实现了,那么如何将ItemTouchHelper与RecyclerView建立关联呢?
接下来就是把这个辅助类绑定到RecyclerView,代码实现如下:
mTaskAdapter = new TaskAdapter(this, availableList);
ItemTouchHelper.Callback callback = new TaskItemTouchHelper(mTaskAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(mRecyclerView);
关联只需要调用attachToRecyclerView
就好了。
2.3.设置分割线
通过RecyclerView的抽象静态内部类ItemDecoration来实现;
public class SpaceItemDecoration extends RecyclerView.ItemDecoration {
private int mItemSpace;
private int mTopSpace;
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
calculateOutRect(outRect, view, parent);
}
public SpaceItemDecoration(int space, int topSpace) {
this.mItemSpace = space;
this.mTopSpace = topSpace;
}
private void calculateOutRect(Rect outRect, View view, RecyclerView parent) {
RecyclerView.Adapter adapter = parent.getAdapter();
if (adapter != null) {
int count = adapter.getItemCount();
int position = parent.getChildPosition(view);
switch (count) {
case 1:
outRect.top = (SCREEN_HEIGHT - ITEM_HEIGHT) / 2;
outRect.right = (SCREEN_WIDTH - ITEM_WIDTH) / 2;
break;
case 2:
if (position % 2 == 0) {
outRect.top = mTopSpace;
} else {
outRect.top = 0;
}
outRect.right = (SCREEN_WIDTH - ITEM_WIDTH) / 2;
break;
case 3:
case 4:
if (position % 2 == 0) {
outRect.top = mTopSpace;
} else {
outRect.top = 0;
}
if (position < 2) {
outRect.right = (SCREEN_WIDTH - ITEM_WIDTH * 2 - mItemSpace) / 2;
}
outRect.left = mItemSpace;
break;
default:
if (position % 2 == 0) {
outRect.top = mTopSpace;
} else {
outRect.top = 0;
}
if (position < 2) {
outRect.right = mItemSpace;
}
outRect.left = mItemSpace;
break;
}
}
}
}
代码实现也比较简单,通过addItemDecoration()来建立关联就可以了;
TaskGridLayoutManager gridLayoutManager = new TaskGridLayoutManager(this, 2,
RecyclerView.HORIZONTAL, true);
mRecyclerView.setLayoutManager(gridLayoutManager);
mRecyclerView.addItemDecoration(new SpaceItemDecoration(100, 150));
传入不同Item之间的间隔,比如:top和gap,在加载Item时会回调getItemOffsets来获取该Item对应的Rect,然后再内部进行处理最终确定Item对应的Rect就可以了。
2.4.UI强调
在平时的滑动或拖拽交互中,为了区分选中的Item,会对其进行强调,比如:选中的item放大、背景高亮等。
此处会用到ItemTouchHelper.Callback中的其他两个方法,onSelectedChanged
和clearView
,在选中时改变视图显示,结束时再进行恢复。
2.4.1.onSelectedChanged
拖拽或滑动发生改变时回调,这时我们可以修改item的视图;
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
//judge state for selected
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
swipeAnimation(viewHolder, true);
}
super.onSelectedChanged(viewHolder, actionState);
}
actionState对应三种state:
ACTION_STATE_IDLE:空闲状态
ACTION_STATE_SWIPE:滑动状态
ACTION_STATE_DRAG:拖拽状态
2.4.2.clearView
拖拽或滑动结束时回调,这时我们要把改变后的item视图恢复到初始状态
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
swipeAnimation(viewHolder, false);
super.clearView(recyclerView, viewHolder);
}
在拖拽或滑动开始和结束时,通过swipeAnimation()进行强调:
private void swipeAnimation(RecyclerView.ViewHolder viewHolder, boolean isStart) {
float[] floatPram;
if (isStart) {
floatPram = new float[]{1.0f, 1.05f};
} else {
floatPram = new float[]{1.05f, 1.0f};
}
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator scaleX = ObjectAnimator.ofFloat(viewHolder.itemView, "scaleX", floatPram);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(viewHolder.itemView, "scaleY", floatPram);
scaleX.setDuration(200);
scaleY.setDuration(200);
animatorSet.playTogether(scaleX,scaleY);
animatorSet.start();
}
三.源码分析
在前面的功能实现代码中,可以看到,在创建完ItenTouchHelper及Callback后,会通过attachToRecyclerView()建立关联,本文就从该方法开始分析:
3.1.attachToRecyclerView
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (recyclerView != null) {
final Resources resources = recyclerView.getResources();
mSwipeEscapeVelocity = resources
.getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
mMaxSwipeVelocity = resources.getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
setupCallbacks();
}
}
从该方法可以看到,主要操作如下:
a.首先进行重复判断,如果是相同的recyclerview,直接返回;
b.如果mRecyclerView不为空,调用了destroyCallbacks,在destroyCallbacks里面对mRecyclerView进行了一些移除和回收操作,说明只能绑定到一个RecyclerView;同时,注意这里判断的主体是mRecyclerView,不是我们传进来的recyclerView,而且我们传进来的recyclerView是支持Nullable的,所以我们可以传个空值走到destroyCallbacks里来做解绑操作
c.最后当传入的recyclerView不为空时,调用setupCallbacks();
前面分析到,destroyCallbacks会进行一些移除和回收操作,那么setupCallbacks()应该是执行初始化操作,一起看一下具体实现:
3.2.setupCallbacks
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.addOnChildAttachStateChangeListener(this);
startGestureDetection();
}
可以看到,在setupCallbacks()内部会执行了四个方法:
1.执行addItemDecoration(this)将自身加入到RecyclerView中的mItemDecorations进行管理,注意:ItemTouchHelper是继承了RecyclerView.ItemDecoration;
2.执行addOnItemTouchListener()对RecyclerView的Item触摸事件进行监听处理;
3.执行addOnChildAttachStateChangeListener对child View是否移除Window进行回调监听;
4.执行startGestureDetection()来进行手势识别监听;
3.3.mOnItemTouchListener
private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
@NonNull MotionEvent event) {
mGestureDetector.onTouchEvent(event);
final int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
.................
if (mSelected == null) {
final RecoverAnimation animation = findAnimation(event);
if (animation != null) {
.................
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) {
final int index = event.findPointerIndex(mActivePointerId);
if (index >= 0) {
checkSelectForSwipe(action, event, index);
}
}
}
@Override
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
mGestureDetector.onTouchEvent(event);
..................
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) {
moveIfNecessary(viewHolder);
}
break;
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (!disallowIntercept) {
return;
}
select(null, ACTION_STATE_IDLE);
}
};
该方法主要功能就是监听MotionEvent进行判断处理,然后执行对不同的手势进行不同的处理,主要有以下三个方法:
a.select
b.checkSelectForSwipe
c.moveIfNecessary
3.3.1.select
void select(@Nullable ViewHolder selected, int actionState) {
if (selected == mSelected && actionState == mActionState) {
return;
}
.........
if (mSelected != null) {
if (prevSelected.itemView.getParent() != null) {
final float targetTranslateX, targetTranslateY;
switch (swipeDir) {
case LEFT:
case RIGHT:
case START:
case END:
targetTranslateY = 0;
targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth(); break;
............
}
...........
} else {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); mCallback.clearView(mRecyclerView, prevSelected);
}
}
..........
mCallback.onSelectedChanged(mSelected, mActionState);
mRecyclerView.invalidate();
}
在该方法内,这里面主要是在拖拽或滑动时对translateX/Y
的计算和处理,然后通过mCallback.clearView和mCallback.onSelectedChanged进行回调,最后调用invalidate()实时刷新。
3.3.2.checkSelectForSwipe
void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
............//进行过滤判断
final ViewHolder vh = findSwipedView(motionEvent);
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;
}
......................
//最终满足swipe的操作
select(vh, ACTION_STATE_SWIPE);
}
该方法时对滑动处理的check,最后也是收敛到select()方法统一处理。
3.3.3.moveIfNecessary
void moveIfNecessary(ViewHolder viewHolder) {
if (mRecyclerView.isLayoutRequested()) {
return;
}
if (mActionState != ACTION_STATE_DRAG) {
return;
}
.............
//最终满足onMove交换的操作
if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
// keep target visible
mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,target, toPosition, x, y);
}
}
该方法时检查拖拽时是否需要交换item,通过mCallback.onMoved进行回调。
3.4.startGestureDetection
private void startGestureDetection() {
mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
mItemTouchHelperGestureListener);
}
3.4.1.ItemTouchHelperGestureListener
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
...........
@Override
public void onLongPress(MotionEvent e) {
.......
View child = findChildView(e);
if (child != null) {
ViewHolder vh = mRecyclerView.getChildViewHolder(child);
if (vh != null) {
...........
if (pointerId == mActivePointerId) {
...............
if (mCallback.isLongPressDragEnabled()) {
select(vh, ACTION_STATE_DRAG);
}
}
}
}
}
}
此处主要是对长按事件的处理,最后也是收敛到select()方法统一处理。
简单总结
a.绑定RecyclerView
b.注册触摸手势监听
c.根据手势,先是内部处理各种校验、位置计算、动画处理、刷新等,然后回调给ItemTouchHelper.Callback
3.5.ItemDecoration
前面讲到,本地继承RecyclerView.ItemDecoration,重写getItemOffsets可以设置分割线,接下来看一下具体的逻辑实现:
3.5.1.addItemDecoration
public void addItemDecoration(@NonNull ItemDecoration decor) {
addItemDecoration(decor, -1);
}
public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
if (mLayout != null) {
mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll or"+ " layout");
}
if (mItemDecorations.isEmpty()) {
setWillNotDraw(false);
}
if (index < 0) {
mItemDecorations.add(decor);
} else {
mItemDecorations.add(index, decor);
}
markItemDecorInsetsDirty();
requestLayout();
}
可以看到,在执行addItemDecoration()会将该decor加入到mItemDecorations中进行管理,从此处可以看到,ItemDecoration可以是多个,可以单独对Rect的各个区域进行单独处理,在存储之后,看一下是如何用到的?
我们知道,ItemDecoration是在RecyclerView进行加载ViewHolder时进行使用,因为要确定各个ViewHolder的显示位置及显示大小,那根据这个思路就好分析了,这里要说明一下,RecyclerView的ViewHolder的显示是由LayoutManager来进行管理的,中间的过程就不一一陈述了,可以直接阅读源码,此处直接从LayoutManager的measureChild进行分析:
3.5.2.measureChild
public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
可以看到,在该方法内会调用RecyclerView的getItemDecorInsetsForChild()来获取到child view对应的Rect,然后来计算child view需要显示的区域;
3.5.3.getItemDecorInsetsForChild
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
.................
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
可以看到,在该方法内会遍历mItemDecorations,通过ItemDecoration的getItemOffsets来获取对应的mTempRect,在赋值给insets,最终返回;
3.5.4.getItemOffsets
在本地实现getItemOffsets时,会先调用super.getItemOffsets(),主要用来对Rect进行置空,主要作用是确保每个Item不会相互影响,即每个item的显示区域可以随意定义;
@Deprecated
public void getItemOffsets(@NonNull Rect outRect, int itemPosition,@NonNull RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,@NonNull RecyclerView parent, @NonNull State state) {
getItemOffsets(outRect, ((LayoutParams)view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
以上就是RecycleView UI实现拖拽即滑动处理的实现及部分源码实现分析!