Android 面试:事件分发8连问,android开发基础入门

public boolean dispatchTouchEvent(MotionEvent event) {

boolean isConsume = false;

if (isViewGroup) {

if (onInterceptTouchEvent(event)) {

isConsume = super.dispatchTouchEvent(event);

}

}

return isConsume;

}

如果是ViewGroup,会先执行到onInterceptTouchEvent方法判断是否拦截,如果拦截,则执行父类ViewdispatchTouchEvent方法。

3.1.3 ViewGroup不拦截会发生什么?

如果ViewGroup不拦截,则会传递到子View

if (!canceled && !intercepted) {

if (actionMasked == MotionEvent.ACTION_DOWN

|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)

|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

final int childrenCount = mChildrenCount;

//遍历子View

if (newTouchTarget == null && childrenCount != 0) {

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

final int childIndex = getAndVerifyPreorderedIndex(

childrenCount, i, customOrder);

final View child = getAndVerifyPreorderedView(

preorderedList, children, childIndex);

//2.判断事件坐标

if (!child.canReceivePointerEvents()

|| !isTransformedTouchPointInView(x, y, child, null)) {

ev.setTargetAccessibilityFocus(false);

continue;

}

//3.传递事件

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

newTouchTarget = addTouchTarget(child, idBitsToAssign);

alreadyDispatchedToNewTouchTarget = true;

break;

}

}

}

}

}

private boolean dispatchTransformedTouchEvent(View child) {

if (child == null) {

handled = super.dispatchTouchEvent(event);

} else {

handled = child.dispatchTouchEvent(event);

}

}

如果不拦截,ViewGroup内主要做以下几件事

1.遍历当前ViewGroup的所有子View

2.判断当前View是否在当前子View的坐标范围内,不在范围内不能接收事件,直接跳过

3.利用dispatchTransformedTouchEvent,如果返回true,则通过addTouchTargetmFirstTouchTarget赋值

4.dispatchTransformedTouchEvent做的主要就是两个事,如果child不为null,则事件分发到child,否则调用super.dispatchTouchEvent,并最终返回结果

5.mFirstTouchTarget是单链表结构,记录消费链,但是在单点触控的时候这个特性没有用上,只是一个普通的TouchTarget对象

3.2 子View是否拦截

public boolean dispatchTouchEvent(MotionEvent event) {

if (onFilterTouchEventForSecurity(event)) {

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;

}

ViewdiapatchTouchEvent逻辑比较简单

1.如果设置了setOnTouchListener并且返回为true,那么onTouchEvent就不再执行

2.否则执行onTouchEvent,我们常用的OnClickListenr就是在onTouchEvent里触发的

所以默认情况下会直接执行onTouchEvent,如果我们设置了setOnClickListener或者setLongClickListener,都会正常触发

3.2.1 如果子View消费事件会怎么样?

上面说了,如果子View消费事件,即dispatchTouchEvent方法返回true

表示这个事件我处理了,那么事件从此结束,ViewGroupdispatchTouchEvent也返回true

最后回到ActivitydispatchTouchEvent,也是直接返回true

//Activity.java

public boolean dispatchTouchEvent(MotionEvent ev) {

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

onUserInteraction();

}

if (getWindow().superDispatchTouchEvent(ev)) {

return true;

}

return onTouchEvent(ev);

}

小结:如果子View消费事件的话,事件就此结束了

3.2.2 如果子View不消费事件会怎么样?

View不拦截事件,那么mFirstTouchTarget就为null,退出循环后,调用了dispatchTransformedTouchEvent方法。

if (mFirstTouchTarget == null) {

handled = dispatchTransformedTouchEvent(ev, canceled, null,

TouchTarget.ALL_POINTER_IDS);

}

小结一下:

1.子View不拦截事件,就回调到了dispatchTransformedTouchEvent

2.然后就调到了super.dispatchTouchEvent

3.那么接下来ViewGroup就跟子View的逻辑一样了,默认执行onTouchEvent,如果设置了setOnTouchLister则执行onTouch

3.3 如果ViewGroup与子View都不拦截会怎么样

如果ViewGroup与子View都不拦截,即mFirstTouchTarget == null,dispatchTouchEvent也返回false

再看看Activity的源码

public boolean dispatchTouchEvent(MotionEvent ev) {

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

onUserInteraction();

}

if (getWindow().superDispatchTouchEvent(ev)) {

return true;

}

return onTouchEvent(ev);

}

答案很明显:会执行ActivityonTouchEvent方法

3.4 后续事件如何分发?

事件分发的处理者已经找到了,看起来任务已经完成了。

但其实事件分发是包括ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL的一系列事件,我们上面分析的都是Action_DOWN的过程

后续事件如何处理?

public boolean dispatchTouchEvent(MotionEvent ev) {

if (!canceled && !intercepted) {

if (actionMasked == MotionEvent.ACTION_DOWN

|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)

|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

//1.遍历子View

//2.判断是否在坐标范围

//3.分发事件,给mFirstTouchTarget赋值

//4.如果分发成功,alreadyDispatchedToNewTouchTarget赋值为true

}

if (mFirstTouchTarget == null) {

handled = dispatchTransformedTouchEvent(ev, canceled, null,

TouchTarget.ALL_POINTER_IDS);

} else {

TouchTarget target = mFirstTouchTarget;

while (target != null) {

final TouchTarget next = target.next;

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {

handled = true;

} else {

if (dispatchTransformedTouchEvent(ev, cancelChild,

target.child, target.pointerIdBits)) {

handled = true;

}

}

predecessor = target;

target = next;

}

}

}

从上可以看出

1.后续的事件不会走对子View的循环判断的方法,因为已经找到了目标View,直接通过mFirstTouchTarget分发

2.如果某个View开始处理拦截事件,后续事件序列只能由它处理

3.5 小结

  • 事件分发的本质就是一个递归方法,通过往下传递,调用dispatchTouchEvent方法,找到事件的处理者,这也就是项目中常见的责任链模式。

  • 在分发过程中,ViewGroup通过onInterceptTouchEvent判断是否拦截事件

  • 在分发过程中,View的默认通过onTouchEvent处理事件

  • 如果底层View不消费,则默认一步步往上执行父元素onTouchEvent方法。

  • 如果所有ViewonTouchEvent方法都返回false,则最后会执行到ActivityonTouchEvent方法,事件分发也就结束了。

4 滑动冲突解决


我们在开发中经常会碰到滑动冲突的问题,比如一个页面同时有横向与竖向两个方向的滑动,这个时候就需要根据情况在Action_MOVE时对事件进行判断和拦截

常见的滑动冲突解决方法有两种:

1.外部拦截法

2.内部拦截法

4.1 外部拦截法

外部拦截法的原理很简单,就是通过我们上面分析的onInterceptTouchEvent进行

外部拦截法的模板代码如下:

//外部拦截法:父view.java

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

boolean intercepted = false;

//父view拦截条件

boolean parentCanIntercept;

switch (ev.getActionMasked()) {

case MotionEvent.ACTION_DOWN:

intercepted = false;

break;

case MotionEvent.ACTION_MOVE:

if (parentCanIntercept) {

intercepted = true;

} else {

intercepted = false;

}

break;

case MotionEvent.ACTION_UP:

intercepted = false;

break;

}

return intercepted;

}

但是这种方式会带来一个问题,如果ACTION_DOWN交给了子View处理,那么后续事件应该会直接被分发给这个view呀,为什么还能被父View拦截的?

我们再来看看dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent ev) {

final boolean intercepted;

if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {

//1.判断拦截

intercepted = onInterceptTouchEvent(ev);

}

// Dispatch to touch targets.

if (mFirstTouchTarget == null) {

//4.后续事件就直接交给ViewGroup处理了

handled = dispatchTransformedTouchEvent(ev, canceled, null,

TouchTarget.ALL_POINTER_IDS);

} else {

while (target != null) {

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {

handled = true;

} else {

//2.cancelChild为ture

final boolean cancelChild = resetCancelNextUpFlag(target.child)

|| intercepted;

if (dispatchTransformedTouchEvent(ev, cancelChild,

target.child, target.pointerIdBits)) {

handled = true;

}

if (cancelChild) {

if (predecessor == null) {

//3.mFirstTouchTarget被置为null

mFirstTouchTarget = next;

} else {

predecessor.next = next;

}

target.recycle();

target = next;

continue;

}

}

}

}

}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,

View child, int desiredPointerIdBits) {

final boolean handled;

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;

}

}

如上可知:

1.首先通过onInterceptTouchEvent方法拦截事件

2.interceptedtrue导致cancelChild也为truedispatchTransformedTouchEvent方法传递Action_CANCEL给子View

3.cancelChild后将mFirstTouchTarget置为空

4.mFirstTouchTarget为空后,后续的事件都由ViewGroup处理了

综上就是外部拦截法能成功的原因

4.2 内部拦截法

接下来看下内部拦截法的模板代码

//父view.java

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {

return false;

} else {

return true;

}

}

//子view.java

@Override

public boolean dispatchTouchEvent(MotionEvent event) {

//父view拦截条件

boolean parentCanIntercept;

switch (event.getActionMasked()) {

case MotionEvent.ACTION_DOWN:

getParent().requestDisallowInterceptTouchEvent(true);

break;

case MotionEvent.ACTION_MOVE:

getParent().requestDisallowInterceptTouchEvent(!parentCanIntercept);

break;

case MotionEvent.ACTION_UP:

break;

}

return super.dispatchTouchEvent(event);

}

内部拦截法是将主动权交给子View,如果子View需要事件就直接消耗,否则交给父容器处理

内部拦截法主要通过requestDisallowInterceptTouchEvent方法控制

我们看下为什么调用这个方式可以实现内部拦截

@Override

public boolean dispatchTouchEvent(MotionEvent ev) {

final boolean intercepted;

//只有ActionDown或者mFirstTouchTarget为空时才会判断是否拦截

if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

if (!disallowIntercept) {

intercepted = onInterceptTouchEvent(ev);

}

}

}

如上所示,原理很简单

1.子View通过requestDisallowInterceptTouchEvent控制mGroupFlags的值,从而控制disallowIntercept的值

2.disallowIntercepttrue时就不会走到onInterceptTouchEvent,外部也就无法拦截了,当需要外部处理时,将disallowIntercept置为false即可

总结


本文详细总结了事件分发机制从屏幕到View的详细过程,下面列出几个问题供读者参考,方便读者判断是否真正掌握了这个知识点

  • 1.简单描述下事件是怎么从屏幕传递到View

  • 2.事件分发过程中有几次责任链分发?

  • 3.为什么事件分发从DecorView -> Activity -> PhoneWindow -> DecorView

  • 4.滑动冲突有几种解决方法?分别介绍一下

  • 5.如果只在onInterceptTouchEventACTION_MOVE中拦截事件,说一下从ViewGroupView的各个Action是如何传递的

  • 6.点击ViewGroup中的一个View,然后手指移动到其他地方然后抬起,事件是如何分发的

  • 7.ViewOnTouchOnTouchEvent有什么关系?OnTouchOnClick事件呢?

  • 8.手写一下长按事件的伪代码

最后

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

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

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

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

最后

其实要轻松掌握很简单,要点就两个:

  1. 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
  2. 多练。 (视频优势是互动感强,容易集中注意力)

你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

阿里P7Android高级教程

下面资料部分截图,诚意满满:特别适合有3-5年开发经验的Android程序员们学习。

附送高清脑图,高清知识点讲解教程,以及一些面试真题及答案解析。送给需要的提升技术、近期面试跳槽、自身职业规划迷茫的朋友们。

Android核心高级技术PDF资料,BAT大厂面试真题解析;

外链图片转存中…(img-etHCfglO-1711626021931)]
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-LHR8slDE-1711626021931)]

最后

其实要轻松掌握很简单,要点就两个:

  1. 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
  2. 多练。 (视频优势是互动感强,容易集中注意力)

你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

阿里P7Android高级教程

下面资料部分截图,诚意满满:特别适合有3-5年开发经验的Android程序员们学习。

[外链图片转存中…(img-Tqt0hbLl-1711626021931)]

附送高清脑图,高清知识点讲解教程,以及一些面试真题及答案解析。送给需要的提升技术、近期面试跳槽、自身职业规划迷茫的朋友们。

Android核心高级技术PDF资料,BAT大厂面试真题解析;
[外链图片转存中…(img-S5CgRL84-1711626021931)]

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值