Android事件分发机制在实战开发中的应用之三(Recycle

2022-06-05  本文已影响0人  门心叼龙

学习的最终目标就是要学以致用,本文所分享的案例都是自己在公司实战开发过程中的真实案例,现在把它分享出来,希望对初学者有所帮助

好久没有写博客了,今天是周末,所以有时间来写一篇,前些天在工作中出现了一个关于滑动冲突的问题,我把解决它的过程记录下来,现在分享出来,以便给大家遇到了类似的问题提供参考。

关于事件分发在三年前曾经写过一个专栏,共有六篇文章,三篇理论,一篇总结,两篇实战,今天再来写一篇关于实战的文章,如果对事件分发流程不熟悉,请先阅读之前我写过的专栏《View事件分发》系列文章,然后再来看这篇文章你会轻松很多。

整个APP首页的布局架构为: BottomNavigationView +TabLayout+ViewPager+SwipeRefreshLayout+NestedScrollView+RecyclerView,
这种结构在现在的app中也是最通用的布局架构方式,打开手机随便点开一个APP几乎都是这种布局方式,不管是无人不用的微信,还是现在火爆的抖音,头条....等无一例外都是这样的布局方式,侧边栏菜单的模式好像对于国人的使用习惯并不合适,滴滴打车APP在前几年也用过侧边栏的方式,不过在后来的版本升级中也改成了大众所熟悉的底边栏菜单。

在APP首页中ViewPager有左右滑动的动作来翻页,在第一页中有个教材学习的横向RecycleView,它也有左右滑动的动作,当在RecycleView中左右滑动的时候,在它在向右滑到头的时候,此时继续向右滑动ViewPager就开始翻页了,ViewPager中已经给我们解决了滑动冲突,看起来貌似没有问题,但是细心的同学可能发现RecycleView在左右滑动的过程中有明显的卡顿,同样的功能和IOS相比较很明显没有IOS那样的丝滑流畅。

具体的问题效果如下:


006.gif

教材学习的RecycleView左右滑动的过程中会明显的感觉到不流畅,有卡顿,这是因为RecycleView在左右滑动的过程中,一部分滑动事件被ViewPager消费了,所以就出现的卡顿,那么只要事件落在教材学习的RecycleView之上就把所有的事件都交给RecycleView来处理即可解决问题,在最顶级的ViewPager可以做如下处理:
首先创建一个方法isBookTouch来判断触摸事件是否在RecycleView中

private fun isBookTouch(event: MotionEvent): Boolean {
        slideViewPagerListener?.getHomeBookRecView()?.let {
            val rect = Rect()
            it.getHitRect(rect)
            if (rect.contains(event.x.toInt(), event.y.toInt())) {
                KLog.v("MYTAG", "isBookTouch true")
                return true
            }
        }
        return false
    }

在isBookTouch方法中我们用到了一个getHitRect方法来判断当前触摸点是否在指定的View上,相关联的有四个方法,分别介绍如下:
1.getHitRect: 获取View可点击矩形左、上、右、下边界相对于父View的左顶点的距离(偏移量)

2.getDrawingRect: 获取View的绘制范围,即左、上、右、下边界相对于此View的左顶点的距离(偏移量),即0、0、View的宽、View的高

3.getLocalVisibleRect: 获取View在第一个可滚动的上级View(父View或祖父View或...)中的可见区域相对于此View的左顶点的距离(偏移量)

4.getGlobalVisibleRect: 获取View在第一个可滚动的上级View(父View或祖父View或...)中的可见区域相对于屏幕左顶点的距离(偏移量)

方案1:

在顶级的ViewPager中做如下处理:

    override fun dispatchTouchEvent(event: MotionEvent): Boolean {

        if (isBookTouch(event)) {
            slideViewPagerListener?.getHomeBookRecView()?.let {
                return it.dispatchTouchEvent(event)
            }
        }
        return super.dispatchTouchEvent(event)
    }

只要判断该滑动事件在在RecycleView中就全部交给RecycleView来处理


001.gif

这样左右滑动非常的流畅,但是出现了一个问题,当滑动起点在教材学习RecycleView上方,下拉出现了卡顿,起点在RecView上,终点在Recview之下,下拉无响应,此操作录屏的无法演示。

方案2:

代码优化如下:

var isBookTouch = false
    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                isBookTouch = isBookTouch(event)
            }
            else -> {
                if (isBookTouch) {
                    slideViewPagerListener?.getHomeBookRecView()?.let {
                        return it.dispatchTouchEvent(event)
                    }
                }
            }
        }
        return super.dispatchTouchEvent(event)
    }

在滑动的时候判断只要是第一个事件在教材学习的RecycleView上那么后序的事件都交给RecycleView来处理,左右滑动很流畅,但是下拉的问题依旧,起点在RecView上,终点在Recview之下,下拉无响应,此操作录屏的无法演示,这是因为事件的在RecycleView之外本应该交给SwipeRefreshView来处理的事件也交给RecycleView来处理了,显然是不合理的。


002.gif

方案3:阈值法

我们要对RecycleView处理的事件要做进一步的限制,上下滑动的时候事件交给SwipeRefreshLayout来处理,左右滑动的事件交给教材学习的RecycleView即可,代码如下:

  var x1 = 0f
  var isBookTouch = false
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                isBookTouch = isBookTouch(event)
                x1 = event.x
            }
            MotionEvent.ACTION_MOVE -> {
                var x2 = event.x
                if (isBookTouch && Math.abs(x2 - x1) > 50) {
                    slideViewPagerListener?.getHomeBookRecView()?.let {
                        return it.dispatchTouchEvent(event)
                    }
                }
                x1 = x2
            }
            MotionEvent.ACTION_UP -> {
                isBookTouch = false
                x1 = 0f
            }
        }
        return super.dispatchTouchEvent(event)
    }

事件在RecycleView之上,且是左右滑动,通过两次滑动x轴的偏移量来判断,只要offset偏移大于50即可,左右滑动流畅,上下滑动下拉也很流畅。
具体效果如下:


003.gif

下面在介绍另外一种方法也可以解决此问题:

方案4:斜率法

通过x轴滑动距离和y轴滑动距离做比较,只要x轴的offset比y轴的offset大则认为是左右滑动,代码如下:

    var x1 = 0f
    var y1 = 0f
    var isBookTouch = false
    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                isBookTouch = isBookTouch(event)
                x1 = event.x
                y1 = event.y
            }
            MotionEvent.ACTION_MOVE -> {
                var x2 = event.x
                var y2 = event.y

                val offsetX = Math.abs(x2 - x1)
                val offsetY = Math.abs(y2 - y1)
                if (isBookTouch && offsetX > offsetY) {
                    slideViewPagerListener?.getHomeBookRecView()?.let {
                        return it.dispatchTouchEvent(event)
                    }
                }
                x1 = x2
                y1 = y2
            }
            MotionEvent.ACTION_UP -> {
                isBookTouch = false
                x1 = 0f
                y1 = 0f
            }
        }
        return super.dispatchTouchEvent(event)
    }

滑动效果如下:


004.gif

左右滑动很流畅,上下滑动下拉刷新也没有问题。

当然在解决这个问题的时候,也出现了一些不该出现的错误,起初用外部拦截法在滑动时间结束的ACTION_UP事件没有给mFirstMotionEvent 变量重新初始化而导致的下拉刷新有偶尔卡死的现象,具体问题代码如下:

 var x1 = 0f
    var x2 = 0f
    val MIN_DISTANCE = 50
    var isBookClick = false
    var mFirstMotionEvent : MotionEvent? = null
    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        //KLog.v("MYTAG", "MainActivity dispatchTouchEvent start")
        if(mFirstMotionEvent == null){
            KLog.v("MYTAG", "mFirstMotionEvent is Null, touch:"+event.action)
            mFirstMotionEvent = event
            isBookClick = isBookRect(event)
        }
        when (event!!.action) {
            MotionEvent.ACTION_DOWN ->{
                KLog.v("MYTAG", "ACTION_DOWN touch")
                x1 = event!!.x
            }
            MotionEvent.ACTION_MOVE ->{
                KLog.v("MYTAG", "ACTION_MOVE touch")
                x2 = event!!.x
                val deltaX: Float = x2 - x1
                if (Math.abs(deltaX) > MIN_DISTANCE) {
                    if(isBookRect(event) && isBookClick){
                        mHomeFragment.getBookRecycleView()?.let {
                            return it.dispatchTouchEvent(event)
                        }
                    }

                }
            }
            MotionEvent.ACTION_UP -> {
                KLog.v("MYTAG", "ACTION_UP touch")
                x2 = event!!.x
                val deltaX: Float = x2 - x1
                if (Math.abs(deltaX) > MIN_DISTANCE) {
                    if(isBookRect(event) && isBookClick){
                        mHomeFragment.getBookRecycleView()?.let {
                            return it.dispatchTouchEvent(event)
                        }
                    }
                }
                //初始化位置不对,导致下拉卡顿
                mFirstMotionEvent = null
                isBookClick = false
            }
        }

        KLog.v("MYTAG", "MainActivity super.dispatchTouchEvent start")
        return super.dispatchTouchEvent(event)
    }

    private fun isBookRect(event: MotionEvent?) : Boolean{
        if (mCurrFragment is HomeFragment) {
            val bookRecycleView = mHomeFragment.getBookRecycleView()
            bookRecycleView?.let {
                val rect = Rect()
                it.getGlobalVisibleRect(rect)
                if (rect.contains(event?.x!!.toInt(), event?.y!!.toInt())) {
                    KLog.v("MYTAG", "bookRecycleView touch")
                    return true
                }
            }
        }
        return false
    }

具体卡顿效果如下:


005.gif

下拉的时候偶尔会出现这种卡死的现象,应该在ACTION_UP时候稍加修改即可解决问题:

MotionEvent.ACTION_UP -> {
                KLog.v("MYTAG", "ACTION_UP touch")
                x2 = event!!.x
                val deltaX: Float = x2 - x1
                //修改了初始化mFirstMotionEvent的位置
                mFirstMotionEvent = null
                if (Math.abs(deltaX) > MIN_DISTANCE) {
                    if(isBookRect(event) && isBookClick){
                        mHomeFragment.getBookRecycleView()?.let {
                            return it.dispatchTouchEvent(event)
                        }
                    }
                }           
   }

好了,关于RecycleView+ViewPager+SwipeRefreshLayout滑动冲突我们今天就分析到这里,具体的核心算法就是:滑动事件在RecycleView上且是左右滑动,则该系列事件都交给RecycleView来处理,否则则交给系统来自行处理,左右滑动用阈值法和斜率法都可以解决问题,能用外部拦截法就尽量使用外部拦截发来解决问题,这样不但处理方便,而且执行的效率也会很高,希望这篇文章对你有所帮助。

上一篇下一篇

猜你喜欢

热点阅读