老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!
在Android系统中,除了输入法窗口之外,还有一种窗口称为输入法对话框,它们总是位于输入窗口的上面。Activity窗口、输入法窗口和输入法对话框的位置关系如图1所示:
图1 Activity窗口、输入法窗口和输入法对话框的位置关系
在前面Android窗口管理服务WindowManagerService组织窗口的方式分析一文中提到,WindowManagerService服务是使用堆栈来组织系统中的窗口的,因此,如果我们在窗口堆栈中观察Activity窗口、输入法窗口和输入法对话框,它们的位置关系就如图2所示:
图2 Activity窗口、输入法窗口和输入法对话框在窗口堆栈中的位置关系
图2中的对象的关系如下所示:
1. 在ActivityManagerService服务内部的Activity组件堆栈顶端的ActivityRecord对象N描述的是系统当前激活的Activity组件。
2. ActivityRecord对象N在WindowManagerService服务内部的窗口令牌列表顶端对应有一个AppWindowToken对象N。
3. AppWindowToken对象N在WindowManagerService服务内部的窗口堆栈中对应有一个WindowState对象N,用来描述系统当前激活的Activity组件窗口。
4. WindowState对象N上面有一个WindowState对象IMW,用来描述系统中的输入法窗口。
5. WindowState对象IMW上面有三个WindowState对象IMD-1、IMD-2和IMD-3,它们用来描述系统中的输入法对话框。
6. 系统中的输入法窗口以及输入法对话框在WindowManagerService服务内部中对应的窗口令牌是由WindowToken对象IM来描述的。
7. WindowToken对象IM在InputMethodManagerService服务中对应有一个Binder对象。
总的来说,就是图2描述了系统当前激活的Activity窗口上面显示输入法窗口,而输入法窗口上面又有一系列的输入法对话框的情景。WindowManagerService服务的职能之一就是要时刻关注系统中是否有窗口需要使用输入法。WindowManagerService服务一旦发现有窗口需要使用输入法,那么就会调整输入法窗口以及输入法对话框在窗口堆栈中的位置,使得它们放置在需要使用输入法的窗口的上面。
接下来,我们就首先分析两个需要调整输入法窗口以及输入法对话框在窗口堆栈中的位置的情景,然后再分析它们是如何在窗口堆栈中进行调整的。
第一个需要调整输入法窗口以及输入法对话框在窗口堆栈中的位置的情景是增加一个窗口到WindowManagerService服务去的时候。从前面Android应用程序窗口(Activity)与WindowManagerService服务的连接过程分析一文可以知道,增加一个窗口到WindowManagerService服务最终是通过调用WindowManagerService类的成员函数addWindow来实现的。接下来我们就主要分析这个函数中与输入法窗口以及输入法对话框调整相关的逻辑,如下所示:
- public class WindowManagerService extends IWindowManager.Stub
- implements Watchdog.Monitor {
- ......
- WindowState mInputMethodWindow = null;
- final ArrayList mInputMethodDialogs = new ArrayList();
- ......
- public int addWindow(Session session, IWindow client,
- WindowManager.LayoutParams attrs, int viewVisibility,
- Rect outContentInsets, InputChannel outInputChannel) {
- ......
- synchronized(mWindowMap) {
- ......
- WindowToken token = mTokenMap.get(attrs.token);
- if (token == null) {
- ......
- if (attrs.type == TYPE_INPUT_METHOD) {
- ......
- return WindowManagerImpl.ADD_BAD_APP_TOKEN;
- }
- ......
- }
- ......
- win = new WindowState(session, client, token,
- attachedWindow, attrs, viewVisibility);
- ......
- boolean imMayMove = true;
- if (attrs.type == TYPE_INPUT_METHOD) {
- mInputMethodWindow = win;
- addInputMethodWindowToListLocked(win);
- imMayMove = false;
- } else if (attrs.type == TYPE_INPUT_METHOD_DIALOG) {
- mInputMethodDialogs.add(win);
- addWindowToListInOrderLocked(win, true);
- adjustInputMethodDialogsLocked();
- imMayMove = false;
- }
- ......
- boolean focusChanged = false;
- if (win.canReceiveKeys()) {
- focusChanged = updateFocusedWindowLocked(UPDATE_FOCUS_WILL_ASSIGN_LAYERS);
- if (focusChanged) {
- imMayMove = false;
- }
- }
- if (imMayMove) {
- moveInputMethodWindowsIfNeededLocked(false);
- }
- ......
- }
- ......
- }
- ......
- }
这个函数定义在文件frameworks/base/services/java/com/android/server/WindowManagerService.java中。
如果当前增加到WindowManagerService服务来的是一个输入法窗口,即参数attrs所描述的一个WindowManager.LayoutParams对象的成员变量type的值等于TYPE_INPUT_METHOD,那么就要求与该输入法窗口所对应的类型为WindowToken的窗口令牌已经存在,否则的话,WindowManagerService类的成员函数addWindow就会直接返回一个错误码WindowManagerImpl.ADD_BAD_APP_TOKEN给调用者。这个类型为WindowToken的窗口令牌是InputMethodManagerService服务请求WindowManagerService服务创建的,即调用WindowManagerService类的成员函数addWindowToken来创建的,具体可以参考前面Android窗口管理服务WindowManagerService组织窗口的方式分析一文。
如果当前增加到WindowManagerService服务来的是一个输入法窗口,那么就会将前面为它所创建的一个WindowState对象win保存在WindowManagerService类的成员变量mInputMethodWindow中,接着还会调用WindowManagerService类的成员函数addInputMethodWindowToListLocked来将该WindowState对象插入到窗口堆栈的合适位置去。
如果当前增加到WindowManagerService服务来的是一个输入法对话框,即参数attrs所描述的一个WindowManager.LayoutParams对象的成员变量type的值等于TYPE_INPUT_METHOD_DIALOG,那么就会将前面为它所创建的一个WindowState对象win添加到WindowManagerService类的成员变量mInputMethodDialogs所描述的一个ArrayList中去,并且先后调用WindowManagerService类的成员函数addWindowToListInOrderLocked和adjustInputMethodDialogsLocked来将该WindowState对象插入到窗口堆栈的合适位置去。
在上述两种情况中,由于用来描述输入法窗口或者输入法对话框的WindowState对象已经被插入到了窗口堆栈中的合适位置,因此,接下来就不再需要考虑移动该输入法窗口或者输入法对话框了,这时候变量imMayMove的值就会被设置为false。
另一方面,如果当前增加到WindowManagerService服务来的既不是一个输入法窗口,也不是一个输入法对话框,并且该窗口需要接收键盘事件,即前面所创建的WindowState对象win的成员函数canReceiveKeys的返回值为true,那么就可能会导致系统当前获得焦点的窗口发生变化,这时候就需要调用WindowManagerService类的成员函数updateFocusedWindowLocked来重新计算系统当前获得焦点的窗口。如果系统当前获得焦点的窗口发生了变化,那么WindowManagerService类的成员函数updateFocusedWindowLocked的返回值focusChanged就会等于true,同时系统的输入法窗口和输入法对话框在窗口堆栈中的位置也会得到调整,即位它们会位于系统当前获得焦点的窗口的上面,因此,这时候变量imMayMove的值也会被设置为false,表示接下来不再需要考虑移动系统中的输入法窗口或者输入法对话框在窗口堆栈中的位置。
最后,如果变量imMayMove的值保持为初始值,即保持为true,那么就说明当前增加的窗口可能会引发系统的输入法窗口和输入法对话框在窗口堆栈中的位置发生变化,因此,这时候就需要调用WindowManagerService类的成员函数moveInputMethodWindowsIfNeededLocked来作检测,并且在发生变化的情况下,将系统的输入法窗口和输入法对话框移动到窗口堆栈的合适位置上去。
从上面的分析就可以知道,在增加一个窗口的过程中,可能需要调用WindowManagerService类的成员函数addInputMethodWindowToListLocked、addWindowToListInOrderLocked、adjustInputMethodDialogsLocked和moveInputMethodWindowsIfNeededLocked来移动系统的输入法窗口和输入法对话框,其中,WindowManagerService类的成员函数addWindowToListInOrderLocked在前面Android窗口管理服务WindowManagerService组织窗口的方式分析一文已经分析过了,本文只要关注其余三个成员函数的实现。
第二个需要调整输入法窗口以及输入法对话框在窗口堆栈中的位置的情景是一个应用程序进程请求WindowManagerService服务重新布局一个窗口的时候。从前面Android窗口管理服务WindowManagerService计算Activity窗口大小的过程分析一文可以知道,应用程序进程请求WindowManagerService服务重新布局一个窗口最终是通过调用WindowManagerService类的成员函数relayoutWindow来实现的。接下来我们就主要分析这个函数中与输入法窗口以及输入法对话框调整相关的逻辑,如下所示:
- public class WindowManagerService extends IWindowManager.Stub
- implements Watchdog.Monitor {
- ......
- public int relayoutWindow(Session session, IWindow client,
- WindowManager.LayoutParams attrs, int requestedWidth,
- int requestedHeight, int viewVisibility, boolean insetsPending,
- Rect outFrame, Rect outContentInsets, Rect outVisibleInsets,
- Configuration outConfig, Surface outSurface) {
- ......
- synchronized(mWindowMap) {
- WindowState win = windowForClientLocked(session, client, false);
- ......
- int attrChanges = 0;
- int flagChanges = 0;
- if (attrs != null) {
- flagChanges = win.mAttrs.flags ^= attrs.flags;
- attrChanges = win.mAttrs.copyFrom(attrs);
- }
- ......
- boolean imMayMove = (flagChanges&(
- WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)) != 0;
- boolean focusMayChange = win.mViewVisibility != viewVisibility
- || ((flagChanges&WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) != 0)
- || (!win.mRelayoutCalled);
- ......
- if (viewVisibility == View.VISIBLE &&
- (win.mAppToken == null || !win.mAppToken.clientHidden)) {
- displayed = !win.isVisibleLw();
- ......
- if ((attrChanges&WindowManager.LayoutParams.FORMAT_CHANGED) != 0) {
- // To change the format, we need to re-build the surface.
- win.destroySurfaceLocked();
- displayed = true;
- }
- ......
- if (win.mAttrs.type == TYPE_INPUT_METHOD
- && mInputMethodWindow == null) {
- mInputMethodWindow = win;
- imMayMove = true;
- }
- if (displayed) {
- focusMayChange = true;
- }
- ......
- } else {
- ......
- if (win.mSurface != null) {
- ......
- // If we are not currently running the exit animation, we
- // need to see about starting one.
- if (!win.mExiting || win.mSurfacePendingDestroy) {
- ......
- if (!win.mSurfacePendingDestroy && win.isWinVisibleLw() &&
- applyAnimationLocked(win, transit, false)) {
- focusMayChange = true;
- win.mExiting = true;
- } else if (win.isAnimating()) {
- // Currently in a hide animation... turn this into
- // an exit.
- win.mExiting = true;
- } else if (win == mWallpaperTarget) {
- // If the wallpaper is currently behind this
- // window, we need to change both of them inside
- // of a transaction to avoid artifacts.
- win.mExiting = true;
- win.mAnimating = true;
- } else {
- if (mInputMethodWindow == win) {
- mInputMethodWindow = null;
- }
- win.destroySurfaceLocked();
- }
- }
- }
- ......
- }
- if (focusMayChange) {
- ......
- if (updateFocusedWindowLocked(UPDATE_FOCUS_WILL_PLACE_SURFACES)) {
- imMayMove = false;
- }
- ......
- }
- // updateFocusedWindowLocked() already assigned layers so we only need to
- // reassign them at this point if the IM window state gets shuffled
- boolean assignLayers = false;
- if (imMayMove) {
- if (moveInputMethodWindowsIfNeededLocked(false) || displayed) {
- // Little hack here -- we -should- be able to rely on the
- // function to return true if the IME has moved and needs
- // its layer recomputed. However, if the IME was hidden
- // and isn't actually moved in the list, its layer may be
- // out of data so we make sure to recompute it.
- assignLayers = true;
- }
- }
- ......
- if (assignLayers) {
- assignLayersLocked();
- }
- ......
- }
- ......
- return (inTouchMode ? WindowManagerImpl.RELAYOUT_IN_TOUCH_MODE : 0)
- | (displayed ? WindowManagerImpl.RELAYOUT_FIRST_TIME : 0);
- }
- ......
- }
这个函数定义在文件frameworks/base/services/java/com/android/server/WindowManagerService.java中。
应用程序进程在请求WindowManagerService服务重新布局一个窗口的时候,这个窗口的一些布局参数可能会发生变化,而这些变化可能会同时引发系统的输入法窗口以及输入法对话框在窗口堆栈中的位置发生变化。如果系统的输入法窗口以及输入法对话框在窗口堆栈中的位置发生了变化,那么就需要调整它们在窗口堆栈中的位置。
WindowManagerService类的成员函数relayoutWindow首先调用根据参数session和client来调用另外一个成员函数windowForClientLocked,以便可以获得用来描述要重新布局的窗口的一个WindowState对象win。
WindowState对象win的成员变量mAttrs指向的是一个WindowManager.LayoutParams对象,该WindowManager.LayoutParams对象的成员变量flags描述的是窗口上一次所设置的布局属性标志位,而参数attrs所描述的一个WindowManager.LayoutParams对象的成员变量flags描述的是窗口当前被设置的布局属性标志位。WindowManagerService类的成员函数relayoutWindow通过对这两个标志位执行一个异或操作,就可以知道窗口的哪些布局属性标志位发生了变化,这些变化就记录在变量flagChanges中。
WindowManagerService类的成员函数relayoutWindow在对WindowState对象win所描述的窗口进行布局之前,还要将参数attrs指的是一个WindowManager.LayoutParams对象的内容拷贝到 WindowState对象win的成员变量mAttrs指向的是一个WindowManager.LayoutParams对象中去。在拷贝的过程中,如果发现这两个WindowManager.LayoutParams对象所描述的窗口布局属性有发生变化,那么这些变化就会记录在变量attrChanges中。
在窗口的布局属性标志中,位WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE表示窗口是否可以获得焦点,另外一个位WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM是用来反转WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE位的作用的。一个窗口是否可以获得焦点意味着它是否需要与输入法窗口交互,即如果一个窗口是可以获得焦点的,那么就意味着它需要与输入法窗口交互,否则就不需要。当一个窗口的WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE位等于1,那么就表示窗口不可以获得焦点,即不需要与输入法窗口交互,但是如果该窗口的WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM位也等于1,那么就表示窗口仍然是需要与输入法窗口交互的。另一方面,如果一个窗口的WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM位等于1,但是该窗口的WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE位等于0,那么就表示窗口仍然是不可以与输入法窗口交互的。因此,当前面得到的变量flagChanges的WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE位或者WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM位发生了变化时,都意味着对WindowState对象win所描述的窗口进行重新布局会影响系统中的输入法窗口以及输入法对话框,即该窗口可能会由需要显示输入法窗口以及输入法对话框,到不需要显示输入法窗口以及输入法对话框,反之亦然。最后得到的变量imMayMove的值等于true就表示要移动系统中的输入法窗口以及输入法对话框在窗口堆栈中的位置。
一个窗口由不可获得焦点到可以获得焦点,或者由可获得焦点到不可以获得焦点,即窗口布局属性标志中的WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE位发生了变化,那么就意味着要重新计算系统当前获得焦点的窗口。从前面分析增加窗口到WindowManagerService服务的情景可以知道,当系统当前获得焦点的窗口发生变化时,也意味着需要系统中的移动输入法窗口以及输入法对话框在窗口堆栈中的位置。除了窗口布局属性标志中的WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE位变化会引发系统当前获得焦点的窗口发生变化之外,还有另外两个因素会引发系统当前获得焦点的窗口发生变化。第一个因素是窗口的可见性发生变化。WindowState对象win的成员变量mViewVisibility描述的是窗口上一次布局时的可见性,而参数viewVisibility描述的是窗口当前的可见性,当它们的值不相等时,就意味着窗口的可见性发生了变化。第二个因素是窗口是第一次被应用程序进程请求WindowManagerService服务布局,这时候WindowState对象win的成员变量mRelayoutCalled的值就会等于false。最后得到的变量focusMayChange等于true,就表示需要重新计算系统当前获得焦点的窗口。
WindowState对象win所描述的窗口在此次重新布局中是否会引起移动系统中的输入法窗口以及输入法对话框在窗口堆栈中的位置,还取决于它在的可见性以及它的绘图表面属性等信息,接下来我们就按照 WindowState对象win所描述的窗口当前是可见还是不可见来分别分析。
我们首先分析WindowState对象win所描述的窗口在此次重新布局中是可见的情景,即参数viewVisibility的值等于View.VISIBLE。注意,如果WindowState对象win所描述的是一个Activity窗口,而该Activity组件是不可见的,那么即使参数viewVisibility的值等于View.VISIBLE,那么WindowState对象win所描述的窗口在此次重新布局中也是认为不可见的。从前面Android应用程序窗口(Activity)与WindowManagerService服务的连接过程分析一文可以知道,当WindowState对象win的成员变量mAppToken的值不等于null时,那么该WindowState对象win描述的是一个Activity窗口,而当该成员变量所指向的一个AppWindowToken对象的成员变量clientHidden的值等于false时,就表示对应的Activity组件是可见的。
WindowState对象win所描述的窗口在上一次布局时的可见性可以调用它的成员函数isVisibleLw来获得。如果WindowState对象win所描述的窗口在上一次布局时是不可见的,那么现在就需要将它设置为可见的,即要将它显示出来,这时候变量displayed的值就会等于true。另一方面,如果WindowState对象win所描述的窗口的绘图表面的像素格式发生了变化,即变量attrChanges的WindowManager.LayoutParams.FORMAT_CHANGED位等于1,那么这时候就需要调用WindowState对象win的成员函数destroySurfaceLocked来销毁它所描述的窗口的绘图表面,以便接下来可以为它重新创建一个新的绘图表面,这时候也会将变量displayed的值设置为true,表示接下来是要显示WindowState对象win所描述的窗口的。如果最终得到的变量displayed的值设置为true,那么就相当于说明WindowState对象win所描述的窗口经历一个由不可见到可见的状态变化,因此就可能会导致系统当前获得焦点的窗口发生变化,这时候就会将变量focusMayChange的值设置为true。
如果WindowState对象win描述的是一个输入法窗口,即它的成员变量mAttrs所描述的一个WindowManager.LayoutParams对象的成员变量type的值等于TYPE_INPUT_METHOD,并且系统中的输入法窗口尚未设置,即WindowManagerService类的成员变量mInputMethodWindow的值等于null,那么就说明接下来要显示的其实是输入法窗口,这情况会导致需要移动系统中的输入法窗口以及输入法对话框在窗口堆栈中的位置,因此,这时候除了需要将WindowState对象win保存在WindowManagerService类的成员变量mInputMethodWindow之外,还需要将变量imMayMove的值设置为true。
我们接下来再分析WindowState对象win所描述的窗口在此次重新布局中是不可见的情景。一个窗口变得不可见了,就意味着可能要销毁它的绘图表面,取决于它的绘图表面是否存在,以及它的退出动画是否已经显示结束。WindowState对象win所描述的窗口的绘图表面保存在它的成员变量mSurface中,因此,当WindowState对象win的成员变量mSurface不等于null的时候,就意味着可能会销毁它所描述的绘图表面。
如果WindowState对象win的成员变量mExiting等于false时,那么就说明该WindowState对象win所描述的窗口的退出动画可能尚未开始,也可能已经结束。另一方面,如果WindowState对象win的成员变量mSurfacePendingDestroy的值等于true,那么就说明该WindowState对象win所描述的窗口的绘图表面正在等待销毁。这两种情况都需要进一步确定接下来是要开始WindowState对象win所描述的窗口的退出动画,还是要销毁WindowState对象win所描述的窗口的绘图表面。
如果WindowState对象win的成员变量mSurfacePendingDestroy的值等于false,那么同时也意味着它所描述的窗口还未开始显示退出动画,因而它的绘图表面就没有进入正在等待销毁的状态。在这种情况下,如果WindowState对象win所描述的窗口是可见的,即它的成员函数isWinVisibleLw的返回值等于true,那么就意味要开始该窗口的退出动画了,这是通过调用WindowManagerService类的成员函数applyAnimationLocked来实现的。WindowState对象win描述的窗口开始退出动画之后,就意味要重新计算系统当前获得焦点的窗口,因此,这时候就会将变量focusMayChange的值设置为true,同时还会将WindowState对象win的成员变量mExiting的值设置为true,表示它描述的窗口正在退出的过程中。
如果WindowState对象win所描述的窗口正在处于退出动画的过程中,即它的成员函数isAnimating的返回值等于true,那么这时候需要确保WindowState对象win的成员变量mExiting的值为true。
如果WindowState对象win所描述的窗口已经结束退出动画,但是它仍然是壁纸窗口的目标,即WindowManagerService类的成员变量mWallpaperTarget的值不等于null,并且它的值就等于WindowState对象win,那么这时候就需要等待壁纸窗口也退出之后,才销毁WindowState对象win所描述的窗口,因此,这时候就需要将WindowState对象win的成员变量mExiting和mAnimating的值设置为true,即假装它所描述的窗口还处于正在退出的过程,这样做是为了等待壁纸窗口退出完成。
如果WindowState对象win所描述的窗口已经结束退出动画,并且它不是壁纸窗口的目标,那么这时候就需要调用它的成员函数destroySurfaceLocked来销毁它的绘图表面了。在销毁WindowState对象win所描述的窗口之前,还会判断它是否就是系统当前的输入法窗口,即WindowManagerService类的成员变量mInputMethodWindow的值是否等于win。如果等于的话,那么就说明系统当前的输入法窗口被销毁了,因此,就需要将WindowManagerService类的成员变量mInputMethodWindow的值设置为null。
经过上面的一系列操作之后,如果最终得到的变量focusMayChange的值等于true,那么就说明需要重新计算系统当前获得焦点的窗口了,这是通过调用WindowManagerService类的成员函数updateFocusedWindowLocked来实现的。一旦WindowManagerService类的成员函数updateFocusedWindowLocked的返回值为true,那么就说明统当前获得焦点的窗口发生了变化,并且系统中的输入法窗口以及输入法对话框也移动到窗口堆栈中的正确位置了,因此,这时候就会将变量imMayMove的值设置为false。
经过上面的一系列操作之后,如果最终得到的变量imMayMove的值等于true,那么就说明有可能需要移动系统中的输入法窗口以及输入法对话框在窗口堆栈中的位置,这是通过调用WindowManagerService类的成员函数moveInputMethodWindowsIfNeededLocked来实现的。一旦系统中的输入法窗口以及输入法对话框在窗口堆栈中的位置发生了移动,那么WindowManagerService类的成员函数moveInputMethodWindowsIfNeededLocked的返回值就等于true,这时候就需要将变量assignLayers的值设置为true,表示要重新计算系统中的窗口的Z轴位置,以便可以同步到SurfaceFlinger服务中去。注意,如果系统中的输入法窗口以及输入法对话框在窗口堆栈中的位置没有发生变化,但是前面得到的变量displayed的值等于true,那么也是需要将变量assignLayers的值设置为true的,因为这个变量displayed的值等于true意味着WindowState对象win所描述的窗口经历了从不可见到可见的状态变化,因此也需要重新计算系统中的窗口的Z轴位置。
经过上面的一系列操作之后,如果最终得到的变量assignLayers的值等于true,那么就需要调用WindowManagerService类的成员函数assignLayersLocked来执行重新计算统中的窗口的Z轴位置的操作了。在后面的文章中,我们再详细分析WindowManagerService服务是如何计算系统中的窗口的Z轴位置的。
从上面的分析就可以知道,在布局一个窗口的过程中,可能需要调用WindowManagerService类的成员函数moveInputMethodWindowsIfNeededLocked来移动系统的输入法窗口和输入法对话框。再结合前面增加窗口的情景,我们就可以知道,在WindowManagerService类中,与输入法窗口以及输入法对话框相关的成员函数有addInputMethodWindowToListLocked、adjustInputMethodDialogsLocked和moveInputMethodWindowsIfNeededLocked,它们的作用如下所示:
A. 成员函数addInputMethodWindowToListLocked用来将输入法窗口插入到窗口堆栈的合适位置,即插入到需要显示输入法窗口的窗口的上面。
B. 成员函数adjustInputMethodDialogsLocked用来移动输入法对话框到窗口堆栈的合适位置,即移动到输入法窗口的上面。
C. 成员函数moveInputMethodWindowsIfNeededLocked用来检查是否需要移动输入法窗口以及输入法对话框。如果需要的话,那么就将它们移动到窗口堆栈的合适位置去,即将输入法窗口移动到需要显示输入法窗口的窗口的上面,而将输入法对话框移动到输入法窗口的上面。
在分析这三个成员函数的实现之前,我们首先分析WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked和moveInputMethodDialogsLocked,它们是两个基本的操作,其中:
D. 成员函数findDesiredInputMethodWindowIndexLocked用来查找输入法窗口在窗口堆栈的正确位置,这个位置刚好就是在需要显示输入法窗口的窗口在窗口堆栈中的上一个位置。
E. 成员函数moveInputMethodDialogsLocked用来将移动输入法对话框移动到输入法窗口的上面去。
接下来我们开始分析上述五个函数的实现。
1. 计算输入法窗口在窗口堆栈中的位置
输入法窗口在窗口堆栈中的位置是通过调用WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked来获得的,它首先找到需要显示输入法的窗口在窗口堆栈中的位置,然后再将这个位置加1,就可以得到输入法窗口在窗口堆栈中的位置。
WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked定义在文件frameworks/base/services/java/com/android/server/WindowManagerService.java中,它的实现比较长,我们分段来阅读:
- public class WindowManagerService extends IWindowManager.Stub
- implements Watchdog.Monitor {
- ......
- int findDesiredInputMethodWindowIndexLocked(boolean willMove) {
- final ArrayList localmWindows = mWindows;
- final int N = localmWindows.size();
- WindowState w = null;
- int i = N;
- while (i > 0) {
- i--;
- w = localmWindows.get(i);
- ......
- if (canBeImeTarget(w)) {
- ......
- // Yet more tricksyness! If this window is a "starting"
- // window, we do actually want to be on top of it, but
- // it is not -really- where input will go. So if the caller
- // is not actually looking to move the IME, look down below
- // for a real window to target...
- if (!willMove
- && w.mAttrs.type == WindowManager.LayoutParams.TYPE_APPLICATION_STARTING
- && i > 0) {
- WindowState wb = localmWindows.get(i-1);
- while (i > 1 && wb.mAppToken == w.mAppToken && !canBeImeTarget(wb)) {
- i--;
- wb = localmWindows.get(i-1);
- }
- if (wb.mAppToken == w.mAppToken && canBeImeTarget(wb)) {
- i--;
- w = wb;
- }
- }
- break;
- }
- }
- mUpcomingInputMethodTarget = w;
参数willMove表示调用者计算输入法窗口在窗口堆栈中的位置的目的。如果它的值等于true,那么就说明调用者获得了输入法窗口在窗口堆栈中的位置之后,接下来就会将输入法窗口移动到需要显示输入法窗口的窗口的上面去,否则的话,就说明调用者只是为了知道输入法窗口在窗口堆栈中的位置,而不打算移动输入法窗口。
在从上到下查找需要显示输入法的窗口的过程中,如果找到一个WindowState对象w,它所描述的窗口需要显示输入法窗口,但是这个窗口其实是一个Activity窗口的启动窗口,即该WindowState对象w的成员变量mAttrs所描述的一个WindowManager.LayoutParams对象的成员变量type的值等于WindowManager.LayoutParams.TYPE_APPLICATION_STARTING,那么由于调用WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked的目的不是用来移动输入法窗口,而是用来查找输入法窗口在窗口堆栈中的确切位置,因此就不能前面所找到的启动窗口看作是一个需要输入法的窗口,因为这个启动窗口只是Activity窗口在显示过程中出现的一个临时窗口。在这种情况下,这段代码就会继续沿着窗口堆栈往下查找另外一个窗口,该窗口一方面是需要显示输入法窗口的,另一方面要与前面所找到的启动窗口对应的是同一个窗口令牌的。如果能找到这样的一个窗口,那么就会将用来描述它的一个WindowState对象wb保存在变量w中。如果找不到这样的一个窗口,那么这段代码就会继续沿着窗口堆栈往下查找另外一个需要显示输入法的窗口。
我们继续往下阅读代码:
- if (willMove && w != null) {
- final WindowState curTarget = mInputMethodTarget;
- if (curTarget != null && curTarget.mAppToken != null) {
- // Now some fun for dealing with window animations that
- // modify the Z order. We need to look at all windows below
- // the current target that are in this app, finding the highest
- // visible one in layering.
- AppWindowToken token = curTarget.mAppToken;
- WindowState highestTarget = null;
- int highestPos = 0;
- if (token.animating || token.animation != null) {
- int pos = 0;
- pos = localmWindows.indexOf(curTarget);
- while (pos >= 0) {
- WindowState win = localmWindows.get(pos);
- if (win.mAppToken != token) {
- break;
- }
- if (!win.mRemoved) {
- if (highestTarget == null || win.mAnimLayer >
- highestTarget.mAnimLayer) {
- highestTarget = win;
- highestPos = pos;
- }
- }
- pos--;
- }
- }
- if (highestTarget != null) {
- ......
- if (mNextAppTransition != WindowManagerPolicy.TRANSIT_UNSET) {
- // If we are currently setting up for an animation,
- // hold everything until we can find out what will happen.
- mInputMethodTargetWaitingAnim = true;
- mInputMethodTarget = highestTarget;
- return highestPos + 1;
- } else if (highestTarget.isAnimating() &&
- highestTarget.mAnimLayer > w.mAnimLayer) {
- // If the window we are currently targeting is involved
- // with an animation, and it is on top of the next target
- // we will be over, then hold off on moving until
- // that is done.
- mInputMethodTarget = highestTarget;
- return highestPos + 1;
- }
- }
- }
- }
当WindowManagerService类的成员变量mInputMethodTarget的值不等于null,并且它描述的是一个Activity窗口时,即它的成员变量mAppToken的值不等于null时,那么就说明当前输入法窗口已经存在一个目标窗口,而这个目标窗口就是使用WindowManagerService类的成员变量mInputMethodTarget所指向的一个WindowState对象来描述的。接下来这段代码就检查该目标窗口是否正在切换的过程中,即是否正在显示切换动画。如果是的话,那么WindowState对象curTarget的成员变量animating的值就会等于true,或者另外一个成员变量animation的值不等于null,这时候就需要在与该目标窗口所对应的窗口令牌token所描述的一组窗口中,找到一个Z轴位置最大的并且不是已经被移除的窗口。WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked的调用者最后就是需要将输入法窗口移动到这个Z轴位置最大的并且不是已经被移除的窗口的上面的。
一个窗口的Z轴位置是记录在用描述它的一个WindowState对象的成员变量mAnimLayer中的,而它是否是已经被移除是记录在这个WindowState对象的成员变量mRemoved中的,因此,如果在窗口令牌token所描述的一组WindowSate对象中,能找到一个WindowSate对象,它的成员变量mAnimLayer的值最大,并且它的成员变量mRemoved不等于true,那么这段代码就会将它保存在变量highestTarget中,并且将它描述的窗口在窗口堆栈中的位置保存在变量highestPos中。
经过前面的一系列计算之后,如果变量highestTarget的值不等于null,那么就说明我们碰到前面所说的特殊的情况,这时候又要分为两种情况来讨论。
第一种情况是当前输入法窗口的目标窗口即将要进入到切换过程,但是这个切换过程尚开始,即WindowManagerService类的成员变量mNextAppTransition的值不等于WindowManagerPolicy.TRANSIT_UNSET。这时候就需要将WindowManagerService类的成员变量mInputMethodTargetWaitingAnim的值设置为true,表示当前输入法窗口的目标窗口正在等待进入切换动画中,并且需要将WindowManagerService类的成员变量mInputMethodTarget修正为变量highestTarget所描述的一个WindowState对象,因为这个WindowState对象才是真正用来描述当前输入法窗口的目标窗口的。
第二种情况是当前输入法窗口的目标窗口已经处于切换的过程了,即变量highestTarget所描述的一个WindowState对象的成员函数isAnimating的返回值为true,并且该目标窗口的Z轴位置大于前面所找到的需要显示输入法窗口的窗口的Z轴,即变量highestTarget所描述的一个WindowState对象的成员变量mAnimLayer的值大于变量w所描述的一个WindowState对象的成员变量mAnimLayer的值。这时候就需要将WindowState对象highestTarget所描述的窗口维持为当前输入法窗口的目标窗口,即将WindowManagerService类的成员变量mInputMethodTarget设置为变量highestTarget,直到WindowState对象highestTarget所描述的窗口的切换过程结束为止。
上述两种情况最后都需要将WindowState对象highestTarget所描述的窗口在窗口堆栈中的位置highestPos加1,然后再返回给调用者,以便调用者接下来可以输入法窗口移动在窗口堆栈的第(highestPos+1)个位置上。
如果我们没有碰到前面所说的特殊的情况,那么WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked就会继续往下执行:
- if (w != null) {
- if (willMove) {
- ......
- mInputMethodTarget = w;
- if (w.mAppToken != null) {
- setInputMethodAnimLayerAdjustment(w.mAppToken.animLayerAdjustment);
- } else {
- setInputMethodAnimLayerAdjustment(0);
- }
- }
- return i+1;
- }
A. 将WindowState对象w保存在WindowManagerService类的成员变量mInputMethodTarget中,以便WindowManagerService服务可以知道当前输入法窗口的目标窗口是什么。
B. 检查WindowState对象w描述的窗口是否是Activity窗口,即检查WindowState对象w的成员变量mAppToken的值是否不等于null。如果WindowState对象w描述的窗口是Activity窗口的话,那么就需要根据WindowState对象w的成员变量mAppToken所描述的一个AppWindowToken对象的成员变量animLayerAdjustment来调整系统中的输入法窗口以及输入法对话框的Z轴位置,即在系统中的输入法窗口以及输入法对话框的现有Z轴位置的基础上再增加一个调整量,这个调整量就保存在WindowState对象w的成员变量mAppToken所描述的一个AppWindowToken对象的成员变量animLayerAdjustment中。这个调整的过程是通过调用WindowManagerService类的成员函数setInputMethodAnimLayerAdjustment来实现的。如果WindowState对象w描述的窗口不是Activity窗口,那么就不需要调整系统中的输入法窗口以及输入法对话框的Z轴位置,但是仍然需要调用WindowManagerService类的成员函数setInputMethodAnimLayerAdjustment来将系统中的输入法窗口以及输入法对话框的Z轴位置调整量设置为0,即将WindowManagerService类的成员变量mInputMethodAnimLayerAdjustment的值设置为0。
C. 将变量i的值加1之后返回给调用者,以便调用者可以将系统中的输入法窗口移动到窗口堆栈中的第(i+1)个位置上。
如果变量w的值等于null,那么就说明WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked在前面没有找到一个需要显示输入法窗口的窗口,我们继续往下阅读它的代码,以便可以了解它是如何处理这种情况的:
- if (willMove) {
- ......
- mInputMethodTarget = null;
- setInputMethodAnimLayerAdjustment(0);
- }
- return -1;
- }
- ......
- }
最后,WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked返回一个-1值给调用者,也是表明系统当前没有需要显示输入法窗口的窗口。
2. 移动输入法对话框移动到输入法窗口的上面
系统中的输入法对话框是需要位于输入法窗口的上面的,因此,我们就需要有一个函数来将输入法对话框移动到输入法窗口的上面去。这个函数就是WindowManagerService类的成员函数moveInputMethodDialogsLocked,它的实现如下所示:
- public class WindowManagerService extends IWindowManager.Stub
- implements Watchdog.Monitor {
- ......
- void moveInputMethodDialogsLocked(int pos) {
- ArrayList dialogs = mInputMethodDialogs;
- final int N = dialogs.size();
- ......
- for (int i=0; i
- pos = tmpRemoveWindowLocked(pos, dialogs.get(i));
- }
- ......
- if (pos >= 0) {
- final AppWindowToken targetAppToken = mInputMethodTarget.mAppToken;
- if (pos < mWindows.size()) {
- WindowState wp = mWindows.get(pos);
- if (wp == mInputMethodWindow) {
- pos++;
- }
- }
- ......
- for (int i=0; i
- WindowState win = dialogs.get(i);
- win.mTargetAppToken = targetAppToken;
- pos = reAddWindowLocked(pos, win);
- }
- ......
- return;
- }
- for (int i=0; i
- WindowState win = dialogs.get(i);
- win.mTargetAppToken = null;
- reAddWindowToListInOrderLocked(win);
- ......
- }
- }
- ......
- }
这个函数定义在文件frameworks/base/services/java/com/android/server/WindowManagerService.java中。
在调用WindowManagerService类的成员函数moveInputMethodDialogsLocked之前,必须要保证系统中的输入法窗口已经被移动到窗口堆栈的正确位置,即已经被移动到需要显示输入法窗口的窗口的上面。这时候参数pos描述的或者是输入法窗口在窗口堆栈中的位置,或者是输入法窗口在窗口堆栈的位置的上一个位置,即输入法对话框在窗口堆栈中的起始位置。参数pos的值还可以小于0,这时候就表示系统当前没有需要显示输入法窗口的窗口。
在移动输入法对话框到输入法窗口的上面之前,首先要将输入法对话框从窗口堆栈中移除,以便接下来可以重新将它们插入到窗口堆栈中。系统中的输入法对话框都保存在WindowManagerService类的成员变量mInputMethodDialogs所描述的一个ArrayList中,通过调用WindowManagerService类的成员函数来tmpRemoveWindowLocked来移除保存在这个ArrayList中的每一个WindowState对象,就可以将系统中的输入法对话框从窗口堆栈中移除中。注意,将一个WindowState对象从窗口堆栈中移除之后,可能会影响参数pos的值。例如,如果参数pos的值大于被移除的WindowState对象原来在窗口堆栈中的位置值,那么在该WindowState对象被移除之后,参数pos的值就要相应地减少1,这样它才能正确地反映输入法窗口在窗口堆栈中的位置,或者输入法对话框在窗口堆栈中的起始位置。WindowManagerService类的成员函数来tmpRemoveWindowLocked在将一个WindowState对象从窗口堆栈中移除的过程中,会正确处理好参数pos的值,这一点可以参考前面Android窗口管理服务WindowManagerService组织窗口的方式分析一文。
未完待续。。。