recycleview中item的侧滑扩展优化与项目中的具体应用

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

之前的文章只是实现了item的侧滑,但在项目中需要添加滑动的优化以及以后的扩展

扩展效果图:

效果图

详情

分析:

  1. 滑动优化是指从部分展开过度到安全展开,添加动画机制或者使用Scroller控制滑动,使其不那么生硬
  2. 添加移动速度,进行展开判断
  3. 提供定制的item,封装菜单view与内容view,各个部位的点击

一、滑动优化

这里本来想借助Scroller来控制滑动,但是发现关闭进行中同时打开菜单,有冲突。解决这个冲突,想到了借助item里的Scroller,进而更新。而不是recycleview的Scroller。这样比较繁琐,以此放弃采用动画(ObjectAnimator)。有兴趣的朋友可以尝试下Scroller。

ObjectAnimator采用默认的插值器,因scrollTo方法的参数是Int类型,以此如下:

    private val objectUpdateListener = object : ValueAnimator.AnimatorUpdateListener {
        override fun onAnimationUpdate(animation: ValueAnimator) {
            if (animation is ObjectAnimator) {
                var target = animation.target as View
                var slide = animation.animatedValue as Int
                target.scrollTo(slide, 0)
            }
        }

    }
    private val closeAnimator: ObjectAnimator by lazy {
        ObjectAnimator.ofInt(this@ItemSlideRecycleView, "slideClose", 0, 0)
            .apply {
                duration = 200
                addUpdateListener(objectUpdateListener)
            }
    }
    private val openAnimator: ObjectAnimator by lazy {
        ObjectAnimator.ofInt(this@ItemSlideRecycleView, "slideOpen", 0, 0)
            .apply {
                duration = 200
                addUpdateListener(objectUpdateListener)
            }
    }
    private fun startCloseAnimator(view: View, start: Int, end: Int) {
        closeAnimator.target = view
        closeAnimator.setIntValues(start, end)
        closeAnimator.start()
    }

    private fun startOpenAnimator(view: View, start: Int, end: Int) {
        openAnimator.target = view
        openAnimator.setIntValues(start, end)
        openAnimator.start()
    }

修改打开与关闭菜单方法:

    /**
     * 菜单展开
     */
    private fun showMenu(view: View) {
        startOpenAnimator(view, view.scrollX, mMenuWidth)
        mMenuShowAllTag = true
    }

    /**
     * 菜单关闭
     */
    private fun closeMenu(view: View) {
        if (closeAnimator.isRunning) {
            closeAnimator.cancel()
            (closeAnimator.target as View).scrollTo(0, 0)
        }
        startCloseAnimator(view, view.scrollX, 0)
        Log.e(TAG, "closeMenu: ${view.hashCode()}")
        mMenuShowAllTag = false
    }

二、添加手指滑动速度判断

当手指的移动速度大于一定值时,也认为是打开菜单的意图。所以在checkEffectiveSlideLength()添加判断。

1.首先需要初始化相关类VelocityTracker

    private val mVelocityTracker: VelocityTracker by lazy {
        VelocityTracker.obtain()
    }

2.添加速度判断

    /**
     * 最小有效滑动速度
     * 向左滑,速度为负数
     */
    private val MIN_SPEED = -400

    /**
     * 检测滑动速度是否符合我们的要求
     */
    private fun checkEffectiveSpeed(): Boolean {
        mVelocityTracker.computeCurrentVelocity(1000)
        return mVelocityTracker.xVelocity <= MIN_SPEED
    }

3.我们需要在onInterceptTouchEvent方法处理ACTION_MOVE事件以及onTouchEvent方法处理ACTION_UP事件添加速度判断。因为一个决定是否拦截此事时间,一个判断是否展开菜单,都需要速度的判断。

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        mVelocityTracker.addMovement(e)
        when (e.action) {
            /*代码省略*/
            MotionEvent.ACTION_MOVE -> {
                if (checkEffectiveSlide(e.x, e.y)) {
                    //查找当前菜单
                    findMotionView(downLastX.toInt(), downLastY.toInt())
                    slideEffiectiveTag = true
                    return true
                }
            }
            /*代码省略*/
            
        }
        var onInterceptTouchEvent = super.onInterceptTouchEvent(e)
        Log.e(TAG, "onInterceptTouchEvent: result=$onInterceptTouchEvent")
        return onInterceptTouchEvent
    }
    /**
     * 检测滑动是否符合我们的要求
     */
    private fun checkEffectiveSlide(x: Float, y: Float): Boolean {
        if (checkEffectiveSpeed()) {
            return true
        }
        var changeX = lastX - x
        var changeY = lastY - y
        if (changeX > mMinSlide && changeX > Math.abs(changeY)) {//水平向右滑动
            return true
        }
        return false
    }
    override fun onTouchEvent(e: MotionEvent): Boolean {
        mVelocityTracker.addMovement(e)
        when (e.action) {
            /*代码省略*/
            MotionEvent.ACTION_UP -> {
                Log.e(TAG, "onTouchEvent: ACTION_UP $slideEffiectiveTag")
                //此标志恢复初始值
                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
    }
    /**
     * 手指抬起,对目标view进行最后的移动
     * 即决定菜单是否展开或关闭
     */
    private fun upFinalMoveToMenuView() {
        mMenuView?.let {
            if (it.scrollX >= mMenuWidth / 2f || checkEffectiveSpeed()) {
                showMenu(it)
            } else {
                closeMenu(it)
            }
        }
    }

三、封装内容与菜单以及点击

这个说什么好呢,好吧,先定义recycleview的适配器抽象类(ItemSlideAdapter),然后抽离相关方法。

 

1.指定容器viewgroup把内容和菜单分离。由适配器进行添加,这里使用了LinearLayout,如下:

    private fun createView(context: Context, contentView: View, menuView: View): ViewGroup {
        return LinearLayout(context).apply {
            layoutParams = RecyclerView.LayoutParams(
                RecyclerView.LayoutParams.MATCH_PARENT,
                RecyclerView.LayoutParams.WRAP_CONTENT
            )
            orientation = LinearLayout.HORIZONTAL
            addView(contentView)
            var layoutParams = menuView.layoutParams
            if (null == layoutParams || layoutParams.width <= 0) {
                throw RuntimeException("getMenuView方法得到的view,必须设置固定的宽度")
            }
            addView(menuView)
        }
    }

2.定义内容Holder(ContentViewHolder)与菜单holder(MenuViewHolder)与内容view和菜单view进行绑定,有点类似listview的ViewHolder。这将在其子类实现。如下:

    abstract class ContentViewHolder(val view: View)
    abstract class MenuViewHolder(val view: View)

3.构造生成ContentViewHolder与MenuViewHolder抽象方法,提供viewType参数是为了扩展。

    //C : ItemSlideAdapter.ContentViewHolder, M : ItemSlideAdapter.MenuViewHolder    
    abstract fun getMenuViewHolder(parent: ViewGroup, viewType: Int): M
    abstract fun getContentViewHolder(parent: ViewGroup, viewType: Int): C

4.创建RecyclerView.ViewHolder的实现类。因为已经将内容和菜单分离下去了,item成了一个空壳,所以可以将其确定。但要包含ContentViewHolder和MenuViewHolder,如下:

    class ItemSlideViewHolder(
        view: ViewGroup,
        var contentViewHolder: ContentViewHolder,
        var menuViewHolder: MenuViewHolder
    ) : RecyclerView.ViewHolder(view)

5.这样的话就可以onCreateViewHolder方法实现了,如下:

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemSlideAdapter.ItemSlideViewHolder {
        var contentViewHolder = getContentViewHolder(parent, viewType)
        var menuViewHolder = getMenuViewHolder(parent, viewType)
        return ItemSlideAdapter.ItemSlideViewHolder(
            createView(
                parent.context,
                contentViewHolder.view,
                menuViewHolder.view
            ), contentViewHolder, menuViewHolder
        )
    }

6.点击的封装,想了一下这个还是不加了吧,在抽象类的实现类里自行加入。因为菜单子view的数量不定,还有内容子view的数量也不定,哪些需要点击也不定。除非都已确定,非要写的话,需要添加好多方法,得不偿失。

7.因此抽象类的全部代码,如下

package com.xinheng.leftslidedeleterecycleview

import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import java.lang.RuntimeException

/**
 * Created by XinHeng on 2019/03/07.
 * describe:
 */
abstract class ItemSlideAdapter<C : ItemSlideAdapter.ContentViewHolder, M : ItemSlideAdapter.MenuViewHolder> :
    RecyclerView.Adapter<ItemSlideAdapter.ItemSlideViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemSlideAdapter.ItemSlideViewHolder {
        var contentViewHolder = getContentViewHolder(parent, viewType)
        var menuViewHolder = getMenuViewHolder(parent, viewType)
        return ItemSlideAdapter.ItemSlideViewHolder(
            createView(
                parent.context,
                contentViewHolder.view,
                menuViewHolder.view
            ), contentViewHolder, menuViewHolder
        )
    }

    override fun onBindViewHolder(holder: ItemSlideViewHolder, position: Int) {
        var contentViewHolder = holder.contentViewHolder
        var menuViewHolder = holder.menuViewHolder
        onBindHolder(contentViewHolder, menuViewHolder, position)
    }

    private fun createView(context: Context, contentView: View, menuView: View): ViewGroup {
        return LinearLayout(context).apply {
            layoutParams = RecyclerView.LayoutParams(
                RecyclerView.LayoutParams.MATCH_PARENT,
                RecyclerView.LayoutParams.WRAP_CONTENT
            )
            orientation = LinearLayout.HORIZONTAL
            addView(contentView)
            var layoutParams = menuView.layoutParams
            if (null == layoutParams || layoutParams.width <= 0) {
                throw RuntimeException("getMenuView方法得到的view,必须设置固定的宽度")
            }
            addView(menuView)
        }
    }

    abstract fun onBindHolder(contentViewHolder: ContentViewHolder, menuViewHolder: MenuViewHolder, position: Int)
    abstract fun getMenuViewHolder(parent: ViewGroup, viewType: Int): M
    abstract fun getContentViewHolder(parent: ViewGroup, viewType: Int): C
    class ItemSlideViewHolder(
        view: ViewGroup,
        var contentViewHolder: ContentViewHolder,
        var menuViewHolder: MenuViewHolder
    ) : RecyclerView.ViewHolder(view)

    abstract class ContentViewHolder(val view: View)
    abstract class MenuViewHolder(val view: View)
}

8.提供一个例子,实现了多种菜单与多种内容搭配。GitHub项目地址。效果图如上

 

四、在我的项目的实际应用

不晓得,当初他们是怎么想的,先上图:(第一行是类似标题可滑动,第二行是具体内容不可滑动)

既然提出了,做呗。还是挺新颖的,哈哈哈。当初想了两个方案:

①分为两个类型的item(A标题,B内容),A可滑动,B不可滑动。

②定制item(包含标题和内容),对mMenuView进行处理,更换view响应scrollTo(scrollBy)方法,即仅使标题响应滑动。

这样比较了一下,第一种方法比较简单。仅需要添加可滑动标志就行,或者item为一个的时候不响应滑动。但是仔细想了一下,这样的话标题的触碰区域较窄,滑动很麻烦,用户体验不好。而第二种可以很好的解决这个问题,并能实现功能。

因此采用第二种方法,recycleview添加接口,在mMenuView中获取我们想滑动的view。具体步骤:

1.添加接口回调

    var onItemSlideRecycleListener: OnItemSlideRecycleListener? = null

    interface OnItemSlideRecycleListener {
        /**
         * 获取真正可滑动view
         * @param view recycleview列表的item
         */
        fun getRealCanSlideMenuView(view: View): ViewGroup
    }
    /**
     * 获取可滑动,且包含菜单的view
     * @param view item
     */
    private fun getRealMenuView(view: View): ViewGroup {
        return if (onItemSlideRecycleListener != null) {
            onItemSlideRecycleListener!!.getRealCanSlideMenuView(view)
        } else {
            view as ViewGroup
        }
    }

2.在获取菜单宽度的时候,以及所有有关移动菜单的方法,更滑我们想要移动的view

    /**
     * 移动当前view
     */
    private fun moveToMenuView(slide: Int) {
        mMenuView?.let {
            val view = getRealSlideView(it)
            mMenuWidth = view.getChildAt(1).measuredWidth
            Log.e("TAG", "moveToMenuView: mMenuWidth=$mMenuWidth")
            if (view.scrollX + slide >= mMenuWidth) {
                //showMenu(view)
                view.scrollTo(mMenuWidth, 0)
                mMenuShowAllTag = true
            } else {
                mMenuShowAllTag = false
                view.scrollBy(slide, 0)
            }
        }
    }
    /**
     * 手指抬起,对目标view进行最后的移动
     * 即决定菜单是否展开或关闭
     */
    private fun upFinalMoveToMenuView() {
        mMenuView?.let { view ->
            val it = getRealSlideView(view)
            if (it.scrollX >= mMenuWidth / 2f || checkEffectiveSpeed()) {
                showMenu(it)
            } else {
                closeMenu(it)
            }
        }
    }
    fun closeMenu() {
        mMenuView?.let { view ->
            val it = getRealSlideView(view)
            closeMenu(it)
        }
    }
    private fun closeNowMenu(x: Int, y: Int) {
        if (null != mMenuView && mMenuShowAllTag) {
            if (!isNowMenu(x, y)) {
                closeMenu(getRealMenuView(mMenuView!!))
                //mMenuShowAllTag = false
                slideEffiectiveTag = false
            }
        }
    }

3.这里说下item为什么不自定义viewgroup,重写scrollTo(scrollBy)。原因:需要在添加获取水平滑动方法,getScrollX被final修饰无法重写;还有recycleview的item必须是我们的自定义的viewgroup。这样的话也可以,但不利于扩展和后期维护,无形中添加了限制。

4.item的布局文件,以及recycleview的内部接口的实现

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:orientation="vertical">
    <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
                  android:orientation="horizontal">
        <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
                      android:orientation="horizontal">
            <View android:layout_width="0dp" android:layout_height="10dp" android:layout_weight="1"/>
            <View android:layout_width="1dp" android:layout_height="30dp" android:background="@color/gray"/>
            <TextView android:layout_width="80dp" android:layout_height="30dp" android:text="编辑"
                      android:gravity="center"/>
            <View android:layout_width="1dp" android:layout_height="30dp" android:background="@color/gray"/>
            <TextView android:layout_width="80dp" android:layout_height="30dp" android:text="预览"
                      android:gravity="center"/>
        </LinearLayout>
        <TextView android:layout_width="60dp" android:layout_height="30dp" android:text="删除" android:gravity="center"
                  android:textColor="#fff" android:background="@color/colorAccent"/>
    </LinearLayout>
    <View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/gray"/>
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
              android:padding="10dp" android:text="         这样比较了一下,第一种方法比较简单。仅需要添加可滑动标志就行,或者item为一个的时候不响应滑动。"/>
    <View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/gray"/>
</LinearLayout>
        recycleView.onItemSlideRecycleListener = object : ItemSlideRecycleView.OnItemSlideRecycleListener {
            override fun getRealCanSlideMenuView(view: View): ViewGroup {
                //仅仅针对此次项目,很明显为第一个子view
                //其他请根据具体情况修改
                return (view as ViewGroup).getChildAt(0) as ViewGroup
            }
        }

5.效果图如上。

6.最后有个问题需要改下之前写的recycleview,当recycleview的item为view时会崩溃。因此我们要么在获取当前触碰的菜单view时,添加判断。还有在判断不是上次菜单view时,要将mMenuView归为null。这样的话当item是view时不响应滑动。如下:

7.附上本次的项目代码,GitHub项目地址

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值