专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
郭霖  ·  从源码到定制:全面解析 Android ... ·  2 天前  
鸿洋  ·  卡顿监测的方方面面 ·  2 天前  
郭霖  ·  HarmonyOS ... ·  5 天前  
鸿洋  ·  初识Android内存优化 ·  4 天前  
鸿洋  ·  Android AppOpsService是什么? ·  5 天前  
51好读  ›  专栏  ›  郭霖

ViewPager,ScrollView嵌套ViewPager滑动冲突解决

郭霖  · 公众号  · android  · 2016-12-06 08:00

正文

今日科技快讯

昨日第三方调查机构 Instrumental 发布了关于三星Note 7电池爆炸的调查报告,表明是因为手机本身的“激进设计”所导致一一Note 7的电池与主板之间的距离非常窄,最窄处仅0.1mm!这使得电池即使在日常使用中也会受到挤压。而压力会使电池正、负极的“分离装置”很容易损坏,正负接触会引发电池发热起火。报告还称,三星的工程师在设计过程中极力加大电池体积,以此换取更大的电池容量。追求更大的电池容量固然是好事,它能为手机带来更持久的续航,但安全始终应该放在第一位!

作者简介

本篇来自 徐俊 的投稿,分析了滑动冲突的原因,以及针对嵌套ViewPager给出了解决方案,希望能帮助有需要的朋友。

徐俊 的博客地址:

http://blog.csdn.net/gdutxiaoxu

前言

这篇博客主要讲解一下几个问题:

  • 粗略地介绍一下View的事件分发机制

  • 解决事件滑动冲突的思路及方法

  • ScrollView 里面嵌套ViewPager导致的滑动冲突

  • ViewPager里面嵌套ViewPager 导致的滑动冲突

  • 轮播图的几种实现方式

效果图

ScrollView 里面嵌套 ViewPager:

ViewPager 里面嵌套 ViewPager:

View事件分发机制

这篇博客不打算详细讲解View的事件分发机制,因为网上已经出现了一系列的好文章,我自己的水平也有限,目前肯定写得不咋的。

先啰嗦一下,View 的事件分发机制主要涉及到以下方法:

  • dispatchTouchEvent ,这个方法主要是用来分发事件

  • onInterceptTouchEvent,这个方法主要是用来拦截事件的(需要注意的是ViewGroup才有这个方法,View没有onInterceptTouchEvent这个方法

  • onTouchEvent,这个方法主要是用来处理事件的

  • requestDisallowInterceptTouchEvent(true),这个方法能够影响父View是否拦截事件,true表示 不拦截事件,false表示拦截事件

下面引用图解 Android 事件分发机制这一篇博客的内容:

  • 仔细看的话,图分为3层,从上往下依次是Activity、ViewGroup、View

  • 事件从左上角那个白色箭头开始,由 Activity 的 dispatchTouchEvent 做分发

  • 箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是调用父类实现。

  • dispatchTouchEvent 和 onTouchEvent 的框里有个【true—->消费】的字,表示的意思是如果方法返回true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。

  • 目前所有的图的事件是针对 ACTION_DOWN 的,对于 ACTION_MOVE 和 ACTION_UP 我们最后做分析。

  • 之前图中的 Activity 的 dispatchTouchEvent 有误(图已修复),只有 return super.dispatchTouchEvent(ev) 才是往下走,返回 true 或者 false 事件就被消费了(终止传递)。

总结

当TouchEvent发生时,首先Activity将TouchEvent传递给最顶层的View,TouchEvent最先到达最顶层 view 的 dispatchTouchEvent ,然后由 dispatchTouchEvent 方法进行分发

  • 如果 dispatchTouchEven t返回 true 消费事件,事件终结。

  • 如果 dispatchTouchEvent 返回 false ,则回传给 父View 的 onTouchEvent 事件处理;

  • onTouchEvent 事件返回 true,事件终结,返回 false,交给 父View 的 OnTouchEvent 方法处理

  • 如果 dispatchTouchEvent 返回 super 的话,默认会调用自己的 onInterceptTouchEvent方法

  • 默认的情况下 interceptTouchEvent 回调用 super方法,super方法 默认返回 false,所以会交给 子View 的 onDispatchTouchEvent 方法处理

  • 如果 interceptTouchEvent 返回 true ,也就是拦截掉了,则交给它的 onTouchEvent 来处理,

  • 如果 interceptTouchEvent 返回 false ,那么就传递给子 view ,由 子view 的 dispatchTouchEvent 再来开始这个事件的分发。

关于更多详细分析,请查看原博客:

图解Android事件分发机制

http://www.jianshu.com/p/e99b5e8bd67b

解决滑动冲突的思路及方法

常见的三种情况

第一种情况,滑动方向不同

第二种情况,滑动方向相同

第三种情况,上述两种情况的嵌套

解决思路

看了上面三种情况,我们知道他们的共同特点是 父View 和 子View 都想争着响应我们的触摸事件,但遗憾的是我们的触摸事件 同一时刻 只能被某一个View或者ViewGroup拦截消费,所以就产生了滑动冲突?那既然同一时刻只能由某一个View或者ViewGroup消费拦截,那我们就只需要 决定在某个时刻由这个View或者ViewGroup拦截事件,另外的 某个时刻由 另外一个View或者ViewGroup拦截事件不就OK了吗?综上,正如 在 《Android开发艺术》 一书提出的,总共 有两种解决方案

以下解决思路来自于 《Android开发艺术》 书籍

下面的两种方法针对第一种情况(滑动方向不同),父View 是 上下滑动,子View 是 左右滑动 的情况

外部解决法

从 父View 着手,重写 onInterceptTouchEvent 方法,在 父View 需要拦截的时候拦截,不要的时候返回false,伪代码大概如下


内部解决法

从子View着手,父View 先不要拦截任何事件,所有的 事件传递给 子View,如果 子View 需要此事件就消费掉,不需要此事件的话就交给 父View 处理。

实现思路 如下,重写 子View 的 dispatchTouchEvent 方法,在 Action_down 动作中通过方法 requestDisallowInterceptTouchEvent(true) 先请求 父View 不要拦截事件,这样保证 子View 能够接受到Action_move事件,再在Action_move动作中根据 自己的逻辑是否要拦截事件,不要的话再交给 父View 处理


ScrollView嵌套ViewPager冲突

外部解决法

如上面所述,从 父View ScrollView着手,重写 OnInterceptTouchEvent 方法,在上下滑动的时候拦截事件,在左右滑动的时候不拦截事件,返回 false,这样确保 子View 的dispatchTouchEvent方法会被调用,代码如下


内部解决法

如上面上述,通过 requestDisallowInterceptTouchEvent(true) 方法来影响 父View 是否拦截事件,我们通过重写 ViewPager 的 dispatchTouchEvent()方法,在左右滑动的时候请求 父View ScrollView不要拦截事件,其他的时候由 子View 拦截事件


注意事项(坑)

当我们ScrollView的最上层的Layout里面多多个孩子的时候,当下面一个孩子是RecyclerView或者ListView的时候,往往会自动滑动到ListView或者RecyclerView 的第一个item,导致进入界面的时候会导致 RecyclerView 上面的 View 被滑动到界面之外,看不见,这时候的用户体验是比较差的

即结构如下面的时候

在Activity中的相关解决方法

于是我查找了相关的资料,在Activity中完美解决,主要要一下两种方法

第一种方法,重写 Activity 的 onWindowFocusChanged()方法,在里面调用 mNoHorizontalScrollView.scrollTo(0,0) 方法,滑动到顶部,因为 onWindowFocusChanged 是在所有View绘制完毕的时候才会回调的,不熟悉的话建议先回去看一下Activity的生命周期的相关介绍


第二种解决方法,调用 RecyclerView上面的View的一下方法,让其获取焦点

view.setFocusable(true);  
view.setFocusableInTouchMode(true);  
view.requestFocus();

这段代码在初始化的时候就让该界面的顶部的某一个控件获得焦点,滚动条自然就显示到顶部了。

在Fragment中的相关解决方法

同样是调用第二种方法,调用RecyclerView上面的View的一下方法,让其获取焦点

view.setFocusable(true);  
view.setFocusableInTouchMode(true);  
view.requestFocus();

这段代码在初始化的时候就让该界面的顶部的某一个控件获得焦点,滚动条自然就显示到顶部了。但是该方法存在缺点,就是当我们上面的view如果滑动到一半的时候,切换到下一个Fragment,在切换回来的时候,RecyclerView的第一个item会自动滑动到顶部。目前我还没有找到相对比较好的解决这个问题的方法。

个人疑点

借鉴于解决Activity的方法,目前我还没有找到一个方法是在Fragemnt界面完全绘制完毕以后回调的方法

ViewPager嵌套ViewPager冲突

内部解决法

从 子View ViewPager着手,重写 子View 的 dispatchTouchEvent方法,在 子View 需要拦截的时候进行拦截,否则交给 父View 处理,代码如下


外部解决法

这个如果要采用外部解决法来解决的话想,相对很麻烦,我提一下自己的个人思路,我们可以先测量 子View 在哪个区域,然后我们在根据我们按下的点是否在区域以内,如果是的话,在根据 子View 时候需要拦截进行处理

讨论


对于这种效果,上面是轮播图的,下面是 RecyclerView 或者 ListView 的,一般有一下几种实现方式

  • 使用我们上述提高的ScrollView里面嵌套ViewPager和RecyclerView,这种实现方式需要自己解决View滑动事件的冲突,同时还有我在上述提高的在Fragment中存在的问题

  • 使用listView的addHeaderView来实现,或者是通过多种不同的item来实现

  • 使用RecyclerView添加headerView来实现,或者复用多种不同的item来实现。关于RecyclerView如何添加headerView可以参考鸿洋大神的这一篇博客:

Android优雅的为RecyclerView添加HeaderView和FooterView

http://blog.csdn.net/lmj623565791/article/details/51854533

  • 使用SupportLibrary中的CoordinatorLayout等控件

其布局文件如下,Activity代码见项目中的SixActivity

关于CoordinatorLayout的更多用法,可以参考我的这一篇博客

使用CoordinatorLayout打造各种炫酷的效果

http://blog.csdn.net/gdutxiaoxu/article/details/52858598

总结

  • 当我们滑动方向不同的时候,采用外部解决法和内部解决法,复杂度差不多。

  • 当我们滑动的方向相同的话,建议采用内部解决法来解决,因为采用外部解决法复杂度比较高。而且有时候我们是采用别人的开源控件,这时候去修改别人的源码可能会发生一些意想不到的bug。

题外话

在这篇博客的最后提高的实现轮播图+list列表的几种实现形式,刚开始是不想写的,后面因为ScrollView里面嵌套ViewPager和RecyclerView在fragment中RecyclerView抢占焦点,在某些情况下用户体验不好,才写出来的,跟这篇博客要讲解的View滑动事件冲突没有多大关系,只是给读者提供多种思路而已

至于CoordinatorLayout,是google IO 2015中提出来的,功能很强大,可以说是专门为了解决嵌套导滑动而产生的,极大地方便了开发者,对于初学者,可以暂时不必掌握它,先把其他的基础学好就好

源码下载地址:

https://github.com/gdutxiaoxu/TouchDemo.git

更多

每天学习累了,看些搞笑的段子放松一下吧。关注最具娱乐精神的公众号,每天都有好心情。

如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。

欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号: