建设有限公司官网,郑州网站seo优,肇庆建设银行招聘网站,企业网站运维效果图简介本文主角是ItemTouchHelper。它是RecyclerView对于item交互处理的一个「辅助类」#xff0c;主要用于拖拽以及滑动处理。以接口实现的方式#xff0c;达到配置简单、逻辑解耦、职责分明的效果#xff0c;并且支持所有的布局方式。功能拆解功能实现4.1、实现接口自…效果图简介本文主角是ItemTouchHelper。它是RecyclerView对于item交互处理的一个「辅助类」主要用于拖拽以及滑动处理。以接口实现的方式达到配置简单、逻辑解耦、职责分明的效果并且支持所有的布局方式。功能拆解功能实现4.1、实现接口自定义一个类实现ItemTouchHelper.Callback接口然后在实现方法中根据需求简单配置即可。class DragCallBack(adapter: DragAdapter, data: MutableListString) : ItemTouchHelper.Callback() {
}ItemTouchHelper.Callback必须实现的3个方法getMovementFlagsonMoveonSwiped其他方法还有onSelectedChanged、clearView等。4.1.1、getMovementFlags用于创建交互方式交互方式分为两种1. 拖拽网格布局支持上下左右列表只支持上下LEFT、UP、RIGHT、DOWN。2. 滑动只支持前后START、END。最后通过makeMovementFlags把结果返回回去makeMovementFlags接收两个参数dragFlags和swipeFlags即上面拖拽和滑动组合的标志位。override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {var dragFlags 0var swipeFlags 0when (recyclerView.layoutManager) {is GridLayoutManager - {// 网格布局dragFlags ItemTouchHelper.LEFT or ItemTouchHelper.UP or ItemTouchHelper.RIGHT or ItemTouchHelper.DOWNreturn makeMovementFlags(dragFlags, swipeFlags)}is LinearLayoutManager - {// 线性布局dragFlags ItemTouchHelper.UP or ItemTouchHelper.DOWNswipeFlags ItemTouchHelper.START or ItemTouchHelper.ENDreturn makeMovementFlags(dragFlags, swipeFlags)}else - {// 其他情况可自行处理return 0}}
}4.1.2、onMove拖拽时回调这里我们主要对起始位置和目标位置的item做一个数据交换然后刷新视图显示。override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {// 起始位置val fromPosition viewHolder.adapterPosition// 结束位置val toPosition target.adapterPosition// 固定位置if (fromPosition mAdapter.fixedPosition || toPosition mAdapter.fixedPosition) {return false}// 根据滑动方向 交换数据if (fromPosition toPosition) {// 含头不含尾for (index in fromPosition until toPosition) {Collections.swap(mData, index, index 1)}} else {// 含头不含尾for (index in fromPosition downTo toPosition 1) {Collections.swap(mData, index, index - 1)}}// 刷新布局mAdapter.notifyItemMoved(fromPosition, toPosition)return true
}4.1.3、onSwiped滑动时回调这个回调方法里主要是做数据和视图的更新操作。override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {if (direction ItemTouchHelper.START) {Log.i(TAG, START---向左滑)} else {Log.i(TAG, END---向右滑)}val position viewHolder.adapterPositionmData.removeAt(position)mAdapter.notifyItemRemoved(position)}4.2、绑定RecyclerView上面接口实现部分我们已经简单写好了逻辑也挺简单总共不超过100行代码。接下来就是把这个辅助类绑定到RecyclerView。RecyclerView显示的实现就是基础的样式就不展开了可以查看源码。val dragCallBack DragCallBack(mAdapter, list)
val itemTouchHelper ItemTouchHelper(dragCallBack)
itemTouchHelper.attachToRecyclerView(mBinding.recycleView)绑定只需要调用attachToRecyclerView就好了。至此简单的效果就已经实现了。下面开始优化和进阶的部分。4.3、设置分割线RecyclerView网格布局实现等分我们一般先是自定义ItemDecoration然后调用addItemDecoration来实现的。但是我在实现效果的时候遇到一个问题因为我加了布局切换的功能在每次切换的时候针对不同的布局分别设置layoutManager和ItemDecoration这就导致随着切换次数的增加item的间隔就越大。addItemDecoration顾名思义是添加通过查看源码发现RecyclerView内部是有一个ArrayList来维护的所以当我们重复调用addItemDecoration方法时分割线是以递增的方式在增加的并且在绘制的时候会从集合中遍历所有的分割线绘制。部分源码Override
public void draw(Canvas c) {super.draw(c);final int count mItemDecorations.size();for (int i 0; i count; i) {mItemDecorations.get(i).onDrawOver(c, this, mState);}//...
}既然知道了问题所在也大概想到了3种解决办法1. 调用addItemDecoration前先调用removeItemDecoration方法remove掉之前所有的分割线。2. 调用addItemDecoration(NonNull ItemDecoration decor, int index)通过index来维护。3. add时通过一个标示来判断添加过就不添加了。好像可行实际上并不太行...因为始终都有两个分割线实例。我们再来梳理一下两种不同的布局都有分割线分割线只需设置一次我想到另外一个办法不对RecyclerView做处理了既然两种布局都有分割线是不是可以把分割线合二为一了然后根据LayoutManager去绘制不同的分割线理论上是可行的事实上也确实可以...自定义分割线class GridSpaceItemDecoration(private val spanCount: Int, private val spacing: Int 20, private var includeEdge: Boolean false) :RecyclerView.ItemDecoration() {override fun getItemOffsets(outRect: Rect, view: View, recyclerView: RecyclerView, state: RecyclerView.State) {recyclerView.layoutManager?.let {when (recyclerView.layoutManager) {is GridLayoutManager - {val position recyclerView.getChildAdapterPosition(view) // 获取item在adapter中的位置val column position % spanCount // item所在的列if (includeEdge) {outRect.left spacing - column * spacing / spanCountoutRect.right (column 1) * spacing / spanCountif (position spanCount) {outRect.top spacing}outRect.bottom spacing} else {outRect.left column * spacing / spanCountoutRect.right spacing - (column 1) * spacing / spanCountif (position spanCount) {outRect.top spanCount}outRect.bottom spacing}}is LinearLayoutManager - {outRect.top spanCountoutRect.bottom spacing}}}}}4.4、选中放大/背景变色为了提升用户体验可以在拖拽的时候告诉用户当前拖拽的是哪个item比如选中的item放大、背景高亮等。网格布局选中变大。列表布局背景变色。这里用到ItemTouchHelper.Callback中的两个方法onSelectedChanged和clearView我们需要在选中时改变视图显示结束时再恢复。4.4.1、onSelectedChanged拖拽或滑动 发生改变时回调这时我们可以修改item的视图。override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {if (actionState ! ItemTouchHelper.ACTION_STATE_IDLE) {viewHolder?.let {// 因为拿不到recyclerView无法通过recyclerView.layoutManager来判断是什么布局所以用item的宽度来判断// itemView.width 500 用这个来判断是否是线性布局实际取值自己看情况if (it.itemView.width 500) {// 线性布局 设置背景颜色val drawable it.itemView.background as GradientDrawabledrawable.color ContextCompat.getColorStateList(it.itemView.context, R.color.greenDark)} else {// 网格布局 设置选中放大ViewCompat.animate(it.itemView).setDuration(200).scaleX(1.3F).scaleY(1.3F).start()}}}super.onSelectedChanged(viewHolder, actionState)
}actionStateACTION_STATE_IDLE 空闲状态。ACTION_STATE_SWIPE 滑动状态。ACTION_STATE_DRAG 拖拽状态。4.4.2、clearView拖拽或滑动 结束时回调这时我们要把改变后的item视图恢复到初始状态。override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {// 恢复显示// 这里不能用if判断因为GridLayoutManager是LinearLayoutManager的子类改用when类型推导有区别when (recyclerView.layoutManager) {is GridLayoutManager - {// 网格布局 设置选中大小ViewCompat.animate(viewHolder.itemView).setDuration(200).scaleX(1F).scaleY(1F).start()}is LinearLayoutManager - {// 线性布局 设置背景颜色val drawable viewHolder.itemView.background as GradientDrawabledrawable.color ContextCompat.getColorStateList(viewHolder.itemView.context, R.color.greenPrimary)}}super.clearView(recyclerView, viewHolder)
}4.5、固定位置在实际需求中交互可能要求我们第一个菜单不可以变更顺序只能固定比如效果中的第一个菜单「推荐」固定在首位这种情况。4.5.1、修改adapter定义一个固定值并设置不同的背景色和其他菜单区分开。class DragAdapter(private val mContext: Context, private val mList: ListString) : RecyclerView.AdapterDragAdapter.ViewHolder() {val fixedPosition 0 // 固定菜单override fun onBindViewHolder(holder: ViewHolder, position: Int) {holder.mItemTextView.text mList[position]// 第一个固定菜单val drawable holder.mItemTextView.background as GradientDrawableif (holder.adapterPosition 0) {drawable.color ContextCompat.getColorStateList(mContext, R.color.greenAccent)}else{drawable.color ContextCompat.getColorStateList(mContext, R.color.greenPrimary)}}//...
}4.5.1、修改onMove回调在onMove方法中判断只要是固定位置就直接返回false。class DragCallBack(adapter: DragAdapter, data: MutableListString) : ItemTouchHelper.Callback() {/*** 拖动时回调*/override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {// 起始位置val fromPosition viewHolder.adapterPosition// 结束位置val toPosition target.adapterPosition// 固定位置if (fromPosition mAdapter.fixedPosition || toPosition mAdapter.fixedPosition) {return false}// ...return true}
}虽然第一个菜单无法交换位置了但是它还是可以拖拽的。效果实现了吗好像也实现了可是又好像哪里不对就好像填写完表单点击提交时你告诉我格式不正确一样你不能一开始就告诉我吗为了进一步提升用户体验可以让固定位置不可以拖拽吗可以ItemTouchHelper.Callback中有两个方法1. isLongPressDragEnabled 是否可以长按拖拽。2. isItemViewSwipeEnabled 是否可以滑动。这俩方法默认都是true所以即使不能交换位置但默认也是支持操作的。4.5.3、重写isLongPressDragEnabled以拖拽举例我们需要重写isLongPressDragEnabled方法把它禁掉然后在非固定位置的时候去手动开启。override fun isLongPressDragEnabled(): Boolean {//return super.isLongPressDragEnabled()return false
}禁掉之后什么时候再触发呢因为我们现在的交互是长按进入编辑那就需要在长按事件中再调用startDrag手动开启。mAdapter.setOnItemClickListener(object : DragAdapter.OnItemClickListener {//...override fun onItemLongClick(holder: DragAdapter.ViewHolder) {if (holder.adapterPosition ! mAdapter.fixedPosition) {itemTouchHelper.startDrag(holder)}}
})ok这样就完美实现了。4.6、其他4.6.1、position因为有拖拽操作下标其实是变化的在做相应的操作时要取实时位置。holder.adapterPosition4.6.2、重置不管是拖拽还是滑动其实本质都是对Adapter内已填充的数据进行操作实时数据通过Adapter获取即可。如果想要实现重置功能直接拿最开始的原始数据重新塞给Adapter即可。源码探索看源码时找对一个切入点往往能达到事半功倍的效果。这里就从绑定RecyclerView开始吧。val dragCallBack DragCallBack(mAdapter, list)
val itemTouchHelper ItemTouchHelper(dragCallBack)
itemTouchHelper.attachToRecyclerView(mBinding.recycleView)实例化ItemTouchHelper然后调用其attachToRecyclerView方法绑定到RecyclerView。5.1、attachToRecyclerViewpublic 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();}
}这段代码其实有点意思的解读一下1. 第一个if判断避免重复操作直接return。2. 第二个if判断调用了destroyCallbacks在destroyCallbacks里面做了一些移除和回收操作说明只能绑定到一个RecyclerView同时注意这里判断的主体是mRecyclerView不是我们传进来的recyclerView而且我们传进来的recyclerView是支持Nullable的所以我们可以传个空值走到destroyCallbacks里来做解绑操作。3. 第三个if判断当我们传的recyclerView不为空时调用setupCallbacks。5.2、setupCallbacksprivate void setupCallbacks() {ViewConfiguration vc ViewConfiguration.get(mRecyclerView.getContext());mSlop vc.getScaledTouchSlop();mRecyclerView.addItemDecoration(this);mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);mRecyclerView.addOnChildAttachStateChangeListener(this);startGestureDetection();
}这个方法里已经大概可以看出内部实现原理了。两个关键点addOnItemTouchListenerstartGestureDetection通过触摸和手势识别来处理交互显示。5.3、mOnItemTouchListenerprivate final OnItemTouchListener mOnItemTouchListener new OnItemTouchListener() {Overridepublic boolean onInterceptTouchEvent(NonNull RecyclerView recyclerView, NonNull MotionEvent event) {mGestureDetector.onTouchEvent(event);if (action MotionEvent.ACTION_DOWN) {//...if (mSelected null) {if (animation ! null) {//...select(animation.mViewHolder, animation.mActionState);}}} else if (action MotionEvent.ACTION_CANCEL || action MotionEvent.ACTION_UP) {select(null, ACTION_STATE_IDLE);} else if (mActivePointerId ! ACTIVE_POINTER_ID_NONE) {//...if (index 0) {checkSelectForSwipe(action, event, index);}}return mSelected ! null;}Overridepublic void onTouchEvent(NonNull RecyclerView recyclerView, NonNull MotionEvent event) {mGestureDetector.onTouchEvent(event);//...if (activePointerIndex 0) {checkSelectForSwipe(action, event, activePointerIndex);}switch (action) {case MotionEvent.ACTION_MOVE: {if (activePointerIndex 0) {moveIfNecessary(viewHolder);}break;}//...}}Overridepublic void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {select(null, ACTION_STATE_IDLE);}
};这段代码删减之后还是有点多不过没关系提炼一下核心通过判断MotionEvent调用了几个方法selectcheckSelectForSwipemoveIfNecessary5.3.1、selectvoid 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()实时刷新。5.3.2、checkSelectForSwipevoid checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {//...if (absDx mSlop absDy mSlop) {return;}if (absDx absDy) {if (dx 0 (swipeFlags LEFT) 0) {return;}if (dx 0 (swipeFlags RIGHT) 0) {return;}} else {if (dy 0 (swipeFlags UP) 0) {return;}if (dy 0 (swipeFlags DOWN) 0) {return;}}select(vh, ACTION_STATE_SWIPE);
}这里是滑动处理的check最后也是收敛到select()方法统一处理。5.3.3、moveIfNecessaryvoid moveIfNecessary(ViewHolder viewHolder) {if (mRecyclerView.isLayoutRequested()) {return;}if (mActionState ! ACTION_STATE_DRAG) {return;}//...if (mCallback.onMove(mRecyclerView, viewHolder, target)) {// keep target visiblemCallback.onMoved(mRecyclerView, viewHolder, fromPosition,target, toPosition, x, y);}
}这里检查拖拽时是否需要交换item通过mCallback.onMoved回调给我们。5.4、startGestureDetectionprivate void startGestureDetection() {mItemTouchHelperGestureListener new ItemTouchHelperGestureListener();mGestureDetector new GestureDetectorCompat(mRecyclerView.getContext(),mItemTouchHelperGestureListener);
}5.4.1、ItemTouchHelperGestureListenerprivate class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {//...Overridepublic 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()方法统一处理。5.5、源码小结1. 绑定RecyclerView。2. 注册触摸手势监听。3. 根据手势先是内部处理各种校验、位置计算、动画处理、刷新等然后回调给ItemTouchHelper.Callback。事儿大概就是这么个事儿主要工作都是源码帮我们做了我们只需要在回调里根据结果处理业务逻辑即可。源码地址https://github.com/yechaoa/MaterialDesign