今天介绍下项目中用到的侧滑删除
- recycleview的侧滑删除
- 优化与项目的具体应用
先上图(简单的):
具体步骤:
1.recycleview垂直方向滑动,保证recycleview的item必须为viewgroup,并且item布局中的菜单view必须在最右边(项目中默认向左滑动有效),出可见屏幕外,指定具体的宽度。LayoutManager采用LinearLayoutManager(也可用GridLayoutManager,只要是垂直方向就行)。item布局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="60dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="59dp"
android:textSize="15sp"
android:text="第几个"
android:gravity="center"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorPrimaryDark"/>
</LinearLayout>
<TextView
android:id="@+id/tv_delete"
android:layout_width="60dp"
android:layout_height="60dp"
android:textSize="15sp"
android:gravity="center"
android:text="删除"
android:textColor="@android:color/white"
android:background="@color/colorAccent"/>
</LinearLayout>
2.准备工作已好,接下来主要是对手势的监听:
- 手指按下,获取当前子view(含菜单);关闭之前已打开的view(菜单)
- 手指移动,移动当前view,显示隐藏的菜单
- 手指抬起,判断是否显示或隐藏菜单
- 隐藏的菜单完全显示后,添加点击监听与回调
首先需要自定义RecycleView;之后子view用菜单进行代替;下面开始一一分析。
一、前三步都是对事件的处理,因此放在一起进行说明。在onInterceptTouchEvent()方法进行拦截和onTouchEvent()方法处理事件。
1.onInterceptTouchEvent()方法
①ACTION_DOWN
不能对ACTION_DOWN进行拦截(子view可能需要点击),可以进行坐标记录,以及关闭非上一个已打开的菜单。
MotionEvent.ACTION_DOWN -> {
//关闭上一个菜单
closeNowMenu(e.x.toInt(), e.y.toInt())
//记录坐标值
updateLastXY(e.x, e.y)
}
private fun closeNowMenu(x: Int, y: Int) {
if (null != mMenuView && mMenuShowAllTag) {
if (!isNowMenu(x, y)) {
closeMenu(mMenuView!!)
mMenuShowAllTag = false
slideEffiectiveTag = false
}
}
}
private fun updateDownLastXY(x: Float, y: Float) {
downLastX = x
downLastY = y
}
private fun updateLastXY(x: Float, y: Float) {
lastX = x
lastY = y
}
②ACTION_MOVE
进行是否为有效移动判断,进行酌情拦截,哈哈。。。(见checkEffectiveSlideLength()方法)添加标志位slideEffiectiveTag,标识此后所有的事件都和移动菜单有关进行拦截。并且需要确定当前触摸的item(含菜单)mMenuView,( findMotionView()方法,见代码,抄自Android的ABListView的方法,做了些修改)
private fun findMotionView(x: Int, y: Int) {
//检查是否为当前菜单
if (isNowMenu(x, y)) {
return
}
val frame = mTouchFrame
for (i in childCount - 1 downTo 0) {
val child = getChildAt(i)
if (child == mMenuView) {
continue
}
if (child.visibility == View.VISIBLE) {
child.getHitRect(frame)
if (frame.contains(x, y)) {
//当前触碰的view
mMenuView = child as ViewGroup
}
}
}
}
/**
* 是否为当前菜单
*/
private fun isNowMenu(x: Int, y: Int): Boolean {
mMenuView?.let {
it.getHitRect(mTouchFrame)
if (mTouchFrame.contains(x, y)) {
return true
}
}
return false
}
③ACTION_UP
若slideEffiectiveTag为true则直接拦截。(之后一定要注意这个标志的恢复初始化,我就是忘了,导致菜单的点击事件无法响应,查了原因才知道ACTION_UP被拦截了,导致事件在传递过程中变成了ACTION_CANCEL)
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
//关闭上一个菜单
closeNowMenu(e.x.toInt(), e.y.toInt())
//记录坐标值
updateLastXY(e.x, e.y)
}
MotionEvent.ACTION_MOVE -> {
var slide = checkEffectiveSlideLength(e.x, e.y)
if (slide > 0f) {
//查找当前菜单
findMotionView(downLastX.toInt(), downLastY.toInt())
slideEffiectiveTag = true
return true
}
}
MotionEvent.ACTION_UP -> {
if (slideEffiectiveTag) {
return true
}
}
}
var onInterceptTouchEvent = super.onInterceptTouchEvent(e)
Log.e(TAG, "onInterceptTouchEvent: result=$onInterceptTouchEvent")
return onInterceptTouchEvent
}
2.onTouchEvent()方法
①ACTION_DOWN
执行此方法,说明子view没有对事件进行消费,添加标志位onTouchEventDownTag,用于当移动有效时获取当前菜单
②ACTION_MOVE
进行是否为有效移动判断(checkEffectiveSlideLength()方法),这个有可能来自onInterceptTouchEvent对ACTION_MOVE的拦截或者onTouchEvent的ACTION_DOWN为true。但是这一步都需要知道当前触摸的菜单是哪一个。
如果onTouchEventDownTag为true,则需要重新确认当前触摸的item(含菜单)mMenuView。因为onTouchEventDownTag为true,说明事件ACTION_MOVE没有经过onInterceptTouchEvent方法,则需要确认mMenuView。
然后进行移动操作(见moveToMenuView()方法),这里借助了scrollBy和scrollTo方法。这里有一点需要注意下,当mMenuView水平滑动距离和手指的水平移动距离和大于mMenuView的宽度则直接完全展示菜单
若为有效滑动或者当前菜单已展开,则消费此事件。还记得onInterceptTouchEvent方法中对ACTION_DOWN的处理么(查看closeNowMenu()方法),当当前触碰的菜单还是上一个时,没有做关闭处理。因此此时遇到当前菜单展开,表示此事件还是和菜单滑动有关,所以进行消费。否则会导致recycleview响应上下滑动,有可能使已展开菜单滑出当前页面。
MotionEvent.ACTION_MOVE -> {
//*****中间代码忽略*****///
if (slideEffiectiveTag || mMenuShowAllTag) {
return true
}
}
/**
* 移动当前view
*/
private fun moveToMenuView(slide: Int) {
mMenuView?.let {
mMenuWidth = it.getChildAt(1).measuredWidth
if (it.scrollX + slide >= mMenuWidth) {
showMenu(it)
} else {
mMenuShowAllTag = false
it.scrollBy(slide, 0)
}
}
}
③ACTION_UP
恢复onTouchEventDownTag标志位的初始值
若slideEffiectiveTag为true即滑动有效。直接返回true,并且对mMenuView是否展开或隐藏进行判断。这里只是对mMenuView的露出部分进行了判断;如果不小于mMenuView宽度的一半则显示,否则隐藏。还有一点需要说明下,slideEffiectiveTag为false时,但是有菜单完全展示了,需要将其关闭。这是防止点击了item(含菜单)菜单之外的view,但其没有对此事件进行消费。
/**
* 检测有效的滑动距离
* 即是否符合我们要求的滑动
*/
private fun checkEffectiveSlideLength(x: Float, y: Float): Float {
var changeX = lastX - x
var changeY = lastY - y
Log.e(TAG, "checkEffectiveSlideLength: changeX=$changeX changeY=$changeY $mMinSlide")
if (changeX > 0 && changeX > Math.abs(changeY)) {//水平向右滑动
if (onTouchEventDownTag) {
findMotionView(downLastX.toInt(), downLastY.toInt())
//此标志恢复初始值,已找到当前触摸的菜单
onTouchEventDownTag = false
}
return changeX
}
updateLastXY(x, y)
return -1f
}
override fun onTouchEvent(e: MotionEvent): Boolean {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
updateLastXY(e.x, e.y)
Log.e(TAG, "onTouchEvent: ACTION_DOWN")
//findMotionView(e.x.toInt(), e.y.toInt())
onTouchEventDownTag = true
}
MotionEvent.ACTION_MOVE -> {
Log.e(TAG, "onTouchEvent: ACTION_MOVE")
var slide = checkEffectiveSlideLength(e.x, e.y)
Log.e(TAG, "onTouchEvent: slide=$slide $slideEffiectiveTag")
if (slide > 0f) {
slideEffiectiveTag = true
moveToMenuView(slide.toInt())
return true
}
if (slideEffiectiveTag || mMenuShowAllTag) {
return true
}
}
MotionEvent.ACTION_UP -> {
Log.e(TAG, "onTouchEvent: ACTION_UP")
//此标志恢复初始值
onTouchEventDownTag = false
if (slideEffiectiveTag) {
if (!mMenuShowAllTag)
upFinalMoveToMenuView()
//此标志恢复初始值
slideEffiectiveTag = false
return true
} else {
//若没有有效滑动,但已展开,则关闭菜单
if (mMenuShowAllTag) {
closeMenu()
}
}
}
}
var onTouchEvent = super.onTouchEvent(e)
Log.e(TAG, "onTouchEvent: result=$onTouchEvent")
return onTouchEvent
}
/**
* 菜单展开
*/
private fun showMenu(view: View) {
view.scrollTo(mMenuWidth, 0)
mMenuShowAllTag = true
}
/**
* 菜单关闭
*/
private fun closeMenu(view: View) {
view.scrollTo(0, 0)
Log.e(TAG, "closeMenu: ${view.hashCode()}")
mMenuShowAllTag = false
}
二、菜单的点击回调
这个没什么好说的,直接在recycleview的adapter适配器中添加点击监听即可。将完成的自定义recycleview加入加项目中,查看效果,这些代码就省略了,下面查看效果图。需要注意的是点击后别忘了调用closeMenu()方法。GitHub自定义recycleview代码
至于其中的事件拦截与消费,本文不是重点。如有错误或指导,欢迎留言。
fun closeMenu() {
mMenuView?.let {
closeMenu(it)
}
}