自定义View之事件分发

本篇文章我们专门来研究一下view层的事件分发机制,我们在学习过程中总会碰到关于事件分发的各种问题,如onTouch和onTouchEvent的关系,setOnTouchListener和setOnClickListener的关系等等,类似这样的问题很多,结论我们都知道,有的时候是死记硬背的,记不长久,本篇文章我们来从源码的角度来分析总结一下各种关系,这样才能理解,便于记忆。

分析工具

//Android源码环境
android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"    
}

//分析工具
Android Studio 2.2.3
Build #AI-145.3537739, built on December 2, 2016
JRE: 1.8.0_112-release-b05 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o

接下来我们正式分析一下view层的事件分发的源码。首先要知道一点,对于view层次的,事件分发主要有两个方法,dispatchTouchEve和onTouchEvent,我们主要对这两种方法进行分析。

一、实例引入

我们先通过自定义一个button来进行分析。自定义的button很简单,就是重写了一下dispatchTouchEve和onTouchEvent两个方法。

public class MyButton extends Button {

    protected static final String TAG = "liji-view-test";

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

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        int action = event.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "onTouchEvent ACTION_UP");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event)
    {
        int action = event.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "dispatchTouchEvent ACTION_UP");
                break;

            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

}

自定义的MyButton很简单,就是重写了view的两个方法,我们在这两个方法中只进行一些log操作,其他不改变。接着我们在activity中使用这个自定义的MyButton。

        mMyButton = (MyButton) findViewById(R.id.myButton);
        mMyButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG,"onClick button click");
            }
        });

        mMyButton.setOnTouchListener(new View.OnTouchListener()
        {
            @Override
            public boolean onTouch(View v, MotionEvent event)
            {
                int action = event.getAction();

                switch (action)
                {
                    case MotionEvent.ACTION_DOWN:
                        Log.d(TAG, "onTouch ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.d(TAG, "onTouch ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.d(TAG, "onTouch ACTION_UP");
                        break;
                    default:
                        break;
                }

                return false;
            }
        });

可以看到,在activity中我们也处理了两个方法,一个是setOnTouchListener、一个是setOnClickListener,然后运行一下,我们可以看看log结果是什么。

D/liji-view-test: dispatchTouchEvent ACTION_DOWN
D/liji-view-test: onTouch ACTION_DOWN
D/liji-view-test: onTouchEvent ACTION_DOWN
D/liji-view-test: dispatchTouchEvent ACTION_MOVE
D/liji-view-test: onTouch ACTION_MOVE
D/liji-view-test: onTouchEvent ACTION_MOVE
D/liji-view-test: dispatchTouchEvent ACTION_MOVE
D/liji-view-test: onTouch ACTION_MOVE
D/liji-view-test: onTouchEvent ACTION_MOVE
D/liji-view-test: dispatchTouchEvent ACTION_MOVE
D/liji-view-test: onTouch ACTION_MOVE
D/liji-view-test: onTouchEvent ACTION_MOVE
D/liji-view-test: dispatchTouchEvent ACTION_MOVE
D/liji-view-test: onTouch ACTION_MOVE
D/liji-view-test: onTouchEvent ACTION_MOVE
D/liji-view-test: dispatchTouchEvent ACTION_UP
D/liji-view-test: onTouch ACTION_UP
D/liji-view-test: onTouchEvent ACTION_UP
D/liji-view-test: onClick button click

可以大概看出来事件响应的顺序是:

dispatchTouchEvent -> onTouch -> onTouchEvent -> onClick

从上面的log可以看出来,onTouch是优先于onClick执行的,并且onTouch执行了多次,一次是ACTION_DOWN,一次是ACTION_UP,还有几次是ACTION_MOVE。因此事件传递的顺序是先经过onTouch,再传递到onClick。

onTouch方法是有返回值的,如果我们尝试把onTouch方法里的返回值改成true,再运行一次就会发现onClick方法不再执行了,这是因为onTouch方法返回true就认为这个事件被onTouch消费掉了,因而不会再继续向下传递。

这其中的缘由究竟是怎么样的?我们通过源码来一探究竟。view事件分发的顺序是从dispatchTouchEvent开始的,所以我们就从它开始分析:

二、源码探究

首先我们进入view的dispatchTouchEvent方法中查看。

    //view.java
    public boolean dispatchTouchEvent(MotionEvent event) {

        //...

        boolean result = false;

        //...

        if (onFilterTouchEventForSecurity(event)) {

            //...

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

       //...

        return result;
    }

我们省略了其中无关的代码,只看对分析有用的代码,我们进入到if中去,首先看到一个对象ListenerInfo的li对象指的是什么,

    static class ListenerInfo {    
        protected OnFocusChangeListener mOnFocusChangeListener;
        protected OnScrollChangeListener mOnScrollChangeListener;
        public OnClickListener mOnClickListener;
        protected OnLongClickListener mOnLongClickListener;
        private OnKeyListener mOnKeyListener;
        private OnTouchListener mOnTouchListener;
        //...     
    }

看到没有,其实这个li指的就是我们设置的一些监听器,包括onTouchListener、onClickListener等等,我们接着分析if中的条件

            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

可以确认这里面的li!=null,所以第一个条件为true,第二个条件我们因为设置了onTouchListener事件监听,所以这里面的li.mOnTouchListener != null也是为true,再看第三个条件(mViewFlags & ENABLED_MASK) == ENABLED,因为我们的button是可以点击的,所以这里面也是为true,如果碰到不可点击的,如ImageView,这里面就是false了,我们到时候另外再谈,我们接着看下面一句代码。

li.mOnTouchListener.onTouch(this, event))

这句代码说明什么?如果我们在setOnTouchListener里面返回true的话,那么我们将直接返回result=true了,如果返回了false的话,那么这个if条件就不成立,所以它将会执行下一行代码if语句端判断-即它将会执行onTouchEvent事件

            if (!result && onTouchEvent(event)) {
                result = true;
            }

因为我们都是设置的默认返回值,所以在一开始的时候我们的log日志显示的顺序是:

dispatchTouchEvent -> onTouch -> onTouchEvent -> onClick

这个时候就看看onTouch返回结果了,返回的结果不同导致的顺序也不同。我们接着看看onTouchEvent的源码,分析一下里面藏了什么东西。

    public boolean onTouchEvent(MotionEvent event) {
        //...

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:

                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        boolean focusTaken = false;

                      //...

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {

                            if (!focusTaken) {

                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                    //...

                    break;

                case MotionEvent.ACTION_DOWN:
                    //...
                    break;

                case MotionEvent.ACTION_CANCEL:
                       //...
                    break;

                case MotionEvent.ACTION_MOVE:
                   //...

                    break;
            }

            return true;
        }

        return false;
    }

我们在onTouchEvent方法中查看一下,省略一些无关的代码,我们发现了其中有一个方法就是在手指松开的时候action=MotionEvent.ACTION_UP的时候,会调用这个performClick方法。我们进入performClick方法中继续查看

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

看到没?这里面就涉及到了onClick事件了,这也间接的证明了,onTouch的事件优先级高于onClick的优先级。

到了这里,我们就可以总结一下关于一开始提出来的几个问题:

1、onTouch和onTouchEvent有什么区别,又该如何使用?

从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。

另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行(&&操作符,如果前面的判断为false的话,后面就不判断了)。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

2、onTouch和onClick优先级

我们从源码中也可以分析得到:onTouch的优先级高于onClick的优先级,其中onClick的事件是在onTouchEvent中产生的。

判断是否发生onTouchEvent事件的条件有三个。(1)设置OnTouchListener监听,(2)该view是否是enable的,(3)在onTouch方法中返回true

如果上述三个条件有一个没有满足即为FALSE的话,那么它将执行onTouchEvent事件同时将产生onClick事件。

3、touch事件的层级传递

我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

说到这里,很多的朋友肯定要有巨大的疑问了。这不是在自相矛盾吗?前面的例子中,明明在onTouch事件里面返回了false,ACTION_DOWN和ACTION_UP不是都得到执行了吗?其实你只是被假象所迷惑了,让我们仔细分析一下,在前面的例子当中,我们到底返回的是什么。参考着我们前面分析的源码,首先在onTouch事件里返回了false,就一定会进入到onTouchEvent方法中,然后我们来看一下onTouchEvent方法的细节。由于我们点击了按钮,就会进入到第14行这个if判断的内部,然后你会发现,不管当前的action是什么,最终都一定会走到第89行,返回一个true。是不是有一种被欺骗的感觉?明明在onTouch事件里返回了false,系统还是在onTouchEvent方法中帮你返回了true。就因为这个原因,才使得前面的例子中ACTION_UP可以得到执行。

那我们可以换一个控件,将按钮替换成ImageView,然后给它也注册一个touch事件,并返回false。在ACTION_DOWN执行完后,后面的一系列action都不会得到执行了。这又是为什么呢?因为ImageView和按钮不同,它是默认不可点击的,因此在onTouchEvent的内部判断时无法进入到if的内部,直接跳到第最后面返回了false,也就导致后面其它的action都无法执行了。

三、总结

接下来我们来总结一下各个事件发生的流程。

针对于view来说,当发生一个事件时(譬如:onTouch事件),这个时候就会调用view的dispatchTouchEvent事件,它拥有boolean类型的返回值,当返回为true时,顺序下发会中断,也就是说,这个onTouch事件是不会继续执行下去了,就执行完一个dispatchTouchEvent事件,当它返回false时事件继续传递到onTouchListener中,这个onTouchListener(onTouch事件)也是一个拥有boolean类型的返回值的方法,默认返回false,这个时候就可以继续执行onClick(在onTouchEvent事件中)事件了,如果onTouch事件返回了true,那么就代表这个事件被它自己给消耗掉了,不会再继续传递。

用一张图来表示下:

image.png

对于View中的dispatchTouchEvent方法,在这个方法内,首先是进行了一个判断,里面有三个条件,如果这三个条件都满足,就返回true,否则就返回onTouchEvent方法执行的结果。对于第一个条件是一个mOnTouchListener变量,这个变量是在View中的setOnTouchListener方法里赋值的,也就是说只要我们给控件注册了touch事件,mOnTouchListener就一定被赋值了。第二个条件是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true。第三个条件最为关键,mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册touch事件时的onTouch方法。也就是说如果我们在onTouch方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。如果我们在onTouch方法里返回false,就会再去执行onTouchEvent(event)方法。

到这里,整个view的事件分发就比较清楚了,接下来我们分析关于viewGroup的事件分发了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容