在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 { ...... WindowState mInputMethodWindow = null; final ArrayList<WindowState> mInputMethodDialogs = new ArrayList<WindowState>(); ...... 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<WindowState> 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;这段代码从上到下遍历WindowManagerService服务内部的窗口堆栈,即WindowManagerService类的成员变量mWindows所描述的一个ArrayList。如果发现有一个窗口是可见的,并且需要显示输入法窗口,那么整个查找过程就会结束。检查一个窗口是否可见以及需要显示输入法窗口是通过调用WindowManagerService类的成员函数canBeImeTarget来实现的。最后得到的需要显示输入法的窗口就使用WindowState对象w中,这个WindowState对象w接下来还会保存在WindowManagerService类的成员变量mUpcomingInputMethodTarget中,表示它即将要成为输入法窗口的目标窗口。
参数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; } } } }这段代码用来处理一种特殊情况,即参数willMove的值等于true,并且前面找到了一个需要显示输入法的窗口w,但是当前输入法窗口已经存在一个目标窗口,并且该目标窗口正在切换的过程中。在这种情况下,调用WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked的函数就需要等到当前输入法窗口的目标窗口的切换过程结束之后,再将输入法窗口移动到窗口w的上面去,换句话说,就是要保持输入法窗口在它当前的目标窗口的上面,直到它当前的目标窗口的切换过程结束为止。这样WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked就需要找到当前输入法窗口的目标窗口在窗口堆栈中的位置,然后再将该位置加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; }如果变量w的值不等于null,那么就说明WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked在前面找到了一个需要显示输入法窗口的窗口。这个窗口是使用WindowState对象w来描述的,并且它在窗品堆栈中的位置记录在变量i中。这时候WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked就会执行以下三个操作:
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对在前面没有找到一个需要显示输入法窗口的窗口的情况的处理很简单。它判断参数willMove的值是否等于true。如果等于true的话,那么就会将WindowManagerService类的成员变量mInputMethodTarget的值设置为null,并且调用WindowManagerService类的成员函数setInputMethodAnimLayerAdjustment来将系统中的输入法窗口以及输入法对话框的Z轴位置调整量设置为0。这实际上是用来通知WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked的调用者,系统当前没有需要显示输入法窗口的窗口。
最后,WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked返回一个-1值给调用者,也是表明系统当前没有需要显示输入法窗口的窗口。
2. 移动输入法对话框移动到输入法窗口的上面
系统中的输入法对话框是需要位于输入法窗口的上面的,因此,我们就需要有一个函数来将输入法对话框移动到输入法窗口的上面去。这个函数就是WindowManagerService类的成员函数moveInputMethodDialogsLocked,它的实现如下所示:
public class WindowManagerService extends IWindowManager.Stub implements Watchdog.Monitor { ...... void moveInputMethodDialogsLocked(int pos) { ArrayList<WindowState> dialogs = mInputMethodDialogs; final int N = dialogs.size(); ...... for (int i=0; i<N; 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<N; i++) { WindowState win = dialogs.get(i); win.mTargetAppToken = targetAppToken; pos = reAddWindowLocked(pos, win); } ...... return; } for (int i=0; i<N; 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组织窗口的方式分析一文。
接下来,我们就分为两种情况来分析输入法对话框在窗口是如何移动到输入法窗口的上面去的。
第一种情况是参数pos的值大于等于0,这表明系统当前存在一个需要显示输入法窗口的窗口,这个窗口是通过WindowManagerService类的成员变量mInputMethodTarget所指向的一个WindowState对象来描述的。
前面提到,参数pos描述的或者是输入法窗口在窗口堆栈中的位置,或者是输入法对话框在窗口堆栈中的起始位置,我们首先要将它统一描述为输入法对话框在窗口堆栈中的起始位置。这时候就需要检查保存在窗口堆栈的第pos个位置的WindowState对象wp,是否就是WindowManagerService类的成员变量mInputMethodWindow所指向的那个WindowState对象。如果是的话,那么就说明参数pos描述的或者是输入法窗口在窗口堆栈中的位置,这时候将它的值增加1,就可以让它表示为输入法对话框在窗口堆栈中的起始位置。
得到了输入法对话框在窗口堆栈中的起始位置pos之后,接下来只需要调用WindowManagerService类的成员函数reAddWindowLocked来依次地将保存在WindowManagerService类的成员变量mInputMethodDialogs所描述的一个ArrayList中的第i个WindowState对象保存在窗口堆栈中的第(pos+i)个以位置上即可,这样就可以将输入法对话框都移动到输入法窗口的上面去了。
注意,在移动的过程中,用来描述每一个输入法对话框的每一个WindowState对象的成员变量mTargetAppToken的值设置为WindowManagerService类的成员变量mInputMethodTarget所描述的一个WindowState对象的成员变量mAppToken的值,以便可以将输入法对话框和输入法窗口的目标窗口设置为同一个窗口。
第二种情况是参数pos的值小于0,这表明系统当前不存在一个需要显示输入法窗口的窗口。这时候就需要根据输入法窗口自身的属性来将它们移动到窗口堆栈的合适的位置上去,这是通过调用WindowManagerService类的成员函数reAddWindowToListInOrderLocked来实现的。WindowManagerService类的成员函数reAddWindowToListInOrderLocked的实现可以参考前面Android窗口管理服务WindowManagerService组织窗口的方式分析一文,这里不再详细。
注意,在移动的过程中,用来描述每一个输入法对话框的每一个WindowState对象的成员变量mTargetAppToken的值会被设置为null,这是因为系统当前不存在一个需要显示输入法窗口的窗口,即这时候每一个输入法对话框都没有目标窗口。
理解了WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked和moveInputMethodDialogsLocked的实现之后,对WindowManagerService类的另外三个成员函数addInputMethodWindowToListLocked、adjustInputMethodDialogsLocked和moveInputMethodWindowsIfNeededLocked的实现就很有帮助,接下来我们就继续分析这三个成员函数的实现。
3. 插入输入法窗口到需要显示输入法窗口的窗口上面
插入输入法窗口到窗口堆栈的合适位置,使得它位于需要显示输入法窗口的窗口上面,这是通过调用WindowManagerService类的成员函数addInputMethodWindowToListLocked来实现的,它的实现如下所示:
public class WindowManagerService extends IWindowManager.Stub implements Watchdog.Monitor { ...... void addInputMethodWindowToListLocked(WindowState win) { int pos = findDesiredInputMethodWindowIndexLocked(true); if (pos >= 0) { win.mTargetAppToken = mInputMethodTarget.mAppToken; ...... mWindows.add(pos, win); mWindowsChanged = true; moveInputMethodDialogsLocked(pos+1); return; } win.mTargetAppToken = null; addWindowToListInOrderLocked(win, true); moveInputMethodDialogsLocked(pos); } ......}这个函数定义在文件frameworks/base/services/java/com/android/server/WindowManagerService.java中。
参数win描述的是要添加到窗口堆栈中去的输入法窗口。
WindowManagerService类的成员函数addInputMethodWindowToListLocked首先调用另外一个成员函数findDesiredInputMethodWindowIndexLocked来计算输入法窗口在窗口堆栈中的位置,并且保存在变量pos。
如果变量pos的值大于等于0,那么就说明WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked在窗口堆栈中找到了一个合适的位置来放置输入法窗口,于是接下来就会参数win所描述的输入法窗口插入在WindowManagerService类的成员变量mWIndows所描述的窗口堆栈的第pos个位置上。由于系统中的输入法对话框要保持在输入法窗口的上面,因此,WindowManagerService类的成员函数addInputMethodWindowToListLocked还需要继续调用另外一个成员函数moveInputMethodDialogsLocked来将系统中的输入法对话框在窗口堆栈中的起始位置设置为(pos+1)。
还有一个地方需要注意的是,前面在调用WindowManagerService类的成员函数addInputMethodWindowToListLocked来计算输入法窗口在窗口堆栈中的位置的时候,已经将用来描述需要显示输入法窗口的Activity窗口的一个WindowState对象保存了WindowManagerService类的成员变量mInputMethodTarget中,因此,这里就需要这个WindowState对象的成员变量mAppToken所指向的一个AppWindowToken对象保存在用来描述输入法窗口的WindowState对象的win的成员变量mTargetAppToken中,以便WindowManagerService服务可以知道当前输入法窗口的目标窗口是什么。
如果变量pos的值小于0,那么就说明WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked没有找一个需要输入法窗口的窗口,因此,这时候就需要调用另外一个成员函数addWindowToListInOrderLocked来将参数win所描述的输入法窗口插入到窗口堆栈中去。WindowManagerService类的成员函数addWindowToListInOrderLocked会根据要目标窗口所对应的窗口令牌在窗口令牌列表中的位置以及是否在窗口堆栈中存在其它窗口等信息来在窗口堆栈中找到一个合适的前位置来放置目标窗口,它的具体实现可以参考前面Android窗口管理服务WindowManagerService组织窗口的方式分析一文。将参数win所描述的输入法窗口插入到窗口堆栈中去之后,WindowManagerService类的成员函数addInputMethodWindowToListLocked还需要继续调用另外一个成员函数moveInputMethodDialogsLocked来调整系统中的输入法对话框。
注意,在调用WindowManagerService类的成员函数moveInputMethodDialogsLocked的时候,传递进去的参数pos的值等于-1,这时候WindowManagerService类的成员函数moveInputMethodDialogsLocked就不是直接调整输入法对话框在窗口堆栈中的位置的,而是调用另外一个成员函数reAddWindowToListInOrderLocked来调整的。
还有另外一个地方需要注意的是,由于前面在调用WindowManagerService类的成员函数findDesiredInputMethodWindowIndexLocked的时候,没有找到一个需要输入法窗口的窗口,因此,这里就需要将参数win所描述的一个WindowState对象的成员变量mTargetAppToken的值设置为null,以便WindowManagerService服务可以知道当前输入法窗口的没有目标窗口。
4. 调整输入法对话框在窗口堆栈的位置
一旦系统中存在需要显示输入法窗口的窗口,那么就需要系统中的输入法对话框在窗口堆栈中的位置,使得它们放置在输入法窗品的上面,这是通过调用WindowManagerService类的成员函数adjustInputMethodDialogsLocked来实现的,如下所示:
public class WindowManagerService extends IWindowManager.Stub implements Watchdog.Monitor { ...... void adjustInputMethodDialogsLocked() { moveInputMethodDialogsLocked(findDesiredInputMethodWindowIndexLocked(true)); } ......}这个函数定义在文件frameworks/base/services/java/com/android/server/WindowManagerService.java中。
WindowManagerService类的成员函数adjustInputMethodDialogsLocked的实现很简单,它首先调用成员函数findDesiredInputMethodWindowIndexLocked来找到输入法窗口在窗口堆栈中的位置,然后再调用成员函数moveInputMethodDialogsLocked来将输入法对话框保存在这个位置之上。
5. 调整输入法窗口在窗口堆栈的位置
当系统中的窗口布局发生了变化之后,例如,当前获得焦点的窗口发生了变化,或者新增了一个窗口,那么都可能需要调整输入法窗口在窗口堆栈中的位置,以便它可以痊于需要显示输入法窗口的窗口的上面,这是通过调用WindowManagerService类的成员函数moveInputMethodWindowsIfNeededLocked来实现的,如下所示:
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,欢迎关注!