Android事件分发机制在实战开发中的应用之三(RecycleView+ViewPager+SwipeRefreshLayout滑动冲突)

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

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

关于事件分发在三年前曾经写过一个专栏,共有六篇文章,三篇理论,一篇总结,两篇实战,今天再来写一篇关于实战的文章,如果对事件分发流程不熟悉,请先阅读之前我写过的专栏《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来处理,否则则交给系统来自行处理,左右滑动用阈值法和斜率法都可以解决问题,能用外部拦截法就尽量使用外部拦截发来解决问题,这样不但处理方便,而且执行的效率也会很高,希望这篇文章对你有所帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容