Android之Touch
事件分发基础
基本触碰事件包括ACTION_DOWN
,ACTION_MOVE
,ACTION_UP
,ACTION_CANCEL
选取Activity为本文的触碰事件讨论的起点,触碰事件传递以Activity,ViewGroup, View的顺序依次传递
Activity.dispatchTouchEvent
Activity做为事件监听的起始点,会先把事件分发给DecorView,如果DecorView (Activity的根ViewGroup(FrameLayout),其子view是一个LinearLayout,这个LinearLayout由一个ViewStub(Title,ActionBar)和ContentView(Activity调用setContentView()设置))不做处理,才回调自己的onTouchEvent()
Activity.dispatchTouchEvent:1
2
3
if (!Winodw.superdispatchTouchEvent()) //DecorView.dispatchTouchEvent()
Activity.onTouchEvent
ViewGroup.dispatchTouchEvent
分析
ViewGroup的分发流程相对其他复杂,大致分成四步:
- 清空Target列表
按下时发生,Target列表包含对事件感兴趣的子View,对于单点触屏,是一个只有单元素的链表,该步只发生在Action_down事件且onIntercepterTouch返回false - 判断拦截
按下时发生,或者在存在Target时发生 - 寻找Target列表
确定不拦截并且不存在Target时发生 - 向Target列表或者自身派发事件
有Target则向Target派发,无Target则向自身派发
伪代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 //step 1: target prepare //Down初始化
if action_down
Targets.accept(action_cancel)
Targets.clear()
//step 2: judge intercept //判断拦截
if action_down||Targets!=null
if isDisallowIntercept
intercept=false;
else
intercept=onInterceptTouch()
else
intercept=true
//step 3:find targets //找消费层
if Taregets==null&&!intercept
Targets.find()
//step 4: dispatch to target or current viewgroup
if Targets.isEmpty() //先判断子View中没有消费对象,自底向上的判断
super.disptchTouch() //即是把Viewgroup当成View继续走流程
else if intercept //如果中途拦截,发给消费对象Cancel信号,拦截层就在下一步变成了消费层
target.cancel()
target=null
else //如果中途无拦截,消费对象将完整消费Down到Up的过程
target.dispatchTouch()
从以上伪代码可以看出,当在ActionDown时onInterceptTouch返回true时候,ViewGroup之后的分发流程与View一样
分析伪代码可以得出:
Down:1
2
3
4
5
6
7//自底向上找消费对象
if !intercept()
findTarget()
if hasTarget()
target.dispatch()
else
super.dispatch()
Move,Up等:(有Target)1
2
3
4
5
6//中途如果拦截,则发Cancel给子Target,并且情况Target
if !intercept()
target.dispatch()
else
cancel(target)
target=null
Move,Up等:(无Target)1
2//无Target的情况还能传到本类,只能说明已经退化成一个View了,也即是当前Viewgroup变成一个View一样处理处在叶子节点消费
super.dispatchTouch() // base onTouchEvent return true when Action down
结论
如果觉得以上过程太过复杂,比较难记,但可以记住以下结论:
图:
1
2
3
4
5
6//对于每个事件过程
拦截层.onInterceptTouchEvent()
拦截层.onInterceptTouchEvent() // 每个下层都是相邻上层ViewGroup.dispatchTouchEvent方法中的消费对象Target
...
拦截层.onInterceptTouchEvent()
最终消费对象.onTouchEvent()逻辑主干:
- ACTION_DOWN 是作为搜索消费对象的过程,这个过程自底向上,找到的消费对象将消费这个Down事件
- 中途如果消费对象的上层ViewGroup拦截,上层ViewGroup会舍弃将当事件转成Cancel给消费对象,并且拦截层将变成消费层
- 如果中途没有拦截发生,消费对象将完整消费Down到Up的过程
- 逻辑分支:
- Down前会重置清空Target
- Up或者Cancel或者Down会清空disallowIntercept标志位,也就是说在事件的起始与结束清空标志位,PS:disallowIntercept由子View.requestDisallowInterceptTouchEvent置位,这个方法只作用于一个事件序列
View.dispatchTouchEvent
View通常作为事件派发的终点,会先把事件传给Listener,如果Listener未处理,再传给自身的onTouchEvent,类似Activity,只是Activity第一步把事件传给DecorView1
2
3
4if listener!=null&&listener.onTouchEvent()
return
else
View.onTouchEvent
事件分发实践
ListView之OnItemClickListener失效
众所周知,ListView的Item中如果包含有Button等Focusable的元素时候,在ListView设置的OnItemClickListener会失效。
整个ItemClick事件的的大致流程如下(这是反推回来的):
在AbsListView的onTouchUp(MotionEvent ev)
有如下代码1
2
3
4
5
6
7
8
9if (inList && !child.hasFocusable()) { //注意此行
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
final AbsListView.PerformClick performClick = mPerformClick;
performClick.mClickMotionPosition = motionPosition;
performClick.rememberWindowAttachCount();
省略....
child表示每一项的Item, 可以看到,如果每一项的Item如果包含Focuable=true的元素(包括本身是focusable也算),这个流程就走不下去了。所以通常的解决办法是:
- 把每一项的布局根View加上
android:descendantFocusability="blocksDescendants"
的属性就可以了,Adapter在newView阶段加上相关属性也可以 - 当然也可以手动去除Button之类的Focuable,添加
android:focusable="false"