引言:现在 GitHub 上酷炫的 Android 控件越来越多,一方面我们可以让 App 各美观,另一方面我们这些开发者也可以从中学习到各种知识。写下这篇博文主要是记录研究自定义控件源码过程中接触到的知识盲区,帮助自己巩固知识的同时,也和大家交流学习,一起进步。
废话不多说,进入正题:
一、概述 View 事件传递机制
用户通过点击、滑动屏幕与 App 产生交互是移动互联网时代的交互基础,那么在 Android 中,用户的点击、滑动是怎么和 Android 系统产生交互的呢?
在 Android 中,我们所说的点击、滑动等事件,都被视为 MotionEvent ,而在 MotionEvent 中,我们的操作行为被归类为以下常量:
- ACTION_DOWN
- ACTION_UP
- ACTION_MOVE
- ACTION_POINTER_DOWN
- ACTION_POINTER_UP
- ACTION_CANCEL
除此以外,为了让系统更好地管理和操作这些事件,MotionEvent 还需要记录事件的发生时间,判断事件是单点触控/多点触控以及事件的发生时间。可能有人会问了,就这么点常量够我们判断我们的手势吗?莫慌,Google 对事件可是有着明确的区分标准呢:一次触控操作,起于 ACTION_DOWN 终于 ACTION_UP。简单的触控操作,如:点击、滑动等,很轻松就能通过这些常量判断出来;而复杂的手势,则需要根据你手指的滑动轨迹不断地对事件坐标进行分析了。
注:为了简化理解,后文中我将把所有和点击、滑动等等有关的事件归类为点击事件
二、View 事件的传递流程
之后的解析都会结合源码进行,没基础的小伙伴要认真跟上哦
在 Android 中,当一个点击事件被传入,首先执行 Actvity 的 dispatchTouchEvent() 方法接收事件,Activity 接收到事件之后交由根布局(即与 Activity 相关联的 Window 中的布局,一般是 ViewGroup,如:常见的 LinearLayout、RelativeLayout等)进行分发,若根布局不需要处理该事件,除非某一个布局在传递过程中通过 onInterceptTouchEvent() 方法将事件拦截,并“消费”该事件(消费的概念将在下面通过源码解释),否则事件将一直向下传递给子布局。若事件传递至最底层子布局中仍未被处理,则会反过来一直向上传递,此时每一级父布局都能处理该事件。若事件回传至根布局仍未被处理,则由 Activity 的 onTouchEvent() 方法终止事件。
注:OnTouchListener 处理事件的优先级高于 onTouchEvent()
这样一大段的叙述看下来,估计很多小伙伴都晕啦,其实俺也很晕哒~为了大家更好地理解,我先用一个 Demo 为大家介绍这个概念,再解析源码:
Demo 代码非常简单,就是自定义 Button 和 LinearLayout,在执行相关的方法时输出Log,Activity 里执行相关方法也输出 Log。点击流程如图:
上图很好地阐述了我们刚刚讲解的 View 事件传递流程,下面来看源码,更深一步地了解其中的机制:
首先,在 Activity 的 dispatchTouchEvent() 方法中对事件进行判断(两个判断语句不用管),然后进入 onTouchEvent() 方法
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
在 onTouchEvent() 方法里面会有一个判断,shouldCloseOnTouch() 就是用于判断事件是否从尾部回传回来,true 代表事件应该不应该向下传递,而 false 代表事件未被消费,应该向下传递,交给子布局处理。
所以我们刚刚一直提到的“消费”的概念就是这个意思,事件在 View 链上传递一个来回,只要被处理了,并且处理它的 View 返回了 false,我们就认为事件被消费,如果返回 true ,我们则认为事件尚未被消费,仍需要向下传递,让对应的 View 处理它。
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
现在 MyLinearLayout 通过 dispatchTouchEvent() 方法接收到事件,我们在 Demo 中只做了点击, dispatchTouchEvent() 方法有涉及 move 和 up,所以我只抽取和 down 有关的那部分来讲解
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
里面的逻辑很简单对吧?先判断要不要拦截,要拦截的话就把 intercepted 设为 true,使得后面的返回值也为 true ,进而让事件被当前 View 消费;不拦截的话,则将事件继续向下传递。
后面的 MyButton 同理。
网上看到几张图把整个流程展现地很好: