Android View 的滑动冲突

01.什么是滑动冲突

  • 当父容器与子 View 都可以滑动时,就会产生滑动冲突。
  • 解决 View 之间的滑动冲突的方法分为两种,分别是外部拦截法和内部拦截法

02.外部拦截法

  • 父容器根据需要在 onInterceptTouchEvent 方法中对触摸事件进行选择性拦截,思路可以看以下伪代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
    intercepted = false;
    break;
    }
    case MotionEvent.ACTION_MOVE: {
    if (满足父容器的拦截要求) {
    intercepted = true;
    } else {
    intercepted = false;
    }
    break;
    }
    case MotionEvent.ACTION_UP: {
    intercepted = false;
    break;
    }
    default:
    break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
    }
  • 思路如下所示

    • 根据实际的业务需求,判断是否需要处理 ACTION_MOVE 事件,如果父 View 需要处理则返回 true,否则返回 false 并交由子 View 去处理
    • ACTION_DOWN 事件需要返回 false,父容器不能进行拦截,否则根据 View 的事件分发机制,后续的 ACTION_MOVE 与 ACTION_UP 事件都将默认交由父容器进行处理
    • 原则上 ACTION_UP 事件也需要返回 false,如果返回 true,那么子 View 将接收不到 ACTION_UP 事件,子 View 的onClick 事件也无法触发

03.内部拦截法

  • 内部拦截法则是要求父容器不拦截任何事件,所有事件都传递给子View,子View根据需求判断是自己消费事件还是传回给父容器进行处理,思路可以看以下伪代码:

    • 子 View 修改其 dispatchTouchEvent 方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
    parent.requestDisallowInterceptTouchEvent(true);
    break;
    }
    case MotionEvent.ACTION_MOVE: {
    int deltaX = x - mLastX;
    int deltaY = y - mLastY;
    if (父容器需要此类点击事件) {
    parent.requestDisallowInterceptTouchEvent(false);
    }
    break;
    }
    case MotionEvent.ACTION_UP: {
    break;
    }
    default:
    break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
    }
    • 父容器修改其 onInterceptTouchEvent 方法
    1
    2
    3
    4
    5
    6
    7
    8
    public boolean onInterceptTouchEvent(MotionEvent event) {
    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
    return false;
    } else {
    return true;
    }
    }
  • 思路所示

    • 内部拦截法要求父容器不能拦截 ACTION_DOWN 事件,否则一旦父容器拦截 ACTION_DOWN 事件,那么后续的触摸事件都不会传递给子View
    • 滑动策略的逻辑放在子 View 的 dispatchTouchEvent 方法的 ACTION_MOVE 事件中,如果父容器需要处理事件则调用 parent.requestDisallowInterceptTouchEvent(false) 方法让父容器去拦截事件

04.滑动冲突实例

  • 场景解释:

    • 为了能使整个Activity界面能够上下滑动,使用了ScrollView,将Tablayout和ViewPager的联合包裹在LinearLayout中,作为一部分。
  • 代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:background="@drawable/bg_autumn_tree_min"/>

    <include layout="@layout/include_reflex_view"/>

    <android.support.design.widget.TabLayout
    android:id="@+id/tab_layout"
    android:layout_width="match_parent"
    android:layout_height="50dp"/>

    <android.support.v4.view.ViewPager
    android:id="@+id/vp_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

    </LinearLayout>

    </ScrollView>
  • 遇到问题:

    • 正常使用情况下,ViewPager中的Fragment没有显示出来,需要设置ViewPager的高度才可以,结果是ViewPager中的Fragment可以正常显示。
    • 由于滑动方法不一样,导致滑动冲突。

05.外部拦截法解决滑动冲突

  • 滑动方向不同之以ScrollView与ViewPager为例的外部解决法

    • 从 父View 着手,重写 onInterceptTouchEvent 方法,在 父View 需要拦截的时候拦截,不要的时候返回false,代码大概如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    举例子:以ScrollView与ViewPager为例
    public class MyScrollView extends ScrollView {

    public MyScrollView(Context context) {
    super(context);
    }

    public MyScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);
    }

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    }

    @TargetApi(21)
    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    }

    private float mDownPosX = 0;
    private float mDownPosY = 0;
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    final float x = ev.getX();
    final float y = ev.getY();
    final int action = ev.getAction();
    switch (action) {
    case MotionEvent.ACTION_DOWN:
    mDownPosX = x;
    mDownPosY = y;
    break;
    case MotionEvent.ACTION_MOVE:
    final float deltaX = Math.abs(x - mDownPosX);
    final float deltaY = Math.abs(y - mDownPosY);
    // 这里是够拦截的判断依据是左右滑动,可根据自己的逻辑进行是否拦截
    if (deltaX > deltaY) {
    return false;
    }
    }
    return super.onInterceptTouchEvent(ev);
    }
    }

06.内部拦截法解决滑动冲突

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    public class MyViewPager extends ViewPager {

    private static final String TAG = "yc";

    int lastX = -1;
    int lastY = -1;

    public MyViewPager(Context context) {
    super(context);
    }

    public MyViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    int x = (int) ev.getRawX();
    int y = (int) ev.getRawY();
    int dealtX = 0;
    int dealtY = 0;
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
    dealtX = 0;
    dealtY = 0;
    // 保证子View能够接收到Action_move事件
    getParent().requestDisallowInterceptTouchEvent(true);
    break;
    case MotionEvent.ACTION_MOVE:
    dealtX += Math.abs(x - lastX);
    dealtY += Math.abs(y - lastY);
    Log.i(TAG, "dealtX:=" + dealtX);
    Log.i(TAG, "dealtY:=" + dealtY);
    // 这里是够拦截的判断依据是左右滑动,可根据自己的逻辑进行是否拦截
    if (dealtX >= dealtY) {
    getParent().requestDisallowInterceptTouchEvent(true);
    } else {
    getParent().requestDisallowInterceptTouchEvent(false);
    }
    lastX = x;
    lastY = y;
    break;
    case MotionEvent.ACTION_CANCEL:
    break;
    case MotionEvent.ACTION_UP:
    break;
    }
    return super.dispatchTouchEvent(ev);
    }
    }