在 Android 开发中,触摸事件的处理是交互体验的核心。一个点击、一个滑动,背后都是一套完整的事件分发链路。本文从 MotionEvent 的基础概念出发,逐步深入到 Activity、ViewGroup、View 的三层分发机制,最后通过实战案例理解事件拦截与消费的边界。
MotionEvent 封装了触摸事件的所有信息:
funonTouchEvent(event:MotionEvent):Boolean{
when(event.actionMasked){
MotionEvent.ACTION_DOWN->{
// 手指按下
valx=event.x
valy=event.y
}
MotionEvent.ACTION_MOVE->{
// 手指移动
}
MotionEvent.ACTION_UP->{
// 手指抬起
}
MotionEvent.ACTION_CANCEL->{
// 事件被取消(通常是被父布局拦截)
}
}
returntrue
}一个完整的触摸操作由一个事件序列组成:
ACTION_DOWN → ACTION_MOVE → ACTION_MOVE → ... → ACTION_UP核心规则: - 事件序列从 ACTION_DOWN 开始,到 ACTION_UP 或 ACTION_CANCEL 结束 - 同一事件序列中的所有事件会分发给同一个 View 处理 - 一旦某个 View 消费了 ACTION_DOWN,后续事件会继续分发给它
触摸事件的分发遵循严格的层级链路:
触摸屏幕
↓
Activity.dispatchTouchEvent()
↓
Window → DecorView
↓
根 ViewGroup.dispatchTouchEvent()
↓
ViewGroup.onInterceptTouchEvent() [是否拦截?]
↓(不拦截)
子 View.dispatchTouchEvent()
↓
View.onTouchEvent() [是否消费?]方法 | 作用对象 | 返回值含义 |
|---|---|---|
dispatchTouchEvent() | Activity / ViewGroup / View | true 表示事件被消费,不再继续分发 |
onInterceptTouchEvent() | 仅 ViewGroup | true 表示拦截事件,自己处理;false 表示不拦截,继续分发给子 View |
onTouchEvent() | ViewGroup / View | true 表示消费事件,后续事件会继续分发给自己;false 表示不消费,事件回传给父布局 |
overridefundispatchTouchEvent(ev:MotionEvent):Boolean{
varhandled=false
// 1. 判断是否拦截
valintercepted=if(ev.actionMasked==MotionEvent.ACTION_DOWN){
onInterceptTouchEvent(ev)
}else{
// 如果之前已经有子 View 处理事件序列,检查是否中途拦截
if(mFirstTouchTarget!=null){
onInterceptTouchEvent(ev)
}else{
true// 没有子 View 处理,自己消费
}
}
// 2. 如果不拦截,分发给子 View
if(!intercepted&&ev.actionMasked==MotionEvent.ACTION_DOWN){
for(childinchildren.reversed()){
if(child.dispatchTouchEvent(ev)){
mFirstTouchTarget=child// 记录消费事件的子 View
handled=true
break
}
}
}
// 3. 如果没有子 View 处理,自己处理
if(mFirstTouchTarget==null){
handled=super.dispatchTouchEvent(ev)// 调用自己的 onTouchEvent
}
returnhandled
}关键点: - mFirstTouchTarget:记录消费 ACTION_DOWN 的子 View,后续事件会直接分发给它 - 子 View 按倒序遍历(后添加的 View 在上层,优先接收事件) - 如果子 View 在 ACTION_DOWN 时返回 false,后续事件不会再分发给它
overridefunonInterceptTouchEvent(ev:MotionEvent):Boolean{
returnwhen(ev.actionMasked){
MotionEvent.ACTION_DOWN->{
// DOWN 事件通常不拦截,给子 View 机会
false
}
MotionEvent.ACTION_MOVE->{
// MOVE 事件可以根据滑动距离判断是否拦截
valdx=ev.x-mLastX
valdy=ev.y-mLastY
abs(dy)>abs(dx)&&abs(dy)>mTouchSlop// 垂直滑动超过阈值,拦截
}
else->false
}
}典型场景: - ScrollView 嵌套 Button:ScrollView 在 ACTION_MOVE 时判断滑动方向,决定是否拦截 - SwipeRefreshLayout:下拉刷新时拦截垂直滑动事件
overridefunonTouchEvent(event:MotionEvent):Boolean{
when(event.actionMasked){
MotionEvent.ACTION_DOWN->{
mPressed=true
returntrue// 消费 DOWN 事件,后续事件会继续分发
}
MotionEvent.ACTION_MOVE->{
if(isOutOfBounds(event)){
mPressed=false
}
returntrue
}
MotionEvent.ACTION_UP->{
if(mPressed){
performClick()// 触发点击
}
mPressed=false
returntrue
}
MotionEvent.ACTION_CANCEL->{
mPressed=false
returnfalse
}
}
returnfalse
}关键点: - 如果在 ACTION_DOWN 返回 false,后续事件不会再分发给这个 View - ACTION_CANCEL 表示事件被父布局拦截,需要清理状态
View 的事件处理有多个入口,优先级为:
OnTouchListener.onTouch()
↓(返回 false)
View.onTouchEvent()
↓(返回 false)
OnClickListener.onClick() [仅在 ACTION_UP 时触发]示例:
button.setOnTouchListener{v,event->
// 优先级最高,可以拦截点击事件
if(event.actionMasked==MotionEvent.ACTION_DOWN){
Log.d("TAG","Touch down, 不触发 onClick")
return@setOnTouchListenertrue
}
false
}
button.setOnClickListener{
Log.d("TAG","Clicked")// 不会执行
}RecyclerView (垂直滑动)
└── Item
└── HorizontalScrollView (横向滑动)
└── 子项问题:当用户横向滑动 HorizontalScrollView 时,RecyclerView 可能会拦截事件导致滑动失效。
classMyHorizontalScrollView@JvmOverloadsconstructor(
context:Context,
attrs:AttributeSet? =null
):HorizontalScrollView(context,attrs){
privatevarstartX=0f
privatevarstartY=0f
overridefunonInterceptTouchEvent(ev:MotionEvent):Boolean{
when(ev.actionMasked){
MotionEvent.ACTION_DOWN->{
startX=ev.x
startY=ev.y
// 通知父布局不要拦截
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE->{
valdx=abs(ev.x-startX)
valdy=abs(ev.y-startY)
if(dx>dy){
// 横向滑动,继续禁止父布局拦截
parent.requestDisallowInterceptTouchEvent(true)
}else{
// 竖向滑动,允许父布局拦截
parent.requestDisallowInterceptTouchEvent(false)
}
}
MotionEvent.ACTION_UP,MotionEvent.ACTION_CANCEL->{
parent.requestDisallowInterceptTouchEvent(false)
}
}
returnsuper.onInterceptTouchEvent(ev)
}
}核心方法: - requestDisallowInterceptTouchEvent(true):请求父布局不拦截当前事件序列 - 根据滑动方向动态切换拦截权
classMyRecyclerView@JvmOverloadsconstructor(
context:Context,
attrs:AttributeSet? =null
):RecyclerView(context,attrs){
privatevarstartX=0f
privatevarstartY=0f
overridefunonInterceptTouchEvent(e:MotionEvent):Boolean{
when(e.actionMasked){
MotionEvent.ACTION_DOWN->{
startX=e.x
startY=e.y
}
MotionEvent.ACTION_MOVE->{
valdx=abs(e.x-startX)
valdy=abs(e.y-startY)
if(dx>dy){
// 横向滑动,不拦截
returnfalse
}
}
}
returnsuper.onInterceptTouchEvent(e)
}
}overridefunonTouchEvent(event:MotionEvent):Boolean{
if(event.actionMasked==MotionEvent.ACTION_DOWN){
returnfalse// ❌ 后续事件不会再分发给这个 View
}
// ...
returntrue
}正确做法:在 ACTION_DOWN 返回 true,即使当前不处理,也要消费事件以接收后续事件。
overridefundispatchTouchEvent(ev:MotionEvent):Boolean{
Log.d("TouchDebug","${javaClass.simpleName}.dispatch: ${ev.actionToString()}")
returnsuper.dispatchTouchEvent(ev)
}
overridefunonInterceptTouchEvent(ev:MotionEvent):Boolean{
valintercept=super.onInterceptTouchEvent(ev)
Log.d("TouchDebug","${javaClass.simpleName}.intercept: ${ev.actionToString()} -> $intercept")
returnintercept
}
overridefunonTouchEvent(event:MotionEvent):Boolean{
valhandled=super.onTouchEvent(event)
Log.d("TouchDebug","${javaClass.simpleName}.onTouch: ${event.actionToString()} -> $handled")
returnhandled
}
privatefunMotionEvent.actionToString():String{
returnwhen(actionMasked){
MotionEvent.ACTION_DOWN->"DOWN"
MotionEvent.ACTION_MOVE->"MOVE"
MotionEvent.ACTION_UP->"UP"
MotionEvent.ACTION_CANCEL->"CANCEL"
else->"UNKNOWN"
}
}在 Android Studio 的 Layout Inspector 中: 1. 触摸界面元素 2. 查看 View Hierarchy 面板 3. 确认事件是否被正确分发到目标 View
ACTION_DOWN 到 ACTION_UP,同一序列的事件会分发给同一 ViewdispatchTouchEvent():事件分发入口onInterceptTouchEvent():ViewGroup 专属,决定是否拦截onTouchEvent():最终处理事件,决定是否消费requestDisallowInterceptTouchEvent() 或在 onInterceptTouchEvent() 中根据滑动方向判断ACTION_DOWN 不拦截,给子 View 机会ACTION_MOVE 判断 dx/dy,避免误拦截ACTION_CANCEL 时重置 UI 状态事件分发机制是 Android 自定义 View 和解决滑动冲突的基础。理解这套链路,可以更精准地控制交互行为,也能快速定位触摸事件相关的 Bug。下次遇到"点击失效"或"滑动卡顿",先看看事件是否被正确分发和消费。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。