如何自己实现Android View Touch事件分发流程
作者:CAZ 发布时间:2023-10-19 14:07:30
目录
MotionEvent
View
ViewGroup
事件拦截
寻找目标视图,分发ACTION_DOWN
分发除ACTION_DOWN外的其他事件
使用
总结
Android Touch事件分发是Android UI中的重要内容,Touch事件从驱动层向上,经过InputManagerService,WindowManagerService,ViewRootImpl,Window,到达DecorView,经View树分发,最终被消费。
本文尝试通过对其中View部分的事件分发,也是与日常开发联系最紧密的部分,进行重写。说是重写,其实是对Android该部分源码进行大幅精简而不失要点,且能够独立运行,以一窥其全貌,而不陷入到源码繁杂的细节中。
以下类均为自定义类,而非Android同名原生类。
MotionEvent
class MotionEvent {
companion object {
const val ACTION_DOWN = 0
const val ACTION_MOVE = 1
const val ACTION_UP = 2
const val ACTION_CANCEL = 3
}
var x = 0
var y = 0
var action = 0
override fun toString(): String {
return "MotionEvent(x=$x, y=$y, action=$action)"
}
}
首先定义MotionEvent,这里将触摸事件action减少为最常用的4种,同时只支持单指操作,因此action取值仅支持4个常量。并且为了简化后续的位置计算,x和y表示的是绝对坐标(相当于getRawX()与getRawY()),而非相对坐标。
View
open class View {
var left = 0
var right = 0
var top = 0
var bottom = 0//1
var enable = true
var clickable = false
var onTouch: ((View, MotionEvent) -> Boolean)? = null
var onClick: ((View) -> Unit)? = null//3
set(value) {
field = value
clickable = true
}
private var downed = false
open fun layout(l: Int, t: Int, r: Int, b: Int) {
left = l
top = t
right = r
bottom = b
}//2
open fun onTouchEvent(ev: MotionEvent): Boolean {
var handled: Boolean
if (enable && clickable) {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
downed = true
}
MotionEvent.ACTION_UP -> {
if (downed && ev.inView(this)) {//7
downed = false
onClick?.invoke(this)
}
}
MotionEvent.ACTION_MOVE -> {
if (!ev.inView(this)) {//7
downed = false
}
}
MotionEvent.ACTION_CANCEL -> {
downed = false
}
}
handled = true
} else {
handled = false
}
return handled
}//5
open fun dispatchTouchEvent(ev: MotionEvent): Boolean {
var result = false
if (onTouch != null && enable) {
result = onTouch!!.invoke(this, ev)
}
if (!result && onTouchEvent(ev)) {
result = true
}
return result
}//4
}
fun MotionEvent.inView(v: View) = v.left <= x && x <= v.right && v.top <= y && y <= v.bottom//6
接下来定义View。(1)定义了View的位置,这里同样表示绝对坐标,而不是相对于父View的位置。(2)同时使用layout方法传递位置,因为我们的重点是View的事件分发而不是其布局与绘制,因此只定义了layout。(3)触摸回调这里直接使用函数类型定义,(4)dispatchTouchEvent先处理了onTouch回调,如果未回调,则调用onTouchEvent,可见二者的优先级。(5)onTouchEvent则主要处理了onClick回调,虽然真实源码中对点击的判断更为复杂,但实际效果与此处是一致的,(6)使用扩展函数来确定事件是否发生在View内部,(7)两处调用配合downed标记确保ACTION_MOVE与ACTION_UP发生在View内才被识别为点击。至于长按等其他手势的监听,因为较为繁琐,这里就不再实现。
ViewGroup
open class ViewGroup(private vararg val children: View) : View() {//1
private var mFirstTouchTarget: View? = null
open fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return false
}//2
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {//3
val intercepted: Boolean
var handled = false
if (ev.action == MotionEvent.ACTION_DOWN) {
mFirstTouchTarget = null
}//4
if (ev.action == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
intercepted = onInterceptTouchEvent(ev)//5
} else {
intercepted = true//6
}
val canceled = ev.action == MotionEvent.ACTION_CANCEL
var alreadyDispatchedToNewTouchTarget = false
if (!intercepted) {
if (ev.action == MotionEvent.ACTION_DOWN) {//7
for (child in children.reversed()) {//8
if (ev.inView(child)) {//9
if (dispatchTransformedTouchEvent(ev, false, child)) {//10
mFirstTouchTarget = child
alreadyDispatchedToNewTouchTarget = true//12
}
break
}
}
}
}
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null)//17
} else {
if (alreadyDispatchedToNewTouchTarget) {//13
handled = true
} else {
val cancelChild = canceled || intercepted//14
if (dispatchTransformedTouchEvent(ev, cancelChild, mFirstTouchTarget)) {
handled = true
}
if (cancelChild) {
mFirstTouchTarget = null//16
}
}
}
if (canceled || ev.action == MotionEvent.ACTION_UP) {
mFirstTouchTarget = null
}//4
return handled
}
private fun dispatchTransformedTouchEvent(ev: MotionEvent, cancel: Boolean, child: View?): Boolean {
if (cancel) {
ev.action = MotionEvent.ACTION_CANCEL//15
}
val oldAction = ev.action
val handled = if (child == null) {
super.dispatchTouchEvent(ev)//18
} else {
child.dispatchTouchEvent(ev)//11
}
ev.action = oldAction
return handled
}
}
最后来实现ViewGroup:(1)子View这里通过构造函数传入, 而不再提供addView等方法,(2)onInterceptTouchEvent简单返回false,主要通过子类继承来修改返回,(3)dispatchTouchEvent是整个实现中最主要的逻辑,来详细解释,这里的实现只包含对单指Touch事件的处理,并且不包含requestDisallowInterceptTouchEvent的情况。
(4)源码中开头和结尾处有清理字段与标记的方法,用于在一个事件序列(由ACTION_DOWN开始,经过若干ACTION_MOVE等,最终以ACTION_UP结束,即整个触摸过程)开头和结束时清理旧数据,这里简化为了将我们类中的唯一字段mFirstTouchTarget(表示整个事件序列的目标视图,在源码中,此变量类型为TouchTarget,实现为一个View的链表节点,以此来支持多指触摸,这里简化为View)置空。
接下来将该方法分为几部分来介绍:
事件拦截
(5)表示在一个事件序列的开始或者已经找到了目标视图的情况下,才需要调用onInterceptTouchEvent判断本ViewGroup是否拦截事件。(6)表示如果ACTION_DOWN没有视图消费,则之后的事件将被拦截,且拦截的View是View树中的顶层View,即Android中的DecorView。
寻找目标视图,分发ACTION_DOWN
(7)当ACTION_DOWN事件未被拦截,(8)则反向遍历子View数组,(9)寻找ACTION_DOWN事件落在其中的View,(10)并将ACTION_DOWN事件传递给该子View,这一步调用了dispatchTransformedTouchEvent,该方法将源码中的方法简化为了三参数,方法名中的Transformed表示,会将Touch事件进行坐标系的变换,而这里为了简化使用的坐标是绝对的,因此不需要变换。此时会调用dispatchTransformedTouchEvent中(11)处向子View分发ACTION_DOWN,child即mFirstTouchTarget。
分发除ACTION_DOWN外的其他事件
(12)对于ACTION_DOWN事件,会将alreadyDispatchedToNewTouchTarget置位,(13)此时会会进入if块,而非ACTION_DOWN事件会进入else块。(14)当该事件是ACTION_CANCEL或者事件被拦截,则在调用dispatchTransformedTouchEvent的(15)处后,将事件修改为ACTION_CANCEL,然后调用(11),将ACTION_CANCEL分发给子View,(16)同时将mFirstTouchTarget置空。当事件序列中的下个事件到来时,会进入(17)处,即最终调用(18),调用上节中View的事件处理,即ViewGroup消费该事件,消费该事件的ViewGroup即拦截了非ACTION_DOWN事件并向子View分发ACTION_CANCEL的ViewGroup。
使用
至此,实现了MotionEvent,View,与ViewGroup,来进行一下验证。
定义三个子类:
class VG1(vararg children: View) : ViewGroup(*children)
class VG2(vararg children: View) : ViewGroup(*children)
class V : View() {
override fun onTouchEvent(ev: MotionEvent): Boolean {
println("V onTouchEvent $ev")
return super.onTouchEvent(ev)
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
println("V dispatchTouchEvent $ev")
return super.dispatchTouchEvent(ev)
}
}
定义一个事件发生方法,由该方法来模拟Touch事件的轨迹与action:
fun produceEvents(startX: Int, startY: Int, endX: Int, endY: Int, stepNum: Int): List<MotionEvent> {
val list = arrayListOf<MotionEvent>()
val stepX = (endX - startX) / stepNum
val stepY = (endY - startY) / stepNum
for (i in 0..stepNum) {
when (i) {
0 -> {
list.add(MotionEvent().apply {
action = MotionEvent.ACTION_DOWN
x = startX
y = startY
})
}
stepNum -> {
list.add(MotionEvent().apply {
action = MotionEvent.ACTION_UP
x = endX
y = endY
})
}
else -> {
list.add(MotionEvent().apply {
action = MotionEvent.ACTION_MOVE
x = stepX * i + startX
y = stepY * i + startY
})
}
}
}
return list
}
接下来就可以验证了,在Android中事件由驱动层一步步传递至View树的顶端,这里我们定义一个三层的布局page,(1)直接将事件序列遍历调用顶层ViewGroup的dispatchTouchEvent来开启事件分发。
fun main() {
val page = VG1(
VG2(
V().apply { layout(0, 0, 100, 100); onClick = { println("Click in V") } }//2
).apply { layout(0, 0, 200, 200) }
).apply { layout(0, 0, 300, 300) }//3
val events = produceEvents(50, 50, 90, 90, 5)
events.forEach {
page.dispatchTouchEvent(it)//1
}
}
程序可以正常执行,打印如下:
V dispatchTouchEvent MotionEvent(x=50, y=50, action=0)
V onTouchEvent MotionEvent(x=50, y=50, action=0)
V dispatchTouchEvent MotionEvent(x=58, y=58, action=1)
V onTouchEvent MotionEvent(x=58, y=58, action=1)
V dispatchTouchEvent MotionEvent(x=66, y=66, action=1)
V onTouchEvent MotionEvent(x=66, y=66, action=1)
V dispatchTouchEvent MotionEvent(x=74, y=74, action=1)
V onTouchEvent MotionEvent(x=74, y=74, action=1)
V dispatchTouchEvent MotionEvent(x=82, y=82, action=1)
V onTouchEvent MotionEvent(x=82, y=82, action=1)
V dispatchTouchEvent MotionEvent(x=90, y=90, action=2)
V onTouchEvent MotionEvent(x=90, y=90, action=2)
Click in V
因为我们在(2)增加了点击事件,以上表示了一次点击的事件分发。也可以重写修改page布局(3)来查看其它情景下的事件分发流程,或者重写VG1,VG2的方法,增加打印并查看。
总结
通过对Android 源码的整理,用约150行代码就能实现了一个简化版的Android Touch View事件分发,虽然为了代码结构的简洁舍弃了部分功能,但整个流程与Android Touch View事件分发是一致的,能够更方便理解这套机制。
来源:https://juejin.cn/post/6904069282657730573


猜你喜欢
- 文章描述在前一篇写了如何将一张GIF动态图分解成一帧一帧的图片,这一篇我们就把喝进去的一瓢水给还回去。即把一张又一张的图片去拼合成一张GIF
- 以下共有4个函数分别是:1.从剪切板获得文字。2.将字符串复制到剪切板。3.从剪切板获得图片。4.复制图片到剪切板。/** * 从剪切板获得
- C#调用C++DLL传递结构体数组的终极解决方案在项目开发时,要调用C++封装的DLL,普通的类型C#上一般都对应,只要用DllImport
- 目录1.问题引出2.解决办法3.另外一种自定义序列化机制(介绍Externalizable)1.问题引出在某些情况下,我们可能不想对于一个对
- 本文实例讲述了Java实现整数分解质因数的方法。分享给大家供大家参考,具体如下:题目内容:每个非素数(合数)都可以写成几个素数(也可称为质数
- 本文实例讲述了Android编程实现状态保存的方法。分享给大家供大家参考,具体如下:1、当我们正在发短信的时候,已经写了几百字了,这时突然来
- 本文实例讲述了C#针对xml的基本操作及保存配置文件应用,分享给大家供大家参考。具体方法如下:引言:这里首先介绍了xml的基本操作,后面写了
- 前言Spring 5发布有两年了,随Spring 5一起发布了一个和Spring WebMvc同级的Spring WebFlux。这是一个支
- 一、新建一个as项目,再新建一个model模块然后再app中的build.gradle中添加model的依赖。然后编译项目。二、编译完成后,
- 目录一、泛型类型二、为什么需要泛型三、类型擦除四、类型擦除的后遗症五、Kotlin 泛型六、上界约束七、类型通配符 & 星号投影八、
- 1. 父工程构建1.1 Maven项目搭建环境版本JDK1.8Maven3.6+Maven模板maven-archetype-size删除父
- 一、什么是加壳?加壳是在二进制的程序中植入一段代码,在运行的时候优先取得程序的控制权,做一些额外的工作。大多数病毒就是基于此原理。二、加壳作
- annotation就是注解的意思,在我们使用的 * 时,可以通过业务层添加的某个注解,对业务方法进行拦截,之前我们在进行统一方法拦截时使用
- 1. 概述官方JavaDocsApi: javax.swing.JPanelJPanel,面板。JPanel 是在开发中使用频率非常高的一般
- Spring3中加强了注解的使用,其中计划任务也得到了增强,现在创建一个计划任务只需要两步就完成了:创建一个Java类,添加一个无参无返回值
- 1 基本概念ThreadLocal类提供了线程局部变量。这些变量与普通变量的不同之处在于,每个访问一个变量(通过其get或set方法)的线程
- 目录一、什么是RPC?二、实现RPC需要解决那些问题?1. 约定通信协议格式2. 序列化方式3. TCP粘包、拆包4. 网络通信框架的选择三
- Strut2判断是否是AJAX调用1. AJAX与传统Form表单实际上,两者一般都是通过HTTP的POST请求。区
- 本文实例为大家分享了java导出csv格式文件的具体代码,供大家参考,具体内容如下导出csv格式文件的本质是导出以逗号为分隔的文本数据imp
- 本文讲述了使用Qt5.3.0开发Android应用的方法,由于官方资料较少,此处记录开发过程遇到的问题及解决方法。具体步骤如下:1.Andr