文本输入是输入法和编辑器协同作用的结果,输入法可以是软键盘、语音等,扩展了 InputMethodService 来实现,编辑器主要是接受文本并显示文本的组件,一般是 EditText,输入法与编辑器的交互一般是当输入框获得焦点,弹出当前系统默认输入法,输入字符,选择候选词,更新输入字符到编辑框,本文将从如下几个方面介绍 Android 输入法机制,如下:

官方文档

IMF 框架

Android 输入放框架主要包括三部分内容:

  1. 客户端,当前输入内容的 App,主要是 InputMethodManagerEditText/TextView
  2. 输入法管理服务,主要是 InputMethodManagerService
  3. 输入法:主要是 InputMethodService

可知最关键的就是 InputMethodManagerInputMethodManagerServiceInputMethodService 三者之间的关系,后文中出现可将其简称为 IMMIMMSIMS,其关系如下:

sequenceDiagram
    participant IMM as 客户端(IMM)
    participant IMMS as 输入法管理服务(IMMS)
    participant IMS as 输入法(IMS)
    IMM->>IMMS: 显示、隐藏输入法
    IMMS->>IMS: 显示、隐藏输入法
    IMS-->>IMM: 返回输入信息

上述三个模块分别对应不同的进程,客户端 IMM 属于 App 应用进程,输入法 IMS 属于输入法自己的进程,输入法管理服务 IMMS 属于系统进程,三者之间需要通过 IPC 机制来进行通信, Androd中最根主要的 IPC 通信方式都就是 Binder 机制。

其中客户端 IMM 与 输入法 IMS 之间的交互已经通过 EditText 及其父类 TextView 实现,如果是自定义的编辑框,需要自行实现与输入法的交互。

输入法拉起流程

当点击一个输入框 EditText 将获得焦点,系统默认的输入法将会被拉起,EditText 继承自 TextView,关键实现在 TextView 中,拉起输入法的关键流程如下:

sequenceDiagram  
autonumber
TextView->>TextView:onKeyDown/onTouchEvent
TextView->>TextView:viewClicked
TextView->>IMM:viewClicked
IMM->>IMM: checkFocus
IMM->>IMM: startInputInner
IMM->>IMMS: startInputOrWindowGainedFocus
IMMS->>IMMS: startInput
IMMS->>IMMS: startInputLocked
IMMS->>IMMS: startInputUncheckedLocked
IMMS->>IMMS: startInputInnerLocked
IMMS->>IMMS: bindCurrentInputMethodServiceLocked
IMMS->>IMMS: onServiceConnected
IMMS->>IMS: attachToken
IMMS->>IMS: createSession
IMS-->>IMMS:  onSessionCreated
IMMS->>IMS: bindInput
IMMS->>IMS: startInput
IMMS->>IMS: showSoftInput
IMS->>IMS: showWindow
IMS->>IMS: showWindowInner
IMS->>IMS: onStartInputView

可知,输入法 IMS 的拉起可由按键和触摸事件触发,经由 IMMIMMS 通过一系列 IPC 调用拉起系统默认输入法 IMS,上述流程中省略了一些操作,实际流程要更复杂,重点关注如下返回值:

1
2
3
4
5
6
7
// InputBindResult.java
// 请求输入法成功
ResultCode.SUCCESS_WITH_IME_SESSION,
// 输入法已经启动,等待IME创建session
ResultCode.SUCCESS_WAITING_IME_SESSION,
// 输入法还未启动,等待输入法绑定
ResultCode.SUCCESS_WAITING_IME_BINDING,

当没有绑定输入法和 session 没创建的时候都在等待输入法绑定和 session 创建,当完成这两步之后客户端会再次请求输入法,为便于理解上图中没有体现这个过程,拉起 IMS 的关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// InputMethodManagerService.java
InputBindResult startInputInnerLocked() {
// ...
// 绑定输入法
// InputMethod.SERVICE_INTERFACE(android.view.InputMethod)
mCurIntent = new Intent(InputMethod.SERVICE_INTERFACE);
mCurIntent.setComponent(info.getComponent());
mCurIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
com.android.internal.R.string.input_method_binding_label);
mCurIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), 0));
// 绑定输入法
if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
// ...
// 绑定成功
return new InputBindResult(
InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
null, null, mCurId, mCurSeq,
mCurUserActionNotificationSequenceNumber);
}
// 绑定失败
return InputBindResult.IME_NOT_CONNECTED;
}

private boolean bindCurrentInputMethodServiceLocked(
Intent service, ServiceConnection conn, int flags) {
// ...
// 绑定输入法
return mContext.bindServiceAsUser(service, conn, flags,
new UserHandle(mSettings.getCurrentUserId()));
}

当绑定输入法 IMS 成功后将获去到 IInputMethod 的远程服务对象 mCurMethod,通过 mCurMethod 就可以操作 IInputMethod.aidl 中定义的相关接口了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mMethodMap) {
if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
// 绑定输入法成功后获得操作输入法的接口IInputMethod
mCurMethod = IInputMethod.Stub.asInterface(service);
if (mCurToken == null) {
Slog.w(TAG, "Service connected without a token!");
unbindCurrentMethodLocked(false);
return;
}
if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
// 发送MSG_ATTACH_TOKEN生成与系统服务IMMS会话的token
executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
MSG_ATTACH_TOKEN, mCurMethod, mCurToken));
// 如果当前绑定到给输入法的客户端存在则重新创建session
if (mCurClient != null) {
clearClientSessionLocked(mCurClient);
requestClientSessionLocked(mCurClient);
}
}
}
}

到此,系统输入法从编辑器 EditText 获得焦点开始到输入法 IMS 服务绑定成功的流程结束,这里注意下 startInputUncheckedLocked 方法,后续 IMMIMMSIMS 三者之间的交互关系都在其中,最终调用 showSoftInput 显示输入法。

输入法管理服务(IMMS)

InputMethodManagerService 是用来管理输入法的系统服务,IMMS 属于系统服务的一部分,随着系统的启动而启动,IMMS 的初始化流程如下图所示:

sequenceDiagram
autonumber
SystemServer->>SystemServer:main
SystemServer->>SystemServer:run
SystemServer->>SystemServer:startOtherServices
SystemServer->>SystemServiceManager: startService
SystemServiceManager->>IMMS:onStart
IMMS->>SystemService:publishBinderService
SystemService->>ServiceManager:  addService  
ServiceManager->>ServiceManagerProxy:addServvice
ServiceManagerProxy->>BinderProxy:transact

如上图 IMMS 的初始化流程,从 SystemServerIMMS 再到 ServiceManager,最后到 ServiceManagerProxy 这里完成 Java 层的系统服务注册流程,BinderProxy 是 native 层 IBinder 在 Java 层的代理对象,其初始化只能通过 Native 层的 javaObjectforIBinder 函数来初始化,不能直接创建,到此系统服务 IMMS 在 Java 层的初始化结束,Native 层的暂时不关注,下面附上 BinderProxy 的定义及注释供参考:

1
2
3
4
5
6
7
8
/**
* Java proxy for a native IBinder object.
* Allocated and constructed by the native javaObjectforIBinder function. Never allocated
* directly from Java code.
*/
final class BinderProxy implements IBinder {
// ...
}


IMMS 的核心实现是 IInputMethodManager.aidl 和 IInputSessionCallback.aidl,前者提供给 IMM 调用,后者提供给 IMS 调用,如下:

  • IInputMethodManager.aidl:定义了操作输入法的公共接口对外提供给客户端 IMM,最常用的比如显示、隐藏、切换软键盘等。
  • IInputSessionCallback.aidl: 用于允许输入法创建会话的时候通知 IMMS,之后 IMMS 就可以绑定输入法 IMS 了。

InputMethodManagerService 实现了 IInputMethodManager.aidl 提供给 IMM 使用,MethodCallback 实现了 IInputSessionCallback.aidl,用来回调 IMS 创建的 IInputMethodSession,关键流程如下:

sequenceDiagram
    autonumber
    IMMS->>IMMS: requestClientSessionLocked
    IMMS->>IInputMethodWrapper: createSession
    IInputMethodWrapper->>InputMethodSessionCallbackWrapper:sessionCreated
    InputMethodSessionCallbackWrapper->>MethodCallback: sessionCreated
    MethodCallback-->>IMMS: onSessionCreated

看下 IMMSonSessionCreated 方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void onSessionCreated(IInputMethod method, IInputMethodSession session,
InputChannel channel) {
synchronized (mMethodMap) {
if (mCurMethod != null && method != null
&& mCurMethod.asBinder() == method.asBinder()) {
if (mCurClient != null) {
// 清理客户端的session
clearClientSessionLocked(mCurClient);
// 初始化SessionState
mCurClient.curSession = new SessionState(mCurClient,
method, session, channel);
// 准备显示输入法
InputBindResult res = attachNewInputLocked(
InputMethodClient.START_INPUT_REASON_SESSION_CREATED_BY_IME, true);
if (res.method != null) {
executeOrSendMessage(mCurClient.client, mCaller.obtainMessageOO(
MSG_BIND_CLIENT, mCurClient.client, res));
}
return;
}
}
}
// Session abandoned. Close its associated input channel.
channel.dispose();
}

如上 onSessionCreated 会绑定客户端到此 IMMS 的关键流程结束。

输入法(IMS)

InputMethodService 为标准 UI 元素(输入视图、候选视图和全屏模式下运行)提供了一个基本框架,但如何使用它们取决于特定的实现者,其是自定义输入法的关键类,其本身是一个 Service 的派生类,如下:

1
public class PinyinIME extends InputMethodService {

自定义输入法要在 AndroidManifest.xml 中进行声明,参考如下:

1
2
3
4
5
6
7
8
<service android:name=".PinyinIME"
android:label="@string/ime_name"
android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>
<action android:name="android.view.InputMethod" />
</intent-filter>
<meta-data android:name="android.view.im" android:resource="@xml/method" />
</service>

其中权限 BIND_INPUT_METHOD 的声明可以使之能够连接到系统,设置了一个与 android.view.InputMethod 操作匹配的 Intent。

IMS 的核心实现是 IInputMethod.aidl 和 IIInputMethodSession.aidl,前者提供给 IMMS 调用,后者提供给 IMM 调用,如下:

  • IInputMethod.aidl:输入法 IMM 是一个 Service,其核心实现就是 IInputMethod.aidl 的实现 IInputMethodWrapper\,提供了操作输入法的核心方法。
  • IInputMethodSession.aidl:提供了客户端 IMM 操作输入法的接口,IMM 中存在 IInputMethodSession 类型的实例 mCurMethod 供其调用。

收到客户端 IMM 输入操作后,输入法 IMS 将会显示输入法布局,可以参考 SoftKeyboardView 的实现,输入法 IMS 的生命周期如下:

image

其关键生命周期如下:

  • onBindInput:客户端 IMM 绑定该输入法 IMS 的时候调用。
  • onStartInput:用户在编辑框开始输入文字的时候调用。
  • onCreateInputView:创建并返回输入区域的视图,第一次显示输入区域视图的时候调用,默认实现返回 null,可实现 onEvaluateInputViewShown 来控制候显示输入区域视图,更改输入区域视图使用 setInputView 进行更改。
  • onCreateCandidatesView:创建并返回显示候选词的视图,第一次显示候选词视图的时候调用,默认实现返回 null,可使用 setCandidatesViewShown 方法控制候选词视图的显示,更改候选视图使用 setCandidatesView 进行更改。
  • onStartInputView:当前页面的输入框获取了焦点时调用。

客户端(IMM)

InputMethodManager 主要用来处理当前应用程序与输入法之间的交互,每次只能运行一个输入法,多个应用程序使用输入法,确保获得焦点并保证只有一个应用程序在使用输入法,应用程序使用 Textview 及其子类默认实现了这种交互,也可以自定义文本编辑器来实现这种交互。

IMM 的核心实现是 IInputMethodClient.aidl 和 IInputContext.aidl,如下:

  • IInputMethodClient.aidl:简单理解为 IMMS 绑定 IMS 的时候通知 IMM 输入法已经准备好了可以进行绑定了,具体是由 IMM 内部实现,对应 mClient
  • IInputContext.aidl:定义操作输入法编辑器 IME 的方法,默认实现是 EditableInputConnection

下面看看 IMM 的初始化流程:

sequenceDiagram
    ViewRootImpl->>WindowManagerGlobal: ViewRootImpl(context,display)
    WindowManagerGlobal->>WindowManagerService: getWindowSession
    WindowManagerService->>Session: openSession
    Session->>InputMethodManagerService: addClient

可知,ViewRootImp 创建的时候会获取 IWindowSession,如果 IWindowSession 为空的时候,会通过 WindowManagerService 创建一个 IWindowSession ,其实现为上图中的 Session,在 Session 创建的时候调用 IMMSaddClient 将客户端 IMM 的关键实现 IInputMethodClientIInputContext 添加到 IMMSIMMS 统一维护。

  • IMMIMMS 的交互主要是 IInputMethodManager.aidl,使用时通过 getSystemService 获取 input_method 系统服务,也就是 IMMS,之后就可以调用输入法管理器 IMMS 提供的公共接口了。
  • IMMIMS 的交互主要是 IInputMethodSession.aidl,定义供客户端操作输入法的方法,IMMS 请求 IInputMethod 创建 IInputMethodSessionIMMS 绑定客户端 IMM 的时候将 IInputMethodSession 传入 IMM

IMS、IMMS和IMM之间的交互

输入法(IMS)、输入法管理服务(IMMS)和客户端(IMM)之间的交互主要是通过一系列 aidl 进行交互的,关键 aidl 如下图所示:

sequenceDiagram
    participant IMM as 客户端(IMM)
    participant IMMS as 输入法管理服务(IMMS)
    participant IMS as 输入法(IMS)
    IMM->>IMMS: IInputMethodManager.aidl
    IMMS->>IMM: IInputMethodClient.aidl
    IMMS->>IMS: IInputMethod.aidl
    IMS->>IMMS: IInputSessionCallback.aidl
    IMS->>IMM: IInputContext.aidl
    IMM->>IMS: IInputMethodSession.aidl

IMS、IMMS和IMM之间的交互如下:

  1. IMM 使用 IInputMethodManager 请求 IMMS
  2. IMMS 绑定 IMS 获得操作输入法的相关接口 IInputMethod
  3. IMMS 请求 IMS 创建 IInputMethodSession
  4. IMMS 通过 IInputMethodClient 告知 IMM 当前 IInputMethodSession
  5. IMMIMS 通过 IInputMethodSessionIInputContext 交互。

IMMIMMSIMS 三者之间的交互都是 IPC 调用,对于使用者来说无需关注三者之间的复杂调用,如上就是 Android 机制下的输入法框架。

事件的分发流程可以说基本上已经阐述清楚,在阅读本篇文章之前,请先阅读下面几篇文章:

还有一个问题是 Android 事件传递过程中 onTouch 和 onClick 事件在整个事件过程中是如何进行事件传递的,下面主要是关于 onTouch 、 onClick 与事件传递过程中调用的先后顺序,将从如下几个方面介绍:

  1. 源码中的 onTouch() 方法
  2. 源码中的 onClick() 方法
  3. onTouch() 与 onClick() 方法之间的关系
  4. 总结

源码中的onTouch()方法

当要设置触摸事件的监听时,使用到 View 类中的 OnTouchListener 接口,然后通过 setOnClickListener 设置对触摸事件的监听,然后就可以通过具体的事件类型去执行某些操作,onTouch() 方法就是接口 OnTouchListener 中定义的方法,在 View 的 dispatchTouchEvent() 方法中调用,下面时 onTouch方法在源码中的具体调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//事件分发
public boolean dispatchTouchEvent(MotionEvent event) {
...
//默认返回值
boolean result = false;
...
//注意判断条件
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}

ListenerInfo li = mListenerInfo;
//注意判断条件
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}

上述代码中,先来看一下最外面的条件 onFilterTouchEventForSecurity() 方法,只关心该方法的返回值即可,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 如果事件正常分发返回true,如果事件被丢弃返回false
* @see #getFilterTouchesWhenObscured
*/
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}

所以该方法正常情况下返回 true,然后关键的条件主要就是 ListenerInfo 是否为 null,mOnTouchListener 是否为 null,以及 onTouch() 方法的返回值,下面是 ListenerInfo 初始化的源码部分,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
//getListenerInfo()的具体调用
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
//ListenerInfo的初始化
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}

显然,当通过 setOnTouchListener() 方法设置触摸事件的监听时就初始化了 ListenerInfo,同在在设置触摸事件监听的时候 mOnTouchListener != null 成立,最后 onTouch() 方法的返回值决定了 dispatchTouchEvent() 方法是否返回 true。所以,当设置了触摸监听事件且 onTouch() 方法返回 true 时,表示事件就此处理也就不再向子 View 传递了,同时,onTouchEvent() 方法也就不再执行,返回 false 则 onTouchEvent() 方法还会执行。

源码中的onClick()方法

当要设置点击事件的事件监听时,使用到 View 类中的 OnClickListener 接口,然后通过 setOnClickListener 设置对单击事件的监听,然后就可以通过具体的事件类型去执行某些操作,onClick() 方法就是 OnClickListener 接口中定义的方法,在 View 的 onTouchEvent() 方法中调用,在下文中也会进一步得到验证,下面是 onClick() 方法在源码中的具体调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//事件处理
public boolean onTouchEvent(MotionEvent event) {
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
...
//如果执行了该方法,其返回值就是onTouchEvent()的返回值
performClick();
...
break;
}
return true;
}
return false;
}

上面代码中至少找到了 onClick() 方法的调用位置,下面是 performClick() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 主要回调了 OnClickListener
*/
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//调用了OnClickListener接口中的onClick()方法
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}

显然,只要我们在代码中通过 setOnClickListener() 方法设置了对单击事件的监听,则对应 View 的 onTouchEvent() 方法返回 true,当然事件就此消费,反之返回 false,那么 onTouch 与 onClick 之间的调用顺序如何,它们之间会互相影响吗,下面就会从案列的角度了解它们之间的关系。

onTouch()与onClick()方法之间的关系

还是之前的案例,MLinearLayout 嵌套 MRelativeLayout,MRelativeLayout 嵌套 MTextView,三个 View 都只是重写了与它们自身相关的事件分发,然后为 MTextView 设置对触摸事件、单击事件的监听,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

findViewById(R.id.textView).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("Event", "TextView-------onTouch---------------return:" + false);
return false;
}
});

findViewById(R.id.textView).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("Event", "TextView-------onClick");
}
});
}
  1. 让 onTouch() 返回 false,查看日志如下:

结论:设置了对触摸事件的监听,onTouch() 方法 false 时 onTouchEvent() 方法在 onTouch() 方法之后执行,事件就此消费,接着接受 ACTION_DOWN 之后的一系列事件,途中使用鼠标,故没有 ACTION_MOVE 事件,还有在 onTouch() 方法返回 false 的情况下 onClick() 执行了。

  1. 让 onTouch() 返回 true,查看日志如下:

结论 :当 onTouch() 返回 true 的时候,正如前面所述 onTouchEvent() 将不会再执行,故 onClick() 也就不会再执行。

总结

onTouch() 方法的返回值决定了 onTouchEvent() 方法要不要执行,如果 onTouch() 返回 true,则 onTouchEvent() 不会再执行,返回 false ,则 onTouchEvent() 继续执行,而 onClick() 的回调是在 onTouchEvent() 方法中调用,onTouchEvent() 不执行则 onClick() 不执行。

上篇文章中主要针对 Activity、ViewGroup 以及 View 中的相关事件方法进行了基本概述,下面主要以案例的方式对 Android 中的事件传递进行归纳。

  1. 默认的事件分发流程
  2. 事件分发
  3. 事件处理
  4. 事件拦截
  5. 总结

阅读本篇文章之前请先阅读:

默认的事件分发流程

Android 事件分发的正常流程,也就是默认情况下事件是如何分发的,首先自定义 ViewGroup 和 自定义 View, 如继承 LinearLayout 自定义 ViewGroup 为 MLinearLayout,继承 TextView 自定义 View 为 MTextView,然后分别重写各自与事件分发相关的事件方法,Activity 也重写自己的事件方法,根据日志观察正常的事件分发流程,布局如下图所示:

通过触发 MTextView 的 onTouch 事件,对各个事件分发方法都默认处理,执行流程如下图所示:

显然,事件分发的起点是 Activity 的 dispatchTouchEvebt() 方法,然后通过一系列动作事件向 ViewGroup 分发,具体表现就是调用 ViewGroup 的 dispatchTouchEvebt() 方法,ViewGroup 的 onInterceptTouchEvent() 方法默认返回 false,也就是默认不拦截事件向子 View(ViewGroup 也是子 View,如 MRelativeLayout),然后,一直向下分发直到最内层的子 View 为止.

当然,事件分发至子 View , 也就是调用子 View 的 dispatchTouchEvebt() 方法,其事件分发方法内部调用了 onTouchEvent() 方法,onTouchEvent() 默认返回 false,表示该 View 不消费事件,反之返回 true 则消费该事件,下面是根据上述执行结果绘制的默认的事件分发流程,如下图所示:

通过上面的验证,至少能够了解各个事件相关方法的执行顺序咯。

事件分发

dispatchTouchEvent() 方法负责事件的分发,决定当前事件是自身消费还是继续向子 View 分发,返回 true,表示事件已经被消费,后续的事件也会继续执行,如 ACTION_MOVE、ACTION_UP等事件,返回 false 则继续向子 View 分发事件,如果是子 View 是 ViewGroup,则先将事件分发给 onInterceptTouchEvent() 方法,从而决定是否拦截该事件。按照上面的案例将 MRelativeLayout 的 dispatchTouchEvent() 方法返回 true ,事件消费且接受之后的一系列事件,日志截图如下图所示:

显然,当 MRelativeLayout 的 dispatchTouchEvent() 方法返回 true 的时候,事件没有向下传递,这里为了更清楚的观察各个事件方法的执行,具体的事件(如 ACTION_DOWN 等)没有输出在日志中,所以,当 dispatchTouchEvent() 方法返回 true 时表示事件已消费。

事件拦截

onInterceptTouchEvent() 方法负责事件的拦截,其返回值决定是否对当前事件进行拦截,返回值为 true,表示拦截当前事件,反之不拦截事件,也就是默认实现继续调用子 View 的 dispatchTouchEvent() 方法进行事件的分发,按照上面的案列将 MRelativeLayout 的 onInterceptTouchEvent() 方法返回值设为 true,拦截当前事件,使事件不在向下传递,日志截图如下图所示:

话说事件拦截之后,那就要进行事件处理了,当然事件处理归 onTouchEvent() 方法负责,onTouchEvent() 可以处理也可以不处理,如果 onTouchEvent() 返回 true 意味着事件处理完成,返回 false 则事件交由父 View 的 onTouchEvent() 方法进行处理,如图示情况,MRelativeLayout 拦截了事件,但是没有处理事件,最终交由 Activity 来处理咯。

事件处理

onTouchEvent() 方法负责事件的处理,其返回值决定是否消费该事件,返回 true 表示当前 View 消费事件,返回 false 表示不消费事件,事件向复 View 的方向传递,按照上面的案例将 MRelativeLayout 的 onTouchEvent() 方法返回值设为 true,日志截图如下图所示:

上面截图中省略了若干 ACTION_MOVE 事件,当某一 View 事件拦截成功之后,ACTION_DOWN 之后的一系列事件将直接由该 View 处理。

总结

  1. 事件分发由父 View 向子 View 进行事件的分发,在分发过程中如果没有被拦截和处理,当分发至最深层次的 View 时,将向上回传事件,最终交由 Activity 进行处理,后续事件也将不会接受;如果中途某个 View 拦截了事件并进行了处理,那么由拦截事件的 View 的 onTouchEvent() 方法进行处理;如果中途某个 View 拦截了事件并且不进行处理,则事件停止向子 View 分发,然后向父 View 的方向交由父 View 的 onTouchEvent() 方法进行处理;
  2. 事件拦截成功之后则要对事件进行处理,事件的处理主要由拦截事件的 View 的 onTouchEvent() 方法或是其父 View 的 onTouchEvent() 来处理,是否处理具体由相应的 onTouchEvent() 方法的返回值决定;
  3. 事件的处理当然是 onTouchEvent() 方法了,返回 true 表示事件就此处理,反之事件未被处理,继续向上回传事件直至被处理。

上篇文章中叙述了 Android 事件分发的大致流程,下面从 Activity、ViewGroup、View 三个方面介绍事件的相关方法,小节如下:

  1. Activity
  2. ViewGroup
  3. View

阅读本篇文章之前请先阅读:

Activity

Activity中主要两个与事件传递相关的方法,dispatchTouchEvent() 和 onTouchEvent(),事件传递由 Activity 的 dispatchTouchEvent() 方法开始。

事件分发

Activity中的事件分发方法:dispatchTounchEvent(),其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
 
//事件分发
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//空方法
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//onTouchEvent()方法默认返回false
return onTouchEvent(ev);
}

上面代码中显然只处理了 ACTION_DOWN 事件, 说明 ACTION_DOWN 事件才会触发事件的分发,接着调用了 Window 类的 superDispatchTouchEvent(ev)方法,这是一个抽象方法,那么当调用这个方法的时候,就会去调用具体子类中的方法,Window 类的具体子类就是 PhoneWindow 类,里面的具体实现的 superDispatchTouchEvent(ev) 方法如下:

1
2
3
4
5
6
7
//Window类里面的抽象方法
public abstract boolean superDispatchTouchEvent(MotionEvent event);
//Window子类PhoneWindow类中superDispatchTouchEvent()方法的具体实现
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}

显然,这里有调用了mDecor.superDispatchTouchEvent(event),mDecor 是一个 PhoneWindow.DecorView 对象,这也是窗口的顶层视图。它是一个真正 Activity 的 root view,它继承了 FrameLayout,通过 super.dispatchTouchEvent,会把 touchevent 派发给各个 activity 的子 view,也就是我们在 Activity.onCreat 方法中 setContentView 时设置的 view,代码参考如下:

1
2
3
4
5
6
7
8
9
//DecorView类声明
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
...
public boolean superDispatchTouchEvent(MotionEvent event) {
//这里又调用了FrameLayout中的dispatchTouchEvent方法
return super.dispatchTouchEvent(event);
}
...
}

由于在 FrameLayout 并没有重写 dispatchTouchEvent(event) 方法,因此我们需要 FrameLayout 的父类也就是 ViewGroup 中该方法的实现,由该方法进行事件的具体分发,这里具体事件分发过程有待研究。

事件处理

Activity 中的事件处理方法:onTouchEvent(),其源码如下:

1
2
3
4
5
6
7
8
9
//事件处理,默认返回false
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
//默认返回false
return false;
}

由于对于onTouchEvent()方法来说,事件传递是向父控件传递的,即使返回false,事件也相当于被消费了。

注意:Activity进行事件分发时,只有return super.dispatchTouchEvent(ev)时,事件才继续向下传递,返回true或false都事件就被消费了,也就是终止了事件的传播。

ViewGroup

ViewGroup中主要三个与事件传递相关的方法:dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent(),具体如下:

事件分发

ViewGroup中的事件分发方法:dispatchTouchEvent(),其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//dispatchTouchEvent()方法默认返回false
boolean handled = false;

//给方法决定是否拦截事件的分发
onInterceptTouchEvent(ev);
...
//默认情况下canceled和intercepted为false
if (!canceled && !intercepted) {
...
//该方法将事件传递给子View
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
}

return handled;
}

这个方法的作用就是遍历ViewGroup中的子View,将事件(ACTION_DOWN)交有子View进行处理,里面主要调用了onInterceptTouchEvent()和dispatchTransformedTouchEvent()方法,onInterceptTouchEvent()默认返回 false,下面是 dispatchTransformedTouchEvent() 方法的主要源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits){
...

if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//进行子 View 的事件分发
handled = child.dispatchTouchEvent(event);
}
...
return handled;
}

显然,dispatchTransformedTouchEvent() 方法主要进行子 View 的事件分发,如果没有子 View,则调用父 View 的 dispatchTouchEvent(event) 方法。

事件拦截

ViewGroup 中的事件分发方法 onInterceptTouchEvent() ,其源码如下:

1
2
3
4
5
public boolean onInterceptTouchEvent(MotionEvent ev) {
...
//默认返回false
return false;
}

该方法默认返回 false,表示不拦截向子 View 的事件分发,该方法在 ViewGroup 的 dispatchTouchEvent() 方法中被调用。

事件处理

ViewGroup 中没有自己的 onTouchEvent() 事件处理方法 ,ViewGroup 继承 View,其事件处理方法就是 View 的事件处理方法,其方法如下:

1
2
3
4
5
public boolean onTouchEvent(MotionEvent event) {
...
//默认返回 false
return false;
}

该方法进行相关事件的处理,如果返回 true ,表示事件被处理,具体使用情况将在在下文中记录。

View

View 中主要两个与事件分发相关的方法:dispatchTouchEvent() 和 onTouchEvent() 方法,具体如下:

事件分发

View 中的事件分发方法是 dispatchTouchEvent(),主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean dispatchTouchEvent(MotionEvent event) {
...
//默认返回值为false
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
...
//调用了onTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}

上述代码中,dispatchTouchEvent() 方法的默认返回值是 false,表示事件继续分发,实际上 dispatchTouchEvent 方法的返回值与 onTouchEvent 方法的返回值有关,如果 onTouchEvent 返回 true,dispatchTouchEvent 的返回值 result 为 true,此时表示事件已消费,当然也可以这样理解,ontouchEvent 的值为 true,本身就表示事件已消费了,下面是执行 ontouchEvent 方法执行的条件:

1
2
3
4
5
6
7
8
9
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
//窗口被遮挡,丢弃touch事件(很少执行)
return false;
}
return true;
}

显然,上述方法在一般情况下肯定返回 true,故肯定能执行到 onTouchEvent方法,调用 View 的 dispatchTouchEvent 方法实际上可简写为如下代码:

1
2
3
4
5
6
7
public boolean dispatchTouchEvent(MotionEvent event) {
...
//默认返回值为false
boolean result = false;
...
return onTouchEvent(event)
}

事件处理

View 中的事件分发方法是 onTouchEvent(),主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public boolean onTouchEvent(MotionEvent event) {
...

//此处返回true的条件是TouchDelegate默认为空,该值主要是关于View的触摸区域的
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

//如果设置了点击事件该条件才会返回true,也就是事件消费咯
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
//各个事件的处理
...
}

return true;
}
//默认返回false,表示不消费事件
return false;
}

事件传至子 View 时有两种结果,要么当前 View 消费该事件,要么不消费时间向上回传,如果不拦截直至 Activity,也不做任何处理,最后将丢弃该事件。

这篇文章是 Android 事件分发机制第二篇,主要记录了 Activity、ViewGroup 和 View 中与事件相关的方法,也就是 dispatchTouchEvent()、onTouchEvent() 和 onInterceptTouchEvent() 方法在 Activity、ViewGroup 和 View 中的不同表现,从代码的角度理解了一下 Android 中的事件关系,关于事件的分发流程将具体在下一篇文章中讲述,可以回复关键字【加群】邀你入交流群。

PS:尽最大的努力,做最坏的打算,你曾经偷过的懒,终有一日会变成打脸的巴掌。

Android 事件分发机制是比较重要的一块内容,总结一下 Android 事件分发的一些知识,计划分为四篇文章来写,内容如下:

  • Android事件分发之基础
  • Android事件分发之源码分析
  • Android事件分发之流程分析
  • onTouch和onClick的那些事儿

本篇是第一篇,希望从整体流程上叙述一下大致的分发过程,使读者能够对事件分发有个初步的了解,内容如下:

  1. View 与 ViewGroup
  2. MotionEvent 对象
  3. View 的事件分发

View 与 ViewGroup

Android 应用中的所有组件都继承了 View 类,View 类是 Android 中所有 UI 组件的基类,View 有一个重要的子类是 ViewGroup,ViewGroup 通常作为其他 View 的容器使用,里面可以包含普通的 View,也可以是其他 ViewGroup,通过 View 与 Viewgroup 的这种关系共同构成了整个 View 树的结构,如 LinearLayout 不仅是一个 View ,还是一个 ViewGroup,里面可以包含各种 View,当然这个 View 也可以是 ViewGroup,以此类推。

在 Android 设备中,操作主要依靠的就是各种手势,比如滑动、拖动、单击等各种操作,这一系列操作可以很方便的与 Android 设备进行交互,前面可知不同 View 处于不同的层级,问题来了,那么当我们操作的时候如何正确的让特定的 View 响应某种操作呢,不同的 View 之间会不会发生滑动冲突呢,答案是肯定的,解决这样的问题就必须的充分了解 View 的工作机制、各种事件的分发过程以及具体的分发对象。

MotionEvent对象

Android 中事件分发的是 MotionEvent 对象,MotionEvent 中封装了许多与各种事件发生位置相关的函数以及各种相关的事件类型,每个 MotionEvent 都包含一系列动作,比如当手指触摸屏幕的一瞬间,系统就会产生一系列的触摸事件对象,每个触摸事件对象都代表这不同的动作,比如按下滑动抬起等动作,这些动作分别对应着 ACTION_DOWN、ACTION_MOVE、ACTION_UP 等具体的事件,这一系列事件一般是由 ACTION_DOWN 事件开始,中间若干 ACTION_MOVE 事件,最后以 ACTION_UP 结束,此外,如果事件被拦截还会触发 ACTION_CANCEL 事件。总之,Android 事件分发的对象就是 MotionEvent 对象,当 MotionEvent 对象产生之后,系统会将这个事件分发给最终能够消费此事件的 View。

View的事件分发

Android 中的事件分发实际上指的就是 View 的事件分发,View 的事件分发主要是下面三个方法:

  1. dispatchTouchEvent()
  2. interceptTouchEvent()
  3. onTouchEvent()

这三个方法分别对应事件的分发、事件的拦截以及事件的处理,另外,View 没有 interceptTouchEvent() 方法,一方面 View 里面没有其他子 View 不需要拦截事件,另一方面可以理解为 View 的 interceptTouchEvent() 方法返回 true,事件本身由该 View 拦截,消不消费是 onTouchEvent() 的事,反正,View 不考虑事件的拦截。

Android 事件分发从 Activity 的 dispatchTouchEvent() 方法开始,通过一系列传递分发给 ViewGroup 的 dispatchTouchEvent() 方法,如果当前 ViewGroup 不拦截事件,则继续向子 View 分发事件,然后以此类推直到被哪个 View 处理,如果没有任何 View 处理该事件,当事件由父 View 向子 View 传递到最深层次的 View 时,事件将向父 View 的方向回传事件,最后交由 Activity 的 onTouchEvent() 进行处理。

如果当前 ViewGroup 拦截该事件,则事件不在向子 View 分发,而是调用其 onTouchEvent() 方法处理事件,当然事件是否处理主要看相应的 onTouchEvent() 方法的返回值,如果 onTouchEvent() 方法返回 true,表示事件就此消费,反之,返回 false 表示事件没有被消费,将交由父 View 的 onTouchEvent() 方法进行处理,如果父 View 中一直没有处理,则最后将交由 Activity 的 onTouchEvent() 进行处理。

如果事件在分发过程中被某个 View 处理了,比如说是 ACTION_DOWN 事件,当该事件被处理了,后面的 ACTION_MOVE、ACTION_UP 事件将由处理该事件的那个 View 直接接收,也就是说一旦某个事件被某个 View 处理了,后面的一系列事件不在判断是否拦截这些事件,而是直接接收,因为一个完整的事件序列始终以 ACTION_DOWN 事件开始,然后若干 ACTION_MOVE 事件,最后以 ACTION_UP 结束。

总结

Android 事件传递机制主要内容大致如上所述,但实际分发流程肯定较为复杂,下一篇将从源码角度来看 Android 事件分发机制。

0%