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

时间: 2021-03-24

Android判断touch事件点是否在view范围内的方法

本文实例讲述了Android判断touch事件点是否在view范围内的方法.分享给大家供大家参考,具体如下: private Rect mChangeImageBackgroundRect = null; private boolean isInChangeImageZone(View view, int x, int y) { if (null == mChangeImageBackgroundRect) { mChangeImageBackgroundRect = new Rect(); }

android事件分发机制的实现原理

android中的事件处理,以及解决滑动冲突问题都离不开事件分发机制,android中的事件流,即MotionEvent都会经历一个从分发,拦截到处理的一个过程.即dispatchTouchEvent(),onInterceptEvent()到onTouchEvent()的一个过程,在dispatchTouchEvent()负责了事件的分发过程,在dispatchTouchEvent()中会调用onInterceptEvent()与onTouchEvent(),如果onInterceptEven

Android View的事件分发详解

1.前言 近两天学习了一下view的事件分发,把自己的理解总结了一遍,只表达了自己认为需要明白的地方,毕竟是菜鸟一枚,不对的地方还请大神们多指教! 2.三个方法 public boolean dispatchTouchEvent(MotionEvent ev) 用于事件的分发,返回结果受以下两个方法的影响,表示是否消耗了事件. public boolean onInterceptTouchEvent(MotionEvent ev) 事件是否被拦截,返回true表示拦截,false表示不拦截 pu

谈谈对Android View事件分发机制的理解

最近因为项目中用到类似一个LinearLayout中水平布局中,有一个TextView和Button,然后对该LinearLayout布局设置点击事件,点击TextView能够触发该点击事件,然而奇怪的是点击Button却不能触发.然后google到了解决办法(重写Button,然后重写其中的ontouchEvent方法,且返回值为false),但是不知道原因,这两天看了几位大神的博客,然后自己总结下. public class MyButton extends Button { private

Android从源码的角度彻底理解事件分发机制的解析(上)

其实我一直准备写一篇关于Android事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了Android事件分发的知识.也有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何使用?为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?为什么图片轮播器里的图片使用Button而不用ImageView?等等--对于这些问题,我并没有给出非常详细的回答,因为我知道如果想要彻底搞明白这些问题,掌握Android事件分发机

解析Android点击事件分发机制

开头说说初衷 网上关于点击事件分发的文章一搜一大堆,标题一看,不是"30分钟让你弄明白XXX"就是"这是讲解XXX最好的文章",满怀憧憬与信心,忍不住兴奋的点进去一看,发现不是代码就全是图,我基本上看完了所有相关的文章,结果硬是看了三个小时也没搞懂.所以最后还是决定自己去试一试,看一看点击事件分发到底是怎么个流程,我写的肯定不会比其他文章好多少,但是呢,带着一个初学者的心,去分析这个东西,自己能弄明白的同时,也让想学习这个的人看了之后有些许收获,那就足够了. 运行的

Android事件分发机制(上) ViewGroup的事件分发

综述 Android中的事件分发机制也就是View与ViewGroup的对事件的分发与处理.在ViewGroup的内部包含了许多View,而ViewGroup继承自View,所以ViewGroup本身也是一个View.对于事件可以通过ViewGroup下发到它的子View并交由子View进行处理,而ViewGroup本身也能够对事件做出处理.下面就来详细分析一下ViewGroup对时间的分发处理. MotionEvent 当手指接触到屏幕以后,所产生的一系列的事件中,都是由以下三种事件类型组成.

Android使用TouchDelegate增加View的触摸范围

本文为大家分享了Android使用TouchDelegate增加View触摸范围的方法,供大家参考,具体内容如下 还不知道TouchDelegate这个东西的可以先看一下API,这里大致说一下它的作用:假设有两个View,分别是v1,v2,我们可以通过v1的setTouchDelegate(bounds, v2)来委派触摸事件,其中bounds是一个Rect.v1中,落在这个范围的TouchEvent都会传给v2. 既然是这样,那我们可以通过设置某个view的parent的touchDelega

Android事件分发机制的详解

Android事件分发机制 我们只考虑最重要的四个触摸事件,即:DOWN,MOVE,UP和CANCEL.一个手势(gesture)是一个事件列,以一个DOWN事件开始(当用户触摸屏幕时产生),后跟0个或多个MOVE事件(当用户四处移动手指时产生),最后跟一个单独的UP或CANCEL事件(当用户手指离开屏幕或者系统告诉你手势(gesture)由于其他原因结束时产生).当我们说到"手势剩余部分"时指的是手势后续的MOVE事件和最后的UP或CANCEL事件. 在这里我也不考虑多点触摸手势(我

Android从源码的角度彻底理解事件分发机制的解析(下)

记得在前面的文章中,我带大家一起从源码的角度分析了Android中View的事件分发机制,相信阅读过的朋友对View的事件分发已经有比较深刻的理解了. 还未阅读过的朋友,请先参考Android从源码的角度彻底理解事件分发机制的解析. 那么今天我们将继续上次未完成的话题,从源码的角度分析ViewGroup的事件分发. 首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子VewGroup,是And

Android AsyncTask完全解析 带你从源码的角度彻底理解

我们都知道,Android UI是线程不安全的,如果想要在子线程里进行UI操作,就需要借助Android的异步消息处理机制.之前我也写过了一篇文章从源码层面分析了Android的异步消息处理机制. 不过为了更加方便我们在子线程中更新UI元素,Android从1.5版本就引入了一个AsyncTask类,使用它就可以非常灵活方便地从子线程切换到UI线程,我们本篇文章的主角也就正是它了. AsyncTask很早就出现在Android的API里了,所以我相信大多数朋友对它的用法都已经非常熟悉.不过今天我

Android View事件分发机制详解

准备了一阵子,一直想写一篇事件分发的文章总结一下,这个知识点实在是太重要了. 一个应用的布局是丰富的,有TextView,ImageView,Button等,这些子View的外层还有ViewGroup,如RelativeLayout,LinearLayout.作为一个开发者,我们会思考,当点击一个按钮,Android系统是怎样确定我点的就是按钮而不是TextView的?然后还正确的响应了按钮的点击事件.内部经过了一系列什么过程呢? 先铺垫一些知识能更加清晰的理解事件分发机制: 1. 通过setC

Android AsyncTask源码分析

Android中只能在主线程中进行UI操作,如果是其它子线程,需要借助异步消息处理机制Handler.除此之外,还有个非常方便的AsyncTask类,这个类内部封装了Handler和线程池.本文先简要介绍AsyncTask的用法,然后分析具体实现. 基本用法 AsyncTask是一个抽象类,我们需要创建子类去继承它,并且重写一些方法.AsyncTask接受三个泛型参数: Params: 指定传给任务执行时的参数的类型 Progress: 指定后台任务执行时将任务进度返回给UI线程的参数类型 Re

Android ViewPager源码详细分析

1.问题 由于Android Framework源码很庞大,所以读源码必须带着问题来读!没有问题,创造问题再来读!否则很容易迷失在无数的方法与属性之中,最后无功而返. 那么,关于ViewPager有什么问题呢? 1). setOffsreenPageLimit()方法是如何实现页面缓存的? 2). 在布局文件中,ViewPager布局内部能否添加其他View? 3). 为什么ViewPager初始化时,显示了一个页面却不会触发onPageSelected回调? 问题肯定不止这三个,但是有这三个问

Android View源码解读 DecorView与ViewRootImpl浅谈

前言 对于Android开发者来说,View无疑是开发中经常接触的,包括它的事件分发机制.测量.布局.绘制流程等,如果要自定义一个View,那么应该对以上流程有所了解.研究.本系列文章将会为大家带来View的工作流程详细解析.在深入接触View的测量.布局.绘制这三个流程之前,我们从Activity入手,看看从Activity创建后到View的正式工作之前,所要经历的步骤.以下源码均取自Android API 21. 从setContentView说起 一般地,我们在Activity中,会在on

Android DownloadProvider 源码详解

Android DownloadProvider 源码分析: Download的源码编译分为两个部分,一个是DownloadProvider.apk, 一个是DownloadProviderUi.apk. 这两个apk的源码分别位于 packages/providers/DownloadProvider/ui/src packages/providers/DownloadProvider/src 其中,DownloadProvider的部分是下载逻辑的实现,而DownloadProviderUi

从源码角度简单看StringBuilder和StringBuffer的异同(全面解析)

概述 StringBuilder和StringBuffer是两个容易混淆的概念,本文从源码入手,简单看二者的异同. 容易知道的是,这两者有一个是线程安全的,而且线程安全的那个效率低. java doc里面的说明 java doc是写源码的人写的注释,先看java doc. StringBuilder A mutable sequence of characters. This class provides an API compatible with StringBuffer, but with

Android View 事件分发机制详解

Android开发,触控无处不在.对于一些 不咋看源码的同学来说,多少对这块都会有一些疑惑.View事件的分发机制,不仅在做业务需求中会碰到这些问题,在一些面试笔试题中也常有人问,可谓是老生常谈了.我以前也看过很多人写的这方面的文章,不是说的太啰嗦就是太模糊,还有一些在细节上写的也有争议,故再次重新整理一下这块内容,十分钟让你搞明白View事件的分发机制. 说白了这些触控的事件分发机制就是弄清楚三个方法,dispatchTouchEvent(),OnInterceptTouchEvent(),o