Android 事件分发
Android 事件分发,指手指点击屏幕后,从 Activity、ViewGroup 到 View 的一系列过程。
简介
Android 系统的窗口机制如下图:
Activity 内有一个 Window 对象,其实现类是 PhoneWindow;
DecorView 为顶层 View,DecorView 是一个 FrameLayout,其中有 TitleView 和 ContentView;
TitleView 为标题栏,ContentView 就是平时在 Activity 的 onCreate() 方法中设置的视图,TitleView 可以用this.requestWindowFeature(Window.FEATURE_NO_TITLE);
隐藏掉,但是必须注意要在 setContentView() 之前,原因如下所示:
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
点击事件 Activity --> ViewGroup
点击事件发生后,首先被调用的是Activity.dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可以看到,其内部先调用了getWindow().superDispatchTouchEvent(ev)
这个方法,getWindow() 返回的 mWindow 是 PhoneWindow 的对象。
mWindow = new PhoneWindow(this, window, activityConfigCallback);
再看看 PhoneWindow.superDispatchTouchEvent() 方法,显然又调用了 DecorView 的 superDispatchTouchEvent() 方法,在该方法中,调用了 FrameLayout.dispatchKeyEvent(event),此时点击事件从 Activity 转到了 ViewGroup 中。
//PhoneWindow
@Override
public boolean superDispatchKeyEvent(KeyEvent event) {
return mDecor.superDispatchKeyEvent(event);
}
//DecorView extends FrameLayout
public boolean superDispatchKeyEvent(KeyEvent event) {
// Give priority to closing action modes if applicable.
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
final int action = event.getAction();
// Back cancels action modes first.
if (mPrimaryActionMode != null) {
if (action == KeyEvent.ACTION_UP) {
mPrimaryActionMode.finish();
}
return true;
}
}
return super.dispatchKeyEvent(event);
}
点击事件 ViewGroup --> View
ViewGroup 与事件分发的方法有三个:
dispatchTouchEvent()
分发事件,每次都会被调用onInterceptTouchEvent()
拦截事件,如果当前 ViewGroup 已经决定拦截事件,那么不会再调用onTouchEvent()
处理点击事件,如果设置了mOnTouchListener
的话,则不会回调本方法
这三个主要方法关系如下(伪代码,来自《Android 开发艺术探索》):
//每次点击事件回调该方法
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
var result = false
if (onInterceptTouchEvent(event)) {//viewGroup 会回调该方法,确认是否拦截点击事件
result = onTouchEvent(event)//对点击事件进行处理
} else {
result = child.dispatchTouchEvent(event)
}
}
return result
}
当 ViewGroup.dispatchTouchEvent() 被调用后,会通过一系列条件判断是由 ViewGroup 拦截该事件,还是由子 View 消耗该事件。
主要流程分为两部分
1.检查是否需要拦截
// http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/view/ViewGroup.java line2567-2582
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);//在这里调用了 onInterceptTouchEvent() 方法,如果已经拦截了
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;
}
...
}
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;
}
- 每次 ACTION_DOWN 事件都需要调用
onInterceptTouchEvent()
方法判断是否需要拦截 - 其他 MotionEvent 事件,如果有能处理点击事件的子 View(
mFirstTouchTarget != null
)且disallowIntercept
为 false 也需要调用onInterceptTouchEvent()
方法判断是否需要拦截,否则不需要拦截 - 其余情况都需要拦截(没有可以处理点击事件的子 View,并且不是 ACTION_DOWN 事件)
如果 ViewGroup 判断要拦截该事件,则会调用dispatchTransformedTouchEvent()
(后面会再讲到)通过他调用继承自 View 的dispatchTouchEvent(MotionEvent event)
方法:
// 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);
} else {
...
}
否则就需要遍历其子 View
2.遍历 ViewGroup 的所有子 View,寻找一个可以处理点击事件的子 View
dispatchTransformedTouchEvent()
调用了子 View 的dispatchTouchEvent()
addTouchTarget()
对mFirstTouchTarget
进行更新
public boolean dispatchTouchEvent(MotionEvent ev) {
// 1. Check for interception.判断是否需要拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//mFirstTouchTarget 表示能处理点击事件的子 View
//FLAG_DISALLOW_INTERCEPT 每次 ACTION_DOWN 都会被重置
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;
}
//2.遍历子 View,寻找可以处理点击事件的子 View
if (!canceled && !intercepted) {
for (int i = childrenCount - 1; i >= 0; i--) {
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
...
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
dispatchTransformedTouchEvent() 方法如下,由于child != null
其内部调用child.dispatchTouchEvent(event)
方法,如此循环直到子 View 是一个 View(单就 ViewGroup 和 View 而论)即将点击事件从 ViewGroup 分发到了 View。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
}
如果有子 View 可以处理点击事件,在addTouchTarget()
方法内部对mFirstTouchTarget
进行更新
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
点击事件 View 内部
View 的点击事件分发主要涉及到两个方法:
dispatchTouchEvent()
onTouchEvent()
其点击事件分发用伪代码表示如下:
public boolean dispatchTouchEvent(MotionEvent event) {
if(mListenerInfo.mOnTouchListener.onTouch(this, event)){
return true;
}else{
return onTouchEvent(event);
}
}
可见 View 的 dispatchTouchEvent() 方法中,如果 View 注册了 OnTouchListener 则会先执行mOnTouchListener.onTouch()
方法,如果该方法返回 false 才会执行onTouchEvent()
。
在看 onTouchEvent() 方法:
- 如果 View 处于不可用状态下,也会消耗点击事件,只不过没有反应
- 如果注册了 OnClickListener 会在 ACTION_UP 的时候调用
mOnClickListener.onClick(this)
public boolean onTouchEvent(MotionEvent event) {
...
if(CLICKABLE&&LONG_CLICKABLE){//LONG_CLICKABLE 默认为 false,CLICKABLE、LONG_CLICKABLE 会在设置点击事件时被设置为 true
switch (action) {
case MotionEvent.ACTION_UP:{
...
performClick();//如果注册了 OnClickListener 则会调用其 onClick() 方法
}
}
}
}
public boolean performClick() {
...
mListenerInfo.mOnClickListener.onClick(this);
...
}
总结
整个 Android 的时间分发始于 Activity,经过 PhoneWindow、DecorView 到达 ViewGroup,再逐层分发到 View 中。
如果底层没有处理点击事件,则又一层层向上返回,直到最顶层消耗掉点击事件。
参考资料
《Android 开发艺术探索》