Android窗口管理服务WindowManagerService对输入法窗口的管理分析
在Android系统中,输入法窗口是一种特殊类型的窗口,它总是位于需要使用输入法的窗口的上面。也就是说,一旦WindowManagerService服务检测到焦点窗口需要使用输入法,那么它就会调整输入法窗口在窗口堆栈中的位置,使得输入法窗口位于在焦点窗口的上面,这样用户可以通过输入法窗口来录入字母或者文字。本文就将详细分析WindowManagerService服务是如何管理系统中的输入法窗口的。
老罗的新浪微博: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 { ...... boolean moveInputMethodWindowsIfNeededLocked(boolean needAssignLayers) { final WindowState imWin = mInputMethodWindow; final int DN = mInputMethodDialogs.size(); if (imWin == null && DN == 0) { return false; } int imPos = findDesiredInputMethodWindowIndexLocked(true); if (imPos >= 0) { // In this case, the input method windows are to be placed // immediately above the window they are targeting. // First check to see if the input method windows are already // located here, and contiguous. final int N = mWindows.size(); WindowState firstImWin = imPos < N ? mWindows.get(imPos) : null; // Figure out the actual input method window that should be // at the bottom of their stack. WindowState baseImWin = imWin != null ? imWin : mInputMethodDialogs.get(0); if (baseImWin.mChildWindows.size() > 0) { WindowState cw = baseImWin.mChildWindows.get(0); if (cw.mSubLayer < 0) baseImWin = cw; } if (firstImWin == baseImWin) { // The windows haven't moved... but are they still contiguous? // First find the top IM window. int pos = imPos+1; while (pos < N) { if (!(mWindows.get(pos)).mIsImWindow) { break; } pos++; } pos++; // Now there should be no more input method windows above. while (pos < N) { if ((mWindows.get(pos)).mIsImWindow) { break; } pos++; } if (pos >= N) { // All is good! return false; } } if (imWin != null) { ...... imPos = tmpRemoveWindowLocked(imPos, imWin); ...... imWin.mTargetAppToken = mInputMethodTarget.mAppToken; reAddWindowLocked(imPos, imWin); ...... if (DN > 0) moveInputMethodDialogsLocked(imPos+1); } else { moveInputMethodDialogsLocked(imPos); } } else { // In this case, the input method windows go in a fixed layer, // because they aren't currently associated with a focus window. if (imWin != null) { ...... tmpRemoveWindowLocked(0, imWin); imWin.mTargetAppToken = null; reAddWindowToListInOrderLocked(imWin); ...... if (DN > 0) moveInputMethodDialogsLocked(-1);; } else { moveInputMethodDialogsLocked(-1);; } } if (needAssignLayers) { assignLayersLocked(); } return true; } ......} 这个函数定义在文件frameworks/base/services/java/com/android/server/WindowManagerService.java中。WindowManagerService类的成员函数moveInputMethodWindowsIfNeededLocked首先检查系统中是否存在输入法窗口和输入法对话框,即检查WindowManagerService类的成员变量mInputMethodWindow的值是否等于null,并且WindowManagerService类的成员变量mInputMethodDialogs所描述的一个ArrayList的大小是否等于0。如果输入法窗口和输入法对话框都不存在的话,那么就不用调整它们在窗口堆栈中的位置了,否则的话,WindowManagerService类的成员变量mInputMethodWindow所指向的一个WindowState对象就会保存在变量imWin中,以便接下来可以通过它来描述系统中的输入法窗口。
在输入法窗口或者输入法对话框存在的情况下,WindowManagerService类的成员函数moveInputMethodWindowsIfNeededLocked接下来就会继续调用另外一个成员函数findDesiredInputMethodWindowIndexLocked来找到输入法窗口在窗口堆栈中的位置,并且保存在变量imPos中。注意,变量imPos的值可能大于等于0,也可能等于-1。当变量imPos的值大于等于0的时候,就说明系统当前存在一个窗口需要显示输入法窗口,而当变量imPos的值等于-1的时候,就说明系统当前不存在一个窗口需要显示输入法窗口,或者系统中不存在输入法窗口。接下来我们分两种情况来分析WindowManagerService类的成员函数moveInputMethodWindowsIfNeededLocked的实现。
第一种情况是变量imPos的值可能大于等于0。这时候可能需要调整输入法窗口在窗口堆栈中的位置,也可能不需要调整输入法窗口在窗口堆栈中的位置,取决于输入法窗口的位置是否已经在窗口堆栈的第imPos个位置上,以及是否所有与输入法相关的窗口都连续在放置在窗口堆栈中。
变量firstImWin描述的是当前位于窗口堆栈中Z轴位置最小的与输入法相关的窗口,它是通过变量imPos来获得的。另外一个变量baseImWin描述的是Z轴位置最小的与输入法相关的窗口。如果这两个变量描述的是同一个窗口,那么就说明输入法窗口的位置已经在窗口堆栈的第imPos个位置上,因此,就有可能不需要调整输入法窗品在窗口堆栈中的位置了。接下来我们就描述如何找到这个Z轴位置最小的与输入法相关的窗口。
如果变量imWin的值不等于null,即WindowManagerService类的成员变量mInputMethodWindow的值不等于null,那么它所描述的窗口就是Z轴位置最小的与输入法相关的窗口,否则的话,Z轴位置最小的与输入法相关的窗口就是位于WindowManagerService类的成员变量mInputMethodDialogs所描述的一个ArrayList的第0个位置上的输入法对话框。这一步得到的Z轴位置最小的与输入法相关的窗口就保存在变量baseImWin中。
如果变量baseImWin所描述的窗口有子窗口,即它所指向的一个WindowState对象的成员变量mChildWindows所描述的一个ArrayList的大小大于0。这时候如果用来描述第一个子窗口的WindowState对象的成员变量mSubLayer的值小于0,那么就说明变量baseImWin所描述的窗口在所有与输入法相关的窗口中的Z轴位置还不是最小的,因为在它的下面还存在着Z轴位置更小的子窗口。在这种情况下,变量baseImWin就会指向这个Z轴位置最小的子窗口。
经过上面的一系列计算之后,如果变量firstImWin和变量baseImWin描述的是同一个窗口,那么还需要继续判断所有与输入法相关的窗口都连续在放置在窗口堆栈中。判断的方法如下所示:
(1). 从窗口堆栈的第(imPos + 1)个位置开始往上查找一个非输入法相关的窗口。
(2). 如果第(1)步能在窗口堆栈中大于等于(imPos+1)的位置pos上找到一个非输入法窗口,那么再继续从第pos个位置开始往上查找一个与输入法相关的窗口。
(3). 如果第(2)步能在窗口堆栈中找到一个与输入法相关的窗口,那么就说明所有与输入法相关的窗口不是连续在放置在窗口堆栈中的,因为在它们中间有一个非输入法相关的窗口,否则的话,就说明所有与输入法相关的窗口都是连续在放置在窗口堆栈中的。
在所有与输入法相关的窗口都是连续在放置在窗口堆栈中的情况下,WindowManagerService类的成员函数moveInputMethodWindowsIfNeededLocked就会直接返回一个false值给调用者,表明不需要调整系统中的输入法窗口以及输入法对话框在窗口堆栈中的位置。
在所有与输入法相关的窗口不是连续在放置在窗口堆栈中的情况下,就需要重新调整系统中的输入法窗口以及输入法对话框在窗口堆栈中的位置。这里又需要分两个情景来讨论。
第一个情景是变量imWin的值不等于null,这时候说明系统中存在一个输入法窗口,因此,就需要调整这个输入法窗口在窗口堆栈中的位置。调整的方法很简单:
(1). 调用WindowManagerService类的成员函数tmpRemoveWindowLocked来从窗口堆栈中移除变量imWin所描述的输入法窗口。在移除的过程中,会同时计算输入法窗口在窗口堆栈中的新位置,这个位置还是保存在变量imPos中。
(2). 调用WindowManagerService类的成员函数reAddWindowLocked重新将变量imWin所描述的输入法窗口插入到窗口堆栈的第imPos个位置中。在插入之前,还会将变量imWin所描述的一个WindowState对象的成员变量mTargetAppToken与WindowManagerService类的成员变量mInputMethodTarget所描述的一个WindowState对象的成员变量mAppToken指向同一个AppWindowToken对象,这样WindowManagerService服务就可以知道imWin所描述的输入法窗口的目标窗口是什么。
(3). 如果系统中还存在输入法对话框,那么就调用WindowManagerService类的成员函数moveInputMethodDialogsLocked来将它们放置在第(imPos+1)个位置上,目的是将它们放置在输入法窗口的上面。
第二个情景是变量imWin的值等于null,这时候说明系统中不存在输入法窗口。在这个情景下,系统中肯定会存在输入法对话框,否则的话,WindowManagerService类的成员函数moveInputMethodWindowsIfNeededLocked在前面就会返回了。因此,WindowManagerService类的成员函数moveInputMethodWindowsIfNeededLocked接下来就会直接调用成员函数moveInputMethodDialogsLocked来将系统中的输入法对话框放置在在第imPos个位置上。
第二种情况是变量imPos的值等于-1。这时候说明系统中不存在需要显示输入法窗口的窗口。这里同样也需要分两个情景来分析。
第一个情景是变量imWin的值不等于null,这时候说明系统中存在一个输入法窗口,因此,就需要调整这个输入法窗口在窗口堆栈中的位置。调整的方法与前面第一种情况的第一个情景是类似的。不过由于事先不知道输入法窗口在窗口堆栈中的位置,因此,这里就会调用WindowManagerService类的成员函数reAddWindowToListInOrderLocked和moveInputMethodDialogsLocked来间接地调整输入法窗口和输入法对话框在窗口堆栈中的位置。注意,在调用WindowManagerService类的成员函数moveInputMethodDialogsLocked的时候,传进去的参数为-1。另外一个地方需要注意的是,在WindowManagerService类的成员函数reAddWindowToListInOrderLocked来间接地调整输入法窗口在窗口堆栈中的位置之前,会将量imWin所描述的一个WindowState对象的成员变量mTargetAppToken的值设置为null,这样WindowManagerService服务就可以知道imWin所描述的输入法窗口没有目标窗口。
第二情景是变量imWin的值等于null,这时候系统中不存在输入法窗口。这个情景与前面第一种情况的第二个情景也是类似的。由于系统中不存在输入法窗口,因此只需要调用WindowManagerService类的成员函数moveInputMethodDialogsLocked来间接地输入法对话框在窗口堆栈中的位置即可,即以参数-1来调用WindowManagerService类的成员函数moveInputMethodDialogsLocked。
至此,我们就分析完成WindowManagerService服务对输入法窗口的基本操作了。从分析的过程中,我们可以得到以下两个结论:
A. 系统中与输入法相关的窗口有两种,一种是输入法窗口,另一种是输入法对话框。
B. 当Z轴位置最大的窗口需要使用输入法时,输入法窗口就会位于它的上面,而输入法对话框又会位于输入法窗口的上面。
在WindowManagerService服务中,还有一种类型的窗口与输入法窗口类似,它总是与Activity窗口粘在一起。不过,这种类型的窗口是位于Activity窗口的下面,刚好与输入法窗口相反,它就是壁纸窗口(Wallpaper)。在接下来的一篇文章中,我们就将继续分析WindowManagerService服务是如何管理系统中的壁纸窗口的。敬请关注!
老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!
