Connor学Android - View事件分发机制源码解析

在这里插入图片描述

Learn && Live

虚度年华浮萍于世,勤学善思至死不渝

前言

Hey,欢迎阅读Connor学Android系列,这个系列记录了我的Android原理知识学习、复盘过程,欢迎各位大佬阅读斧正!原创不易,转载请注明出处:http://t.csdn.cn/W5DRa,话不多说我们马上开始!

1.Activity对点击事件的分发过程

点击事件用MotionEvent表示,当一个点击操作发生时,事件最先传递给当前的Activity,由Activity的dispatchTouchEvent派发事件,具体的工作则由Activity内部的Window完成,而Window会将时间进一步传递给DecorView,这个DecorView一般就是当前界面的底层容器(setContentView设置的View的父容器),可以通过Activity.getWindow().getDecorView()获得。

// Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // (1)
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

在注释(1)处,事件会交给Activity所附属的Window分发事件,如果返回true,则整个事件循环就结束了,返回false则说明所有的子View的onTouchEvent均返回了false,即事件没人处理,此时就会调用Activity的onTouchEvent

Window是一个抽象类,其superDispatchTouchEvent方法也是一个抽象方法。经查源码,PhoneWindow是Window的唯一实现类,实现了superDispatchTouchEvent方法

// PhoneWindow#superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    //mDecor是DecorView的实例
    return mDecor.superDispatchTouchEvent(event);
}

很明显是一个由Window向DecorView分发事件的过程,那么DecorView究竟是什么呢?

private final class DecorView 
    extends FrameLayout
    implements RootViewSurfaceTaker {
    private DecorView mDecor;
    
    @Override
    public final View getDecorView() {
        if(mDecor == null) {
            installDecor();
        }
        return mDecor;
    }
}

可以看出,DecorView继承自FrameLayout,而FrameLayout继承了ViewGroup。因此获取DecorView对象,再调用其父类的dispatchTouchEvent,实际上就是完成了Window向顶级View(ViewGroup)分发事件的过程

结论

当一个点击事件发生时,调用顺序如下

(1)事件最先传到Activity的dispatchTouchEvent()进行事件分发

(2)调用Window类实现类PhoneWindow的superDispatchTouchEvent()

(3)调用DecorView的superDispatchTouchEvent(),明确DecorView继承FrameLayout

(5)最终调用DecorView父类的dispatchTouchEvent(),即ViewGroup的dispatchTouchEvent()

2.顶级View对点击事件的分发过程

// ViewGroup#dispatchTouchEvent
// 发生ACTION_DOWN事件或者已经发生过ACTION_DOWN,并且将mFirstTouchTarget赋值,才进入此区域,主要功能是拦截器
final boolean intercepted;
// (1)
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    // (2)
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    //默认情况下会进入该方法
    if (!disallowIntercept) {
        // (3)
        intercepted = onInterceptTouchEvent(ev); 
        ev.setAction(action);
    } else {
        intercepted = false;
    }
} else {
    // 当没有触摸targets,且不是down事件时,开始持续拦截触摸。
    intercepted = true;
}

actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null

(1)mFirstTouchTarget用于指向ViewGroup的某一子元素,即当事件由ViewGroup的子元素成功处理时,会被赋值并指向子元素

(2)mFirstTouchTarget != null代表ViewGroup不拦截事件而是继续交由子元素处理

(3)actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null表示当ACTION_MOVE和ACTION_UP时间到来时,同时ViewGroup不拦截事件,则ViewGroup不会再调用onInterceptTouchEvent方法拦截后续事件

disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0

(1)FLAG_DISALLOW_INTERCEPT标记位由requestDisallowInterceptTouchEvent方法来设置,一般由子View设置。一旦设置,ViewGroup将无法拦截除ACTION_DOWN以外的其他点击事件(因为ACTION_DOWN会重置这个标志位,导致子View的设置失效)

(2)disallowIntercept表示是否禁用事件拦截的功能,默认是false,即不禁用

当ViewGroup不拦截事件时,事件会下发给当前ViewGroup的子View或ViewGroup处理

总结

当一个事件序列的ACTION_DOWN时间到来,ViewGroup调用onInterceptTouchEvent方法判断是否拦截

  • 若拦截,mFirstTouchTarget不赋值,继续处理,且不会再向子元素分发本次事件序列

  • 若不拦截则继续向子元素传递,子元素如果处理了事件,则ViewGroup不会再接收该事件

// (1)
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);
 	// (1)-1
  	if (childWithAccessibilityFocus != null) {
    	if (childWithAccessibilityFocus != child) {
        	continue;
     	}
     	childWithAccessibilityFocus = null;
     	i = childrenCount - 1;
  	}
    // (1)-2
  	if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
  	 	ev.setTargetAccessibilityFocus(false);
     	continue;
   	}
 
    // (2)
   	newTouchTarget = getTouchTarget(child);
   	if (newTouchTarget != null) {
       	newTouchTarget.pointerIdBits |= idBitsToAssign;
       	break;
    }
 
    // (3)
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // 获取TouchDown的时间点
        mLastTouchDownTime = ev.getDownTime();
        // 获取TouchDown的Index
        if (preorderedList != null) {
           	for (int j = 0; j < childrenCount; j++) {
               	if (children[childIndex] == mChildren[j]) {
                	mLastTouchDownIndex = j;
                    break;
                }
           }
         } else {
         	mLastTouchDownIndex = childIndex;
         }
 
    	//获取TouchDown的x,y坐标
    	mLastTouchDownX = ev.getX();
    	mLastTouchDownY = ev.getY();
    	//添加TouchTarget,则mFirstTouchTarget != null。
    	newTouchTarget = addTouchTarget(child, idBitsToAssign);
    	//表示以及分发给NewTouchTarget
    	alreadyDispatchedToNewTouchTarget = true;
    	break;
    }
}

(1)遍历ViewGroup的所有子元素,依次判断是否能够接收到点击事件,判断条件如下

  • 注释(1)-1,子元素是否获取了用户焦点
  • 注释(1)-2,子元素是否可见或点击事件的坐标是否落在子元素的区域内

(2)获取当前的TouchTarget(表示的意义与mFirstTouchTarget同理),若不为空,代表当前子元素接收了事件,结束遍历,否则继续

(3)当前View没有接收事件,则会调用dispatchTransformedTouchEvent(实际上就是dispatchTouchEvent)继续向下分发

  • 如果dispatchTransformedTouchEvent返回true,代表有子元素接收了事件,更新newTouchTarget(mFirstTouchTarget)
  • 如果dispatchTransformedTouchEvent返回false,则由子元素继续完成后续的分发操作
if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}

(4)如果遍历了所有子元素后事件都没有被合适地处理(ViewGroup没有子元素或子元素未处理事件),ViewGroup会自己处理事件

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

public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}

mOnTouchListener != null

public void setOnTouchListener(OnTouchListener l) { 
    mOnTouchListener = l;  
}

mOnTouchListener是在View类下setOnTouchListener方法里赋值的

即只要我们给控件设置了OnTouchListener,mOnTouchListener就一定被赋值(不为空)

(mViewFlags & ENABLED_MASK) == ENABLED

该条件是判断当前点击的控件是否enable。由于很多View默认是enable的,因此该条件恒定为true

mOnTouchListener.onTouch(this, event))

  • 如果在onTouch方法返回true,就会让上述三个条件全部成立,从而整个方法直接返回true。
  • 如果在onTouch方法里返回false,就会去执行onTouchEvent(event)方法。

下面看onTouchEvent(event)方法

public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;
    
    // (1)
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
    }
    
    // (2)
    if (mTouchDelegate != null) {  
        if (mTouchDelegate.onTouchEvent(event)) {  
            return true;  
        }  
    }
    
     // (3)
    if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }
                    if (!mHasPerformedLongPress) {
                        removeLongPressCallback();
                        if (!focusTaken) {
                            if (mPerformClick == null) {  
                                mPerformClick = new PerformClick();  
                            }  
                            if (!post(mPerformClick)) {
                                // (4)
                                performClick();
                            }
                        }
                    }
                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }
                    if (prepressed) {
                        mPrivateFlags |= PRESSED;  
                        refreshDrawableState();
                        postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration());  
                    } else if (!post(mUnsetPressedState)) {
                        mUnsetPressedState.run();
                    }
                    removeTapCallback();  
                }  
                break;  
            case MotionEvent.ACTION_DOWN:  
                ...
                break;  
            case MotionEvent.ACTION_CANCEL:  
                ...
                break;  
            case MotionEvent.ACTION_MOVE:  
                ...
                break;  
        }  
        return true;  
    }
    return false;  
}

(1)当View处于不可用状态(disable),依然会消耗事件。只要它的clickable或longClickable有一个为true,onTouchEvent就返回true

(2)如果View设置有代理,则还会执行TouchDelegate的onTouchEvent方法,这里暂不展开介绍

(3)只要View的clickable或longClickable有一个为true,就会消耗事件,分别对DOWN、MOVE、UP、CANCEL事件做具体的处理

(4)在处理ACTION_UP事件时,会触发performClick方法,如果View设置了OnClickListener,则会调用onClick方法

public boolean performClick() {  
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  
 	// (4)
    if (mOnClickListener != null) {  
        playSoundEffect(SoundEffectConstants.CLICK);  
        mOnClickListener.onClick(this);  
        return true;  
    }  
    return false;  
}

一点补充

(1)View的LONG_CLICKABLE属性默认为false,而CLICKABLE属性则与具体的View有关,可点击的为true,不可点击的为false

(2)通过setLongClickable、setClickable可分别改变上述两个属性

(3)setOnClickListener会自动将View的CLICKABLE设为true,setOnLongClickListener会自动将View的LONG_CLICKABLE设为true

public void setOnClickListener(OnClickListener l) {  
    if (!isClickable()) {  
        setClickable(true);  
    }  
    mOnClickListener = l;  
}

public void setOnLongClickListener(OnLongClickListener l) {
	if(!isLongClickable()) {
        setLongClickable(true);
    }
    getListenerInfo().mOnLongClickListener = l;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ConnorYan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值