今天介绍下项目中用到的侧滑删除

  • recycleview的侧滑删除
  • 优化与项目的具体应用

先上图(简单的):

Android RecyclerView左滑显示删除 android侧滑删除控件_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.准备工作已好,接下来主要是对手势的监听: 

  1. 手指按下,获取当前子view(含菜单);关闭之前已打开的view(菜单)
  2. 手指移动,移动当前view,显示隐藏的菜单
  3. 手指抬起,判断是否显示或隐藏菜单
  4. 隐藏的菜单完全显示后,添加点击监听与回调

首先需要自定义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)
        }
    }