Android事件分发


最近看了《Android开发艺术探索》,记下笔记

Android事件类型

事件类型主要ACTION_DOWN、ACTION_MOVE、ACTION_UP三种,一个事件序列,包含了一个ACTION_DOWN、若干个ACTION_MOVE和一个ACTION_UP。

事件传递的三个重要方法

public boolean dispatchTouchEvent(MotionEvent event)
用来事件分发,返回true,则表示消耗当前事件,返回值受当前view的onTouchEvent和子View的dispatchTouchEvent影响
public boolean onInterceptTouchEvent(MotionEvent ev)
在上诉方法内部调用,表示是否拦截事件,这个方法只在ViwGroup存在;如果返回true,则事件部分发给子View,事件会交给自身的onTouchEvent()处理
public boolean onTouchEvent(MotionEvent event)
表示事件是否消耗,返回true,表示当前view消耗事件,返回false,则事件交给父视图的onTouchEvent()处理
以下伪代码可以形象的表现三者的关系:

public boolean dispatchTouchEvent(MotionEvent ev) {
	boolean dis = false;
	if(onInterceptTouchEvent(ev)){
		dis = onTouchEvent(ev);
	}else{
		dis = child.dispatchTouchEvent(ev);
	}
	return dis;
}

事件传递的结论

从Android开发艺术探索书中摘录整理了一些结论:

  1. 某个view一旦决定拦截,那么这个事件序列都交给它来处理,它的onInterceptTouchEvent不会再调用
  2. 事件传递由dispatchTouchEvent开始,事件会按照嵌套层次由外向内传递,传递到最内层view时,交由它的onTouchEvent处理,该方法如果消费该事件,则返回true,如果不消费则返回false,这时事件会向外层传递,交给它的父视图的onTouchEvent处理,以此类推
  3. viewGroup默认不拦截任何事件,源码中onInterceptTouchEvent默认返回false
  4. view的onTouchEvent默认都会消耗事件(返回true),除非他是不可点击的(clickable和longClickable同时为false)view的longClickable默认都为false,clickable要看情况,比如Button默认为true,TextView默认为false
  5. view的事件触发顺序是先执行onTouch方法再执行onTouchEvent再执行onClick方法,如果onTouch返回true,则onClick不会调用
  6. Activity中事件的传递顺序是Activity—>Window—>根View—>布局对应的ViewGroup—>View

ViewGroup中事件的传递

先看ViewGroup的dispatchTouchEvent方法

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            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 {
                intercepted = true;
            }

由上面代码可以看出,有两种情况会判断是否拦截当前事件(调用onInterceptTouchEvent):事件类型为ACTION_DOWN或者mFirstTouchTarget != null。那么mFirstTouchTarget 啥时候不为null呢,答案是当前事件不拦截,事件交给子view时。那么反过来,如果当前view拦截事件,则mFirstTouchTarget为null,后续事件为MOVE和UP事件,则actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null都为false,这也印证了第1条结论:当前view拦截当前事件,则事件序列后续的其他事件都交给处理,onInterceptTouchEvent不会再调用。
再看onInterceptTouchEvent调用前的一个判断,事件拦截方法还受FLAG_DISALLOW_INTERCEPT标志位影响,这个标志位可以通过requestDisallowInterceptTouchEvent方法修改,这个方法可以用来处理滑动冲突。
viewGroup不拦截事件,事件会交给他的子View处理,继续看它的dispatchTouchEvent方法:

if (!canceled && !intercepted) {

...
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);


                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {								
                                    //****判断view能否接受到点击事件,不能就取下一个view判断
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                //****如果子view的dispatchTouchEvent()返回true,addTouchTarget中会对mFirstTouchTarget进行赋值,并跳出循环
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                
}

代码做了精简,由代码可以看出这部分逻辑:

  1. 如果ViewGroup不拦截事件,则先遍历了它的子元素,然后判断子元素是否能获取到点击事件,衡量标准是:子元素是否在播放动画和点击事件的坐标是否在子元素区域内(具体逻辑可以看canViewReceivePointerEvents和isTransformedTouchPointInView的实现)。
  2. 子元素可以接收到点击事件,则调用dispatchTransformedTouchEvent,注意第三个参数child不为null,这个方法实际就是调用了子View的dispatchTouchEvent,至此事件被分发给子View.
    看下dispatchTransformedTouchEvent的具体实现
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            boolean handled;
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            return handled;

如果子view的dispatchTouchEvent返回true,则dispatchTransformedTouchEvent返回true,我们继续回到viewGroup事件不拦截处理逻辑中看,我只把核心逻辑摘抄出来:

 for (int i = childrenCount - 1; i >= 0; i--) {

       if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
           newTouchTarget = addTouchTarget(child, idBitsToAssign);
           //****如果子view的dispatchTouchEvent()返回true,addTouchTarget中会对mFirstTouchTarget进行赋值,并跳出循环
           alreadyDispatchedToNewTouchTarget = true;
           break;
        }
}                            

dispatchTransformedTouchEvent返回为true,会调用addTouchTarget方法,他里面干了啥呢?我们看看他的具体实现:

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

由上面代码我们可以得出结论,事件不拦截时,会遍历viewGroup中的元素,如果某个子view答应处理,即dispatchTouchEvent返回true,则跳出循环,此时事件已经交给子view处理,子view再执行这样的分发逻辑,从而完成事件由外向内分发;第二点当前事件不拦截时mFirstTouchTarget !=null,拦截时,mFirstTouchTarget ==null,不再调用onInterceptTouchEvent方法。
继续往下看,在viewGroup遍历子view逻辑后有如下判断逻辑:

 if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} 

该代码表示遍历完所有子元素,事件都没有被处理(ViewGroup没有子元素或子元素都不处理),则再次调用dispatchTransformedTouchEvent方法,注意此时该方法的第三个参数为null,由dispatchTransformedTouchEvent内部实现可知,此时会调用super.dispatchTouchEvent(event);即事件交给view来处理。由此我们可以得出结论:1、事件由外层向内层通过dispatchTransformedTouchEvent分发,如果内层的dispatchTransformedTouchEvent返回true,则代表内层view消耗事件;如果viewGroup没有内层view或内层所有子view都不处理事件,则事件由他自己处理。这就像生活中经理有个任务需要处理,先交给主管,主管再交给组员处理,如果组员能处理,则主管就不用处理了,如果组员处理不了,那就只能主管再处理,如果主管也处理不了,就只能经理自己处理了。还有一种可能,经理下面没有人,那任务分发不出去,只能经理自己处理了。
至此,ViewGroup的分发和拦截逻辑已经分析完,接下来看view的dispatchTouchEvent和onTouchEvent方法

View中事件的传递

从view的事件分发开始看起

public boolean dispatchTouchEvent(MotionEvent 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;
            }
}

view的事件分发就简单很多,由于view不是一个容器,因此没有子元素,不存在事件的向内传递,事件需要它自己处理。我们看到它首先判断有没有mOnTouchListener ,如果由则调用mOnTouchListener #onTouch方法,而onTouch方法返回true,则result=true,则onTouchEvent不会调用,由此我们得出结论:onTouch优先级高于onTouchEvent。
接着看onTouchEvent的实现:

public boolean onTouchEvent(MotionEvent event) {
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
       if (clickable) {
            switch (action) {
                case MotionEvent.ACTION_UP:
               ...
			if (!post(mPerformClick)) {
               performClickInternal();
              }
               ...
            break;  
            ...
            return true}

private boolean performClickInternal() {
        ...

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

        return result;
}

由上面代码可知,只要view的CLICKABLE和LONG_CLICKABLE有一个为true,那么他就会消耗这个事件,即onTouchEvent返回true,不管他是不是DISABL状态。然后就是当ACTION_UP事件发生时会触发performClick,如果view设置了OnClickListener,那么最终会调用它的onClick方法。结合前面的结论,我们就可以印证第4、5条结论。到这里事件的分发机制源码已经分析完了。

链接:
解决这 8 个问题,Android事件分发再往前一步
Android View的事件分发机制和滑动冲突解决方案
Android事件分发机制详解:史上最全面、最易懂

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值