开源库源码学习–DslAdapter的侧滑功能

前言

关于RecyclerView的Adapter封装的很多,今天说的这个是DslAdapter,为什么说这个呢,因为最近有个侧滑的需求,之前的项目代码有点繁琐,逻辑不是很清晰,所以重点关注一下这个库的侧滑按钮部分代码实现和原理。

项目github地址: github.com/angcyo/DslA…

正文

这个DslAdapter的设计思想就是为了更方便的写出布局,而且利用Dsl的风格来写代码,把我们平时的布局都认为是一个RecyclerView的type类型,利用RecyclerView的多type特性来一次性完成,特别方便,大家可以具体去github使用,这里我主要是研究侧滑相关的。

SwipeHelper

该项目中侧滑相关的都是通过这个SwipeHelper类来实现,我们来看一下这个类的实现。

class SwipeMenuHelper(var swipeMenuCallback: SwipeMenuCallback) : ItemDecoration(),
    OnChildAttachStateChangeListener {
        ...
    }
复制代码

这里继承ItemDecoration和OnChildAttachStateChangeListener 2个类,其中ItemDecoration就是Item装饰,很多功能都是通过这个类来实现。

ItemDecoration

这个就是大名鼎鼎的ItemDecoration类,用来对RecyclerView的Item加上装饰的类,具体什么装饰呢,比如我们平时用的分割线就是继承至这个,当然更多功能需要用到它下面的3个方法:

ItemDecoration导图.png

关于这个类的用法我们是必须要掌握的,有了这个类我们可以自定义给Item添加各种装饰布局,具体用法可以参考其他博客,比如:

www.jianshu.com/p/fe94636ef…

好了,ItemDecoration就说到这里。

OnChildAttachStateChangeListener

这个是子View的Attach回调,这个类也非常有用,当我们处理RecyclerView的Item滑动到不可见时,这个方法的回调就是处理的逻辑的好办法,主要有2个方法:

childAttachStateChanged.png

RecyclerView.OnItemTouchListener

除了上面2个类,还需要知道一个RecyclerView自带的类,这个类非常重要,也是RecyclerView处理和拦截事件非常重要的类。

OnItemTouchListener导图.png

由上面图中介绍可知,在Touch事件传递给RecyclerView自己处理或者它的ItemView处理前可以进行拦截,这里就好办了,根据这个思路,我们便可以对Touch事件进行拦截,有需要(侧滑)的操作则进行拦截,其他的则不用处理。

手势监听

前面说了事件拦截,那就要区分出是什么事件,才能进行拦截,所以这里需要手势识别辅助类,来判断事件的状态,是Down、Scroll还是Fling状态,这里直接推荐实现SimpleOnGestureListener接口即可。

未命名文件(21).png

事件传递

有了上面的基础了解,我们便可以进行逻辑梳理和流程处理了,也就是对Touch事件进行拦截。

未命名文件(13).png

单击

对于单击来说,事件序列就是DOWN -> UP,这种情况处理比较简单,分以下几种情况

第一种是没有item被侧滑的,这时就是普通的点击事件,需要传递给Item来处理,这时DOWN和UP都不需要进行拦截和处理。

单击.gif

第二种是已经有item被侧滑了,这时不管是点击哪里,都是收起侧滑按钮操作,这种情况需要处理DOWN事件,直接进行收起侧滑Item的操作,对于UP事件不进行处理,这时既需要收起侧滑按钮,也需要把点击事件传递给RecyclerView。

单击2.gif

滑动

对于滑动来说,事件序列就是DOWN -> MOVE -> …. -> MOVE -> UP,这种情况就比较复杂了,下面简单说一下:

当DOWN事件发生时,没有出现上面第二种情况,这时会认为需要处理后续Touch事件,如果这时下一个事件是MOVE事件,则继续交由手势监听类来处理,如果手势监听类触发onScroll回调,说明发生了滑动,这时再根据Item是否设置了侧滑属性以及滑动方向等综合判断是否要拦截事件MOVE事件以及滑动Item。

未命名文件(14).png

看下面gif图可以看出当触发Scroll时进行位移Item,在 UP事件触发时决定是打开还是收起:

正常滑动出.gif

注意一下这里的UP事件处理要非常仔细,不仅仅要判断Scroll时的情况,也就是滑动的总距离是否大于阈值,还要判断Fling的情况。

1、对于Scroll时,当总的scroll距离_scroll < 0时,说明往左滑动了,这时需要判断上一次的scroll方向(会有多次scroll,在UP前的最后一段scroll,注意这里的scroll必须大于slop才算,手抖动不算),如果是左边,则滑动距离大于阈值即可,如果是向右,则滑动的距离大于(宽度-阈值)才认为是意图打开右边菜单。

滑动最后反方向.gif

比如这里,虽然最后UP时滑动时,状态是Scroll,但是最后一段长距离的Scroll是向右的,这时意图也是关闭,除非左边侧滑已经非常多了,也就是大于(侧滑宽度-阈值)才打开侧滑。

2、对于Fling时,就不需要判断滑动距离了,就需要判断滑动速度是否大于阈值即可,同时需要判断方向,当滑动是向左且速度方向是向左或者滑动向右且速度方向是向右则是打开侧滑,其他情况都是关闭侧滑。

Fling同方向.gif

这种情况是左滑且速度是向左,所以是打开侧滑。

Fling反方向.gif

这种情况是左滑但是速度是向右,不论滑动多远也不用管阈值都是进行关闭侧滑。

ItemView滑动

前面我们可以得知了滑动已经触发的逻辑了,那我们需要梳理一下有几种情况要对ItemView进行滑动,以及如何滑动。

ItemView滑动.png

这里对2种情况的滑动都做了统一入口,其中Fling或者UP处理情况都是直接需要滑动一大段距离,所以要加个属性动画渐变值:

//直接滑动某个ViewHolder到x y
fun scrollSwipeMenuTo(viewHolder: RecyclerView.ViewHolder, x: Float = 0f, y: Float = 0f) {
    if (_lastValueAnimator?.isRunning == true) {
        return
    }
    _recyclerView?.apply {
        val startX = _scrollX
        val startY = _scrollY
        //添加属性动画,让滑动有动画效果
        val valueAnimator = ValueAnimator.ofFloat(0f, 1f)
        valueAnimator.addUpdateListener {
            val fraction: Float = it.animatedValue as Float
            val currentX = startX + (x - startX) * fraction
            val currentY = startY + (y - startY) * fraction

            _scrollX = currentX
            _scrollY = currentY
            //最终都是调用这个方法来位移ItemView
            swipeMenuCallback.onSwipeTo(_recyclerView!!, viewHolder, currentX, currentY)
        }
        valueAnimator.addListener(onEnd = {
            if (x == 0f && y == 0f) {
                //关闭菜单
                _swipeMenuViewHolder = null
            } else {
                _swipeMenuViewHolder = viewHolder
            }
            _lastValueAnimator = null
        }, onCancel = {
            _lastValueAnimator = null
        })
        valueAnimator.duration =
            ItemTouchHelper.Callback.DEFAULT_SWIPE_ANIMATION_DURATION.toLong()
        valueAnimator.start()
        _lastValueAnimator = valueAnimator
    }
}
复制代码

既然最后都是调用onSwipeTo方法,来看一下:

open fun onSwipeTo(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    x: Float,
    y: Float
) {
    dslAdapterItem(recyclerView, viewHolder)?.run {
        itemSwipeMenuTo(viewHolder as DslViewHolder, x, y)
    }
}
复制代码
/**侧滑菜单滑动至多少距离, 重写此方法, 自行处理UI效果*/
var itemSwipeMenuTo: (itemHolder: DslViewHolder, dX: Float, dY: Float) -> Unit =
    { itemHolder, dX, dY ->
        onItemSwipeMenuTo(itemHolder, dX, dY)
    }
复制代码
/**请将menu, 布局在第1个child的位置, 并且布局的[left]和[top]都是0
 * 默认的UI效果就是, TranslationX.
 * 默认实现暂时只支持左/右滑动的菜单, 上/下滑动菜单不支持
 * */
open fun onItemSwipeMenuTo(itemHolder: DslViewHolder, dX: Float, dY: Float) {
    val parent = itemHolder.itemView
    if (parent is ViewGroup && parent.childCount > 1) {
        //菜单最大的宽度, 用于限制滑动的边界
        val menuWidth = itemSwipeWidth(itemHolder)
        val tX = clamp(dX, -menuWidth.toFloat(), menuWidth.toFloat())
        parent.forEach { index, child ->
            if (index == 0) {
                if (itemSwipeMenuType == SWIPE_MENU_TYPE_FLOWING) {
                    if (dX > 0) {
                        child.translationX = -menuWidth + tX
                    } else {
                        child.translationX = parent.mW() + tX
                    }
                } else {
                    if (dX > 0) {
                        child.translationX = 0f
                    } else {
                        child.translationX = (parent.mW() - menuWidth).toFloat()
                    }
                }
            } else {
                child.translationX = tX
            }
        }
    }
}
复制代码

这里采用了一个很讨巧的方法,把菜单布局放在第一个child位置,然后根据情况位移即可。

这里又有2种滑动情况,一种是侧滑按钮跟在ItemView后面,一种是侧滑按钮在ItemView底部,对于这2种情况的处理也就是位移动画时的起始位置不一样而已,在上面代码已有区分,看一下效果图:

效果图.gif

结语

到这里也就把侧滑的原理说的差不多了,具体原理可以去查看源代码,上面也有GitHub地址和实现的类。。通过这个侧滑的学习我觉得最大的收获就是RecyclerView侧滑事件的拦截以及事件的处理,尤其是区分Scroll和Fling的地方,设计的很巧妙。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享