文章目录
  1. 1. 事件分发基础
    1. 1.1. Activity.dispatchTouchEvent
    2. 1.2. ViewGroup.dispatchTouchEvent
      1. 1.2.1. 分析
      2. 1.2.2. 结论
    3. 1.3. View.dispatchTouchEvent
  2. 2. 事件分发实践
    1. 2.1. ListView之OnItemClickListener失效

事件分发基础

基本触碰事件包括ACTION_DOWNACTION_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的分发流程相对其他复杂,大致分成四步:

  1. 清空Target列表
    按下时发生,Target列表包含对事件感兴趣的子View,对于单点触屏,是一个只有单元素的链表,该步只发生在Action_down事件且onIntercepterTouch返回false
  2. 判断拦截
    按下时发生,或者在存在Target时发生
  3. 寻找Target列表
    确定不拦截并且不存在Target时发生
  4. 向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()
  • 逻辑主干:

    1. ACTION_DOWN 是作为搜索消费对象的过程,这个过程自底向上,找到的消费对象将消费这个Down事件
    2. 中途如果消费对象的上层ViewGroup拦截,上层ViewGroup会舍弃将当事件转成Cancel给消费对象,并且拦截层将变成消费层
    3. 如果中途没有拦截发生,消费对象将完整消费Down到Up的过程
  • 逻辑分支:
    1. Down前会重置清空Target
    2. Up或者Cancel或者Down会清空disallowIntercept标志位,也就是说在事件的起始与结束清空标志位,PS:disallowIntercept由子View.requestDisallowInterceptTouchEvent置位,这个方法只作用于一个事件序列

View.dispatchTouchEvent

View通常作为事件派发的终点,会先把事件传给Listener,如果Listener未处理,再传给自身的onTouchEvent,类似Activity,只是Activity第一步把事件传给DecorView

1
2
3
4
if 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
9
if (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也算),这个流程就走不下去了。所以通常的解决办法是:

  1. 把每一项的布局根View加上android:descendantFocusability="blocksDescendants"的属性就可以了,Adapter在newView阶段加上相关属性也可以
  2. 当然也可以手动去除Button之类的Focuable,添加android:focusable="false"
文章目录
  1. 1. 事件分发基础
    1. 1.1. Activity.dispatchTouchEvent
    2. 1.2. ViewGroup.dispatchTouchEvent
      1. 1.2.1. 分析
      2. 1.2.2. 结论
    3. 1.3. View.dispatchTouchEvent
  2. 2. 事件分发实践
    1. 2.1. ListView之OnItemClickListener失效