Android事件分发机制的本质
将点击事件向某个View进行传递并且最终得到处理,即当一个点击事件发生后,系统需要将这个事件传递给一个具体的View处理,这个事件的传递过程就是事件分发过程
事件在那些对象传递
Activity、ViewGroup、View分发流程:Activity(Window)-> ViewGroup -> Viewsuper:调用父类方法true:处理事件,事件不在继续往下传递false:不处理事件,事件也不继续传递,交给父控件的onTouchEvent()处理传递:
Activity -> ViewGroup -> View 从上往下调用dispatchTouchEvent()
View -> ViewGroup -> Activity 从下往上调用onTouchEvent()
1. Activity的事件分发
当一个点击事件发生时,事件最先到达Activity的dispatchTouchEvent()进行事件分发
public boolean dispatchTouchEvent(MotionEvent ev) {
//一个事件的开始总是从DOWN开始
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//默认空方法,每当按键、触摸、trackBall事件分发到当前的Activity就会被调用,
//如果想在Activity运行的时候能够感知用户正在与设备交互,重写此方法
onUserInteraction();
}
//getWindow()=Window抽象类,唯一的实现类PhoneWindow
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//mDecor=DecorView,是视图顶层View,继承FrameLayout,所有界面的父类
return mDecor.superDispatchTouchEvent(event);
}
DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
//由于DecorView继承自FrameLayout,FrameLayout继承自ViewGroup
//super.dispatchTouchEvent为ViewGroup 的dispatchTouchEvent
return super.dispatchTouchEvent(event);
}
即getWindow().superDispatchTouchEvent(ev)就是执行了ViewGroup.dispatchTouchEvent(event)
说明事件就是从Activity传递到ViewGroup中
总结:
1. 事件最先传递到Activity的dispatchTouchEvent()进行事件分发
2. 调用Window唯一实现类PhoneWindow的 superDispatchTouchEvent()
3. 调用DecorView的superDispatchTouchEvent()
4. 最终调用DecorView父类FrameLayout的dispatchTouchEvent()即ViewGroup的dispatchTouchEvent()
2.View事件分发
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
//分析(1)
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
//如果分析(1)中onTouch()返回false(onTouch()未消费事件),
return result;
}
分析(1):
第一个条件:li != null,经过跟踪发现在View中发现了 getListenerInfo() 那么这个方法又是什么时候调用的呢?
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
继续跟踪发现,getListenerInfo()方法在多个地方使用到,比如setOnClickListener(),setOnLongClickListener(),setOnTouchListener()等等,这里也正好找到了li.mOnTouchListener != null赋值地方setOnTouchListener()
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
第三个条件:(mViewFlags & ENABLED_MASK) == ENABLED,即判断当前控件是否是enable,一般控件都是默认enable
第四个条件:li.mOnTouchListener.onTouch(this, event),其实这个方法就是回调控件注册的touch事件的onTouch()方法,也就是如果我们在onTouch()里返回true,这四个条件也就满足了,从而让整个方法直接返回了true,如果我们在onTouch()方法返回false,就会执行onTouchEvent()方法。
根据上面的研究,我们可以得出结论:onTouch()方法优先于onClick()执行,如果onTouch()方法返回true(事件被消费)因而事件不会继续向下传递,onClick()方法也将不在执行
我们发现如果条件不通过,则会执行onTouchEvent()方法,可想而知onClick()方法也必然在onTouchEvent()方法中,接着看onTouchEvent源码:
public boolean onTouchEvent(MotionEvent event) {
...
final int action = event.getAction();
//只要控件是CLICKABLE(点击)、LONG_CLICKABLE(长按)有一个为true,onTouchEvent就一定消费事件
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:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
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)) {
//分析(1)
performClickInternal();
}
}
}
...
}
return true;
}
return false;
}
分析(1):跟踪performClickInternal()方法
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//如果注册了OnClickListener事件就会回调onClick()方法
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
view的事件分发源码解析到此结束,上总结:
1. onTouch()优先于onClick()先执行
2. 控件被点击时:
(1) 如果onTouch返回false,执行onTouchEvent()方法,进而执行onClick()方法
(2) 如果onTouch返回true,则事件被onTouch()消费,onClick()就不会执行了。
3.ViewGroup事件分发
测试用例:(1)只点击Button (2)点击ViewGroupB
测试结果:当点击Button的时候,执行Button 的Onclick()方法,但是ViewGroupB 的onTouch()不会执行,只有点击了ViewGroupB时才会触发其onTouchEvent()
测试结论:Button的onClick()将事件消费了,因此事件不会继续向下传递
测试用例:(1)重写Button的onTouch事件返回true,点击Button
测试结果:Button的onTouch消费了事件,onClick()收不到事件
测试用例:(1)单独点击ViewGroupB (2)单独ViewGroupA (3)重写B的onTouch事件返回true
测试结果:(1)当单独点击ViewGroupB的时候A/B 的onTouch()都会被触发;(2)单独单击ViewGroupA时触发了A的onTouch()
(3)单独点击ViewGroupB时A的onTouch()没有被触发了,因为已经被 B消费了
源码分析:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 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);
//恢复mFirstTouchTarget==null
resetTouchState();
}
// Check for interception.
final boolean intercepted;
//按下并且首次
//mFirstTouchTarget:判断当前的ViewGroup是否拦截了事件,如果拦截了mFirstTouchTarget=null,如果没有拦截
//并交由子view处理,则mFirstTouchTarget!=null
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//分析(1)
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//分析(2)
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;
}
}
...
return handled;
}
分析(1):首先必须是ACTION_DOWN状态,一次完整的事件序列应该是从DOWN开始UP结束,mFirstTouchTarget的意义是:当前ViewGroup是否拦截了事件,如果拦截了,mFirstTouchTarget=null,如果没有拦截交由子View来处理,则mFirstTouchTarget!=null。假设当前的ViewGroup拦截了事件(disallowIntercept=false),mFirstTouchTarget!=null为false,如果这时触发ACTION_DOWN事件,则会执行onInterceptTouchEvent()方法;如果触发的是ACTION_MOVE、ACTION_UP事件,则不再执行onInterceptTouchEvent()方法,而是直接设置了intercepted=true,此后的一个事件序列均由这个ViewGroup处理。
分析(2):FLAG_DISALLOW_INTERCEPT标志主要是禁止ViewGroup拦截除了DOWN之外的事件,一般通过子View的requestDisallowInterceptTouchEvent来设置。
总结:当ViewGroup要拦截事件的时候,那么后续的事件都要由他处理,而不再调用onInterceptTouchEvent()方法,其中onInterceptTouchEvent默认false,如果想要ViewGroup拦截则重写方法返回true。
继续往下面分析:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//倒序遍历子元素是否能够接收点击事件(从最上层View开始往内层遍历)
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;
}
//判断触摸点是否在子view的范围或者子view是否正在播放动画,如果不符合要求则continue
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);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//分析(1)
// 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;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}}}
...
}
分析(1):dispatchTransformedTouchEvent()事件分发实际的处理方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
//如果没有子view
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
return handled;
}
如果存在子View,则调用View的dispatchTouchEvent()方法,如果ViewGroup没有子View,则调用super.dispatchTouchEvent()方法即View的dispatchTouchEvent()方法就回到了View的分发上面了。
4. 点击事件分发的传递规则
从上面分析得出,伪代码
fun dispatchTouchEvent(ev: MotionEvent): Boolean {
var result = false
if (onInterceptTouchEvent(ev)) {
result = super.onTouchEvent(ev)
} else {
result = child.dispatchTouchEvent(ev)
}
return result
}
onInterceptTouchEvent()方法和onTouchEvent()方法都是在dispatchTouchEvent()方法中调用的。
我们知道他们的传递规则是由上而下,如果到最底层的View一直还没有消耗事件则又从下而上传递
如下图:(网络配图侵权删)
举个栗子:(摘抄自刘望舒的《Android进阶之光》)
在金庸的《倚天屠龙记》中,武当派实力强劲,按照身份和实力区分,分别是武当掌门张
三丰、武当七侠、武当弟子。这时突然有一个敌人来犯,这个消息首先会汇报给武当掌门张三丰。张三丰
当然不会亲自出马,因此他就将应战的任务交给武当七侠之一的宋远桥(onInterceptTouchEvent()返回
false);宋远桥威名远扬,也不会应战,因此他就把应战的任务交给武当弟子宋青书
(onInterceptTouchEvent()返回 false);武当弟子宋青书没有手下,他只能自己应战。在这里我们把武当
掌门张三丰比作顶层 ViewGroup,将武当七侠之一的宋远桥比作中层ViewGroup,武当弟子宋青书比作底层
View。那么事件的传递流程就是:武当掌门张三丰(顶层ViewGroup)→武当七侠宋远桥(中层
ViewGroup)→武当弟子宋青书(底层View)。因此得出结论,事件由上而下传递返回值的规则如下:为
true,则拦截,不继续向下传递;为false,则不拦截,继续向下传递。
接下来讲解点击事件由下而上的传递。当点击事件传给底层的 View 时,如果其onTouchEvent()方法
返回true,则事件由底层的View消耗并处理;如果返回false则表示该View不做处理,则传递给父View的
onTouchEvent()处理;如果父View的onTouchEvent()仍旧返回false,则继续传递给该父View的父View
处理,如此反复下去。
再返回上面武侠的例子。武当弟子宋青书发现来犯的敌人是混元霹雳手成昆,他打不过成昆
(onTouchEvent()返回 false),于是就跑去找宋远桥,宋远桥来了,发现自己也打不过成昆
(onTouchEvent()返回 false),就去找武当掌门张三丰,张三丰用太极拳很轻松地打败了成昆
(onTouchEvent()返回true)。因此得出结论,事件由下而上传递返回值的规则如下:为true,则处理
了,不继续向上传递;为false,则不处理,继续向上传递。
5.总结
-
onToutch()和onTouchEvent()的区别
这两个方法都是在View的dispatchTouchEvent()中调用的;但onTouch优先于onTouchEvent()执行,如果在onTouch()中返回true,onTouchEvent() 将不再执行。
另外,onTouch能够得到执行只需两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable,因为如果是非enable,那么注册的onTouch事件永远无法得到执行,对于这一类控件,重写onTouchEvent方法来监听touch事件 -
Touch事件的后续事件层级传递
当dispatchTouchEvent()在进行事件分发的时候,只有前一个事件返回true,才会收到后一个事件(比如:在执行ACTION_DOWN时返回了false,后面一系列的ACTION_MOVE、ACTION_UP事件都不会执行了)
dispatchTouchEvent()和onTouchEvent()消费事件、终结事件传递返回 true,那么收到ACTION_DOWN,也会收到ACTION_MOVE、ACTION_UP
如果在某个对象(Activity、ViewGroup、View)的onTouchEvent()消费事件(返回true),那么ACTION_MOVE和ACTION_UP事件从上往下到这个View后就不在往下传递了,而是直接传给自己的onTouchEvent()并结束本次事件传递的过程。
完结散花。。
来源:CSDN
作者:吴唐人
链接:https://blog.csdn.net/wu996489865/article/details/103652082