前提条件源码是基于android 26分析的
基础知识
什么是触摸事件
触摸事件,是 Android 用来描述你的手对屏幕做的事情的最小单元。关键词有两个:手势(你的手对屏幕做的事情)、最小单元。
手势:按下、移动、抬起、点击事件、长按事件、滑动事件。
最小单元:不可在拆分,比如按下(down)、移动(move)、抬起(up)就是不可再拆分的。而点击事件、长按事件、滑动事件则是由那些不可再拆分的事件组合起来的。比如点击事件是由按下、抬起组成的。长按事件是按下并保持一段时间不动。注意一下滑动事件和移动(move)的区别,滑动事件可以理解为是由若干个“move”组合起来的,系统对触摸的捕捉是很灵敏的,手指放在屏幕上稍微抖一下,都会产生“move”事件。
这里还有一个取消(cancel)事件,暂时不予理会
MotionEvent 对象
MotionEvent 对象就是对触摸事件相关信息的封装
事件类型(包括但不局限于):ACTION_DOWN(按下)、ACTION_ MOVE(移动)、ACTION_ UP(抬起)。
坐标系
坐标还分参考系,有以屏幕作为参考系的坐标值,也有以被触摸的 View 作为参考系的坐标值。
MotionEvent 的对象内的方法与坐标系是存在一定关联的具体的可参考下面的图
View坐标系
一个完整的事件序列
触摸事件与一个完整的事件序列的区别
-
触摸事件
最小单元,任何一个对屏幕的操作都是由多个触摸事件构成的,触摸事件分别有按下、移动、抬起、取消等 -
一个完整的事件序列
ACTION_DOWN(一个)–>ACTION_MOVE(数量不定)–>ACTION_UP(一个),从 ACTION_DOWN 到 ACTION_UP,称为一个完整的事件序列,即某一次手指按下到手指抬起算是一个完整的事件序列,下一次手指按下到抬起是另一个完整的事件序列。
笔者没有真的考究过 Android 官方是不是真的有“事件序列”这么一个概念性的名词,只是为了分析源码方便理解才这样说的,这也是android“拟人化”行为的一种标识吧,因为从ViewGroup 的源码里面,确实是可以找到在遇到 ACTION_DOWN 时清除某些标记位(以避免受到前一个事件序列的影响)
为什么要事件分发
简单地讲,事件分发就是为了解决“谁来干活”的问题。当一个事件发生,有超过一个View(或者 ViewGroup)能够对它进行响应(处理)时,就要确定到底谁来处理这个事件。
View的事件分发
View的事件分发,是指 View.java 源码里对触摸事件进行传递的流程
流程图
View 事件分发的流程图是一个主干流程,去掉一些细节的流程
流程图如下
View的事件分发
源码
public boolean dispatchTouchEvent(MotionEvent event) {
....//省略无关的代码
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
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
....
return result;
}
可以看到关键代码那里的条件就是流程图上面的条件
我们再来看onTouchEvent的源码,这里就是平常设置点击事件的回调地方
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
....//省略无关代码
//如果view的clickable属性设置为false也不会响应点击事件
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) {
....//省略无关代码
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)) {
performClick();
}
}
}
....//省略无关代码
}
mIgnoreNextUpEvent = false;
break;
return true;//返回这个的话就说明事件已经消耗了
}
return false;//返回这个的话就说明事件抛回给它的父类处理
}
流程说明
有可能处理事件的有两个地方,一个是外部设置的 OnTouchListener 监听器,即OnTouchListener的onTouch(),一个是 View 内部的处理方法,即 onTouchEvent()。而且外部设置的监听器优先获取事件。
当外部设置的监听器处理了事件(即有设置监听器、View 处于 ENABLE 状态、监听器在onTouch()里面返回 true 三者均成立),dispatchTouchEvent() 返回 true,当前 View的 onTouchEvent() 不会触发。
如果外部的监听器不处理事件,则dispatchTouchEvent() 的返回值由 View 自己的onTouchEvent()决定。
注意一下,对于上级 ViewGroup(也就是当前 View 的父容器)而言,它只认识子 View的dispatchTouchEvent()方法,不认识另外两个处理事件的方法。子View的OnTouchListener 和 onTouchEvent() 都是在自己的 dispatchTouchEvent() 里面调用的,他们两个会影响 dispatchTouchEvent() 的返回值,但是对于上级 ViewGroup 而言,它只认识 dispatchTouchEvent() 的返回值就足够了。
ViewGroup 的事件分发
流程图
流程如下图所示:
ViewGroup 的事件分发
源码
public boolean dispatchTouchEvent(MotionEvent ev) {
....//省略无关代码
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
/**
* 第一步:对于ACTION_DOWN进行处理(Handle an initial down)
* 因为ACTION_DOWN是一系列事件的开端,当是ACTION_DOWN时进行一些初始化操作.
* 从源码的注释也可以看出来:清除以往的Touch状态(state)开始新的手势(gesture)
* cancelAndClearTouchTargets(ev)中有一个非常重要的操作:
* 将mFirstTouchTarget设置为null!!!!
* 随后在resetTouchState()中重置Touch状态标识
*/
// 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);
resetTouchState();
}
....//省略无关代码
// Check for interception.
// 使用变量intercepted来标记ViewGroup是否拦截Touch事件的传递.
final boolean intercepted;
//ACTION_DOWN事件或者已经找到了事件分发的目标对象,在一个事件序列是非ACTION_DOWN事件都会进入这里
//对应流程图中的第一个判断分支
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//设置禁止拦截的标记 1.ACTION_DOWN事件与非ACTION_DOWN事件都会走这里
//因为在其他地方可能调用了requestDisallowInterceptTouchEvent(boolean disallowIntercept)来影响这个变量的值,
//所以在一个事件序列中如果多个事件之后确定了是由谁来处理事件,就在这个事件序列中满足一定条件可以设置这个值
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//再次询问是否会拦截 1.ACTION_DOWN事件与非ACTION_DOWN事件都会走这里
//常说事件传递中的流程是:dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
//当禁止拦截判断时(即disallowIntercept为true)设置intercepted = false
intercepted = false;
}
} else {
//当事件不是ACTION_DOWN并且mFirstTouchTarget为null(即没有Touch的目标组件)时
//设置 intercepted = true表示ViewGroup执行Touch事件拦截的操作。
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
....//省略无关代码
//不是ACTION_CANCEL并且ViewGroup的拦截标志位intercepted为false(不拦截)
if (!canceled && !intercepted) {
....//省略无关代码
//处理ACTION_DOWN事件.这个环节比较繁琐.
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
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;
//遍历viewGroup下所有的子view找到可以消费事件的子view,并且把事件消费的目标赋值
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
....//省略无关代码
//这个条件是第二次发生点击事件(第二个事件序列),这个快速找到事件消费的目标
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;
}
/**
* 如果上面的if不满足,当然也不会执行break语句.
* 于是代码会执行到这里来.
*
* 调用方法dispatchTransformedTouchEvent()将Touch事件传递给子View做
* 递归处理(也就是遍历该子View的View树)
* 该方法很重要,看一下源码中关于该方法的描述:
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
* 将Touch事件传递给特定的子View.
* 该方法十分重要!!!!在该方法中为一个递归调用,会递归调用dispatchTouchEvent()方法!!!!!!!!!!!!!!
* 在dispatchTouchEvent()中:
* 如果子View为ViewGroup并且Touch没有被拦截那么递归调用dispatchTouchEvent()
* 如果子View为View那么就会调用其onTouchEvent(),这个就不再赘述了.
*
*
* 该方法返回true则表示子View消费掉该事件,同时进入该if判断.
* 满足if语句后重要的操作有:
* 1 给newTouchTarget赋值
* 2 给alreadyDispatchedToNewTouchTarget赋值为true.
* 看这个比较长的英语名字也可知其含义:已经将Touch派发给新的TouchTarget
* 3 执行break.
* 因为该for循环遍历子View判断哪个子View接受Touch事件,既然已经找到了
* 那么就跳出该for循环.
* 4 注意:
* 如果dispatchTransformedTouchEvent()返回false即子View
* 的onTouchEvent返回false(即Touch事件未被消费)那么就不满足该if条件,也就无法执行addTouchTarget()
* 从而导致mFirstTouchTarget为null.那么该子View就无法继续处理ACTION_MOVE事件
* 和ACTION_UP事件!!!!!!!!!!!!!!!!!!!!!!
* 5 注意:
* 如果dispatchTransformedTouchEvent()返回true即子View
* 的onTouchEvent返回true(即Touch事件被消费)那么就满足该if条件.
* 从而mFirstTouchTarget不为null!!!!!!!!!!!!!!!!!!!
* 6 小结:
* 对于此处ACTION_DOWN的处理具体体现在dispatchTransformedTouchEvent()
* 该方法返回boolean,如下:
* true---->事件被消费----->mFirstTouchTarget!=null
* false--->事件未被消费---->mFirstTouchTarget==null
* 因为在dispatchTransformedTouchEvent()会调用递归调用dispatchTouchEvent()和onTouchEvent()
* 所以dispatchTransformedTouchEvent()的返回值实际上是由onTouchEvent()决定的.
* 简单地说onTouchEvent()是否消费了Touch事件(true or false)的返回值决定了dispatchTransformedTouchEvent()
* 的返回值!!!!!!!!!!!!!从而决定了mFirstTouchTarget是否为null!!!!!!!!!!!!!!!!从而进一步决定了ViewGroup是否
* 处理Touch事件.这一点在下面的代码中很有体现.
*
*
*/
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;
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 (preorderedList != null) preorderedList.clear();
}
//该条件表示没有找到子view接收事件并且之前的mFirstTouchTarget不为空
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
//newTouchTarget指向了最初的TouchTarget
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
}
/**
* 分发Touch事件至target
*
* 经过上面对于ACTION_DOWN的处理后mFirstTouchTarget有两种情况:
* 1 mFirstTouchTarget为null
* 2 mFirstTouchTarget不为null
*
* 当然如果不是ACTION_DOWN就不会经过上面较繁琐的流程
* 而是从此处开始执行,比如ACTION_MOVE和ACTION_UP
*/
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//简要分析
//1.子view不处理事件会执行这里
//2.无法找到事件消费的目标
//都是走super.dispatchTouchEvent(event);
/**
* 详细分析
* 情况1:mFirstTouchTarget为null
*
* 经过上面的分析mFirstTouchTarget为null就是说Touch事件未被消费.
* 即没有找到能够消费touch事件的子组件或Touch事件被拦截了,
* 则调用ViewGroup的dispatchTransformedTouchEvent()方法处理Touch事件则和普通View一样.
* 即子View没有消费Touch事件,那么子View的上层ViewGroup才会调用其onTouchEvent()处理Touch事件.
* 在源码中的注释为:No touch targets so treat this as an ordinary view.
* 也就是说此时ViewGroup像一个普通的View那样调用dispatchTouchEvent(),且在dispatchTouchEvent()
* 中会去调用onTouchEvent()方法.
* 具体的说就是在调用dispatchTransformedTouchEvent()时第三个参数为null.
* 第三个参数View child为null会做什么样的处理呢?
* 请参见下面dispatchTransformedTouchEvent()的源码分析
*
*
*/
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
/**
* 情况2:mFirstTouchTarget不为null即找到了可以消费Touch事件的子View 多数情况下后续Touch事件可以传递到该子View,
* 但是在事件还没确定是viewGroup还是view处理时会一直流经两个的事件流中
*/
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//对于非ACTION_DOWN事件继续传递给目标子组件进行处理,依然是递归调用dispatchTransformedTouchEvent()
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//对于非ACTION_DOWN事件如果中途拦截了(intercepted = true),这个条件会把mFirstTouchTarget置为null
//等下一个touch事件就会把事件给viewGroup自己处理
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
/**
* 处理ACTION_UP和ACTION_CANCEL
* Update list of touch targets for pointer up or cancel, if needed.
* 在此主要的操作是还原状态
*/
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
网友评论