测试demo代码
RecyclerView recyclerView = new RecyclerView(context);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
final TouchAdapter adapter = new TouchAdapter();
recyclerView.setAdapter(adapter);
ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT | ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder viewHolder1) {
// adapter.swap(viewHolder,viewHolder1);
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
}
});
helper.attachToRecyclerView(recyclerView);
这样当recyclerview长按就可以拖动子item上下左右摆动了,但是并不会交换顺序,所以默认ItemTouchHelper实现的就是上下左右拖拽item但是不会自动交换,只是会回调交换的函数。下面一点点分析函数调用流程
注册监听器
helper.attachToRecyclerView(recyclerView);
内部调用设置很多监听器和初始化
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(this.mRecyclerView.getContext());
this.mSlop = vc.getScaledTouchSlop();
this.mRecyclerView.addItemDecoration(this);
//添加这个装饰器,下文介绍,其实就是用来绘制的
this.mRecyclerView.addOnItemTouchListener(this.mOnItemTouchListener);
//监听Item触摸事件,由RecyclerView事件分发而来
this.mRecyclerView.addOnChildAttachStateChangeListener(this);
this.startGestureDetection();
//手势监听,比如长按判断
}
事件传递流程
Down:
- RecyclerView.onInterceptTouchEvent
- RecyclerView.listener.onInterceptTouchEvent
- RecyclerView.OnItemTouchListener.onInterceptTouchEvent( 见demo:this.mRecyclerView.addOnItemTouchListener(this.mOnItemTouchListener);
) - ItemTouchHelper.this.mGestureDetector.onTouchEvent(event);
其实就是ItemTouchHelper注册了RecyclerView事件监听,然后就传递给mGestureDetector处理,mGestureDetector检测到长按事件通知出来,
这时候ItemTouchHelper也就知道长按了,然后
调用
View child = ItemTouchHelper.this.findChildView(e);
if (child != null) {
ViewHolder vh = ItemTouchHelper.this.mRecyclerView.getChildViewHolder(child);
if (vh != null) {
if (!ItemTouchHelper.this.mCallback.hasDragFlag(ItemTouchHelper.this.mRecyclerView, vh)) {
return;
}
根据手势找到属于哪一个View和ViewHolder,如果设置flag拖拽支持,则继续;
ItemTouchHelper.this.select(vh, 2);
主要是初始化this.mSelected = selected;
并且设置回味动画对象ItemTouchHelper.RecoverAnimation
这样ItemTouchHelper就知道当前有一个选中的ViewHolder
Move:
- RecyclerView.onTouchEvent(MotionEvent e)
- RecyclerView.dispatchOnItemTouch(e)
- RecyclerView.OnItemTouchListener.onTouchEvent()
- ViewHolder viewHolder = ItemTouchHelper.this.mSelected;
- switch(action) {
case 2:recyclerview.invalidate();
这几步骤分析Move的时候,其实也是从RecyclerView传递出来事件然后最终导致RecyclerView绘制
RecyclerView绘制看的会有点晕,其实再看
public void onDraw(Canvas c) {
super.onDraw(c);
int count = this.mItemDecorations.size();
for(int i = 0; i < count; ++i) {
((RecyclerView.ItemDecoration)this.mItemDecorations.get(i)).onDraw(c, this, this.mState);
}
}
RecyclerView绘制会触发ItemDecoration重新绘制的
Up:
case MotionEvent.ACTION_UP:
select(null, ACTION_STATE_IDLE);
标记当前选中为null
同时也会触发this.mRecyclerView.invalidate();
分析ItemDecoration
public class ItemTouchHelper extends ItemDecoration
看onDraw方法会触发ItemTouchUIUtilImpl.java的onChildDraw和onChildDrawOver
onChildDrawOver是空方法
onChildDraw是核心是view.setTranslationX(dX);
view.setTranslationY(dY);
其实就是x,y偏移,说来说去就是按照手移动设置x,y的偏移
偏移量计算规则
private void getSelectedDxDy(float[] outPosition) {
if ((mSelectedFlags & (LEFT | RIGHT)) != 0) {
outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft();
} else {
outPosition[0] = mSelected.itemView.getTranslationX();
}
if ((mSelectedFlags & (UP | DOWN)) != 0) {
outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop();
} else {
outPosition[1] = mSelected.itemView.getTranslationY();
}
}
参数mDx是当前手和之前落下的距离差,见
void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) {
float x = ev.getX(pointerIndex);
float y = ev.getY(pointerIndex);
this.mDx = x - this.mInitialTouchX;
this.mDy = y - this.mInitialTouchY;
参数mSelectedStartX 是选中初始位置,见
this.mSelectedStartX = (float)selected.itemView.getLeft();
select方法里面设置的,注意mInitialTouchY是onLongPress设置手势落下点
,俩个参数不一样
通知交换ViewHolder回调
在ItemTouchHelper监听滑动时候发现当前选中不为空切拖拽模式
ItemTouchHelper.this.updateDxDy(event, ItemTouchHelper.this.mSelectedFlags, activePointerIndex);
ItemTouchHelper.this.moveIfNecessary(viewHolder);
ItemTouchHelper.this.mRecyclerView.removeCallbacks(ItemTouchHelper.this.mScrollRunnable);
ItemTouchHelper.this.mScrollRunnable.run();
ItemTouchHelper.this.mRecyclerView.invalidate();
其中ItemTouchHelper.this.moveIfNecessary(viewHolder);就是触发回调交换顺序
分析浮在RecyclerView最上面实现原理
Build.VERSION.SDK_INT < 21
改写拖拽的view位置getChildDrawingOrder
mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
@Override
public int onGetChildDrawingOrder(int childCount, int i) {
if (mOverdrawChild == null) {
return i;
}
int childPosition = mOverdrawChildPosition;
if (childPosition == -1) {
childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
mOverdrawChildPosition = childPosition;
}
if (i == childCount - 1) {
return childPosition;
}
return i < childPosition ? i : i + 1;
}
Build.VERSION.SDK_INT >= 21见ItemTouchUIUtilImpl
if (Build.VERSION.SDK_INT >= 21) {
if (isCurrentlyActive) {
Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
if (originalElevation == null) {
originalElevation = ViewCompat.getElevation(view);
float newElevation = 1f + findMaxElevation(recyclerView, view);
ViewCompat.setElevation(view, newElevation);
view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
}
}
}
分析总结
默认长按的情况,进入拖拽模式,随着手移动,计算x,y偏移量,然后
手势触发绘制不是直接让view调用setTranlateX方法,而是recyclerview.invalidate,然后触发ItemDecoration绘制调用onDraw方法,
然后设置拖拽的view设置setTranlateX以及setTranlateY