android相对布局的事件分发,如何自己实现Android View Touch事件分发流程

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 {

val list = arrayListOf()

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事件分发是一致的,能够更方便理解这套机制。

以上就是如何自己实现Android View Touch事件分发流程的详细内容,更多关于实现Android View Touch事件分发流程的资料请关注脚本之家其它相关文章!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值