scrollview嵌套viewpager或recyclerview冲突解决

ScrollView里面嵌套ViewPager

ViewPager里面嵌套ViewPager


View的 事件分发机制

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

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

  • dispatchTouchEvent ,这个方法主要是用来分发事件的
  • onInterceptTouchEvent,这个方法主要是用来拦截事件的(需要注意的是ViewGroup才有这个方法,View没有onInterceptTouchEvent这个方法
  • onTouchEvent 这个方法主要是用来处理事件的
  • requestDisallowInterceptTouchEvent(true),这个方法能够影响父View是否拦截事件,true 表示父 View 不拦截事件,false 表示父 View 拦截事件

下面引用图解 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 方法进行分发,

  • 如果dispatchTouchEvent返回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 事件分发机制,真心推荐,写得很好。


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

常见的三种情况

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

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

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

解决思路

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

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

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

外部解决法

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

 
  1. @Override

  2. public boolean onInterceptTouchEvent(MotionEvent ev) {

  3. final float x = ev.getX();

  4. final float y = ev.getY();

  5.  
  6. final int action = ev.getAction();

  7. switch (action) {

  8. case MotionEvent.ACTION_DOWN:

  9. mDownPosX = x;

  10. mDownPosY = y;

  11.  
  12. break;

  13. case MotionEvent.ACTION_MOVE:

  14. final float deltaX = Math.abs(x - mDownPosX);

  15. final float deltaY = Math.abs(y - mDownPosY);

  16. // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截

  17. if (deltaX > deltaY) {

  18. return false;

  19. }

  20. }

  21.  
  22. return super.onInterceptTouchEvent(ev);

  23. }

  24.  
  •  

内部解决法

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

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

 
  1. @Override

  2. public boolean dispatchTouchEvent(MotionEvent ev) {

  3. int x = (int) ev.getRawX();

  4. int y = (int) ev.getRawY();

  5. int dealtX = 0;

  6. int dealtY = 0;

  7.  
  8. switch (ev.getAction()) {

  9. case MotionEvent.ACTION_DOWN:

  10. dealtX = 0;

  11. dealtY = 0;

  12. // 保证子View能够接收到Action_move事件

  13. getParent().requestDisallowInterceptTouchEvent(true);

  14. break;

  15. case MotionEvent.ACTION_MOVE:

  16. dealtX += Math.abs(x - lastX);

  17. dealtY += Math.abs(y - lastY);

  18. Log.i(TAG, "dealtX:=" + dealtX);

  19. Log.i(TAG, "dealtY:=" + dealtY);

  20. // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截

  21. if (dealtX >= dealtY) {

  22. getParent().requestDisallowInterceptTouchEvent(true);

  23. } else {

  24. getParent().requestDisallowInterceptTouchEvent(false);

  25. }

  26. lastX = x;

  27. lastY = y;

  28. break;

  29. case MotionEvent.ACTION_CANCEL:

  30. break;

  31. case MotionEvent.ACTION_UP:

  32. break;

  33.  
  34. }

  35. return super.dispatchTouchEvent(ev);

  36. }

  37.  
  38.  
  •  

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

外部解决法

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

 
  1. /**

  2. * @ explain:这个ScrlloView不拦截水平滑动事件,

  3. * 是用来解决 ScrollView里面嵌套ViewPager使用的

  4. * @ author:xujun on 2016/10/25 15:28

  5. * @ email:gdutxiaoxu@163.com

  6. */

  7. public class VerticalScrollView extends ScrollView {

  8.  
  9. public VerticalScrollView(Context context) {

  10. super(context);

  11. }

  12.  
  13. public VerticalScrollView(Context context, AttributeSet attrs) {

  14. super(context, attrs);

  15. }

  16.  
  17. public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {

  18. super(context, attrs, defStyleAttr);

  19. }

  20.  
  21. @TargetApi(21)

  22. public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int

  23. defStyleRes) {

  24. super(context, attrs, defStyleAttr, defStyleRes);

  25. }

  26.  
  27. private float mDownPosX = 0;

  28. private float mDownPosY = 0;

  29.  
  30. @Override

  31. public boolean onInterceptTouchEvent(MotionEvent ev) {

  32. final float x = ev.getX();

  33. final float y = ev.getY();

  34.  
  35. final int action = ev.getAction();

  36. switch (action) {

  37. case MotionEvent.ACTION_DOWN:

  38. mDownPosX = x;

  39. mDownPosY = y;

  40.  
  41. break;

  42. case MotionEvent.ACTION_MOVE:

  43. final float deltaX = Math.abs(x - mDownPosX);

  44. final float deltaY = Math.abs(y - mDownPosY);

  45. // 这里是否拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截

  46.  
  47. if (deltaX > deltaY) {// 左右滑动不拦截

  48. return false;

  49. }

  50. }

  51.  
  52. return super.onInterceptTouchEvent(ev);

  53. }

  54. }

  •  

内部解决法

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

 
  1. /**

  2. * @ explain:这个 ViewPager是用来解决ScrollView里面嵌套ViewPager的 内部解决法的

  3. * @ author:xujun on 2016/10/25 16:38

  4. * @ email:gdutxiaoxu@163.com

  5. */

  6. public class MyViewPager extends ViewPager {

  7.  
  8. private static final String TAG = "xujun";

  9.  
  10. int lastX = -1;

  11. int lastY = -1;

  12.  
  13. public MyViewPager(Context context) {

  14. super(context);

  15. }

  16.  
  17. public MyViewPager(Context context, AttributeSet attrs) {

  18. super(context, attrs);

  19. }

  20.  
  21. @Override

  22. public boolean dispatchTouchEvent(MotionEvent ev) {

  23. int x = (int) ev.getRawX();

  24. int y = (int) ev.getRawY();

  25. int dealtX = 0;

  26. int dealtY = 0;

  27.  
  28. switch (ev.getAction()) {

  29. case MotionEvent.ACTION_DOWN:

  30. dealtX = 0;

  31. dealtY = 0;

  32. // 保证子View能够接收到Action_move事件

  33. getParent().requestDisallowInterceptTouchEvent(true);

  34. break;

  35. case MotionEvent.ACTION_MOVE:

  36. dealtX += Math.abs(x - lastX);

  37. dealtY += Math.abs(y - lastY);

  38. Log.i(TAG, "dealtX:=" + dealtX);

  39. Log.i(TAG, "dealtY:=" + dealtY);

  40. // 这里是否拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截

  41. if (dealtX >= dealtY) { // 左右滑动请求父 View 不要拦截

  42. getParent().requestDisallowInterceptTouchEvent(true);

  43. } else {

  44. getParent().requestDisallowInterceptTouchEvent(false);

  45. }

  46. lastX = x;

  47. lastY = y;

  48. break;

  49. case MotionEvent.ACTION_CANCEL:

  50. break;

  51. case MotionEvent.ACTION_UP:

  52. break;

  53.  
  54. }

  55. return super.dispatchTouchEvent(ev);

  56. }

  57. }

  •  

注意事项(坑)

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

即结构如下面的时候

在Activity中的相关解决方法

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

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

 
  1.  
  2. private void scroll() {

  3. mNoHorizontalScrollView.scrollTo(0,0);

  4. }

  5.  
  6. @Override

  7. public void onWindowFocusChanged(boolean hasFocus) {

  8. super.onWindowFocusChanged(hasFocus);

  9. if(hasFocus && first){

  10. first=false;

  11. scroll();

  12. }

  13. }

  14.  
  15.  
  •  

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

 
  1. view.setFocusable(true);

  2. view.setFocusableInTouchMode(true);

  3. view.requestFocus();

  •  

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

在Fragment中的相关解决方法

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

 
  1. view.setFocusable(true);

  2. view.setFocusableInTouchMode(true);

  3. view.requestFocus();

  •  

这段代码在初始化的时候就让该界面的顶部的某一个控件获得焦点,滚动条自然就显示到顶部了。但是该方法存在缺点,就是当我们上面的view如果滑动到一半的时候,切换到下一个Fragment,在切换回来的时候,RecyclerView的第一个item会自动滑动到顶部。目前我还没有找到相对比较好的解决这个问题的方法,大家知道相关解决方法的话也欢迎联系我,可以加我 微信或者在留言区评论,谢谢。

网友提供的解决方案

关于 ViewPagerActivity 在Fragment页面切换的时候,RecyclerView抢占焦点的问题已经解决,特别 感谢Jianqiu,他的博客地址:http://niorgai.github.io/

在 ViewPagerActivity 里面的 Fragment的 代码中加入以下代码,可以阻止 RecyclerView 的子 View 获得焦点,从而阻止 RecyclerView 抢占位置。

 
  1. // 是为了确保mNoHorizontalScrollView他的子孙不能获得焦点

  2. mNoHorizontalScrollView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);

  •  

详细代码见项目中的ListFragement

个人疑点

借鉴于解决Activity的方法,目前我还没有找到一个方法是在Fragemnt界面完全绘制完毕以后回调的方法,如果大家知道怎样处理的 话,欢迎大家提出来


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

内部解决法

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

 
  1. public class ChildViewPager extends ViewPager {

  2.  
  3. private static final String TAG = "xujun";

  4. public ChildViewPager(Context context) {

  5. super(context);

  6. }

  7.  
  8. public ChildViewPager(Context context, AttributeSet attrs) {

  9. super(context, attrs);

  10. }

  11.  
  12. @Override

  13. public boolean dispatchTouchEvent(MotionEvent ev) {

  14. int curPosition;

  15.  
  16. switch (ev.getAction()) {

  17. case MotionEvent.ACTION_DOWN:

  18. getParent().requestDisallowInterceptTouchEvent(true);

  19. break;

  20. case MotionEvent.ACTION_MOVE:

  21. curPosition = this.getCurrentItem();

  22. int count = this.getAdapter().getCount();

  23. Log.i(TAG, "curPosition:=" +curPosition);

  24. // 当当前页面在最后一页和第0页的时候,由父亲拦截触摸事件

  25. if (curPosition == count - 1|| curPosition==0) {

  26. getParent().requestDisallowInterceptTouchEvent(false);

  27. } else {//其他情况,由孩子拦截触摸事件

  28. getParent().requestDisallowInterceptTouchEvent(true);

  29. }

  30.  
  31. }

  32. return super.dispatchTouchEvent(ev);

  33. }

  34. }

  •  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值