Android进阶知识:事件分发与滑动冲突

1、前言

Android学习一段时间,需求做多了必然会遇到滑动冲突问题,比如在一个ScrollView中要嵌套一个地图View,这时候触摸移动地图或者放大缩小地图就会变得不太准确甚至没有反应,这就是遇到了滑动冲突,ScrollView中上下滑动与地图的触摸手势发生冲突。想要解决滑动冲突就不得不提到Android的事件分发机制,只有吃透了事件分发,才能对滑动冲突的解决得心应手。

2、事件分发机制相关方法

Android事件分发机制主要相关方法有以下三个:

  • 事件分发:public boolean dispatchTouchEvent(MotionEvent ev)
  • 事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
  • 事件响应:public boolean onTouchEvent(MotionEvent ev)

以下是这三个方法在Activity、ViewGroup和View中的存在情况:

相关方法 Activity ViewGroup View
dispatchTouchEvent yes yes yes
onInterceptTouchEvent no yes no
onTouchEvent yes yes yes

这三个方法都返回一个布尔类型,根据返回的不同对事件进行不同的分发拦截和响应。一般有三种返回truefalsesuper引用父类对应方法。

dispatchTouchEvent 返回true:表示改事件在本层不再进行分发且已经在事件分发自身中被消费了。
dispatchTouchEvent 返回 false:表示事件在本层不再继续进行分发,并交由上层控件的onTouchEvent方法进行消费。

onInterceptTouchEvent 返回true:表示将事件进行拦截,并将拦截到的事件交由本层控件 的onTouchEvent 进行处理。
onInterceptTouchEvent 返回false:表示不对事件进行拦截,事件得以成功分发到子View。并由子ViewdispatchTouchEvent进行处理。

onTouchEvent 返回 true:表示onTouchEvent处理完事件后消费了此次事件。此时事件终结,将不会进行后续的传递。
onTouchEvent 返回 false:事件在onTouchEvent中处理后继续向上层View传递,且有上层ViewonTouchEvent进行处理。

除此之外还有一个方法也是经常用到的:

  • public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)

它的作用是子View用来通知父View不要拦截事件。下面先写一个简单的Demo来看一下事件分发和传递:

简单的日志的Demo:

这里的代码只是自定义了两个ViewGroup和一个View,在其对应事件分发传递方法中打印日志,来查看调用顺序情况,所有相关分发传递方法返回皆是super父类方法。
例如:
MyViewGroupA.java:

public class MyViewGroupA extends RelativeLayout {
    public MyViewGroupA(Context context) {
        super(context);
    }
    public MyViewGroupA(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_UP");
                break;
        }        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
                break;
        }        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
                break;
        }        return super.onTouchEvent(event);
    }
}

其他的代码都是类似的,这里再贴一下Acitivity里的布局:

<?xml version="1.0" encoding="utf-8"?>
<com.example.sy.eventdemo.MyViewGroupA xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/viewGroupA"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    tools:context=".MainActivity">

    <com.example.sy.eventdemo.MyViewGroupB
        android:id="@+id/viewGroupB"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_centerInParent="true"
        android:background="@android:color/white">

        <com.example.sy.eventdemo.MyView
            android:id="@+id/myView"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_centerInParent="true"
            android:background="@android:color/holo_orange_light" />
    </com.example.sy.eventdemo.MyViewGroupB>
</com.example.sy.eventdemo.MyViewGroupA>

Demo中的Activity布局层级关系:


除去外层Activity和Window的层级,从MyViewGroup开始是自己定义的打印日志View。接下来运行Demo查看日志:

 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onTouchEvent:ACTION_DOWN
 D/MainActivity: onTouchEvent:ACTION_DOWN
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MainActivity: onTouchEvent:ACTION_MOVE
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MainActivity: onTouchEvent:ACTION_UP

结合日志可以大概看出(先只看ACTION_DOWN事件):
事件的分发顺序:Activity-->MyViewGroupA-->MyViewGroupB-->MyView自顶向下分发
事件的响应顺序:MyView-->MyViewGroupB-->MyViewGroupA-->Activity自底向上响应消费

同时这里通过日志也发现一个问题:

  • 问题一为什么这里只有ACTION_DOWN事件有完整的从Activity到ViewGroup再到View的分发拦截和响应的运行日志,为什么ACTION_MOVEACTION_UP事件没有?

接着再测试一下之前提的requestDisallowInterceptTouchEvent方法的使用。现在布局文件中将MyView添加一个属性android:clickable="true"。此时在运行点击打印日志是这样的:

 /-------------------ACTION_DOWN事件------------------
 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 /-------------------ACTION_MOVE事件------------------
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
 D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
 D/MyView: dispatchTouchEvent:ACTION_MOVE
 D/MyView: onTouchEvent:ACTION_MOVE
 /-------------------ACTION_UP事件------------------
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
 D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_UP
 D/MyView: dispatchTouchEvent:ACTION_UP
 D/MyView: onTouchEvent:ACTION_UP

这下ACTION_MOVEACTION_UP事件也有日志了。接下来在MyViewGroupB的onInterceptTouchEvent的方法中修改代码如下:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
                return false;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
                return true;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
                return true;
        }
        return false;
    }

也就是拦截下ACTION_MOVEACTION_UP事件不拦截下ACTION_DOWN事件,然后在运行查看日志:

 /------------------ACTION_DOWN事件------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 /------------------ACTION_MOVE事件-----------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
 D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
 /------------------ACTION_UP事件-------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
 D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupB: onTouchEvent:ACTION_UP
 D/MainActivity: onTouchEvent:ACTION_UP

根据日志可知ACTION_MOVEACTION_UP事件传递到MyViewGroupB就没有再向MyView传递了。接着在MyView的onTouchEvent方法中调用requestDisallowInterceptTouchEvent方法通知父容器不要拦截事件。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }

再次运行查看日志:

 /------------------ACTION_DOWN事件------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 /------------------ACTION_MOVE事件-----------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
 D/MyView: dispatchTouchEvent:ACTION_MOVE
 D/MyView: onTouchEvent:ACTION_MOVE
 /------------------ACTION_UP事件-------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
 D/MyView: dispatchTouchEvent:ACTION_UP
 D/MyView: onTouchEvent:ACTION_UP

这时可以发现ACTION_MOVEACTION_UP事件又传递到了MyView中并且两个ViewGroup中都没有执行onInterceptTouchEvent方法。 明显是requestDisallowInterceptTouchEvent方法起了作用。但是又出现了两个新问题。

  • 问题二:为什么将设置clickable="true"之后ACTION_MOVEACTION_UP事件就会执行了?
  • 问题三:requestDisallowInterceptTouchEvent方法是怎样通知父View不拦截事件,为什么连onInterceptTouchEvent方法也不执行了?

想弄明白这些问题就只能到源码中寻找答案了。

3、事件分发机制源码

在正式看源码之前先讲一个概念:事件序列

我们常说的事件,一般是指从手指触摸到屏幕在到离开屏幕这么一个过程。在这个过程中其实会产生多个事件,一般是以ACTION_DOWN作为开始,中间存在多个ACTION_MOVE,最后以ACTION_UP结束。我们称一次ACTION_DOWN-->ACTION_MOVE-->ACTION_UP过程称为一个事件序列。

ViewGroup中有一个内部类TouchTarget,这个类将消费事件的View封装成一个节点,使得可以将一个事件序列的DOWNMOVEUP事件构成一个单链表保存。ViewGroup中也有个TouchTarget类型的成员mFirstTouchTarget用来指向这个单链表头。在每次DOWN事件开始时清空这个链表,成功消费事件后通过TouchTarget.obtain方法获得一个TouchTarget,将消费事件的View传入,然后插到单链表头。后续MOVEUP事件可以通过判断mFirstTouchTarget来知道之前是否有能够消费事件的View。

TouchTarget的源码:

private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        private static final Object sRecycleLock = new Object[0];
        private static TouchTarget sRecycleBin;
        private static int sRecycledCount;

        public static final int ALL_POINTER_IDS = -1; // all ones

        // The touched child view.
        //接受事件的View
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;

        // The next target in the target list.
        //下一个TouchTarget的地址
        public TouchTarget next;

        private TouchTarget() {
        }

        public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
            if (child == null) {
                throw new IllegalArgumentException("child must be non-null");
            }

            final TouchTarget target;
            synchronized (sRecycleLock) {
                if (sRecycleBin == null) {
                    target = new TouchTarget();
                } else {
                    target = sRecycleBin;
                    sRecycleBin = target.next;
                     sRecycledCount--;
                    target.next = null;
                }
            }
            target.child = child;
            target.pointerIdBits = pointerIdBits;
            return target;
        }

        public void recycle() {
            if (child == null) {
                throw new IllegalStateException("already recycled once");
            }

            synchronized (sRecycleLock) {
                if (sRecycledCount < MAX_RECYCLED) {
                    next = sRecycleBin;
                    sRecycleBin = this;
                    sRecycledCount += 1;
                } else {
                    next = null;
                }
                child = null;
            }
        }
    }
Activity中的dispatchTouchEvent方法:

接下来正式按照分发流程来阅读源码,从Activity的dispatchTouchEvent方法开始看起,事件产生时会先调用这个方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

方法中先判断事件类型是ACTION_DOWN事件会执行onUserInteraction方法,onUserInteraction方法在Activity中是一个空实现,在当前Activity下按下Home或者Back键时会调用此方法,这里不是重点,这里重点是关注下ACTION_DOWN事件,ACTION_DOWN类型事件的判断,在事件传递的逻辑中非常重要,因为每次点击事件都是以ACTION_DOWN事件开头,所以ACTION_DOWN事件又作为一次新的点击事件的标记。

紧接着看,在第二个if判断中根据getWindow().superDispatchTouchEvent(ev)的返回值决定了整个方法的返回。

如果getWindow().superDispatchTouchEvent(ev)</

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值