Android窗口管理服務WindowManagerService對輸入法窗口(Input Method Window)的管理分析

       在Android系統中,輸入法窗口是一種特殊類型的窗口,它總是位於需要使用輸入法的窗口的上面。也就是說,一旦WindowManagerService服務檢測到焦點窗口需要使用輸入法,那麼它就會調整輸入法窗口在窗口堆棧中的位置,使得輸入法窗口位於在焦點窗口的上面,這樣用戶可以通過輸入法窗口來錄入字母或者文字。本文就將詳細分析WindowManagerService服務是如何管理系統中的輸入法窗口的。

       在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,歡迎關注!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章