Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)(1)

2 基础实例现象


2-1 例子

这个例子布局等还和上一篇的例子相似,只是重写了Button和LinearLayout而已,所以效果图不在提供,具体参见上一篇。

首先我们简单的自定义一个Button(View的子类),再自定义一个LinearLayout(ViewGroup的子类),其实没有自定义任何属性,只是重写部分方法(添加了打印,方便查看)而已,如下:

public class TestButton extends Button {

public TestButton(Context context, AttributeSet attrs) {

super(context, attrs);

}

@Override

public boolean dispatchTouchEvent(MotionEvent event) {

Log.i(null, “TestButton dispatchTouchEvent-- action=” + event.getAction());

return super.dispatchTouchEvent(event);

}

@Override

public boolean onTouchEvent(MotionEvent event) {

Log.i(null, “TestButton onTouchEvent-- action=” + event.getAction());

return super.onTouchEvent(event);

}

}

public class TestLinearLayout extends LinearLayout {

public TestLinearLayout(Context context, AttributeSet attrs) {

super(context, attrs);

}

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

Log.i(null, “TestLinearLayout onInterceptTouchEvent-- action=” + ev.getAction());

return super.onInterceptTouchEvent(ev);

}

@Override

public boolean dispatchTouchEvent(MotionEvent event) {

Log.i(null, “TestLinearLayout dispatchTouchEvent-- action=” + event.getAction());

return super.dispatchTouchEvent(event);

}

@Override

public boolean onTouchEvent(MotionEvent event) {

Log.i(null, “TestLinearLayout onTouchEvent-- action=” + event.getAction());

return super.onTouchEvent(event);

}

}

如上两个控件很简单吧,不解释,继续看其他代码:

<?xml version="1.0" encoding="utf-8"?>

<com.zzci.light.TestLinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”

android:orientation=“vertical”

android:gravity=“center”

android:layout_width=“fill_parent”

android:layout_height=“fill_parent”

android:id=“@+id/mylayout”>

<com.zzci.light.TestButton

android:id=“@+id/my_btn”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”

android:text=“click test”/>

</com.zzci.light.TestLinearLayout>

public class ListenerActivity extends Activity implements View.OnTouchListener, View.OnClickListener {

private TestLinearLayout mLayout;

private TestButton mButton;

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

mLayout = (TestLinearLayout) this.findViewById(R.id.mylayout);

mButton = (TestButton) this.findViewById(R.id.my_btn);

mLayout.setOnTouchListener(this);

mButton.setOnTouchListener(this);

mLayout.setOnClickListener(this);

mButton.setOnClickListener(this);

}

@Override

public boolean onTouch(View v, MotionEvent event) {

Log.i(null, “OnTouchListener–onTouch-- action=”+event.getAction()+" --"+v);

return false;

}

@Override

public void onClick(View v) {

Log.i(null, “OnClickListener–onClick–”+v);

}

}

到此基础示例的代码编写完成。没有啥难度,很简单易懂,不多解释了。

2-2 运行现象

当直接点击Button时打印现象如下:

TestLinearLayout dispatchTouchEvent-- action=0

TestLinearLayout onInterceptTouchEvent-- action=0

TestButton dispatchTouchEvent-- action=0

OnTouchListener–onTouch-- action=0 --com.zzci.light.TestButton

TestButton onTouchEvent-- action=0

TestLinearLayout dispatchTouchEvent-- action=1

TestLinearLayout onInterceptTouchEvent-- action=1

TestButton dispatchTouchEvent-- action=1

OnTouchListener–onTouch-- action=1 --com.zzci.light.TestButton

TestButton onTouchEvent-- action=1

OnClickListener–onClick–com.zzci.light.TestButton

分析:你会发现这个结果好惊讶吧,点击了Button却先执行了TestLinearLayout(ViewGroup)的dispatchTouchEvent,接着执行TestLinearLayout(ViewGroup)的onInterceptTouchEvent,接着执行TestButton(TestLinearLayout包含的成员View)的dispatchTouchEvent,接着就是View触摸事件的分发流程,上一篇已经讲过了。也就是说当点击View时事件派发每一个down,up的action顺序是先触发最父级控件(这里为LinearLayout)的dispatchTouchEvent->onInterceptTouchEvent->然后向前一级传递(这里就是传递到Button View)。

那么继续看,当直接点击除Button以外的其他部分时打印如下:

TestLinearLayout dispatchTouchEvent-- action=0

TestLinearLayout onInterceptTouchEvent-- action=0

OnTouchListener–onTouch-- action=0 --com.zzci.light.TestLinearLayout

TestLinearLayout onTouchEvent-- action=0

TestLinearLayout dispatchTouchEvent-- action=1

OnTouchListener–onTouch-- action=1 --com.zzci.light.TestLinearLayout

TestLinearLayout onTouchEvent-- action=1

OnClickListener–onClick–com.zzci.light.TestLinearLayout

分析:你会发现一个奇怪的现象,派发ACTION_DOWN(action=0)事件时顺序为dispatchTouchEvent->onInterceptTouchEvent->onTouch->onTouchEvent,而接着派发ACTION_UP(action=1)事件时与上面顺序不同的时竟然没触发onInterceptTouchEvent方法。这是为啥呢?我也纳闷,那就留着下面分析源码再找答案吧,先记住这个问题。

有了上面这个例子你是不是发现包含ViewGroup与View的事件触发有些相似又有很大差异吧(PS:在Android中继承View实现的控件已经是最小单位了,也即在XML布局等操作中不能再包含子项了,而继承ViewGroup实现的控件通常不是最小单位,可以包含不确定数目的子项)。具体差异是啥呢?咱们类似上篇一样,带着这个实例疑惑去看源码找答案吧。

3 Android 5.1.1(API 22) ViewGroup触摸屏事件传递源码分析


通过上面例子的打印我们可以确定分析源码的顺序,那就开始分析呗。

3-1 从ViewGroup的dispatchTouchEvent方法说起

前一篇的3-2小节说在Android中你只要触摸控件首先都会触发控件的dispatchTouchEvent方法(其实这个方法一般都没在具体的控件类中,而在他的父类View中)。这其实是思维单单局限在View的角度去看待的,这里通过上面的例子你是否发现触摸控件会先从他的父级dispatchTouchEvent方法开始派发呢?是的,所以咱们先从ViewGroup的dispatchTouchEvent方法说起,如下:

public boolean dispatchTouchEvent(MotionEvent ev) {

if (mInputEventConsistencyVerifier != null) {

mInputEventConsistencyVerifier.onTouchEvent(ev, 1);

}

// 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)) {

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) {

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;

}

// If intercepted, start normal event dispatch. Also if there is already

// a view that is handling the gesture, do normal event dispatch.

if (intercepted || mFirstTouchTarget != null) {

ev.setTargetAccessibilityFocus(false);

}

// 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;

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 preorderedList = buildOrderedChildList();

final boolean customOrder = preorderedList == null

&& isChildrenDrawingOrderEnabled();

final View[] children = mChildren;

for (int i = childrenCount - 1; i >= 0; i–) {

final int childIndex = customOrder

? getChildDrawingOrder(childrenCount, i) : i;

final View child = (preorderedList == null)

? children[childIndex] : preorderedList.get(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;

}

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)) {

// 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;

}

}

}

// 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 {

// 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;

if (dispatchTransformedTouchEvent(ev, cancelChild,

target.child, target.pointerIdBits)) {

handled = true;

}

if (cancelChild) {

if (predecessor == null) {

mFirstTouchTarget = next;

} else {

predecessor.next = next;

}

target.recycle();

target = next;

continue;

}

}

predecessor = target;

target = next;

}

}

// 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;

}

我勒个去!!!这比View的dispatchTouchEvent方法长很多啊,那就只关注重点分析吧。

第一步,17-24行,对ACTION_DOWN进行处理。

因为ACTION_DOWN是一系列事件的开端,当是ACTION_DOWN时进行一些初始化操作,从上面源码中注释也可以看出来,清除以往的Touch状态然后开始新的手势。在这里你会发现cancelAndClearTouchTargets(ev)方法中有一个非常重要的操作就是将mFirstTouchTarget设置为了null(刚开始分析大眼瞄一眼没留意,结果越往下看越迷糊,所以这个是分析ViewGroup的dispatchTouchEvent方法第一步中重点要记住的一个地方),接着在resetTouchState()方法中重置Touch状态标识。

第二步,26-47行,检查是否要拦截。

在dispatchTouchEvent(MotionEvent ev)这段代码中使用变量intercepted来标记ViewGroup是否拦截Touch事件的传递,该变量类似第一步的mFirstTouchTarget变量,在后续代码中起着很重要的作用。if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这一条判断语句说明当事件为ACTION_DOWN或者mFirstTouchTarget不为null(即已经找到能够接收touch事件的目标组件)时if成立,否则if不成立,然后将intercepted设置为true,也即拦截事件。当当事件为ACTION_DOWN或者mFirstTouchTarget不为null时判断disallowIntercept(禁止拦截)标志位,而这个标记在ViewGroup中提供了public的设置方法,如下:

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {

// We’re already in this state, assume our ancestors are too

return;

}

if (disallowIntercept) {

mGroupFlags |= FLAG_DISALLOW_INTERCEPT;

} else {

mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;

}

// Pass it up to our parent

if (mParent != null) {

mParent.requestDisallowInterceptTouchEvent(disallowIntercept);

}

}

所以你可以在其他地方调用requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,从而禁止执行是否需要拦截的判断。当disallowIntercept为true(禁止拦截判断)时则intercepted直接设置为false,否则调用onInterceptTouchEvent(ev)方法,然后将结果赋值给intercepted。那就来看下ViewGroup与众不同与View特有的onInterceptTouchEvent方法,如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {

return false;

}

看见了吧,默认的onInterceptTouchEvent方法只是返回了一个false,也即intercepted=false。所以可以说明上面例子的部分打印(dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent),这里很明显表明在ViewGroup的dispatchTouchEvent()中默认(不在其他地方调运requestDisallowInterceptTouchEvent方法设置禁止拦截标记)首先调用了onInterceptTouchEvent()方法。

第三步,49-51行,检查cancel。

通过标记和action检查cancel,然后将结果赋值给局部boolean变量canceled。

第四步,53-函数结束,事件分发。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
TouchEvent方法设置禁止拦截标记)首先调用了onInterceptTouchEvent()方法。

第三步,49-51行,检查cancel。

通过标记和action检查cancel,然后将结果赋值给局部boolean变量canceled。

第四步,53-函数结束,事件分发。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-t6yh5jx8-1715488588401)]

[外链图片转存中…(img-AyDTIRST-1715488588403)]

[外链图片转存中…(img-1x9wZCuM-1715488588404)]

[外链图片转存中…(img-C7x6GkqX-1715488588405)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 26
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值