本篇文章承接上一篇《深入理解Android:卷三》深入理解控件系统读书笔记(中),从输入事件在控件树中的派发和处理,以及Activity层面,继续深入了解Android的控件系统。
6.5 深入理解输入事件的派发
控件树中的输入事件派发是由ViewRootImpl
为起点,沿着控件树一层一层传递给目标控件,最终再回到ViewRootImpl
的一个环形过程。当一个输入事件被派发给ViewRootImpl
所在的窗口时,InputEventReceiver
的Looper
会被唤醒并触发InputEventReceiver.onInputEvent()
回调,控件树的输入事件派发便起始于这一回调
6.5.1 触摸模式
可以获取焦点的控件分为两类:
- 在任何情况下都可以获取焦点的控件,如文本框
- 仅在键盘操作时可以获取焦点的控件,如菜单项、按钮等
而触摸模式(TouchMode)正是为了管理两者的差异而引入的概念,Android通过进入或退出触摸模式实现两者之间的无缝切换。这个概念是一个系统级概念,也就是说会对所有窗口产生影响。系统是否处于触摸模式取决于WMS
的一个成员变量mInTouchMode
,而确定是否进入或者退出触摸模式则取决于用户对某一个窗口执行的操作。
窗口的ViewRootImpl
会根据用户操作,通过WMS
的接口setInTouchMode()
设置WMS.mInTouchMode
使得系统进入或退出触摸模式。而当其他窗口进行relayout
操作时会在WMS.relayoutWindow()
的返回值中添加或删除RELAYOUT_RES_IN_TOUCH_MODE
标记使得它们得知系统目前的操作模式。
注意,只有拥有ViewRootImpl
的窗口才能影响触摸模式,或对触摸模式产生响应。通过WMS
的接口直接创建的窗口必须手动地维护触摸模式。
6.5.2 控件焦点
1. 获取焦点的条件
当系统处于触摸模式时,仅当拥有FOCUSABLE_IN_TOUCH_MODE
标记的控件才能获取焦点
2. 获取焦点
void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) {
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
//把PFLAG_FOCUSED标记加入mPrivateFlags中,表示此控件已经拥有焦点
mPrivateFlags |= PFLAG_FOCUSED;
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
//将这一变化告知父控件,目的是保证控件树中只有一个控件拥有焦点。并且在viewRootImpl中触发一次“”遍历“”以便对控件树进行重绘
if (mParent != null) {
mParent.requestChildFocus(this, this);
}
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
//通知对此控件焦点变化感兴趣的监听者。InputMethodManager的focusIn()和focusOut()也会在这里被调用以更新输入法的状态
onFocusChanged(true, direction, previouslyFocusedRect);
//更新控件的Drawable状态,使得控件在随后的绘制中国得以高亮显示
refreshDrawableState();
}
}
3. 控件树的焦点体系
mParent.requestChildFocus()
的实现者是ViewGroup
及ViewRootImpl
。ViewGroup
实现的目的之一是用于将焦点从上一个焦点控件手中夺走,即将PFLAG_FOCUSED
标记从控件的mPrivateFlags
中移除。而另一个目的是将这一操作继续向控件树的根部进行回溯,直到ViewRootImpl
。ViewRootImpl
的requestChildFocus()
会将焦点控件保存起来备用,并引发一次“遍历”。
新的焦点体系的建立过程是通过在ViewGroup.requestChildFocus()
方法的回溯过程中进行mFocused=child
这一赋值操作完成的。当回溯完成后,mFocused=child
将会建立起一个单向链表,使得从根控件开始通过mFocused
可以沿着这一单向链表找到位于链表尾端的实际拥有焦点的控件。
而旧有的焦点体系的销毁过程则是通过在回溯过程中调用
mFocused.unFocus()
完成
@Override
void unFocus() {
if (mFocused == null) {
//表明位于链表的尾端,自身是焦点的实际拥有者
super.unFocus();
} else {
//将unFocus()传递给下一个控件
mFocused.unFocus();
mFocused = null;
}
}
可见ViewGroup.unFocus()
将unFocus()
调用沿着mFocused
所描述的链表沿着控件树向下遍历,直到焦点的实际拥有者。焦点的实际拥有者会拥有ViewGroup.unFocus()
,它会将PFLAG_FOCUSED
移除,并更新DrawableState
以及onFocusChanged()
方法的调用
4. ViewGroup的requestFocus()
在ViewGroup
上调用requestFocus()
会根据其DescendantsFocusability
特性的不同而产生三种不同的结果。注意ViewGroup.onRequestFocusInDescendants()
会负责遍历其所有子控件,并将requestFocus()
转发给他们,该方法中的direction
并不是控件在屏幕上的位置,而是他们在mChildren
列表中的位置,因此只有递增(FOCUS_FORWARD
)或递减两种
5. 下一个焦点控件的查找
最终的实现逻辑会走到FocusFinder.findNextFocus()
public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
mFocusedRect.set(focusedRect);
return findNextFocus(root, null, mFocusedRect, direction);
}
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
View next = null;
ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
//1. 首先将尝试依照开发者的设置选择下一个拥有焦点的控件
if (focused != null) {
next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
}
if (next != null) {
return next;
}
//2. 使用内置算法查找
ArrayList focusables = mTempList;
try {
focusables.clear();
//3. 将控件树中所有可以获取焦点的控件存储到focusable列表中
effectiveRoot.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
//4. 调用findNextFocus()另一个重载完成查找
next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
}
} finally {
focusables.clear();
}
return next;
}
查找下一个焦点的内置算法,在查找最合适的候选焦点时,有如下几个比较原则:
- 与起始位置比较,倘若一个控件A位于指定方向上,而控件B位于指定方向的另一侧,则控件A是更佳候选。
- 将其实位置沿着查找方向延伸到无限远,行程的形式被称为BEAM一条杠。倘若一个控件A与BEAM存在交集,而另一个控件B没有,则与BEAM存在交集的控件A为更佳候选
- 当无法通过BEAM确定更佳候选时(如两个控件与BEAM同时存在交集,或同时不存在交集),则通过比较两控件与焦点控件相邻边的中点的距离进行确定,距离近着为更佳候选。注意在进行距离计算时
FocusFinder
为指定方向增加了一个权重,以LEFT方向查找为例,其距离计算公式为(13dxdx + dy*dy),也就是说这个距离对于X方向的距离更佳敏感。
6.5.3 输入事件派发的综述
输入系统的派发终点是InputEventReceiver
,作为空间系统最高级别的管理者,ViewRootImpl
便是InputEventReceiver
的一个用户,它从InputEventReceiver
中获取时间,然后将它们按照一定的流程派发给所有可能感兴趣的对象,包括View
、PhoneWindow
、Activity
以及Dialog
等
1.ViewRootImpl`的输入事件队列
在ViewRootImpl.setView()
中,新的窗口被创建之后,ViewRootImpl
使用WMS
分配的InputChannel
以及当前线程的Looper
一起创建了InputEventReceiver
的子类WindowInputEventReceiver
的一个实例,并将其保存在ViewRootImpl.mInputEventReceiver
成员中,这标记着从设备驱动到本窗口的输入事件通道的正式建立。至此,每当有输入事件到来时,ViewRootImpl
都可以通过WindowInputEventReceiver.onInputEvent()
回调得到这个事件并进行处理
@Override
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
//1.将InputEvent对应的InputEventReceiver封装成QueuedInputEvent
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
// Always enqueue the input event in order, regardless of its time stamp.
// We do this because the application or the IME may inject key events
// in response to touch events and we want to ensure that the injected keys
// are processed in the order they were received and we cannot trust that
// the time stamp of injected events are monotonic.
//2. 追加到单向链表,ViewRootImpl将会沿着链表从头至尾地逐个处理输入事件
QueuedInputEvent last = mPendingInputEventTail;
if (last == null) {
mPendingInputEventHead = q;
mPendingInputEventTail = q;
} else {
last.mNext = q;
mPendingInputEventTail = q;
}
mPendingInputEventCount += 1;
Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
mPendingInputEventCount);
if (processImmediately) {
//3. 倘若第三个参数为true,则直接在当前线程中开始对输入事件的处理工作
doProcessInputEvents();
} else {
//4. 否则将处理事件的请求发送给主线程的Handler,随后进行处理。这是为了避免旧事件尚未处理完毕时开始了新的事件处理流程
scheduleProcessInputEvents();
}
}
doProcessInputEvents()
在所有输入事件处理完成之前它都不会释放主线程的占用权。这种处理方式使得performTraversals()
无法在单个输入事件处理后立刻得到执行,因输入事件所导致的requestLayout()
或invalidate()
操作会在输入事件全部处理完毕之后由一次performTraversals()
统一完成。当队列中存在较多事件时这种方式带来的效率提升不言而喻。
2. 分道扬镳的事件处理
在deliverInputEvent()
方法中,不同类型的输入事件的处理开始分道扬镳:
-
deliverKeyEvent()
,用于派发按键类型的事件。它选择的是基于焦点的派发策略。 -
deliverPointerEvent()
,用于派发标准的触摸事件。它选择的是基于位置的派发策略。 -
deliverTrackballEvent()
,用于派发轨迹球事件。 -
deliverGenericMotionEvent()
,用于派发其他的Motion
事件。悬浮世佳、游戏手柄等会在这里被处理。
3.共同的中点——finishInputEvent()
6.5.4 按键事件的派发
按键事件的派发流程就是沿着mFocused
成员所构成的单向链表进行遍历的过程。
围绕着输入法,ViewRootImpl.deliverKeyEvent()
揭示了按键事件派发的三个阶段。受限控件树中的控件可以在输入法处理按键事件之前,通过View.dispatchKeyEventPreIme()
方法获得处理机会。倘若控件未在此时消费事件,那么按键事件将会被派发给输入法。倘若输入法也没有消费这一事件,则ViewRootImpl.deliverKeyEventPostIme()
将使得控件第二次有机会处理此事件
1. 按键事件的初次派发
开发者可以通过重写View.onKeyPreIme()
获得优先于输入法进行按键事件的处理。同样,也可以通过重写View.dispatchKeyPreIme()
做同样的事。区别在于,前者仅当控件拥有焦点时才会被调用,而后者是mFocused
链表上的所有控件都会被调用。(这个区别同样适用于其他输入事件相关的dispatchXXX()
和onXXX()
)
2. 输入法对按键事件的处理
输入法所在的窗口时无法获得焦点的,因此需要将按键事件派发给处于焦点状态的窗口。ViewRootImpl
将会在收到事件后首先转发给输入法,当输入法对此事件不感兴趣时再将其发送给控件树。
派发给输入法的条件是mLastWasImTarget
成员为true,即本窗口可能是输入法的输入目标。这一取值来源于窗口的LayoutParams.flags
中FLAG_NOT_FOCUSABLE
及FLAG_ALT_FOCUSABLE_IM
两个标记的存在情况。
InputMethodManager.dispatchKeyEvent()
方法将会通过Binder
将按键事件发送给当前输入法所在的InputMethodService
,并在那里通过onKeyXXX()
系列事件处理方法中得到处理。
3. 按键事件的最终派发
deliverKeyEventPostIme()
负责按键事件的最终派发,onKeyDown()
系列回调以及onKeyListener()
监听者都会得到触发,同时一些系统内置的按键功能也将在这里进行处理。
按照优先级,可以列出如下几个可能消费事件的对象或行为:
-
TouchMode
。如果导致了触摸模式的终止,此事件会被消费 - 控件树中的控件。
View.dispatchKeyEvent()
方法会将事件派发给控件树 -
mFallbackEventHandler
。与PhoneWindowManager
类似,它提供了一个进行系统级按键处理的场所,只不过它的处理优先级低得多,当需要为某个按键定义一个系统级的功能,并允许应用程序修改此按键的功能时,可以在PhoneFallbackEventHandler
类中进行实现,例如使用音量减调整系统音量,应用程序也可以将音量键挪作他用。注意每个ViewRootImpl
都有各自的PhoneFallbackEventHandler
该实例。 - 焦点游走。它主要感兴趣的是方向键和TAB键的按下事件,它将根据按键选择一个焦点的查找方向,然后通过
View.focusSearch()
方法选择一个控件并使其获得焦点。
6.6 Activity与控件系统
6.6.1 PhoneWindow
Window
类有三个最核心的组件:WindowManager.LayoutParams
、一棵控件树以及Window.Callback
。目前Android中使用的Window
类的实现就是PhoneWindow
,Window
类中提供了用于修改LayoutParams
的接口等通用功能实现,而PhoneWindow
类则负责具体的外观模板的实现。简而言之,它就是一个用于快速构建窗口外观的工具类。值得注意的是,它和PhoneWindowManager
之间没有任何关系。后者只是WMS的一个组成部分,用于提供与窗口管理相关的策略。
1.选择窗口外观与设置显示内容
Activity.requestWindowFeature()
决定了窗口的外观模板,Activity.setContentView()
则设置一棵控件树用于显示在Activity
中。前者要在后者之前调用,否则无效。
其中Activity.setContentView()
的核心调用链是:Activity.setContentView()
->PhoneWindow.setContentView()
->PhoneWindow.installDecor()
->PhoneWindow.generateLayout()
2.DecorView的特点
DecorView
与PhoneWindow
的关系十分密切,它利用自己根控件爱的身份为PhoneWindow
偷取了很多控件系统内部的信息,其中就包括控件的生命周期信息以及输入事件。
DecorView
作为控件树的根,并不像其他ViewGroup
那样将事件派发给子控件,而是将事件发送给Window.Callback
。作为Window.Callback
的实现者的Activity
或Dialog
自然就有能力接收输入事件。当DecorView
接收到事件之后,会首先将其交给Callback
(通常是Activity
或Dialog
)的dispatchTouchEvent()
过目,后者会将事件交还给DecorView
进行常规的事件派发,倘若事件在派发过程中没有被消费掉,Callback
再自行消费这一事件,对于其他事件也是一样。因此,Callback的实现者如Activity或Dialog中的dispatchXXX()会咸鱼控件树中的任何一个控件进行事件处理,而它们的onXXX()则仅当事件没有被任何一个控件树消费时才有机会进行事件处理。
6.2 Activity窗口的创建与显示
Activity创建完之后的第一件事就是进行初始化,完成窗口令牌等重要信息的移交,而初始化就发生在Activity.attch()
中。在该方法中,Activity获取了创建窗口所需的所有条件:PhoneWindow
、WindowManager
,以及一个来自AMS的窗口令牌。
Activity窗口的显示发生在Activity.onResume()
之后:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
.............
// 1. Activity.onResume()会被调用
r = performResumeActivity(token, clearHide, reason);
if (r != null) {
final Activity a = r.activity;
..............
//2.创建窗口。当ActivityClientRecord的window成员为null时,表示此Activity尚未创建窗口。
//此时需要将PhoneWindow中的控件树交给WindowManager完成窗口的创建。这种情况对应于
//Activity初次创建的情况(即onCreate()被调用的情况)。如果Activity因为某种原因被暂停,如新的
//Activity覆盖其上或者用户按了Home键,虽说Activity不再处于Resume状态,
//但是其窗口并没有从WMS中移除,只是它不可见而已
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
//设置窗口类型为BASE_APPLICATION。这表示窗口属于一个Activity
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
if (localLOGV) Slog.v(
TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
// Get rid of anything left hanging around.
cleanUpPendingRemoveWindows(r, false /* force */);
// The window is now visible if it has been added, we are not
// simply finishing, and we are not starting another activity.
//3. 使Activity可见
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
.......
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
}
可见,当Activity.onResume()
被调用时,Activity
的窗口其实尚未显示甚至尚未创建。也就是说Activity
的显示发生在onResume()
之后。其实除非Activity
被销毁(onDestroy()
),其所属窗口都会存在于WMS之中,这期间的onStart()
/onStop()
所导致的可见性的变换都是通过修改DecorView()
的可见性实现窗口的显示与隐藏的。