深度遍历讲解Android事件分发机制

本文将以树的深度遍历方式来讲解DOWN事件的分发流程,以单链表的线性遍历方式讲解MOVE、UP事件的分发流程。

1. Android控件对应的多叉树


假设我们有一个这样的场景

屏幕上有一个FrameLayout名叫vp1。vp1有三个子控件vp2、vp3、vp4,它们的类型是FrameLayout。vp2有三个子控件v1、v2、v3。vp3有三个子控件v4、v5、v6。vp4有三个子控件v7、v8、v9。子控件的类型都是View。为了方便起见我们假设这些控件都是铺满屏幕的。我们将vp1控件树翻译成多叉树。

解释一下树节点ft的含义

ft的是ViewGroup类中的mFirtstTouchTarget成员变量的简称。它对应的数据结构是单链表。当DOWN事件在onTouchEvent方法中返回true,会回溯设置父View的ft指针。

举个例子

当DOWN事件从vp1开始分发。 假设DOWN事件在v7的onTouchEvent方法中返回true。 那么v7的父控件vp4的ft会指向v7(可以这样简单理解,实际上指向的是v7封装的一个TouchTarget对象)。同理vp1的ft会指向vp4。所以就生成了一条vp1->vp4->v7的分发路径。这很重要。

DOWN事件的分发是从vp1开始,假设所有的ViewGroup都不拦截事件,所有的View都不处理事件。事件会沿着最后一个子控件做深度遍历分发。以下用intercept代替onInterceptTouchEvent,touch代替onTouchEvent。

调用流程如下

vp1(intercept) -> vp4(intercept) -> v9(touch) -> v8(touch) ->v7 (touch) -> vp4(touch)-> vp3 -> v6(touch) -> v5(touch) -> v4(touch) -> vp3(touch)-> vp2 -> v3(touch) -> v2(touch) -> v1(touch) -> vp2(touch)-> vp1(touch)

这只是DOWN事件分发的一个Case。根据是否拦截,View是否处理事件。它的遍历路径也会不一样。

MOVE、UP事件分发也是从vp1开始,不同于DOWN事件的深度遍历方式,它们是通过ft的分发路径线性遍历。深度遍历是比较耗时的。如果vp1有后代View分发了事件。那么必然会通过ft生成一条分发路径。MOVE事件只需沿着分发路径线性分发就可以了。还是用上面的例子。如果v7分发了DOWN事件。那么MOVE、UP事件的分发即vp1(intercept) -> vp4(intercept) -> v7(touch)

2. 手势事件类型


本文主要讲解四种事件类型,DOWN、MOVE、UP、CANCEL。用户划动手机屏幕然后离开。Android系统首先会触发DOWN事件,紧接着一连串的MOVE事件,以UP事件收场。注意:触摸屏幕,事件之间没有任何依赖。有可能只有其中一种事件被分发。也有可能有多种事件类型被分发

| 事件类型 | 解释 |

| :-- | :-- |

| ACTION_DOWN | 手指按下屏幕 |

| ACTION_MOVE | 手指划动屏幕 |

| ACTION_CANCEL | 事件被取消 |

| ACTION_UP | 手指离开屏幕 |

3. 事件分发涉及到的方法


| 方法名 | 解释 |

| :-- | :-- |

| dispatchTouchEvent | 事件分发逻辑 |

| onInterceptTouchEvent | 是否拦截事件 (ViewGroup专属) |

| onTouchEvent | 是否处理事件 |

  1. dispatchTouchEvent方法是Android系统内部实现的事件分发逻辑。返回值为boolean类型。true表示该View或ViewGroup处理了事件,反之返回false。返回值含义同onTouchEvent的返回值。dispatchTouchEvent与onTouchEvent的区别在于,默认情况下前者的返回值依赖于后者的返回值,而且前者的侧重点在于制定事件分发的流程,后者的侧重点在于View或者ViewGroup是否处理该事件。

  2. onInterceptTouchEvent方法是ViewGroup专属的方法。当返回值为true表示ViewGroup(假设vp1)需要拦截掉该事件。这里有两种情况

2.1 处理DOWN事件时,如果onInterceptTouchEvent返回值为true,那么事件会直接交给vp1的onTouchEvent处理,如果返回false,交由vp1的最后一个View处理。

2.2 处理MOVE、UP事件时,如果vp1.ft为null,此时不会调用vp1的onInterceptTouchEvent方法。如果vp.ft不为null而且vp1的onInterceptTouchEvent方法返回true,那么将会在vp1处生成CANCEL事件交由vp4分发,先后置空vp4,vp1的ft对象,接下来的MOVE、UP事件只会调用vp1的onTouchEvent方法​。

  1. onTouchEvent方法返回值同dispatchTouchEvent方法。如果DOWN事件在某个View的onTouchEvent方法中返回true,那么其它的View的onTouchEvent将没有被执行的机会,换句话说对于同一次事件分发有且仅有一个控件能够处理事件。只有DOWN事件的返回值才有意义。其它类型事件的返回值并不会影响事件分发的流程。我们以v8的onTouchEvent的DOWN事件返回值为例。

3.1. v8 DOWN事件返回true。表示v8处理该事件。在v8分发事件之前应该是 vp1(onInterceptTouchEvent) -> vp4(onInterceptTouchEvent) -> v9(onTouchEvent)-> v8(onTouchEvent)。此时v8返回true。系统会中断vp4的child遍历(不再将事件交由v7分发)。向上回溯设置vp4的ft指向v8,vp1的ft指向vp4。

3.2. v8 DOWN事件返回false。事件继续交由v8的亲兄弟v7分发。

4.DOWN事件的分发流程


DOWN事件分发到vp1,会调用vp1的onInterceptTouchEvent。这里有拦截和不拦截两种情况。

  1. 如果返回true,vp1拦截DOWN事件,DOWN事件直接交由vp1的onTouchEvent处理。

  2. 如果返回false,vp1不拦截DOWN事件,DOWN事件将会交由vp1的最后一个子View分发。即交由vp4分发。

拦截方法以此类推,如果vp4不拦截DOWN事件,将交由v9分发事件。因为v9是View类型。没有拦截方法,所以会直接调用v9的onTouchEvent方法。该方法有处理和不处理两种情况。

  1. 如果返回false,v9不处理事件。那么事件继续向前分发交由v8分发,同理调用v8的onTouchEvent方法,v8不处理事件,继续交由v7处理。v7也不处理,vp4的子View到此遍历完成。此时vp4的ft为空,直接调用vp4的onTouchEvent方法。

  2. 如果返回true,v9处理事件。系统会中断vp4的子View遍历,DOWN事件分发结束。同时往上递归回溯设置父View的ft对象。vp4的ft指向v9,vp1的ft指向vp4。

总结:DOWN事件是通过深度遍历分发事件的。

5. MOVE、UP事件的分发流程


MOVE事件分发到VP1。它与DOWN事件的区别是,它并不一定会调用onInterceptTouchEvent。它只有当vp1的ft不为空时才会调用onInterceptTouchEvent方法,否则会直接拦截掉事件。

  1. 当vp1的ft为空。直接拦截掉MOVE事件。调用VP1的onTouchEvent方法,注意这里onTouchEvent方法的返回值不会影响事件分发的流程。

  2. 当vp1的ft不为空(当有后代View的onTouchEvent方法返回了true)。调用onInterceptTouchEvent方法,如果返回true,同上,直接调换用VP1的onTouchEvent方法。如果返回false,会通过ft的单链表线性分发事件。

UP事件同MOVE事件。这里就不分析了。

总结:MOVE、UP事件是通过线性遍历分发的。

6. CANCEL事件触发时机以及分发流程


前面我们讲到的DOWN、MOVE、UP事件都是由手触摸屏幕产生的。并没有讲到CANCEL是如何产生的。CANCEL不是由手触摸屏幕产生的。它是由系统生成。并且分发给View的。有一种场景会触发系统产生CANCEL事件。

还是上面的控件树。假设手机在屏幕的上半部分,所有的ViewGroup都不拦截事件,V9处理DOWN事件,当划动到屏幕的下半部分时,VP1拦截MOVE事件。当手指从上划动到下面时。系统将在vp1处,产生一个CANCEL事件,交由vp4分发。CANCEL事件的分发也是通过ft线性分发。当ViewGroup分发CANCEL事件后,会将ViewGroup的ft置为空。即将VP1,VP4的ft置为空。

7. 通过实例讲解事件分发流程


本段将通过三个场景实战讲解事件分发流程

1. 所有的View都不分发事件,所有的ViewGroup都不拦截事件

2. 只有v7 onTouchEvent DOWN事件返回true

3. 只有v7处理事件,但是vp1在屏幕下半部分拦截MOVE事件

自定义MyView

class MyView @JvmOverloads constructor(

context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0

) : View(context, attrs, defStyleAttr) {

var name: String = “”

var mOnTouchValue = false//是否分发事件

@RequiresApi(Build.VERSION_CODES.KITKAT)

override fun onTouchEvent(event: MotionEvent): Boolean {

Log.d(“MyView”, “$name onTouchEvent ${MotionEvent.actionToString(event.action)}”)

if (mOnTouchValue) {

return true

}

return super.onTouchEvent(event)

}

}

自定义MyFrameLayout

class MyFrameLayout @JvmOverloads constructor(

context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0

) : FrameLayout(context, attrs, defStyleAttr) {

var name: String = “”

var mOnTouchValue = false//是否分发事件

var mDispatchValueSuper = true //是否调用super.dispatchTouchEvent,如果false 直接return true

var mRegionInterceptor = false//在某个特定区域 拦截事件

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {

return if (mDispatchValueSuper) {

super.dispatchTouchEvent(ev)

} else {

true

}

}

@RequiresApi(Build.VERSION_CODES.KITKAT)

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {

Log.d(“MyFrameLayout”, “$name onInterceptTouchEvent ${MotionEvent.actionToString(ev.action)}”)

if (mRegionInterceptor) {

val touchY = ev?.y

if (touchY != null) {

if (touchY > measuredHeight / 2) {

return true

}

}

}

return super.onInterceptTouchEvent(ev)

}

@RequiresApi(Build.VERSION_CODES.KITKAT)

override fun onTouchEvent(event: MotionEvent): Boolean {

Log.d(“MyFrameLayout”, “$name onTouchEvent ${MotionEvent.actionToString(event.action)}”)

if (mOnTouchValue) {

return true

}

return super.onTouchEvent(event)

}

}

7.1 场景一 所有的View都不分发事件,所有的ViewGroup都不拦截事件

class TouchOneActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

// setContentView(R.layout.activity_touch_one)

//所有的view都不分发事件

val view1 = MyView(this)

val view2 = MyView(this)

val view3 = MyView(this)

val vp2 = MyFrameLayout(this)

vp2.addView(view1)

vp2.addView(view2)

vp2.addView(view3)

val view4 = MyView(this)

val view5 = MyView(this)

val view6 = MyView(this)

val vp3 = MyFrameLayout(this)

vp3.addView(view4)

vp3.addView(view5)

vp3.addView(view6)

val view7 = MyView(this)

val view8 = MyView(this)

val view9 = MyView(this)

val vp4 = MyFrameLayout(this)

vp4.addView(view7)

vp4.addView(view8)

vp4.addView(view9)

val vp1 = MyFrameLayout(this)

vp1.addView(vp2)

vp1.addView(vp3)

vp1.addView(vp4)

setContentView(vp1)

view1.name = “view1”

view2.name = “view2”

view3.name = “view3”

view4.name = “view4”

view5.name = “view5”

view6.name = “view6”

view7.name = “view7”

view8.name = “view8”

view9.name = “view9”

vp1.name =“vp1”

vp2.name =“vp2”

vp3.name =“vp3”

vp4.name =“vp4”

}

}

日志打印如下

Vp1 onInterceptTouchEvent Down

Vp4 onInterceptTouchEvent Down

V9 onTouchEvent Down

V8 onTouchEvent Down

V7 onTouchEvent Down

Vp4 onTouchEvent Down

Vp3 onInterceptTouchEvent Down

V6 onTouchEvent Down

V5 onTouchEvent Down

V4 onTouchEvent Down

Vp3 onTouchEvent Down

Vp2onInterceptTouchEvent Down

V3 onTouchEvent Down

V2 onTouchEvent Down

V1 onTouchEvent Down

Vp2 onTouchEvent Down

Vp1 onTouchEvent Down

分发图如下

7.2 场景二 只有v7 onTouchEvent DOWN事件返回true

class TouchTwoActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_touch_two)

//View 7 分发事件

val view1 = MyView(this)

val view2 = MyView(this)

val view3 = MyView(this)

val vp2 = MyFrameLayout(this)

vp2.addView(view1)

vp2.addView(view2)

vp2.addView(view3)

val view4 = MyView(this)

val view5 = MyView(this)

val view6 = MyView(this)

val vp3 = MyFrameLayout(this)

vp3.addView(view4)

vp3.addView(view5)

vp3.addView(view6)

val view7 = MyView(this)

view7.mOnTouchValue = true

val view8 = MyView(this)

val view9 = MyView(this)

val vp4 = MyFrameLayout(this)

vp4.addView(view7)

vp4.addView(view8)

vp4.addView(view9)

val vp1 = MyFrameLayout(this)

vp1.addView(vp2)

vp1.addView(vp3)

vp1.addView(vp4)

setContentView(vp1)

view1.name = “view1”

view2.name = “view2”

view3.name = “view3”

view4.name = “view4”

view5.name = “view5”

view6.name = “view6”

view7.name = “view7”

view8.name = “view8”

view9.name = “view9”

vp1.name =“vp1”

vp2.name =“vp2”

vp3.name =“vp3”

vp4.name =“vp4”

}

}

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

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

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

img

img

img

img

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

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

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

推荐学习资料


  • 脑图
    360°全方位性能调优

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

…(img-6m5GaNml-1712470402238)]

[外链图片转存中…(img-JDvrCimt-1712470402239)]

[外链图片转存中…(img-ko2INjbj-1712470402239)]

[外链图片转存中…(img-4eSEQ0Ay-1712470402239)]

[外链图片转存中…(img-6bm3BrR9-1712470402240)]

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

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

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

推荐学习资料


  • 脑图
    [外链图片转存中…(img-OdJovcI4-1712470402240)]
    [外链图片转存中…(img-lhLgSfiV-1712470402240)]
    [外链图片转存中…(img-tphDpryp-1712470402240)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值