[关闭]
@xujun94 2016-10-26T14:40:02.000000Z 字数 10573 阅读 3831

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

这篇博客主要讲解一下几个问题
- 粗略地介绍一下View的事件分发机制
- 解决事件滑动冲突的思路及方法
- ScrollView 里面嵌套ViewPager导致的滑动冲突
- ViewPager里面嵌套ViewPager 导致的滑动冲突
- 轮播图的几种实现方式

文章首发地址CSDN:http://blog.csdn.net/gdutxiaoxu/article/details/52939127

先看一下效果图

ScrollView里面嵌套ViewPager

ViewPager里面嵌套ViewPager


View的 事件分发机制

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

先啰嗦一下,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 方法进行分发,

关于更多详细分析,请查看原博客图解 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. final int action = ev.getAction();
  6. switch (action) {
  7. case MotionEvent.ACTION_DOWN:
  8. mDownPosX = x;
  9. mDownPosY = y;
  10. break;
  11. case MotionEvent.ACTION_MOVE:
  12. final float deltaX = Math.abs(x - mDownPosX);
  13. final float deltaY = Math.abs(y - mDownPosY);
  14. // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
  15. if (deltaX > deltaY) {
  16. return false;
  17. }
  18. }
  19. return super.onInterceptTouchEvent(ev);
  20. }

内部解决法

从子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. switch (ev.getAction()) {
  8. case MotionEvent.ACTION_DOWN:
  9. dealtX = 0;
  10. dealtY = 0;
  11. // 保证子View能够接收到Action_move事件
  12. getParent().requestDisallowInterceptTouchEvent(true);
  13. break;
  14. case MotionEvent.ACTION_MOVE:
  15. dealtX += Math.abs(x - lastX);
  16. dealtY += Math.abs(y - lastY);
  17. Log.i(TAG, "dealtX:=" + dealtX);
  18. Log.i(TAG, "dealtY:=" + dealtY);
  19. // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
  20. if (dealtX >= dealtY) {
  21. getParent().requestDisallowInterceptTouchEvent(true);
  22. } else {
  23. getParent().requestDisallowInterceptTouchEvent(false);
  24. }
  25. lastX = x;
  26. lastY = y;
  27. break;
  28. case MotionEvent.ACTION_CANCEL:
  29. break;
  30. case MotionEvent.ACTION_UP:
  31. break;
  32. }
  33. return super.dispatchTouchEvent(ev);
  34. }

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

外部解决法

如上面所述,从 父ViewScrollView着手,重写 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. public VerticalScrollView(Context context) {
  9. super(context);
  10. }
  11. public VerticalScrollView(Context context, AttributeSet attrs) {
  12. super(context, attrs);
  13. }
  14. public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
  15. super(context, attrs, defStyleAttr);
  16. }
  17. @TargetApi(21)
  18. public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int
  19. defStyleRes) {
  20. super(context, attrs, defStyleAttr, defStyleRes);
  21. }
  22. private float mDownPosX = 0;
  23. private float mDownPosY = 0;
  24. @Override
  25. public boolean onInterceptTouchEvent(MotionEvent ev) {
  26. final float x = ev.getX();
  27. final float y = ev.getY();
  28. final int action = ev.getAction();
  29. switch (action) {
  30. case MotionEvent.ACTION_DOWN:
  31. mDownPosX = x;
  32. mDownPosY = y;
  33. break;
  34. case MotionEvent.ACTION_MOVE:
  35. final float deltaX = Math.abs(x - mDownPosX);
  36. final float deltaY = Math.abs(y - mDownPosY);
  37. // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
  38. if (deltaX > deltaY) {
  39. return false;
  40. }
  41. }
  42. return super.onInterceptTouchEvent(ev);
  43. }
  44. }

内部解决法

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

  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. private static final String TAG = "xujun";
  8. int lastX = -1;
  9. int lastY = -1;
  10. public MyViewPager(Context context) {
  11. super(context);
  12. }
  13. public MyViewPager(Context context, AttributeSet attrs) {
  14. super(context, attrs);
  15. }
  16. @Override
  17. public boolean dispatchTouchEvent(MotionEvent ev) {
  18. int x = (int) ev.getRawX();
  19. int y = (int) ev.getRawY();
  20. int dealtX = 0;
  21. int dealtY = 0;
  22. switch (ev.getAction()) {
  23. case MotionEvent.ACTION_DOWN:
  24. dealtX = 0;
  25. dealtY = 0;
  26. // 保证子View能够接收到Action_move事件
  27. getParent().requestDisallowInterceptTouchEvent(true);
  28. break;
  29. case MotionEvent.ACTION_MOVE:
  30. dealtX += Math.abs(x - lastX);
  31. dealtY += Math.abs(y - lastY);
  32. Log.i(TAG, "dealtX:=" + dealtX);
  33. Log.i(TAG, "dealtY:=" + dealtY);
  34. // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
  35. if (dealtX >= dealtY) {
  36. getParent().requestDisallowInterceptTouchEvent(true);
  37. } else {
  38. getParent().requestDisallowInterceptTouchEvent(false);
  39. }
  40. lastX = x;
  41. lastY = y;
  42. break;
  43. case MotionEvent.ACTION_CANCEL:
  44. break;
  45. case MotionEvent.ACTION_UP:
  46. break;
  47. }
  48. return super.dispatchTouchEvent(ev);
  49. }
  50. }

注意事项(坑)

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

即结构如下面的时候

在Activity中的相关解决方法

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

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

  1. private void scroll() {
  2. mNoHorizontalScrollView.scrollTo(0,0);
  3. }
  4. @Override
  5. public void onWindowFocusChanged(boolean hasFocus) {
  6. super.onWindowFocusChanged(hasFocus);
  7. if(hasFocus && first){
  8. first=false;
  9. scroll();
  10. }
  11. }

第二种解决方法,调用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会自动滑动到顶部。目前我还没有找到相对比较好的解决这个问题的方法,大家知道相关解决方法的话也欢迎联系我,可以加我 微信或者在留言区评论,谢谢

个人疑点

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


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

内部解决法

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

  1. public class ChildViewPager extends ViewPager {
  2. private static final String TAG = "xujun";
  3. public ChildViewPager(Context context) {
  4. super(context);
  5. }
  6. public ChildViewPager(Context context, AttributeSet attrs) {
  7. super(context, attrs);
  8. }
  9. @Override
  10. public boolean dispatchTouchEvent(MotionEvent ev) {
  11. int curPosition;
  12. switch (ev.getAction()) {
  13. case MotionEvent.ACTION_DOWN:
  14. getParent().requestDisallowInterceptTouchEvent(true);
  15. break;
  16. case MotionEvent.ACTION_MOVE:
  17. curPosition = this.getCurrentItem();
  18. int count = this.getAdapter().getCount();
  19. Log.i(TAG, "curPosition:=" +curPosition);
  20. // 当当前页面在最后一页和第0页的时候,由父亲拦截触摸事件
  21. if (curPosition == count - 1|| curPosition==0) {
  22. getParent().requestDisallowInterceptTouchEvent(false);
  23. } else {//其他情况,由孩子拦截触摸事件
  24. getParent().requestDisallowInterceptTouchEvent(true);
  25. }
  26. }
  27. return super.dispatchTouchEvent(ev);
  28. }
  29. }

外部解决法

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


讨论

对于这种效果,上面是轮播图的,下面是RecyclerView或者ListView的,一般有一下几种实现方式
- 使用我们上述提高的ScrollView里面嵌套ViewPager和RecyclerView,这种实现方式需要自己解决View滑动事件的冲突,同时还有我在上述提高的在Fragment中存在的问题
- 使用listView的addHeaderView来实现,或者是通过多种不同的item来实现
- 使用RecyclerView添加headerView来实现,或者复用多种不同的item来实现。关于RecyclerView如何添加headerView可以参考鸿洋大神的这一篇博客 Android 优雅的为RecyclerView添加HeaderView和FooterView
- 使用SupportLibrary中的CoordinatorLayout等控件

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

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <android.support.design.widget.CoordinatorLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android"
  4. xmlns:app="http://schemas.android.com/apk/res-auto"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. android:background="@android:color/background_light"
  8. android:fitsSystemWindows="true"
  9. >
  10. <android.support.design.widget.AppBarLayout
  11. android:layout_width="match_parent"
  12. android:layout_height="300dp"
  13. android:fitsSystemWindows="true"
  14. android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
  15. >
  16. <android.support.design.widget.CollapsingToolbarLayout
  17. android:layout_width="match_parent"
  18. android:layout_height="match_parent"
  19. app:layout_scrollFlags="scroll|snap">
  20. <android.support.v4.view.ViewPager
  21. android:id="@+id/viewPager"
  22. android:layout_width="match_parent"
  23. android:layout_height="match_parent"
  24. >
  25. </android.support.v4.view.ViewPager>
  26. <TextView
  27. android:id="@+id/tv_page"
  28. android:layout_width="match_parent"
  29. android:layout_height="wrap_content"
  30. android:layout_gravity="bottom"
  31. android:gravity="right"
  32. android:text="1/10"
  33. android:textColor="#000"/>
  34. </android.support.design.widget.CollapsingToolbarLayout>
  35. </android.support.design.widget.AppBarLayout>
  36. <android.support.v7.widget.RecyclerView
  37. android:id="@+id/recyclerView"
  38. android:layout_width="match_parent"
  39. android:layout_height="match_parent"
  40. app:layout_behavior="@string/appbar_scrolling_view_behavior">
  41. </android.support.v7.widget.RecyclerView>
  42. </android.support.design.widget.CoordinatorLayout>

关于CoordinatorLayout的更多用法,可以参考我的这一篇博客使用CoordinatorLayout打造各种炫酷的效果


总结

题外话

参考文章:图解 Android 事件分发机制

文章首发地址CSDN:http://blog.csdn.net/gdutxiaoxu/article/details/52939127

源码下载地址:https://github.com/gdutxiaoxu/TouchDemo.git

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注