【Android源码】View的事件分发

时间:2021-7-21 作者:qvyue

目录:

【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 mOnLayoutChangeListeners;
    protected OnScrollChangeListener mOnScrollChangeListener;
    private CopyOnWriteArrayList 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去处理这个事件。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。