美文网首页
Android事件分发机制在实战开发中的应用之三(Recycle

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

作者: 门心叼龙 | 来源:发表于2022-06-05 19:53 被阅读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来处理,否则则交给系统来自行处理,左右滑动用阈值法和斜率法都可以解决问题,能用外部拦截法就尽量使用外部拦截发来解决问题,这样不但处理方便,而且执行的效率也会很高,希望这篇文章对你有所帮助。

    相关文章

      网友评论

          本文标题:Android事件分发机制在实战开发中的应用之三(Recycle

          本文链接:https://www.haomeiwen.com/subject/kszrmrtx.html