Android事件分发体系-源码解析

一、点击事件的传递规则

所谓的点击事件的事件分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生以后,系统将这个事件传递给具体View的过程。 当一个点击事件产生后,他的传递过程遵循如下顺序: Activity->Window->View,即,事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View(即DecorView),顶级View接到事件后,就会按照事件分发机制去分发事件。 事件分发的三个重要方法:

//View#dispatchTouchEvent 
//用来进行事件分发,如果事件能够传递给当前View,那么改方法一定会被调用,
//返回结果受当前View的onTouchEvent和下级View的diapatchTouchEvent方法的影响,表示是否消耗当前事件;
public boolean dispatchTouchEvent(MotionEvent event)
//ViewGroup#onInterceptTouchEvent
//在dispatchTouchEvent 方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用。
//返回结果表示是否拦截当前事件
public boolean onInterceptTouchEvent(MotionEvent ev)
//View#onTouchEvent
//在dispatchTouchEvent 方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,
//如果不消耗,则在同一个事件序列中,当前View不会再次接收到事件。
public boolean onTouchEvent(MotionEvent event)

以上三个方法的关系可以通过下面的伪代码表示:

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

通过上面代码,可以清楚的了解到:

对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent返回true,就表示它要拦截当前事件,事件就会交给这个ViewGroup处理,接着就会调用onTouchEvent处理事件;

如果这个ViewGroup的onInterceptTouchEvent方法返回false,就表示它不拦截当前事件,这时事件就会继续传递给它的子元素,通过调用 子元素的.dispatchTouchEvent,分发给子元素,如此反复直到事件被处理。

注:如果View设置了OnTouchListener,其优先级比onTouchEvent高,onClickListener的优先级最低。 即onTouch方法先被调用,如果onTouch返回false,才会调用到onTouchEvent;如果View还设置了OnClickListener,onTouchEvent方法中还会调用到onClick。


二、事件分发源码解析

1、Activity事件分发源码解析

点击事件产生之后,首先传递到Activity的 dispatchTouchEvent ,先从Activity#dispatchTouchEven分析:

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        //这个方法是个空方法,
        //作用:当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法,
        //可以通过这个方法实现屏保的功能。
        onUserInteraction();
    }

    //分析1
    if (getWindow().superDispatchTouchEvent(ev)) {
        
        // 若getWindow().superDispatchTouchEvent(ev)的返回true
        // 则Activity.dispatchTouchEvent()就返回true,则方法结束。
        // 即 :该点击事件停止往下传递 & 事件传递过程结束
        // 否则:继续往下调用Activity.onTouchEvent
        return true;
    }
    //分析3
    return onTouchEvent(ev);
}

分析1:

getWindow().superDispatchTouchEvent(ev)即PhoneWindow#superDispatchTouchEvent

说明:

  1. getWindow() 获取Window类的对象
  2. Window类是抽象类,其唯一实现类是PhoneWindow类;即此处的Window类对象就是PhoneWindow类对象
  3. Window类的superDispatchTouchEvent() 是 抽象方法,由子类PhoneWindow类实现;

然后再看PhoneWindow#superDispatchTouchEvent(),如下:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    //分析2
    //mDecor = 顶层View(DecorView)的实例对象
    return mDecor.superDispatchTouchEvent(event);
}

分析2

mDecor.superDispatchTouchEvent(event)即DecorView.superDispatchTouchEvent

说明:

  1. DecorView是最顶层View
  2. DecorView继承自FrameLayout,是所有界面的父类
  3. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup

然后再看mDecor.superDispatchTouchEvent(event),

public boolean superDispatchTouchEvent(MotionEvent event) {
    //调用父类的方法=ViewGroup#dispatchTouchEvent(),将事件传递到ViewGroup去处理
    return super.dispatchTouchEvent(event);
}

最后调用到ViewGroup#dispatchTouchEvent,这里返回结果到最初调用处,即getWindgetWindow().superDispatchTouchEvent(ev);

分析3:

onTouchEvent(ev)即Activity#onTouchEvent(ev)

public boolean onTouchEvent(MotionEvent event) {
        // 当一个点击事件未被Activity下任何一个子View接收或处理时
        // 应用场景:处理发生在Window边界外的触摸事件
        // 分析4
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
        //只有在点击事件在Window边界外才会返回true,一般情况都返回false,分析完毕
    }

分析4:

mWindow.shouldCloseOnTouch(this, event)

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    // 主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
    if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
            && isOutOfBounds(context, event) && peekDecorView() != null) {
        return true;
    }
    return false;
    // 返回true:说明事件在边界外,即 消费事件
    // 返回false:未消费(默认)
}

总结一下:调用流程:

false

true

开始

Activity.dispatcherTouchEvent

getWindow.superDispatchTouchEvent

mDecor.superDispatchTouchEvent

Activity.dispatcherTouchEvent返回值=Activity.onTouchEvent

Activity.dispatcherTouchEvent返回true

Activity.onTouchEvent事件结束

事件分发结束

结束

2、顶级View的事件分发源码解析

从ViewGroup#dispatchTouchEvent开始,分段分析 ViewGroup#diapatchTouchEvent 代码片段2:

//这是代码片段2
// 是否拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
        //步骤1
            
    //mGroupFlags一般用于子View中,通过requestDisallowInterceptTouchEvent 方法重置
    //disallowIntercept这个值对于MotionEvent.ACTION_DOWN事件无影响,
    //ViewGroup 在 ACTION_DOWN 事件中会重置mGroupFlags;
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    //当disallowIntercept=false时,调用自己的onInterceptTouchEvent,询问是否需要拦截
    //disallowIntercept默认为false,只有当子View强制调用requestDisallowInterceptTouchEvent,
    //设置disallowIntercept为true时,执行4;
    if (!disallowIntercept) {
        //步骤3
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        //步骤4 不拦截
        intercepted = false;
    }
} else {
//步骤2
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}

// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
    ev.setTargetAccessibilityFocus(false);
}

说明:

1、当事件类型为MotionEvent.ACTION_DOWN或mFirstTouchTarget不为空时,拦截当前事件;

2、当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素;即:当ViewGroup不拦截事件,并且将事件交给子元素处理的时候,mFirstTouchTarget != null 成立; 反之,一旦事件交由 ViewGroup 拦截时,mFirstTouchTarget != null 就不成立

3、那么当 ACTION_MOVE 和 ACTION_UP 来的时候,由于 (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件返回的是 false,将导致 ViewGroup 的 onInterceptTouchEvent 不会被调用,并且同一序列中的其他事件都会交给它处理。

4、FLAG_DISALLOW_INTERCEPT标记位,是通过requestDisallowInterceptTouchEvent方法设置的,一般用于子View中;FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其它点击事件。

5、下面分析为什么是无法拦截除了ACTION_DOWN以外的点击事件,代码: ViewGroup#diapatchTouchEvent 代码片段1:

//这是代码片段1
//1执行完才会执行2,不清楚的看完整代码

//当事件为ACTION_DOWN时,重置FLAG_DISALLOW_INTERCEPT标记位
// 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);
    //重置mGroupFlags的状态
    resetTouchState();
}

这段代码在代码片段1之前执行;

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

接着来看当 ViewGroup 不拦截事件的时候,事件会分发交由他的子 View 来处理,如下代码:

ViewGroup#diapatchTouchEvent 代码片段3:

//这是代码片段3
final View[] children = mChildren;
//遍历ViewGroup的所有子元素
for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

    // If there is a view that has accessibility focus we want it
    // to get the event first and if not handled we will perform a
    // normal dispatch. We may do a double iteration but this is
    // safer given the timeframe.
      if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            continue;
        }
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;
    }
    //判断子元素是否能接收到点击事件 
    //衡量标准:1、子元素是否在播动画;1、点击事件的坐标是否在子元素的坐标区域内。
    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        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);
    //dispatchTransformedTouchEvent方法内,调用子元素的dispatchTouchEvent方法,成功把事件传递给子元素
    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;
    }

首先遍历 ViewGroup 的所有子元素,然后判断子元素是否能接收到点击事件,是否能接受点击事件主要由两点来衡量,一个是子元素是否在播放动画,一个是点击事件的坐标是否在子元素的做区域内。如果子元素满足这两个条件,那么事件就会交给它来处理。可以看到,dispatchTransformedTouchEvent 实际上调用的就是子元素的 dispatchTouchEvent 方法。在它的内部有如下一段代码,因为上边的代码中 child 传递的不为空,因此它会直接调用子元素的 dispatchTouchEvent 方法,这件事件就交给子元素处理了。从而完成一轮事件分发。

dispatchTransformedTouchEvent方法中的部分代码:

//ViewGroup#dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
 //......
 
 if (child == null) {
     //如果child为空的,
    handled = super.dispatchTouchEvent(transformedEvent);
 } else {
     //...........
    //true,则当前子View消耗此事件;false,则交给父View,父View继续轮询,看下一个子View要不要处理此事件;
    handled = child.dispatchTouchEvent(transformedEvent);
 }
 //........
}

如果子元素的 dispatchTouchEvent 返回 true(先不考虑事件在子元素内部的分发),那么 mFirstTouchTarget 会被赋值,同时跳出 for 循环,如下代码:

ViewGroup#diapatchTouchEvent 代码片段4:

//这是代码片段4
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break

这几行代码完成了对 mFirstTouchTarget 的赋值并终止了子元素的遍历,如果子元素的 dispatchTouchEvent 返回 false,ViewGroup 就会把事件分发给下一个子 View (如果还存在下一个子 View 的话)。

其实 mFirstTouchTarget 真正的赋值过程是在 addTouchTarget 方法的内部完成的,从下边 addTouchTarget 方法的内部结构可以看出, mFirstTouchTarget 其实是一种单链表结构,mFirstTouchTarget 是否被赋值,将直接影响到 ViewGroup 对事件的拦截策略,如果 mFirstTouchTarget == null,那么 ViewGroup 将默认拦截接下来同一序列中所有的事件。 下面看mFirstTouchTarget的赋值:

/**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

如果遍历所有的子元素后都没有找到合适的处理,这包含了两种情况:

  1. ViewGroup 没有子元素;
  2. 子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false,这一般是因为在 onTouchEvent 方法中返回了 false,ViewGroup 将事件分发给下一个子元素了。 在上边两种情况下,ViewGroup 会自己处理点击事件,代码如下:
//这是代码片段5
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    //这里的第三个参数是null,
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);

这里注意到第三个参数为null,也就是传递的View是空的,根据前面dispatchTransformedTouchEvent这个方法的实现,当view为null时,就转到了View的dispatchTouchEvent方法,即点击事件开始交由View处理;

3、View对点击事件的处理过程

从View#dispatchTouchEvent()方法开始:

public boolean dispatchTouchEvent(MotionEvent event) {
        //....
        //代码片段1
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            //当ACTION_DOWN事件到来时,停止动画
            stopNestedScroll();
        }
        
        //.....
    }
public boolean dispatchTouchEvent(MotionEvent event) {
        //....
        //代码片段1
        //....
        //代码片段2
       if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            
            //ListenerInfo为View的静态内部类,内部持有所有的监听事件的对象;
            if (li != null//如果由设置过任何一个监听,这个值都不会为null
            && li.mOnTouchListener != null//View调用setOnTouchListener()设置Touch监听
            && (mViewFlags & ENABLED_MASK) == ENABLED//View状态是enable
            && li.mOnTouchListener.onTouch(this, event)//返回true/false
            ) {
            //上面条件同时满足时,onTouch返回true
                result = true;
            }

            //如果事件被onTouch消费了,就不会再调用onTouchEvent方法了
            //onTouchEvent方法的调用与否取决于onTouch的返回值
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        //.....
    }

结论: onTouchListener的优先级高于onTouchEvent;也就是onTouc优先于比onTouchEvent调用;

下面看onTouchEvent实现:

public boolean onTouchEvent(MotionEvent event) {

//...


     //代码片段1
     //View处于不可用状态下,点击事件的处理
      final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

     if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        
    //.....
    
}

如果View是Clickable|Long_Clickable时,返回结果都为true,也就是View能够消费此事件,但是单击和长按事件不会执行。

接着,如果View设置有代理,那么还会执行TouchDelegate的onTouchEvent方法。

public boolean onTouchEvent(MotionEvent event) {

 //...
 //代码片段1
 //...
 //代码片段2
 if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
}

下面看onTouchEvent对点击事件的具体处理,

public boolean onTouchEvent(MotionEvent event) {

    //....
    //代码片段1
    //...
    //代码片段2
    //...
    //代码片段3
    //TOOLITIP可以看成LONG_CLICKABLE
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    
                    ...
                    
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        ...

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 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();
                                }
                            }
                        }
                        ...

                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    ...
                    break;

                case MotionEvent.ACTION_CANCEL:
                    ...
                    break;

                case MotionEvent.ACTION_MOVE:
                    ...
                    break;
            }

            return true;
        }
        //...
}

从代码看,只有View的CLICKABLE和LONG_CLICKABLE有一个为true,那么他就会消耗这个事件,即onTouchEvnet方法返回true,不管他是不是DISABLE状态。

然后就是当ACTION_UP事件发生时,会触发performClick方法,如果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);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

View的LONG_CLICKABLE属性默认是false,而CLICKABLE默认属性与具体View有关,确切说是可点击的View,其CLICKABLE默认为true,不可点击的View,其CLICKABLE默认为false; 通过setClickable和setLongClickable分别可以改变CLICKABLE和LONG_CLICKABLE的状态,且setOnClickListener会自动将View的CLICKABLE设为true,setOnLongClickListener会自动将LONG_CLICKABLE设为true;

源码略.......

到这里就结束了

总结:

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

  2. 正常情况下一个事件只能被一个View拦截且消耗,这一条的原因可以参考3,因为一旦一个元素拦截了某些事件,那么同一个事件序列内的所有事件都会直接交给他处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其它的View处理。

  3. 某个View一旦决定拦截,那么这一个事件序列只能由他处理(如果事件序列能够传递给他的话),并且它的onIntercepterTouchEvent不会被调用。

  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列中的其它事件都不会交给它来处理。并且事件将重新交由它的父元素处理,及父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,他们他就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它处理了。

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

  6. ViewGroup默认不拦截任何事件,源码中可查看默认返回false。

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

  8. View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false,clickable属性要分情况,比如Button的clickable属性默认为true,TextView的属性默认是false;

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

  10. onClick会发生的前提是当前View是可点击的,并且它收到了down和up事件。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值