Android的View事件分发机制原理

1. WindowManagerService

WindowManagerService是一个独立的进程,拥有自己的main方法。它的内部有多个List用于存放各种各样状态的Activity。

WindowManagerService接收到屏幕的点击事件后,就会分发给其内部正在显示的activity,这个就是activity的点击事件被分发的基本原理。

总之Acitivity的dispatchTouchEvent就是这样被调用的,而MotionEvent也是WindowManagerService生成的。

2. Activity和Window

事件分发机制的入口是Acitivity类的dispatchTouchEvent,他会调用内部的mWindow来继续分发事件。

//Acitivity的
public class Activity{
	@UnsupportedAppUsage
    private Window mWindow;
    
    public boolean dispatchTouchEvent(MotionEvent ev) {
    	// 不重要,忽略
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        
        // 不要关注别的,主要关注这行代码
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        
        return onTouchEvent(ev);
    }
    
	public Window getWindow() {
        return mWindow;
    }
}

那这个Window又是什么呢?简单介绍一下。Window本身是一个抽象类,他的实例是phoneWindow对象,说白了就是每个Activity都会持有一个phoneWindow对象。

public abstract class Window {}
public class PhoneWindow extends Window implements MenuBuilder.Callback {}

注意,Window是一个抽象类,而PhoneWindow继承了Window,所以其实Window类并没有多少View相关的方法(因为没有继承View类)。

但是PhoneWindow内部有一个DecorView。DecorView中有两个View,TitleView和ContentView。TtitleView就是我们平时一直隐藏的actionBar,而ContentView就是我们平时写的xml文件,还记不记得我们每个Activity都一定会写的setContentView,实际上设置的就是DecorView的contentView。

最后再看一下事件从Activity分发到PhoneWindow后又做了些什么

public class PhoneWindow extends Window implements MenuBuilder.Callback {
	private DecorView mDecor;
	
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
}

传到了DecorView,再看看DecorView中做了什么

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {

	
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        
        // 源码中是一行长代码,为了方便阅读我把它分开了
        // 这部分是有关Window的回调,暂时不关注
        if(cb != null && !mWindow.isDestroyed() && mFeatureId < 0)
        	return cb.dispatchTouchEvent(ev);
        	
        // 重点关注这行
       	return super.dispatchTouEvent(ev);
    }
}

最终是调用了父类的dispatchTouchEvent,我们都知道FrameLayout本身就是ViewGroup的一种,接下来就要看View和ViewGroup的事件是如何分发的。

总结

  1. Activity内部持有一个PhoneWindow,而PhoneWindow又持有一个DecorView,DevorView其实就是ActionBar加上我们的ContentView。
  2. 事件分发流程是Activity-> PhoneWindow-> DecorView。
  3. DecorView本质是FrameLayout,即ViewGroup。

3. ViewGroup

刚才也说了,DecorView的本质是一个ViewGroup,这意味着事件分发本质上只有三个类参与,Activity,ViewGroup和View。

事件分发,只有存在可以分发的对象,才能把事件分发出去,View本身就是一个控件,已经是一个个体了,所以无法执行分发的这个操作。
ViewGroup是控件组,他的内部有子控件,存在分发事件的对象,所以ViewGroup可以执行分发的这个行为。

分发主要是通过dispatchTouchEvent这个方法,先看一下:

public boolean dispatchTouchEvent(MotionEvent ev) {

	boolean handled = false; // 这个变量表示事件是否被处理
	final int actionMasked = action & MotionEvent.ACTION_MASK; // 表示事件的类型
	
	......
	
	return handled;
}

顺便介绍一下事件的不同类型:

MotionEvent.ACTION_DOWN按下View(所有事件的开始)
MotionEvent.ACTION_UP抬起View(与DOWN对应)
MotionEvent.ACTION_MOVE滑动View
MotionEvent.ACTION_CANCEL结束事件(非人为原因)

每当我们开始分发事件的时候,先要判断事件是否被拦截,是否拦截主要根据onInterceptTouchEvent方法和disallowIntercept共同决定。如果不拦截事件,就开始分发事件内容。

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); 
        } else { // 如果启用了禁用拦截功能,就不会拦截该点击事件
            intercepted = false;
        }
    } else { 
        intercepted = true;
    }

	if (!canceled && !intercepted) {
		.....
		分发点击事件
		......
	}

	......其他代码......
}

/**
 * 这个方法,返回true的条件太多,一般可以看成返回false
 * 也就是说,一般情况下,ViewGroup不拦截事件
 * 如果用户有特别需求,需要拦截事件,就重写该方法,然后返回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;
    }
    return false;
}

事件分发的本质分为两种,一种是当前ViewGroup中,有可以被分发事件的子控件,事件就分发给它;另一种是事件无法被任何子控件接收,ViewGroup就自己处理事件。


public boolean dispatchTouchEvent(MotionEvent ev) {

	......省略前面那一段判断intercepted的代码......
		
	if (!canceled && !intercepted) {
		......这里会省略一些我认为不重要的代码......
		
		final int childrenCount = mChildrenCount;
		if (childrenCount != 0) {
			 final View[] children = mChildren;
			 // 注意,这个地方是倒序获取子View
			 for (int i = childrenCount - 1; i >= 0; i--) {
			 	// 下面这两行,不要去管他具体是怎么实现的,你就知道他是获取子View的下标和实例就行了
			 	final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
				// 第一个是判断child是否能接收事件,第二个是判断点击事件是否在子View的范围内,两个条件不满足任意一个,就跳过该子View			
				// 这里不要去管这两个方法是怎么实现的,我们根据方法名能知道方法的作用就好了
				if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) 
                	continue;
                
                // 分发转换为点击事件
				if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
					// 此处和源码部分不符,我有意省略了TouchTarget相关的内容,我认为关联性不强,但是逻辑应该是对的
				    handled = true;
				}
			 }
		}
	}

	
    if (mFirstTouchTarget == null) {
        // 分发转为点击事件,但是这里子View传的是空
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
        }
	return handled;
}

// 重点关注第三个参数,View child
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
        	// 当传入的child为空时,该View就会自己处理这个事件
            handled = super.dispatchTouchEvent(event);
        } else {
        	// 否则就让子View来处理这个事件
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    
    .......省略一大段.......
}

最后一段的内容是这样,当我们的事件没有被拦截时,就会遍历这个ViewGroup所持有的子View,判断到某个View如果在事件点击的范围内时,就调用dispatchTransformedTouchEvent这个方法,将该子View作为参数传进去。

这里要注意,对于遍历的方式,ViewGroup是倒着遍历他的子View集合的,原因是。每当我们往ViewGroup中添加View时,后添加的View总是会显示在屏幕的上方从而盖住先添加的view,而用户点击到这个地方时,他肯定是想点击他所看到的控件,也就是后添加的View,所以这里需要用倒着的顺序遍历。
在这里插入图片描述

如果遍历完所有的子View,还是没能处理这个点击事件(比如说这个事件点击到了该ViewGroup的空白地点,即上图中白色部分),还是调用dispatchTransformedTouchEvent这个方法,此时child传空

再来看看dispatchTransformedTouchEvent这个方法,当我们传入的child为空时,就会调用super.dispatchTouchEvent(event);
即ViewGroup的父类View的dispatchTouchEvent;而传入的child不为空时,就调用child的dispatchTouchEvent(event);

ViewGroup总结

最后总结一下ViewGroup的事件分发流程

  1. dispatchTouchEvent:判断该事件是否被拦截,如果被拦截,由该ViewGroup自己处理事件
  2. dispatchTouchEvent:没有被拦截,倒序遍历所有的子View,选择处于点击范围内的子View,让他处理该事件
  3. dispatchTouchEvent:如果所有子View都不能处理该事件,由该ViewGroup自己处理事件
  4. dispatchTransformedTouchEvent:处理事件时调用的方法,如果child变量传null,则调用该ViewGroup父类View的dispatchTouchEvent(即自己处理点击事件);child不为空,调用child的dispatchTouchEvent(即子View处理点击事件)。
  5. onInterceptTouchEvent:判断是否拦截事件,一般返回false。如果有特殊需要重写该方法。

4. View

最后看看View在事件分发过程中做了什么,按照道理来说,只有持有子控件的ViewGroup,才能将事件分发给别人。View本身是一个单独的控件,并不存在可以分发事件的对象。

从刚刚ViewGroup的源码中,我们知道了View的事件分发也是从dispatchTouchEvent开始。

public boolean dispatchTouchEvent(MotionEvent event) {
	boolean result = false;
	// 这个条件你就默认他为true就行了
 	if (onFilterTouchEventForSecurity(event)) {
        ListenerInfo li = mListenerInfo;
        // 注意看li.mOnTouchListener.onTouch(this, event))这个条件,他会调用onTouch方法,这个方法是一个接口
        // 我们如果有设置touch监听事件的话,就会调用这个方法
        if (li != null 
        		&& li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

		// 如果没有TouchListener监听事件或者onTouch没有返回true时就执行onTouchEvent(event)
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    
    return result;
}

dispatchTouchEvent主要做了两件事,他会先检测该View有没有设置TouchListener接口和onTouch方法,如果有就执行。

然后如果TouchListener.onTouch方法返回false,就执行onTouchEvent方法。

public boolean onTouchEvent(MotionEvent event) {
	
	final int action = event.getAction();
	// 看变量名就知道,是检测是否可以点击的变量
	final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

	if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
					......
                    performClickInternal();
					......
                    break;
                case MotionEvent.ACTION_DOWN:
					.......
                    break;
                case MotionEvent.ACTION_CANCEL:
					......
                    break;
                case MotionEvent.ACTION_MOVE:
                  	.......
                    break;
            }
            return true;
        }
        return false;
    }
}

onTouchEvent就是处理点击事件,如果该事件可以点击就一定返回true,否则一定返回false。

然后就是根据不同的事件类型进行处理,最后单独列出performClickInternal()这个方法,这个方法最终会调用到OnClickLisener.onClick这个方法,这就表示。我们平时的单击控件,实际上是在手指抬起来之后,才处理的。

参考材料

Android事件分发机制详解:史上最全面、最易懂 - 简书
https://www.jianshu.com/p/38015afcdb58
Activity 与 Window、PhoneWindow、DecorView 之间的关系详解_Chin_Style的博客-CSDN博客_phonewindow
https://blog.csdn.net/weixin_41101173/article/details/79685305
码牛学院VIP课程 2020-7-12 事件分发机制

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值