type
status
date
slug
summary
tags
category
password
icon
⾃定义单 View 的触摸反馈
- 重写 onTouchEvent(),在⽅法内部定制触摸反馈算法
- 是否消费事件取决于 ACTION_DOWN 事件是否返回 true
- MotionEvent
- getActionMasked() 和 getAction() 怎么选?
- 选 getActionMasked()。因为到了多点触控时代,getAction() 已经不够准确
- 那为什么有些地⽅(包括 Android 源码⾥)依然在⽤ getAction()? 因为它们的场景不考虑多点触控
- POINTER_DOWN / POINTER_UP:多点触控时的事件
- getActionIndex():多点触控时⽤到的⽅法
- 关于 POINTER_DOWN、POINTER_UP 和 getActionIndex(),后⾯多点触控的课程⾥会详细讲
一个事件序列,只有处理 ACTION_DOWN 事件的 onTouchEvent() 返回值会有作用,表示是否接管这个事件序列。true 表示接管,一旦接管,后续事件序列中的事件,都直接交给这个 View;false 表示不接管,一旦不接管,后续事件队列中的事件,都不会交给这个 View;而处理 ACTION_MOVE、ACTION_UP 和 ACTION_CANCEL 事件时,onTouchEvent() 返回值是没有作用的。
当自定义一个 ViewGroup 时,最好重写一下它的shouldDelayChildPressedState()
方法,该方法的作用是延迟子 View 的按下状态,主要用于自己需要滑动时,用户按下内部子View,不知道用户是想点击子 View 还是滑动自己,就需要延迟改变按下的状态,这样延迟后,用户已经做出下一步动作了,就可以知道用户是想点击还是滑动。如果自身需要滑动,最好重写这个方法返回 true,否则就返回 false,默认是返回 true,但是大部分情况下是不需要延迟的。所以还是重写一下返回 false 比较好
View.onTouchEvent() 的源码逻辑
- 当⽤户按下(ACTION_DOWN):
- 如果不在滑动控件中,切换⾄按下状态,并注册⻓按计时器
- 如果在滑动控件中,切换⾄预按下状态,并注册按下计时器
- 当进⼊按下状态并移动(ACTION_MOVE):
- 重绘 Ripple Effect
- 如果移动出⾃⼰的范围,⾃我标记本次事件失效,忽略后续事件
- 当⽤户抬起(ACTION_UP):
- 如果是按下状态并且未触发⻓按,切换⾄抬起状态并触发点击事件,并清除⼀切状态
- 如果已经触发⻓按,切换⾄抬起状态并清除⼀切状态
- 当事件意外结束(ACTION_CANCEL):
- 切换⾄抬起状态,并清除⼀切状态
ToolTip:新版 Android 加⼊的「⻓按提示」功能。
⾃定义 ViewGroup 的触摸反馈
- 除了重写 onTouchEvent() ,还需要重写 onInterceptTouchEvent()
- onInterceptTouchEvent() 不⽤在第⼀时间返回 true,⽽是在任意⼀个事件⾥,需要拦截的时候返回 true 就⾏
- 在 onInterceptTouchEvent() 中除了判断拦截,还要做好拦截之后的⼯作的准备⼯作(主要和onTouchEvent() 的代码逻辑⼀致)
触摸反馈的流程

- Activity.dispatchTouchEvent()
- 递归: ViewGroup(View).dispatchTouchEvent()
- ViewGroup.onInterceptTouchEvent()
- child.dispatchTouchEvent()
- super.dispatchTouchEvent()
- View.onTouchEvent()
- Activity.onTouchEvent()
View.dispatchTouchEvent()
- 如果设置了 OnTouchListener,调⽤ OnTouchListener.onTouch()
- 如果 OnTouchListener 消费了事件,返回 true
- 如果 OnTouchListener 没有消费事件,继续调⽤⾃⼰的 onTouchEvent(),并返回和onTouchEvent() 相同的结果
- 如果没有设置 OnTouchListener,同上
ViewGroup.dispatchTouchEvent()
- 如果是⽤户初次按下(ACTION_DOWN),清空 TouchTargets 和 DISALLOW_INTERCEPT 标记
- 拦截处理
- 如果不拦截并且不是 CANCEL 事件,并且是 DOWN 或者 POINTER_DOWN,尝试把 pointer(⼿指)通过 TouchTarget 分配给⼦ View;并且如果分配给了新的⼦ View,调⽤child.dispatchTouchEvent() 把事件传给⼦ View
- 看有没有 TouchTarget
- 如果没有,调⽤⾃⼰的 super.dispatchTouchEvent()
- 如果有,调⽤ child.dispatchTouchEvent() 把事件传给对应的⼦ View(如果有的话)
- 如果是 POINTER_UP,从 TouchTargets 中清除 POINTER 信息;如果是 UP 或 CANCEL,重置状态
TouchTarget
场景:
一个 ViewGroup 内部可能有多个子 ViewGroup 或者 View,当用户的多个手指作用在不同的 View 上,在 ViewGroup 看来这是多点触控,但是某个 View 可能只是被其中的一个手指操作,那么这个 View 应该只接收到单指触控的事件,这种情况就需要 ViewGroup 做事件的转换
- 作⽤:记录每个⼦ View 是被哪些 pointer(⼿指)按下的
- 结构:单向链表
- 作者:shuouyang
- 链接:https://notion-tree.vercel.app/article/fedaf3ee-8a6d-474b-88c6-7462e8821f62
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。