自定义onTouchEvent触摸反馈
MotionEvent.ACTION_UP包含了按下+抬起的事件组合,可以判断这个动作是一个点击事件,响应点击事件performClick()
class TouchView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
override fun onTouchEvent(event: MotionEvent): Boolean {
//抬起事件(背后是按下+抬起的事件序列)
if(event.actionMasked == MotionEvent.ACTION_UP){
performClick()
}
//true表示事件被消费了, 并且其上层布局这块区域的点击事件不会被消费(调用)
return true;
}
}
return true的含义
可以理解为这是一个事件所有权的标志,只跟DOWN事件有关
如图:
- 黄色区域与绿色区域有重叠,黄色、绿色、灰色区域均注册了点击事件
- 此时当黄色区域的onTouchEvent返回true的时候,黄色区域的点击事件被消费,而绿色区域灰色区域的事件不会被消费,也就是不会响应
- 如果黄色区域返回false,那么事件会逐级被消费,黄色->绿色->灰色
event.actionMasked和event.action
event.actionMasked也可以写为event.action,建议使用event.action,大多数view源码用的都是event.action
- getActionMasked() 和 getAction() 怎么选?
- 选 getActionMasked()。因为到了多点触控时代,getAction() 已经 不够准确
- 那为什么有些地方(包括 Android 源码里)依然在用 getAction()? 因为它们的场景不考虑多点触控
- POINTER_DOWN / POINTER_UP:多点触控时的事件
- getActionIndex():多点触控时用到的方法
- 关于 POINTER_DOWN、POINTER_UP 和 getActionIndex(),请往下看
View.onTouchEvent() 的源码逻辑
- 当用户按下(ACTION_DOWN):
- 如果不在滑动控件中,切换至按下状态,并注册长按计时器
- 如果在滑动控件中,切换至预按下状态,并注册按下计时器
- 当进入按下状态并移动(ACTION_MOVE):
- 重绘 Ripple Effect
- 如果移动出自己的范围,自我标记本次事件失效,忽略后续事件
- 当用户抬起(ACTION_UP):
- 如果是按下状态并且未触发长按,切换至抬起状态并触发点击事件,并清除 一切状态
- 如果已经触发长按,切换至抬起状态并清除一切状态
- 当事件意外结束(ACTION_CANCEL):
- 切换至抬起状态,并清除一切状态
自定义 ViewGroup 的触摸反馈
- 除了重写 onTouchEvent() ,还需要重写 onInterceptTouchEvent() ,如果重写onInterceptTouchEvent,那么也需要重写onTouchEvent
- onInterceptTouchEvent() 不用在第一时间返回 true,而是在任意一个事件里, 需要拦截的时候返回 true 就行(默认false,需要拦截的时候才去拦截)
- 在 onInterceptTouchEvent() 中除了判断拦截,还要做好拦截之后的工作的准备 工作(主要和 onTouchEvent() 的代码逻辑一致)
- 只有有子view的view才有onInterceptTouchEvent,如ViewGroup,没有子view的view没有onInterceptTouchEvent,如EditText
class TouchLayout(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
TODO("Not yet implemented")
}
override fun shouldDelayChildPressedState(): Boolean {
return super.shouldDelayChildPressedState()
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
// return super.onInterceptTouchEvent(ev)
//这里处理事件拦截逻辑,如大于某个阈值才拦截事件
val delta = ev.y -100
if (delta>100){
return true
}else{
return false
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
//重写事件处理逻辑
return super.onTouchEvent(event)
}
}
触摸反馈的流程
-
Activity.dispatchTouchEvent()
- 递归: ViewGroup(View).dispatchTouchEvent()
- ViewGroup.onInterceptTouchEvent()
- child.dispatchTouchEvent()
- super.dispatchTouchEvent()
- View.onTouchEvent()
- Activity.onTouchEvent()
//1. View View.dispatchTouchEvent(); public boolean dispatchTouchEvent(MotionEvent event){ retrun onTouchEvent(); } //2. ViewGroup ViewGroup.dispatchTouchEvent(); public boolean dispatchtouchEvent(MotionEvent event){ boolean result; if(interceptTouchEvent()){ result = onTouchEvent(); }else{ result = 子View的dispatchTouchEvent } return result; } - 递归: ViewGroup(View).dispatchTouchEvent()
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
- 作用:记录每个子 View 是被哪些 pointer(手指)按下的
- 结构:单向链表