前言
关于自定义View系列的文章,好久没有写了。今天抽空看了下Android开发艺术探索。正好看到了View的事件分发机制,所以将它写成笔记记录下来。
关于View的事件分发,我起初是学习郭神的2篇文章。感觉其实也没有什么。大致也就了解下。不过看完其他很多优秀的文章和书籍后,才知道自己too young too simple。下面我们就一起来分析下Android的时间分发机制。
关于事件分发机制,其实网上的文章已经有很多了。我简单的看了几篇,发现写的都很好。之所以写这篇文章,主要是记录自己的学习过程,其次也想帮助和我一样的的初学者更加理解与掌握,才是本篇的目的。
注:本文源码 API=25 同时本文较长,可以先收藏再好好阅读
概念
在学习事件分发之前我们先来了解下,什么是事件分发。所谓点击事件(Touch)的事件分发,其实就是对MotionEvent(Touch的封装)事件的分发过程,即当一个MotionEvent产生以后,系统需要把这这个事件传递给那个具体的View。这个传递的过程就是事件分发过程。
1.MotionEvent
那么MotionEvent又是什么呢?
这个类就是记录手指接触屏幕后所产生的一系列的事件(也就是说我们事件分发其实就是分发MotionEvent这个对象)。这个类里包含了一系列的事件。事件的类型与含义如下:
事件类型 | 具体动作 |
---|---|
MotionEvent.ACTION_DOWN | 按下View(所有事件的开始) |
MotionEvent.ACTION_UP | 抬起View(与DOWN对应) |
MotionEvent.ACTION_MOVE | 滑动View |
MotionEvent.ACTION_CANCEL | 结束事件(非人为原因) |
下面列举2个我们常见的点击事件序列:
- 事件序列 : DOWN->UP 点击屏幕后松开 (常见的点击事件)
- 事件序列 : DOWN->MOVE->MOVE->...->MOVE->UP 点击屏幕滑动一会 (常见的滑动屏幕)
用图来概括如下:
事件系列
图片来源
2. 事件分发的顺序
事件分发的顺序是Activity->ViewGroup->View。也就是说在默认情况下。最后消费事件的都是View。虽然我们现在还没有开始深入讲解。但是结合我们日常开发的情况我们可以想到下面这张流程图:
事件分发概括.png
这张流程图就算我们没有了解事件分发,通过我们一直的使用规则来看,也是非常容易理解的。细心的小伙伴会发现。为什么Activity向下分发第一个就是ViewGroup,如果我们布局中只有一个简单View控件(如TextView)呢?还记得我们在讲View的绘制流程中介绍的吗?我们布局加载中的顶级View是DecorView(继承FrameLayout),他本是就是一个ViewGroup。不了解的可以回头看下这篇文章。
2. 事件分发的核心方法
在对事件分发机制概念,以及结合平时我们经验总结出来的原理后。下面我们就来通过源码来去将我们的想法串联起来。不过在看源码之前,我们要先讲下在事件分发机制中三个至关重要的方法。如下:
方法 | 作用 | 调用时刻 | 返回结果 |
---|---|---|---|
dispatchTouchEvent(MotionEvent event) | 用来进行事件分发 | 在三个方法中第一个被调用。 如果事件能够传递给当前View/ViewGroup,那么此方法一定会调用 | 表示是否消耗当前事件 |
onInterceptTouchEvent(MotionEvent ev) | 用来判断是否拦截某个事件(ViewGroup有此方法,View没有) | 在dispatchTouchEvent()方法中调用 如果当前View拦截了某个事件,那么同一事件序列将不再会调用此方法。 | 表示是否拦截当前事件 |
onTouchEvent(MotionEvent event) | 用来处理点击事件 | 在dispatchTouchEvent()方法中调用,不消耗当前事件,那么当前View在同一事件序列中无法再接受到事件 | 表示是否消耗当前事件 |
这三个方法就是事件分发机制中的核心三个方法,也是我们下面在源码中重要去分析的三个方法。他们三者之间的关系可以概述如下(注意这是一段伪代码。在任何类中并没有此方法。只是为了对解释三个方法关系):
代码语言:javascript复制/**
* 点击事件产生后
*/
// 步骤1:调用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false; //代表 是否会消费事件
// 步骤2:判断是否拦截事件
if (onInterceptTouchEvent(ev)) {
// a. 若拦截,则将该事件交给当前View进行处理
// 即调用onTouchEvent ()方法去处理点击事件
consume = onTouchEvent (ev) ;
} else {
// b. 若不拦截,则将该事件传递到下层
// 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
// 直到点击事件被最终处理为止
consume = child.dispatchTouchEvent (ev) ;
}
// 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
return consume;
三个方法的解释在加上这段伪代码,就很好理解三者的关系了:对于一个跟ViewGroup,点击事件产生后,首先会传给它,这时它的dispatchTouchEvent就会被调用,开始进行事件的分发,首先会进行判断,判断当前ViewGroup是否进行了拦截。如果进行拦截,那么ev(点击事件)就会交给ViewGroup去处理。不再向下传递。分发结束。如果没设置拦截。那么就会调用ViewGroup中所包含的子控件的dispatchTouchEvent (ev)方法,并将事件ev向下传递。如果子控件还是ViewGroup继续上面的循环。知道将事件最终被处理消费掉。这么一看,这不正好对应了我们前面总结的流程图嘛。看来我们将事件分发的大致流程已经都搞清楚了。
源码分析
从上面来看,好像事件分发机制也就这些东西了。好像我们都掌握了。其实不然,不过如果你上面的都理解了,说你对Android事件分发机制了个整体认识,那就一点都不为过了。不过事件分发还远不止这么简单。里面还是有很多需要注意的点和事件在分发过程中的一些规则。下面我们就从源码的角度来一一探索。
1. Activity事件分发
上面我们说了当一个事件的产生首先是传递个Activity。由Activity来进行事件的分发。那么我就看下Activity#dispatchTouchEvent():
代码语言:javascript复制 public boolean dispatchTouchEvent(MotionEvent ev) {
// 一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//实现屏保功能
//是一个空方法
//当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//当一个点击事件未被Activity下任何一个View接收 / 处理
//或是发生在Window边界外的触摸事件就会调用
return onTouchEvent(ev);
}
这个方法非常短。这里我们重点看第二个if语句。这里的getWindow().superDispatchTouchEvent(ev)点进去是Window#superDispatchTouchEvent()。这是一个抽象方法。不过相信看过我前面文章的小伙伴一定知道这个方法的实现实在PhoneWindow中。那么找到这个方法如下:
代码语言:javascript复制 //PhoneWindow#superDispatchTouchEvent()
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
/**
* 这里调用了mDecor.superDispatchTouchEvent(event);方法。这个mDecor就* 是DecorView 下面我们继续跟踪
*/
//DecorView#superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
//DecorView继承FrameLayout 那么他本是就是一个ViewGroup
//那么这个方法最后就会调用到ViewGroup#dispatchTouchEvent()
return super.dispatchTouchEvent(event);
}
可以看到最后Activity的分发过程最后就是将事件交给顶级DecorView去进行事件分发。然后它又会调用ViewGroup#dispatchTouchEvent()。OK!到这里我们就将我们的事件由Activity->ViewGroup的传递。并将返回值设置成true。表示这个事件已经被我们消耗掉了。
这里还有一点需要注意。从源码中我们可以看到,假设Activity下的所有View或者我们点击了Window边界以外,那么就会调用Activity#onTouchEvent(ev);这个方法:
代码语言:javascript复制public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
//Window#shouldCloseOnTouch()
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
// 主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
// 返回true:说明事件在边界外,即 消费事件
return true;
}
// 返回false:未消费(默认)
return false;
}
这部分了解即可,并不是我们的重点。
2. ViewGroup事件分发
通过上面的分析,现在事件已经从Activity->ViewGroup。那么我们就分析ViewGroup#dispatchTouchEvent()方法:
代码语言:javascript复制 @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
resetTouchState();
}
// Check for interception.
/**
*讲解一
*/
//检查是否拦截事件
//1.当事件为ACTION_DOWN时
//2. 当ViewGroup不拦截事件交给子元素处理 条件成立 即mFirstTouchTarget != null
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); // 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;
}
......
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//非MotionEvent.ACTION_CANCEL并且没有拦截事件
//进入if语句,对ViewGroup的子元素进行遍历
/**
*讲解三
*/
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
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;
//对子元素进行遍历
for (int i = childrenCount - 1; i >= 0; i--) {
//1.子元素是否在做动画
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//2.事件左边是否落在子元素区域内
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//接受点击事件的View根据1.2条件判断
//是否能够接受点击事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
//不符合要求 执行下一个循环
continue;
}
......
//调用此方法进行事件分发处理 如果有子元素
//那么参数中的child!=null
/**
*讲解四
*/
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 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;
//条件满足跳出循环
//这里是跳出内部for循环不是外部的
//其实就是break的用法
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
/**
*讲解五
*/
//对mFirstTouchTarget 进行赋值
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);
}
......
}
}
}
/**
*讲解六
*/
// 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 {
......
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//MotionEvent.ACTION_UP 后 清空mFirstTouchTarget
/**
*讲解七
*/
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
......
}
关于ViewGroup的事件分发源码(即dispatchTouchEvent()方法),还是比较长的,同时也是难点,下面我们将上面的代码拆分来看,来具体分析。
part1
代码语言:javascript复制·
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
resetTouchState();
}
// Check for interception.
/**
*讲解一
*/
//检查是否拦截事件
//1.当事件为ACTION_DOWN时
//2. 当ViewGroup不拦截事件交给子元素处理 条件成立 即mFirstTouchTarget != null
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); // 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;
}
- 讲解一 这个方法首先会判断,是否拦截当前事件。这个条件第一个好理解。一个事件的系列一定是由DOWN开始的,那么就会进入条件语句。调用onInterceptTouchEvent(ev)开始进行事件拦截。关于第二个代码中有解释。这个方法是在那里还原初始值与赋值请看下面。
- 讲解二 事件开始时会调用 resetTouchState();清空mFirstTouchTarget
part2
代码语言:javascript复制//非MotionEvent.ACTION_CANCEL并且没有拦截事件
//进入if语句,对ViewGroup的子元素进行遍历
/**
*讲解三
*/
if (!canceled && !intercepted) {
......省略
//对子元素进行遍历
for (int i = childrenCount - 1; i >= 0; i--) {
//1.子元素是否在做动画
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//2.事件左边是否落在子元素区域内
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//接受点击事件的View根据1.2条件判断
//是否能够接受点击事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
//不符合要求 执行下一个循环
continue;
}
......
//调用此方法进行事件分发处理 如果有子元素
//那么参数中的child!=null
/**
*讲解四
*/
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 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;
//条件满足跳出循环
//这里是跳出内部for循环不是外部的
//其实就是break的用法
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
/**
*讲解五
*/
//对mFirstTouchTarget 进行赋值
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语句,判断条件为没有对事件进行拦截,同时事件没有结束。对ViewGroup的子元素进行遍历
- 讲解️四 通过判断,将ViewGroup的子元素进行遍历,找到能够处理点击事件的子元素并调用dispatchTransformedTouchEvent()方法,进行事件的分发。
- 讲解五 当子元素能够处理点击事件,就调用addTouchTarget()方法,对mFirstTouchTarget()方法进行赋值。那么下次再进入讲解一方法。
part3
代码语言:javascript复制 /**
*讲解六
*/
// 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 {
......
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//MotionEvent.ACTION_UP 后 清空mFirstTouchTarget
/**
*讲解七
*/
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
- 讲解六 讲解三中的if语句不成立表示对事件进行拦截。那么直接走到了讲解六,并且mFirstTouchTarget == null没有在子元素的遍历中赋值,即条件成立。执行dispatchTransformedTouchEvent()方法。
- 讲解七 在同一事件系列结束后调用resetTouchState();对mFirstTouchTarget清空还原。
这样我们就将ViewGroup#dispatchTouchEvent()方法分析完成了。
在上面的讲解中我们多此提到mFirstTouchTarget与dispatchTransformedTouchEvent()方法。前者已经说明了他的作用与赋值及清空还原的位置。对于后者,这个方法其实就是ViewGroup对事件分发。看下他的源码:
代码语言:javascript复制 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) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
......
}
是ViewGroup#dispatchTransformedTouchEvent()方法,其他方法省略。可以可看到,如果ViewGroup有子元素同时子元素可以处理点击事件。那么就会调用子元素的child.dispatchTouchEvent(event);方法。如果是child是ViewGroup继续上面的循环,如果子元素是View,那么就或调用View.dispatchTouchEvent(event)方法。关于这个方法我们后面分析。如果child为空(即讲解六),那么就会调用super.dispatchTouchEvent(event)方法,那么就会调用ViewGroup父类的,即View.dispatchTouchEvent(event)方法,ViewGroup自己处理点击事件。最后都会默认(是默认不是一定) 调用onTounchEvent()方法。
通关对源码与这七个重要部分的讲解。我们可以总结如下几点:
- 一个事件序列只能一个View进行拦截且消耗。由讲解三我们知道。如果拦截事件,就不会进入if语句对子元素进行遍历与事件分发。同时又讲解六我们知道,如果拦截了某一事件(如MOVE)那么统一事件序列内的所有事件都交给它处理。
- 某个View一旦拦截事件,那么这一事件序列只能有它来处理(由1可知)。同时我们知道既然拦截就无法进入讲解三中,那么mFirstTouchTarget就无法被赋值,那么讲解一中的条件就不成立。所以调用onInterceptTouchEvent()不会再被调用。其实通过1.我们也都理解,如果拦截那么同一事件序列的所有事件都间给当前View处理。你拦截就说明你必须全都处理。那我还问你干啥。
- 。dispatchTransformedTouchEvent()方法中,当dispatchTouchEvent()的返回值与dispatchTransformedTouchEvent()返回值相同,由讲解六得知。这样会直接影响ViewGroup#dispatchTouchEvent()返回值(两者相同)。也就是说:View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTounchEvent()返回false)。那么统一事件序列的其他事件都不会交给他处理。并会重新交给父元素(注意是父元素,不是父类)去处理,即父元素onTounchEvent()会被调用。
- onInterceptTouchEvent()默认返回false,即默认不拦截任何事件。
- View没有onInterceptTouchEvent()。一旦事件传递给他,那么他的onTouchEvent就会调用。
关于ViewGroup的源码分析我们也就到这里了。有的啰嗦。不过详细才能跟好的理解与全面
3. View事件分发
由上面dispatchTransformedTouchEvent()方法可知,最后方法无论是ViewGroup消耗还是View消耗都会调用View#dispatchTouchEvent()方法。那么我们就来看这个方法:
代码语言:javascript复制 public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
......
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED //是否是enable可点击的 按钮默认都是可点击的 ImageView 不可点击
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//调用onTouchEvent(event)方法 返回值直接影响此方法的返回值
//返回值与次方法相同
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
View的dispatchTouchEvent比较简单。首先判断mOnTouchListener!=null同时li.mOnTouchListener.onTouch(this, event)返回为true那么result = true;。那么下面的 if (!result && onTouchEvent(event)) 中的第一个条件就不会成立所以onTouchEvent(event)永远不会得到执行。有此可见onTouch()优先级要高于onTouchEvent(event)。
下面我们看下默认情况进入onTouchEvent(event)方法中:
代码语言:javascript复制 public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
/**
*讲解一
*/
//首先判断当前View是不是DISABLED不可用状态
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
//如果不可用 同时当前控件的clickable与long_clickable
//与CONTEXT_CLICKABLE全是false
//那么才返回false
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
//如果View有代理会执行这个方法
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
/**
*讲解二
*/
//只要控件的clickable与long_clickable
//与CONTEXT_CLICKABLE 有一个为true 就进入次循环
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
......
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)) {
/**
*讲解三
*/
//onClickListener监听在此方法中
performClick();
}
}
}
......
}
break;
......
}
//默认返回 true
/**
*讲解四
*/
return true;
}
return false;
}
这部分代码比较简单。主要的都有注释。如果控件!=DISABLED,那么就会进入同时讲解二判断有一个成立。就会进入switch语句。当接收到MotionEvent.ACTION_UP是。最后执行performClick()方法.这个方法代码如下:
代码语言:javascript复制 public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
可以看到我们的设置setOnclickListener就是在这个赋值,li.mOnClickListener.onClick(this);就会调用我们的onClik方法。
那么有的同学会问View的longClickable默认是false,同时TextView的clickable也为false,那么为何我们给TextView设置setOnclickListener也能生效。我们下来看下TextView源码其他默认clickable=false的控件是一样的。:
代码语言:javascript复制 public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
//设置clickable
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
//设置longclickable
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
可以看到在设置监听时,方法内部已经帮我们设置了。
下面我们在针对onTouchEvent(MotionEvent event)方法来拆分分析下:
part1
代码语言:javascript复制 /**
*讲解一
*/
//首先判断当前View是不是DISABLED不可用状态
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
//如果不可用 同时当前控件的clickable与long_clickable
//与CONTEXT_CLICKABLE全是false
//那么才返回false
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
//如果View有代理会执行这个方法
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
- 讲解一 由代码可知即使控件是DISABLED状态,只要clickable与longclickable有一个返回true那么此方法就返回true,即事件被消费。但是不会执行onClick()方法。这点通过代码很容易理解。
part2
代码语言:javascript复制/**
*讲解二
*/
//只要控件的clickable与long_clickable
//与CONTEXT_CLICKABLE 有一个为true 就进入次循环
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
......
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)) {
/**
*讲解三
*/
//onClickListener监听在此方法中
performClick();
}
}
}
......
}
break;
......
}
//默认返回 true
/**
*讲解四
*/
return true;
}
- 讲解二 讲解二中只要有一个条件满足。就会进入switch语句。当接收到MotionEvent.ACTION_UP时(前提MotionEvent.ACTION_DOWN也接收到了)会经过判断最后执行 performClick();方法。
- 讲解三 performClick()方法内部会执行我们设置的监听,即onClick()方法。
- 讲解四 由代码可知只要讲解二中的if语句成立,不管进入switch中的任何ACTION或是都不进入,返回值都是true,即事件消费了。同时讲解四也证明默认情况下是返回true
总结
下面我们用流程图在来总结下:
总体流程总结
图片来源 侵权即删
流程图真的懒得画了。一篇文章学习得3.4天。写出来又得很长时间,所以大家勿怪。同时这张图结合文章理解起来简直是so easy。
其实关于Android事件分发机制优秀的文章由很多。如果观看一篇文章无法完全掌握,就多看几篇文章。然后自己总结,结合。反正最后能理解成自己的就算成功了。
结语
本人是个菜鸟,如果文章哪里有错误,欢迎指出。有问题也可以留言。最后如果文章对您有帮助。感谢支持。
优秀干货
Android事件分发机制
Android事件分发机制详解:史上最全面、最易懂
《Android开发艺术探索》