欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

如何自己实现Android View Touch事件分发流程

程序员文章站 2022-03-24 08:21:40
android touch事件分发是android ui中的重要内容,touch事件从驱动层向上,经过inputmanagerservice,windowmanagerservice,viewroot...

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

以上就是如何自己实现android view touch事件分发流程的详细内容,更多关于实现android view touch事件分发流程的资料请关注其它相关文章!