总述
事件分发是将点击事件(
MotionEvent)传递到某个具体的View/ViewGroup然后被处理的过程。事件大致传递的流程是从Activity -> ViewGroup -> View。
其中事件类型MotionEvent分为以下几种类型,
| 事件类型 | 具体动作 |
|---|---|
MotionEvent.ACTION_DOWN |
按下手指(所有事件的开始) |
MotionEvent.ACTION_UP |
抬起手指 |
MotionEvent.ACTION_MOVE |
滑动手指 |
MotionEvent.ACTION_CANCEL |
非人为的原因结束事件 |
事件分发涉及到的核心方法,
| 方法 | 作用 | 被调用的地方 |
|---|---|---|
dispatchTouchEvent |
分发事件 | 点击事件能够传递给当前View时,该方法被调用 |
onTouchEvent |
处理点击事件 | dispatchTouchEvent()内部被调用 |
onInterceptTouchEvent |
判断是否拦截该点击事件(ViewGroup独有方法) |
在ViewGroup.dispatchTouchEvent()中被调用 |
源码分析
Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//getWindow().superDispatchTouchEvent(ev)方法最终会传递到ViewGroup中的dispatchTouchEvent里
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//Activity中这个方法无论分发按键事件、触摸事件或者轨迹球事件都会被调用
//可以复写它做一些特殊需求,例如长时间用户未点击则应用锁屏
public void onUserInteraction() {
}
//这里顺带提一下onUserLeaveHint方法,在用户手动离开当前Activity时,会调用该方法
//比如用户主动切换任务,短按home进入桌面等。系统自动切换activity不会调用此方法,如来电,灭屏等。
//应用场景是可以做统一的监听,比如要监听用户点了Home键跳回到桌面后,APP需要自动跳转到解锁界面,我们可以在这里实现逻辑
protected void onUserLeaveHint() {
}
//mWindow.shouldCloseOnTouch用于判断触摸事件是否滑出了屏幕以外,
//在窗口范围外事件是没法被被接受的,该事件没有视图可以接收,所以返回true消费掉
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
ViewGroup
/**
* ViewGroup.dispatchTouchEvent
* 简化过的代码
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// 调用onInterceptTouchEvent()询问是否拦截事件
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// disallowIntercept 表示 是否禁用事件拦截的功能(默认为false)
// 可通过requestDisallowInterceptTouchEvent()方法修改
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 遍历当前ViewGroup下的所有子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
// 判断当前遍历的View是否是被点击的View,从而找到当前被点击的View
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 内部调用了该View的dispatchTouchEvent()
// 从而实现了事件从ViewGroup到子View的传递
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
// 调用子View的dispatchTouchEvent后是有返回值的
// 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
// 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
// 即将ViewGroup的点击事件拦截掉
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
// 若点击的是空白处(即无任何View接收事件)
// 拦截事件(手动复写onInterceptTouchEvent(),从而让其返回true)
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
// 调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
// 因此会执行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick()
}
}
/**
* ViewGroup.onInterceptTouchEvent()
* 作用:是否拦截事件
* 说明:
* 返回true = 拦截,事件停止往下传递(复写onInterceptTouchEvent(),使其返回true)
* 返回false = 不拦截(默认)
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
View
/**
* View.dispatchTouchEvent()
* 只有以下3个条件都为真,dispatchTouchEvent() 才返回true;否则执行onTouchEvent()
*
* 1. mOnTouchListener != null (mOnTouchListener变量在View.setOnTouchListener()方法里赋值)
*
* 2.(mViewFlags & ENABLED_MASK) == ENABLED (判断当前点击的控件是否enable,大多数控件默认都是enable的)
*
* 3.mOnTouchListener.onTouch(this, event)
* 回调mOnTouchListener.onTouch方法return true or false
* (该方法需要在复写的时候手动赋值,如果不需要View执行onTouchEvent那么可以return true)
*/
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
如果onTouch没有消费掉事件(return false),那么会调用View.onTouchEvent方法。
以下是View.onTouchEvent的源码分析,
/**
* 源码分析:View.onTouchEvent()
* 简化过的代码
*/
public boolean onTouchEvent(MotionEvent event) {
...
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
...
// 若该控件可点击,则进入switch判断中
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
// a. 抬起手指事件
case MotionEvent.ACTION_UP:
...
// 执行performClick()
performClick();
break;
// b. 按下手指
case MotionEvent.ACTION_DOWN:
...
break;
// c. 结束事件(非人为原因)
case MotionEvent.ACTION_CANCEL:
...
break;
// d. 滑动事件
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
// 若该控件可点击,就一定返回true
return true;
}
// 若该控件不可点击,就一定返回false
return false;
}
/**
* performClick()
*/
public boolean performClick() {
/*
只要为控件注册过点击事件setOnClickListener(),就会回调onClick() 并返回true
*/
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
核心流程总结
