android view的事件分发机制

1.点击事件的传递规则
所谓点击事件的事件分发,其实就是对MotionEvent事件的分发过程,当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的view,这个过程就是分发的过程。事件的分发过程是由三个方法共同完成的: DispathTouchEvent,onInterceptTouchEvent和onTouchEvent。

         1. public boolean dispatchTouchEvent(MotionEvent ev)
        用于进行事件的分发。如果事件能够传递给当前的view,那么此方法一定会被调用。返回结果受当前view的onTouchEvent和子view的DispathTouchEvent方法的影响

        2. public boolean onInterceptTouchEvent(MotionEvent ev)
        这个方法是在dispatchTouchEvent中调用,用来判断是否拦截某个事件, 如果当前view拦截了某个事件,那么在同一个事件序列当中,onInterceptTouchEvent这个方法不会再被调用,返回结果表示是否拦截当前事件

        3. public boolean onTouchEvent(MotionEvent event)
        此方法也是在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件, 如果不消耗的话,则在同一个事件序列中,当前view无法再次接受到事件

这里我们用一段伪代码表示一下上面3个方法之间的关系
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent()) {// 当前拦截了此事件
        consume = onTouchEvent();// 执行此view的onTouchEvent()方法
    } else {// 没有拦截
        consume = child.dispatchTouchEvent(MotionEvent ev);// 调用子view的dispatchTouchEvent()方法
    }
    return consume;
}

通过上面的伪代码,我们可以了解一下传递规则:对于一个viewgroup来说,点击事件发生后,首先会传递给dispatchTouchEvent方法,然后判断这个 viewgroup 的onInterceptTouchEvent是否拦截,返回true就表示它拦截了当前的事件,那么这个事件就会交给这个 viewgroup处理,既是它的onTouchEvent会被调用。如果返回false表示不拦截,那么这个事件就会传递给它的子view的dispatchTouchEvent,然后一直到这个事件被最终处理。

当一个view(这里是view)需要处理事件时,如果这个view设置了onTouchListener,那么 onTouchListener的onTouch方法就会被调用,如果onTouch返回false,那么这个view的onTouchEvent方法才会被调用,如果返回true, onTouchEvent就不会被调用。由此可见, onTouchListener的优先级比 onTouchEvent要高,然后onClickListener中的onclick方法是在 onTouchEvent中调用,onclick方法的优先级是最低的,处于事件传递的末尾(这个结论在下面分析源码中可以得到)。

当一个点击事件产生后,它的传递顺序是activity -> window -> view,顶级的view接受到事件后,就会按照事件分发机制去分发事件。这里考虑一个情况,如果一个view的onTouchEvent方法返回了false,那么事件会传递到view的父容器的 onTouchEvent将会被调用,然后依次类推。如果所有的元素都不处理这个事件的话,那么最总会传递给activity处理,即activity的 onTouchEvent会被调用 (这个结论在下面分析源码中可以得到)

关于事件传递的机制,这里给出一些结论,这些结论可以在下面的源码分析中得到证实:

1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕那一刻结束,在这个过程中所产生的一系列的事件,这个事件序列已down事件开始,中间含有数量不定的move事件,最终以up事件结束

2. 正常情况下一个事件序列只能被一个view拦截且消耗,因为一旦一个袁术拦截了某次事件,那么同一个事件序列内所有事件都会直接交给它处理,但是通过特殊手段可以做到,比如一个view将本来应该由自己处理的事件通过onTouchEvent强行传递给其他view处理。

3. 某个view一旦决定拦截,那么这一个事件序列都只能由它处理,并且它的onInterceptTouchEvent不会再被访问到,也就是说一旦这个view拦截了一个事件后,那么系统就会把同一个事件序列内的其他方法都直接交给这个view处理了,因此就不会再调用这个view的onInterceptTouchEvent去询问是否还要拦截了

4. 某个view一旦开始处理事件,如果它不消耗action_down这个事件(onTouchEvent 里面返回了false),那么同一事件序列的其它事件都不会交给它来处理,并且会把这个事件交给父元素处理,既是父元素的onTouchEvent会被调用。意思就是,事件一旦交给一个view处理,那么它就必须消耗掉,否则同一事件序列的其他事件就不会交给它处理了。

5. 如果view不消耗action_down以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前view可以持续收到后续的事件,最终这些消失的点击事件都会传递给activity处理。

6. viewgroup默认不拦截任何事件,默认返回false

7. view没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用

8. view的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longclickable同时为false)。view的longclickable默认是false,而clickable要分情况,比如button是true,textview是false。

9. view的enable属性不影响onTouchEvent的默认返回值。哪怕一个view是disable状态,只要它的clickable或者longclickable有一个为true,那么它的返回值就是true

10. onclick发生的前提是,view是可以点击的,并且它收到了up和down的事件

11.事件的传递是由外向内的,即事件总是先传递给父元素,然后再由父元素分发到子元素,但是可以通过requestDisallowInterceptTouchEvent方法在子元素中干预父元素的事件分发过程,但是在action_down事件除外

上面的这些结论可以在下面的源码分析得到证实



2.通过研究源码证实结论
2.1 activity对点击事件的分发过程
当一个点击操作发生时,事件最先传递当前的activity,由activity的DispathTouchEvent来进行事件的分发,具体工作是由activity内部的window来完成的。window会将事件传递给decor view,decor view 一般就是当前界面的底层容器(既是setContentView所设置的view的父容器),通过activity.getWindow.getDecorView()可以获得。

下面是activity的dispatchTouchEvent的源码:
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
首先事件会交给window进行分发,如果返回的是true,那么整个事件循环就结束了,如果返回false就意味着没人处理,那么就会交给activity的onTouchev处理

接下来我们来看, window是如何把事件传递给viewgroup的
通过源码我们知道window是一个抽象类,而superDispatchTouchEvent是一个抽象方法,所以我们要找到window的实现类。然而window的实现类是phonewindow,我们查看phonewindow的superDispatchTouchEvent方法
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
这里的mDecor是一个DecorView
@Override
public final View getDecorView() {
    if (mDecor == null) {
        installDecor();
    }
    return mDecor;
}
我们知道((ViewGroup)(getWindow().getDecorView().findViewById(android.R.id.content))).getChildAt(0);可以获取到setContentView设置的view,然而这个mDecor就是getWindow().getDecorView()返回的view,所以我们setContentView设置view的父容器就是mDecor。DecorView继承至framelayout且是父view,所以最终事件都会传递给view

2.2 顶级view对点击事件的分发过程
这里大致回顾一下事件分发:点击事件到达了顶部的view(一般是一个viewgroup)以后,会调用这个viewgroup的DispathTouchEvent,然后如果这个viewgroup拦截了这个事件(即onInterceptTouchEvent返回true),则事件由viewgroup处理,这时如果viewgroup的onTouchListener被设置了,则onTouch会被调用,那么这时需要看onTouch返回值来判断onTouchEvent是否会被执行。如果viewgroup不拦截这个事件,那么会传递给子view,调用子view的DispathTouchEvent,然后子view的执行和父view是一样的。

我们先看 viewgroup(注意这里是viewgroup)的的dispathTouchEvent方法的一个片段
final boolean intercepted;
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;
}
viewgroup要想走到拦截方法onInterceptTouchEvent那里,要经过两个判断,if (actionMasked == MotionEvent.ACTION_DOWN  || mFirstTouchTarget != null),actionMasked == MotionEvent.ACTION_DOWN好理解,这个mFirstTouchTarget != null是什么意思呢?这个可以在dispathTouchEvent后面的代码找到作用,现在来说就是, 当事件被viewgroup拦击了,mFirstTouchTarget为null,当viewgroup不拦截,mFirstTouchTarget不为null。然后当action_move和action_up事件到来时,由于if (actionMasked == MotionEvent.ACTION_DOWN  || mFirstTouchTarget != null)这个条件为false,将导致viewg的onInterceptTouchEvent并不会被调用到,并且同一序列的其他事件都会交给它处理的。

当然这里面还有一种特殊情况,那就是 FLAG_DISALLOW_INTERCEPT,这个标记为是通过 requestDisallowInterceptTouchEvent方法来设置的,一般用于子view。这个标记一旦被子view设置了后,viewgroup就无法再拦截除了action_down以外的事件了。为什么除了action_down事件呢,因为如果是action_down事件的话,viewgroup会重置FLAG_DISALLOW_INTERCEPT的值。
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();
}
在resetTouchState中改变了重置了这个值。因此,当面对action_down时,viewgroup总会询问是否拦截这个事件。

我们在这里可以得到上面 第3条的结论:当viewgroup决定拦截事件后,那么系统就会把同一个事件序列内的其他方法都直接交给这个view处理了,就不会再调用这个view的onInterceptTouchEvent去询问是否还要拦截了。还有可以证实 第11条结论:requestDisallowInterceptTouchEvent方法在子元素中干预父元素的事件分发过程,但是在action_down事件除外。

如果当前这个viewgroup拦截了action_down事件,那么mFirstTouchTarget为null,接下来的move和up事件再来传递,if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件为false,就不用再走onInterceptTouchEvent方法了,直接在viewgroup的onTouchEvent中执行了。
FLAG_DISALLOW_INTERCEPT这个标签起作用的前提是,拦截了action_down这个事件


接下来我们看viewgroup不拦截的情况下,事件会向下分发事件,分发给它的ziview
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = customOrder
            ? getChildDrawingOrder(childrenCount, i) : i;
    final View child = (preorderedList == null)
            ? children[childIndex] : preorderedList.get(childIndex);
    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        continue;
    }

    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // Give it the new pointer in addition to the ones it is handling.
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }

    resetCancelNextUpFlag(child);
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // Child wants to receive touch within its bounds.
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
            // childIndex points into presorted list, find original index
            for (int j = 0; j < childrenCount; j++) {
                if (children[childIndex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                }
            }
        } else {
            mLastTouchDownIndex = childIndex;
        }
        mLastTouchDownX = ev.getX();
        mLastTouchDownY = ev.getY();
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }
}
首先for循环遍历了所有的子元素,然后判断子元素是否能接受到点击事件,如果能接受到点击事件,那么这个事件就会传递给它。dispatchTransformedTouchEvent方法实际上就是调用子view的dispatchTouchEvent,dispatchTransformedTouchEvent内部有一段代码:
if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}
如果child不为null,就调用子view的dispatchTouchEvent方法。如果子view的dispatchTouchEvent返回true的话,mFirstTouchTarget会被赋值(在addTouchTarget中赋值),然后跳出循环。
如果mFirstTouchTarget为null,那么viewgroup就会默认拦截同一序列中所有的点击事件(if (actionMasked == MotionEvent.ACTION_DOWN  || mFirstTouchTarget != null) )。
如果遍历了所有子元素后,事件都没有被处理,那么就分两种情况:第一种没有子元素,第二种子元素处理了点击事件,但是dispatchTouchEvent返回了false,这种情况一般是onTouchEvent返回false,
在这两种情况下,viewgroup会自己处理点击事件,这里证实了 第4条结论
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}
从上面代码看到,如果子view都不处理调事件,那么mFirstTouchTarget为null,然后dispatchTransformedTouchEvent方法执行, 注意第三个参数传入的是null,如果是null的话,就会转到view的dispatchTouchEvent方法中,点击事件就交给了view处理( 这里是view,不是viewgroup


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

.....

    return result;
}
因为view是一个单独的元素,没有子元素,没有onInterceptTouchEvent方法。系统会先判断有没有设置onTouchListener,如果onTouch方法返回true,那么onTouchEvent不会被调用。

然后我们再看onTouchEvent方法,先看一个片段:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn't respond to them.
    return (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
不可用状态下的view照样会消耗点击事件

继续看源码
if (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_UP:
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

...

                if (!mHasPerformedLongPress) {
                    // This is a tap, so remove the longpress check
                    removeLongPressCallback();

                    // Only perform take click actions if we were in the pressed state
                    if (!focusTaken) {
                        // Use a Runnable and post this rather than calling
                        // performClick directly. This lets other visual state
                        // of the view update before click actions start.
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            performClick();
                        }
                    }
                }

...

                removeTapCallback();
            }
            break;
     }
...
     return true;
}
从代码看出来,只要view的clickable和longclickable中有一个为true,就会消耗此事件,最后会看到return true,代表消耗事件,这里可以证实了 8、9、10结论。然后如果view设置OnClickListener,那么performClick方法内部就会调用它的onclick方法。
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;
}
然后通过setOnclickListener方法可以将view的clickable设置为true,textview默认是false,button默认是true

到这里,点击事件的分发机制的源码就分析完了,上面的结论也得到了证实
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值