背景
之前分享过一个拖拽recycleview的item到列表之外的功能,分析了实现的难点,现在继续分享实现过程。
之前的文章清空这里https://www.sunofbeach.net/a/842817116162752512
分析功能
- 之前提到过,rv的item只能显示在rv所在的区域中,item的view是不能显示在外面的,这一点已经卡主我们了
- 既然不能,那我们复制一份itemView,然后手指移动的时候把这个复制品跟随手指移动,可行?
- 复制后的view放哪里?我们在根布局可以新增透明的容器来容纳它
- 复制触发之后,把复制的view显示在原来item的位置上,然后把item的内容隐藏,手指移动的时候item就消失了
- 当手指松开的时候,做个动画,item跑回来开始的位置上,隐藏同时把item原来的数据显示出来
- 复制品移动到某个区域中松开的时候,就触发删除,当然可以是任意逻辑,删除了就删除rv中对用的position,更新adapter
基本上就是这么做了,当时我把上面的逻辑写在本子上,一个一个实现,最后真的做好了。思路很重要,剩下就是api和编译了,好的电脑可以让你省很多时间,调试编译最耗时。
复制一个View
何时复制?我们的条件是:长按复制。 给rv的item整一个长按事件,假设你已经给rv设置长按事件了,怎么实现,方法很多,这里就不展开,代码是kt的如果是java的同学看不懂那就将就将就.
override fun onItemLongClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int): Boolean {
//长按事件
generateItem(adapter, view, position)
//消费这个长按
return true
}
长按事件触发,这个容易理解,先尝试复制
private fun generateNewItem(view: View?) {
view?.let {
//得到当前长按view的在屏幕中位置
it.getLocationOnScreen(clickPosition)
//得到这个view
val bitmap = getBitmapByItemView(view)
//生成一个新的imageview,这个是因为我这里item是个图片,这个可以是任意的view
dragItem = ImageView(this)
//通过bitmap复制一个出来
val newBitmap = copyItemView(bitmap)
//设置这个新的view的宽高,和之前那个一样
val lp = FrameLayout.LayoutParams(newBitmap.width, newBitmap.height)
//把长按的时候记录的相对屏幕的坐标xy设置给新的复制品,让他们重叠在一起
lp.leftMargin = clickPosition.get(0)
lp.topMargin = clickPosition.get(1) - getBarHeight()
//设置参数
dragItem?.layoutParams = lp
dragItem?.setImageBitmap(newBitmap)
//我们在根布局容纳复制品的容器,显示显示,并把复制品添加到上面
flMoveContent.visibility = View.VISIBLE
flMoveContent.addView(dragItem)
}
}
这里我们处理复制,和把这个复制品添加到外部容器中,接下来手指拖动的时候整个rv会滚动起来,我们需要拦截一下,拦截条件就是:触发长按后,开始拦截rv的事件,不做滚动,手指移动的时候,我们接管这个事件,包括:down,move,up
处理item跟随手指移动
实现接口RecyclerView.OnItemTouchListener
处理是否消费事件,上面说了条件:长按触发消费,松开手指后,释放,继续让rv默认处理
onInterceptTouchEvent
处理具体的down,move,up,cancel
onTouchEvent
onRequestDisallowInterceptTouchEvent
适配相关配置如下
private fun initAdapter() {
//new适配器
dragAdapter = DragAdapter()
//关闭rv的item动画
RVUtils.disableAnim(rvTestDrag)
//3列的方式展示
val manager = GridLayoutManager(this, 3)
rvTestDrag.layoutManager = manager
rvTestDrag.adapter = dragAdapter
//长按事件
dragAdapter.setOnItemLongClickListener(this)
//item的触摸事件
rvTestDrag.addOnItemTouchListener(this)
dragAdapter.setNewData(list)
}
标记是否拦截rv的触摸
override fun onItemLongClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int): Boolean {
//长按事件
generateNewItem(view)
//标记:当前rv不可滚动,事件拦截下来,我们自己处理
rvCanMove = false
//消费这个长按
return true
}
这里就是处理拦截和触摸了,流程:当长按之后,我们自己处理触摸,释放手指后,触摸归还rv。触摸期间,我们需要把手指移动的坐标同步到外部的复制品中松开了,就计算距离,通过简单线性动画移动到原位。
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {
//触发长按之后,item的触摸事件来到这里了.MotionEvent返回手指移动的位置.以及up事件
//得到当前手指位置
val x2 = e.rawX
val y2 = e.rawY
when (e.action) {
MotionEvent.ACTION_MOVE -> {
val moveX = x2 - lastX
val moveY = y2 - lastY
dragItem?.apply {
val tranX = translationX + moveX
val tranY = translationY + moveY
//移动这个复制出来的item
translationX = tranX
translationY = tranY
}
}
MotionEvent.ACTION_UP -> {
//判断区域,手指的位置,是否在垃圾桶矩形内
if (e.rawX >= loc.get(0) && e.rawX <= endX
&& e.rawY >= loc.get(1) && e.rawY <= endY
) {
//当手指松开之后,标记设置rv可以自由处理触摸
rvCanMove = true
//根容器移除复制品,同时隐藏起来
flMoveContent.removeAllViews()
flMoveContent.visibility = View.GONE
// TODO: 2020-09-04 进入矩形之后,做我们的操作,比如触发删除
Toast.makeText(this, "已经收藏好了", Toast.LENGTH_SHORT).show()
//deleteItem()
dragAdapter.data.removeAt(currentIndex)
dragAdapter.notifyDataSetChanged()
} else {
//恢复位置,如果item不是在垃圾桶位置,松开了.就需要返回原来的位置
val currentLocalPosition = IntArray(2)
dragItem?.apply {
//得到现在复制品位置
getLocationOnScreen(currentLocalPosition)
//拿到位置,飞回去
val ani = AniUtils.getTranslateAnimation(
//开始点击的位置,和现在up的位置计算出飞行的距离
clickPosition.get(0) - currentLocalPosition.get(0),
clickPosition.get(1) - currentLocalPosition.get(1),
//动画时间
1000,
Animation.ABSOLUTE,
false
)
doAnimation(this, ani)
}
}
}
}
//更新最新的位置
lastX = x2
lastY = y2
}
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
//触发down的时候,把手指的位置记录好
lastX = e.rawX
lastY = e.rawY
}
MotionEvent.ACTION_MOVE -> {
// TODO: 2020-09-04 log,查看位置移动位置
}
MotionEvent.ACTION_UP -> {
//释放的时候,必须恢复可以滚动,移除伪装的view(复制品)
resetItem()
}
MotionEvent.ACTION_CANCEL -> {
//释放的时候,必须恢复可以滚动,移除伪装的view(复制品)
resetItem()
}
}
//是否消费
return !rvCanMove
}
最后的长按代码
override fun onItemLongClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int): Boolean {
//长按的时候,把原始的item设置隐藏,显示我们复制的view
dragAdapter.data[position].isHide = true
dragAdapter.notifyItemChanged(position)
currentIndex = position
//长按事件
generateNewItem(view)
rvCanMove = false
//消费这个长按
return true
}
我们的布局设置
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.x.rv.drag.ItemDragActivity">
<!--列表距离右边100dp-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTestDrag"
android:layout_width="match_parent"
android:background="#888888"
android:layout_height="match_parent"
android:layout_marginRight="100dp" />
<!--矩形100*100,item到这个位置,就需要出发我们删除,屏幕最右边-->
<TextView
android:id="@+id/iv_collection"
android:layout_width="100dp"
android:layout_height="100dp"
android:text="收入囊中"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--和根布局一样大的容器,放复制品用的,默认隐藏-->
<FraqmeLayout
android:id="@+id/flMoveContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
看适配器代码
package com.x.rv.drag
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.x.R
import com.x.rv.adapter.DragItem
class DragAdapter() : BaseQuickAdapter<DragItem, BaseViewHolder>(R.layout.item_drag) {
override fun convert(helper: BaseViewHolder, item: DragItem) {
helper.setVisible(R.id.ivContent, !item.isHide)
helper.setImageResource(R.id.ivContent, item.bg)
}
}
布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/ivContent"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="5dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_on" />
</RelativeLayout>
总结
功能主要是思路有了,再慢慢细分实现,当时写这个功能的时候ios的同事也在烦恼,做得差不多了后大家讨论实现居然神相似,哈哈。
功能用到知识 rv的基本数据显示 长按事件 复制view 获取view在屏幕中位置 触摸处理 拦截view事件 简单view动画
最后看看完整的效果图
如果你有更骚的方式记得来分享~
2022年03月23日14:28:55
代码上传到https://xiaozhuanlan.com/topic/1275869043