【Android源码】View的事件分发

目录:

QWQIX7REKZF%38DNA$G%(GP.png

一、 setContentView

Activity中setContentView的源码,主要作用就是:

生成DecorView,并把通过Activity的theme和Java代码里面设置的Feature匹配得到的layoutResource与DecorView绑定,然后再把我们传进去的layoutResId添加到DecorView上,最后再添加一个onContentChange()的回调

二、 View的事件分发机制

用户的点击事件会被系统封装成一个类:MotionEvent,当MotionEvent产生后,就会在各层View之间传递,这个传递过程就是点击事件分发

其中,事件分发其主要作用的是三个方法:

  • dispatchTouchEvent() —— 进行事件分发
  • onInterceptTouchEvent() —— 进行事件拦截,在dispatchTouchEvent()中调用,但View没有提供这个方法(因为View一般就是最后一层,此时不必再对事件进行拦截,事件不会再继续传递下去)
  • onTouchEvent() —— 用来处理点击事件,在dispatchTouchEvent()中进行调用

1. View的事件分发机制

产生点击事件后,事件会从Activity ==> PhoneWindow ==> DecorView => ViewGroup ==> (...ViewGroup...) ==> View

事件就这样一层一层的从上往下传递,我们直接从ViewGroup的dispatchTouchEvent()开始看。

ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    //...
    // Handle an initial down.
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Throw away all previous state when starting a new touch gesture.
        // The framework may have dropped the up or cancel event for the previous gesture
        // due to an app switch, ANR, or some other state change.
        cancelAndClearTouchTargets(ev);
        //初始化之前的状态
        resetTouchState();
    }

    // Check for interception.
    final boolean intercepted;
    //这里使用了mFirstTouchTarget这个属性
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }
    //...
    return handled;
}

在dispatchTouchEvent()方法里,会先事件进行判断,看看是否为DOWN,如果是,则调用resetTouchState()方法。因为ACTION_DOWN事件是新事件的开始,所以需要调用resetTouchState()方法初始化之前的状态

private void resetTouchState() {
    clearTouchTargets();
    //...
}

private void clearTouchTargets() {
    TouchTarget target = mFirstTouchTarget;
    if (target != null) {
        do {
            TouchTarget next = target.next;
            target.recycle();
            target = next;
        } while (target != null);
        mFirstTouchTarget = null;
    }
}

这里会对mFirstTouchTarget进行赋null值的操作。而mFirstTouchTarget是用来标记当前ViewGroup是否拦截了事件,如果拦截:mFirstTouchTarget=null,如果不拦截并交给子View来处理:mFirstTouchTarget!=null

而mFirstTouchTarget在dispatchTouchEvent()方法中继续用来作为判断,==假设此时事件被拦截了==,那么mFirstTouchTarget != null为false,如果当前点击事件为ACTION_DOWN,那么就会就会调用onInterceptTouchEvent()方法,如果当前事件是ACTION_UPACTION_MOVE,那么就会直接执行intercepted = true;,此后的事件都将交由这个ViewGroup处理

//当事件被拦截时,即 mFirstTouchTarget != null 为false,此时只有ACTION_DOWN事件,才会去调用onInterceptTouchEvent()方法
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    //FLAG_DISALLOW_INTERCEPT:留给子View去禁止ViewGroup拦截
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent()(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
}  else {
    //事件被拦截时,ACTION_UP和ACTION_MOVE都将会进到这里
    intercepted = true;
}

 public boolean onInterceptTouchEvent(MotionEvent ev) {
     if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
         && ev.getAction() == MotionEvent.ACTION_DOWN
         && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
         && isOnScrollbarThumb(ev.getX(), ev.getY())) {
         return true;
     }
     //默认返回false
     return false;
 }

除了ViewGroup自己去拦截ACTION_UPACTION_MOVE事件外,子View还可以通过FLAG_DISALLOW_INTERCEPT标志位来禁止ViewGroup拦截ACTION_UPACTION_MOVE事件,途径就是通过子View调用requestDisallowInterceptTouchEvent()这个方法来改变标记位

onInterceptTouchEvent()方法默认返回false,不拦截事件,如果想要拦截,可以自定义ViewGroup重写这个方法

我们继续回到ViewGroup的dispatchTouchEvent()上面来,看看事件是如何被分发给子View的

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    //...
    final View[] children = mChildren;
    //遍历子元素(倒序遍历),从最上层的子View往内层遍历
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

        //判断子View是否能获取到点击事件
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                //子View能接收到点击事件点击事件,交由子View处理
                continue;
            }
            childWithAccessibilityFocus = null;
            //双重迭代
            i = childrenCount - 1;
        }

        //判断触摸点位置是否在子View的范围内,或者子View是否在播放动画
        if (!child.canReceivePointerEvents()
            || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            //该子View不符合上面两个条件,开始遍历下一个
            continue;
        }
    }
    //...

    resetCancelNextUpFlag(child);
    //dispatchTransformedTouchEvent()方法里面会对View的事件分发进行处理
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        //...
    }
    ev.setTargetAccessibilityFocus(false);
    //...
    return handled;
}


private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        //如果有子View,就去调用子View的dispatchTouchEvent()方法,如果没有子View,就去调用ViewGroup的父类——View的dispatchTouchEvent方法
        if (child == null) {
            //调用父类——View里面的dispatchTouchEvent()方法
            handled = super.dispatchTouchEvent(event);
        } else {
            //调用子View里面的dispatchTouchEvent()方法
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    //...
}

ViewGroup的dispatchTouchEvent()在遍历完children之后,就开始对时间进行分发,dispatchTransformedTouchEvent()里面就是对事件分发的处理,他会先去查看自己(ViewGroup)有没有子View,有就调用自己的子View去处理,没有就交给自己的父类——View处理,所以最终都是调用到了View去处理dispatchTouchEvent()

1)View的dispatchTouchEvent():

我们来看View.java里面的这个方法

public boolean dispatchTouchEvent(MotionEvent event) {
    
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        //把所有的监听事件封装成了一个对象,这里面存放了View所有Listener信息,如onTouchListener
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED  //是否是enable,如果View设置了android:enabled="false",就都不能执行了
                && li.mOnTouchListener.onTouch(this, event)) {  //如果你mOnTouchListener返回的是false,那么result就为false
            result = true;
        }
        //这里的onTouchEvent()能不能执行,完全取决于onTouch()方法的返回值,所以onTouch()方法的优先级大于onTouchEvent()
        if (!result && onTouchEvent(event)) {//如果result = false,就会执行onTouchEvent,如果result = true,就不会执行就会执行onTouchEvent
            result = true;
        }
    }
    //返回
    return result;
}

boolean isAccessibilityFocusedViewOrHost() {
    return isAccessibilityFocused() || (getViewRootImpl() != null && getViewRootImpl()
            .getAccessibilityFocusedHost() == this);
}

static class ListenerInfo {
    protected OnFocusChangeListener mOnFocusChangeListener;
    private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;
    protected OnScrollChangeListener mOnScrollChangeListener;
    private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;
    public OnClickListener mOnClickListener;
}

这个方法里面,其实就是利用 ==短路与== 的特性:当前面的条件不符合时,就不再判断后面的条件了,所以就通过这种方式,让enable属性控制mOnTouchListener方法的执行,和让result变量控制onTouchEvent的执行

onTouch()方法之前,会先判断View的enable属性,如果enable被设置了android:enabled="false",那么后面的onTouch()、onTouxhEvent()、onClick()都不会得到执行。

2)View的onTouchEvent():

我们看源码:

public boolean onTouchEvent(MotionEvent event) {
    //只要View的CLICKABLE和LONG_CLICKABLE有一个为true,clickable就会为true,那么onTouchEvent就会返回true,消耗这个事件
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();
    
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
                    performClickInternal();
                }
                break;
        }
         return true;
    }
    return false;
}

private boolean performClickInternal() {
    // Must notify autofill manager before performing the click actions to avoid scenarios where
    // the app has a click listener that changes the state of views the autofill service might
    // be interested on.
    notifyAutofillManagerOnClick();

    return performClick();
}

public boolean performClick() {
    //如果view设置了click事件,那么onClick()方法就会被执行
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        //调用点击事件
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    return result;
}

View的CLICKABLELONG_CLICKABLE可以通过setClickable()、setLongClickable()方法来设置,也可以通过View的setOnClickListener()、setOnLongClickListener()来设置,他们会自动把View设置为CLICKABLELONG_CLICKABLE

这里其实就解释了,为什么我们OnTouchListener里面返回false的时候,因为View的onClickListener是在OnTouch.UP后面才调用的

3)覆写onTouchEvent()

当onTouchEvent返回true后,这个方法就没有去调用super.onTouchEvent()方法,View内部的onTouchEvent()方法就不能得到执行,就不能去调用performClick()方法,那么li.mOnClickListener.onClick(this);就不能执行。所以onClickListener就不能得到执行

所以View内部的优先级:enable > onTouch > onTouchEvent > onClick

2. View事件分发的传递规则

上面的一连串过程,我们可以归纳为几行伪代码

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    //onInterceptTouchEvent默认返回false,如果我们自定义ViewGroup时,重写了这个方法,让他返回true,那么我们就能拦截点击事件
    if(onInterceptTouchEvent(ev)){
        //拦截点击事件后,开始在自己内部分发点击事件
        result = onTouchEvent();
    }else{
        //不拦截点击事件,交由子View去处理,这个步骤重复下去,最终会调用最底层的View,View是没有子View的,所以最后就会调用View的dispatchTouchEvent()方法,一般情况下,最终会调用最底层View的onTouchEvent()方法
        result = child.dispatchTouchEvent(ev);
    }
    return result;
}

事件从Activity ==> PhoneWindow => DecorView => ViewGroup ==> .... ==> View

如果最顶层的ViewGroup开始,一直不拦截事件,事件最终会传递到最底层的View上面去,由于底层View,没有子View了,所以会调用View的onTouch方法。这就是事件由上向下传递

如果底层的View不处理这个事件,就会让自己的onTouchEvent返回false,从这里开始,事件开始向上传递,如果中途的ViewGroup也不处理这个事件,最终就会传递到顶层的ViewGroup,由顶层的ViewGroup去处理这个事件。

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

推荐阅读更多精彩内容