我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

如何自己实现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<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事件分发流程的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android自定义View事件分发流程详解

这篇文章主要为大家介绍了Android自定义View事件分发流程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-02-02

如何实现Android触摸事件分发的原理分析

如何实现Android触摸事件分发的原理分析,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。一:前言最近在学Android的触摸事件分发,我觉得网上说的太杂太乱,而且有很多博客都
2023-06-26

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录