LinearLayout 单 Button 点击事件分发
从分析最简单的activity布局开始,假设我们的页面只有一个线性布局和一个按钮。
XML
//layout_activity.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="单击事件" />
</LinearLayout>
从 DecorView 到 LinearLayout
上篇文章可知,点击事件到了decorView从而触发了viewgroup的dispatch事件。
那么decoreview是什么,和我们的xml布局有什么关系呢
Android 中 Activity 的视图呈现遵循固定的三层嵌套层级,
DecorView(Framlayout)
├─ 标题栏 / ActionBar 容器
└─ 内容容器(Framlayout)「com.android.internal.R.id.content」
└─ 开发者布局「setContentView 加载」
由此可见,xml中的LinearLayout实际上是DecoreView的子view的子View.
Button 的点击事件触发流程
第一个动作是ACTION_DOWN,从DecorView层的ViewGroup开始分发。跟踪源码看下
先看第一部分
java
//ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {//当前例子中首先到达的ev.action为DOWN
/第一步:****判断点击事件有效性 可略过*****/
//这一部分判断我们使用的机器版本,除非是aosp开发人员指定机器编译选项,这里默认都是null
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
//这里检测是否开启了无障碍服务(就是平常手机设置里面的无障碍),我们按照正常情况,即不开启,不会进入这个if
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
/****判断点击事件有效性 *****/
/***第二步****/
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {//判断点击事件和view是否正常,通常为true
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);//清空上一个点击事件维系的点击链表
resetTouchState();//暗藏乾坤的方法,对后面影响极大
}
/*****第三步******/
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//根据这个属性来判断是否直接将intercept设置为false
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事件,做一些预处理。在这里resetTouchState还蛮关键,后面说。
- 确定当前viewgroup(这个例子中,依然是我们的decoreview)是否拦截该事件。
在第3步中,我们可以看到确定是否拦截的一个条件是disallowIntercept,代码中的逻辑如下
disallowIntercept
├─ false ===> intercept:onInterceptTouchEvent
└─true ===> intercept:false
再来看disallowIntercept的取值:
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
其中
mGroupFlag默认为0。FLAG_DISALLOW_INTERCEPT = 0x80000不变
看上去disallowIntercept会永远为true
这里呢,就是第二步的resetTouchState的方法的调控作用了
java
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//这一句的代码很重要,表示mGroupFlags中的这个FLAG_DISALLOW_INTERCEPT所在的标志位改为0
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
在对mGroupFlags进行值改变后,
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
disallowIntercept的值就会为false了。因为mGroupFlags & FLAG_DISALLOW_INTERCEPT的值必为0(这一部分不懂的话可以看「位掩码」相关的知识)。
回归上面的第三步中的intercept的取值,可见,接收了ACTION_DOWN事件的ViewGroup,其intercept的取值必然是onIntercept的返回。
java
/***
* 除非是特定情况的鼠标点击事件,否则默认返回false
***/
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;
}
前三步看完,接着往下看:
这里又分了三步,承接前面的三步,这里概括为第四、第五、第六步。
java
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
/*****第四步(不用细看)******/
/****设置是否无障碍服务***** 此处不进入该if*****/
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation./**检测动作是否被取消 结果为false****/
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
/**检测动作是否来自鼠标点击 结果为false****/
// Update list of touch targets for pointer down, if needed.
final boolean isMouseEvent = ev.getSource() ==InputDevice.SOURCE_MOUSE;
/**检测是否为可分发给多个子view. 默认为false****/
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
/*****第五步*******/
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {//条件符合,进入)
// If the event is targeting accessibility 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 =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : 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--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
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)) {
// 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();
}
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.pointerIdBits |= idBitsToAssign;
}
}
}
先来总结下这三步的意思:
- 第四步:对触摸动作的属性检查,是否无障碍,是否鼠标,是否多子View接收(不用管)
- 第五步:这一步主要是查找承接当前点击事件的view, ViewGroup本身或者它的子View(不会蔓延到孙子辈)
- 第六步:根据第五步找到的承接view,去执行它的点击事件的分发流程。
从第五步开始看:
这里可以简化为三小步:
1.拿到用户点击的坐标
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
2.获取当前viewgroup的所有子view并排序
final ArrayList<View> preorderedList = buildTouchDispatchChildList();//这里除非是自定义的viewGroup里面对特定的子View指定了顺序,返回的就是viewgroup的子View.[0]是xml里面第一个子view,[1]是第二个,依次轮推
3.对所有的子view从前到后排,这里的前和后是视觉效果上的前后。也就是说如果没有指定Z,都是在xml里面的从后到前排。
for (int i = childrenCount - 1; i >= 0; i--) {//这里注意,是倒序排,也就是视觉上的view从前往后排。
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
//如果child不可见或者不可点,或者touchevent没落在子view的空间内,都跳过
continue;
}
//找到了第一个合格的子view
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);
上面三步,找到了应该接收这个点击事件的子view,结合例子,当前viewgroup的实例是decoreView,我们点击了button,那么此时这个child实例应该是decoreview的直接子view,也就是id为com.android.internal.R.id.content的framelayout。
找到后执行了getTouchTarget(child)方法,构造了一个TouchTarget。鉴于mFirstTouchTarget,所以此处返回null.
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
第六步(重点来了)
第六步一开始执行了这句,我们跳入看看这个if语句的方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
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) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
这里呢,可以看出,不管那种情况,只要是child不是null,那么最后返回的handle就是child.dispatchTouchEvent(transformedEvent);在实例中,就是执行id为com.android.internal.R.id.content的framelayout的dispatchTouchEvent,同样是执行viewgroup的dispatchTouchEvent嘛,按照之前的分析,又会走到child.dispatchTouchEvent(transformedEvent);
而此时的child是xml布局中的LinearLayout,也是ViewGroup也会执行这个dispatchTouchEvent,又会走到child.dispatchTouchEvent(transformedEvent);而这时的child,终于到了button。
┌─────────────────────────┐
│ 触摸事件触发(屏幕点击) │
└──────────────┬──────────┘
▼
┌─────────────────────────┐
│ DecoreView.dispatchTouchEvent() │
│ (核心判断:child ≠ null) │
└──────────────┬──────────┘
▼
┌──────────────────────────┐
│ com.android.internal.R.id.content
| → FrameLayout.dispatchTouchEvent()│
│ (ViewGroup类型,复用同分发逻辑;child ≠ null) │
└──────────────┬───────────┘
▼
┌───────────────────────────┐
│ XML根布局 → LinearLayout.dispatchTouchEvent() │
│ (ViewGroup类型,复用同分发逻辑;child ≠ null) │
└──────────────┬────────────┘
▼
┌────────────────────────────┐
│ 最终目标 → Button.dispatchTouchEvent() │
│ (View类型,无子View,分发终止,执行自身事件处理) │
└────────────────────────────┘
至于button怎么执行dispatchTouchEvent,下一篇吧。