源码阅读分析 - View的Touch事件分发

其实 Android 事件分发机制在早几年一直都困扰着我,那时候处理事件分发的自定义 View 脑子都是一片白,老感觉处理不好。后来自己看了 android 源码,也阅读了很多大牛的文章才算彻底明白,总之掌握 Android 事件分发机制是必不可少的,而 Android 事件分发机制绝对不是三言两语就能说得清的。

而今天由于我们自定义 View 进阶的需要,自己也是筹备了很久。目前虽然网上相关的文章也不少,很多也写得非常详细,但是多数文章只是讲了讲理论,然后配合 Log 打印一下结果而已。而我准备不仅带着大家从源码的角度进行分析,还需要理论结合实践写几个关于这方面的效果,这样相信我们会有更深的理解。阅读源码讲究由浅入深,循序渐进,我们就不像其他文章一样搞混合了,先讲 View 的 Touch 事件分发,然后再讲 ViewGroup 的事件分发,最后再写个几次效果。我们一贯的套路都是理论结合实践,由浅入深

先来看几个效果,如前几次我们写自定义评分控件的 RatingBar 复写了 onTouchEvent(),这里只是举个例子:

public class RatingBar extends View {
    public RatingBar(Context context) {
        super(context);
    }

    public RatingBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public RatingBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("TAG", "onTouchEvent execute -> " + event.getAction());
        return super.onTouchEvent(event);
    }
}

如果想要给这个控件注册一个点击事件,只需要调用:

mRatingBar.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d("TAG","onClick execute");
    }
});

如果想给这个按钮再添加一个touch事件,只需要调用:

mRatingBar.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.d("TAG", "onTouch execute -> " + event.getAction());
        return false;
    }
});

上面的代码如果运行起来,哪一个会先执行呢? 如果是靠猜,那么我们来试一下就知道了,运行程序点击,打印结果如下:

运行效果

可以看到,onTouch 是优先于优先于 onTouchEvent 优先于 onClick 执行的,并且 onTouch 和 onTouchEvent 执行了两次,一次是 ACTION_DOWN ,一次是 ACTION_UP (你还可能会有多次 ACTION_MOVE 的执行,如果你在上面触摸)。因此事件传递的顺序是先经过 onTouch ,然后经过 onTouchEvent ,再传递到 onClick 。

如果留心观察你会发现 setOnTouchListener 是有返回值的,如果返回 ture ,再次运行一下会怎样?

运行效果

我们发现,onTouchEvent 和 onClick 方法不再执行了!为什么会这样呢?你可以先理解成 onTouch 方法返回 true 就认为这个事件被 onTouch 消费掉了,因而不会再继续 onTouchEvent 和 onClick 。到目前位置如果你清楚了,那么面试的时候基本靠背,那么自己写效果的时候基本靠蒙。我们肯定不能局限于这个装态,接下了我们就带着疑问从源码的角度分析一下,为什么会出现上述情况?

首先我们需要知道,你点击或者或者触摸任何一个 View 都会调用 View 的 dispatchTouchEvent() 方法,我们就从这里开始分析源码:

    public boolean dispatchTouchEvent(MotionEvent event) {
        // 省略部分代码 ...

        boolean result = false;

        // 省略部分代码 ...

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            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;
            }
        }
        
        // 返回 result
        return result;
    }

省略掉部分代码之后,这个方法就变得非常的简洁了,只有短短几行代码!我们可以看到,在这个方法内,首先是进行了一个判断,如果li != null,mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED 和 mOnTouchListener.onTouch(this, event) 这三个条件都为真,result 就是 true,否则就去执行 onTouchEvent(event) 方法并返回。
  那么 ListenerInfo 到底是什么?我们可以看下源码,这其实就是有关 View 所有事件的一个集合类,如 OnFocusChangeListener , OnScrollChangeListener , OnClickListener 、、、

    static class ListenerInfo {
        /**
         * Listener used to dispatch focus change events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnFocusChangeListener mOnFocusChangeListener;

        /**
         * Listeners for layout change events.
         */
        private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;

        protected OnScrollChangeListener mOnScrollChangeListener;

        /**
         * Listeners for attach events.
         */
        private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

        /**
         * Listener used to dispatch click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        public OnClickListener mOnClickListener;

        /**
         * Listener used to dispatch long click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnLongClickListener mOnLongClickListener;

        /**
         * Listener used to dispatch context click events. This field should be made private, so it
         * is hidden from the SDK.
         * {@hide}
         */
        protected OnContextClickListener mOnContextClickListener;

        /**
         * Listener used to build the context menu.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnCreateContextMenuListener mOnCreateContextMenuListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;

        private OnHoverListener mOnHoverListener;

        private OnGenericMotionListener mOnGenericMotionListener;

        private OnDragListener mOnDragListener;

        private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;

        OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
    }

先看一下条件 li.mOnTouchListener 这个变量是在哪里赋值的呢?我们寻找之后在View里发现了如下方法:

    /**
     * Register a callback to be invoked when a touch event is sent to this view.
     * @param l the touch listener to attach to this view
     */
    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

第二个条件 mOnTouchListener 正是在 setOnTouchListener 方法里赋值的,也就是说只要我们给控件注册了 touch 事件,mListenerInfo 和 mListenerInfo.mOnTouchListener 就一定被赋值了。

第三个条件(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,默认都是enable的,因此这个条件恒定为 true 。

第四个条件就比较关键了,mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册 touch 事件时的 onTouch 方法。也就是说如果我们在 onTouch 方法里返回true,就会让这三个条件全部成立,从而 result 是 true , 那么 onTouchEvent 就不会被执行 。如果我们在 onTouch 方法里返回 false,就会去执行 onTouchEvent() 方法。

现在我们可以结合前面的例子来分析一下了,首先在 dispatchTouchEvent 中最先执行的就是 onTouch 方法,因此 onTouch 肯定是要优先于 onTouchEvent 方法,也是印证了刚刚的打印结果。而如果在 onTouch 方法里返回了 true,不会再执行 onTouchEvent 。但是到目前位置我们还没有看到 onClick 执行,但是我们可以猜到,onClick的调用肯定是在onTouchEvent(event)方法中的!那我们马上来看下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:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        performClick();
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                    // 省略部分代码
            }

            return true;
        }

        return false;
    }

    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;
    }

相较于刚才的 dispatchTouchEvent 方法,onTouchEvent 方法复杂了很多,不过没关系,我们只挑重点看就可以了。switch 中如果当前的事件是抬起手指,则会进入到 MotionEvent.ACTION_UP 这个 case 当中。在经过种种判断之后,会执行到 performClick() 方法,可以看到,只要 mListenerInfo.mOnClickListener 不是 null,就会去调用它的 onClick 方法,那 mListenerInfo.mOnClickListener 又是在哪里赋值的呢?我们大概能猜到肯定在 setOnclickLstener 方法中:

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

View 的 Touch 事件分发我们就讲到这里了,下一节我将带着大家一起了解 ViewGroup 的事件分发和事件拦截,在源码的基础上写几个效果,我想应该可以说就堪称完美了。

所有分享大纲:Android进阶之旅 - 自定义View篇

视频讲解地址:http://pan.baidu.com/s/1hr6ql72

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

推荐阅读更多精彩内容