第8章 理解Window和WindowManager

Window表示一個窗口,是View的實際管理者。在第4章的事件分發(fā)中已經(jīng)知道了,點擊事件是通過Window->DecorView->View來傳遞的。

Window是一個抽象類,具體實現(xiàn)是PhoneWindow類。我們可以通過WindowManager來操作Window,具體實現(xiàn)是在WindowManagerService中實現(xiàn)的;WindowManager和WindowManagerService通過IPC連接。

8.1 Window和WindowManager

8.1.1 創(chuàng)建一個Window

首先,通過WindowManager添加一個窗口:

        Button button = new Button(this);
        button.setText("button");
        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_APPLICATION, 0,
                PixelFormat.TRANSPARENT);
        lp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
        lp.gravity = Gravity.LEFT | Gravity.TOP;
        lp.x = 100;
        lp.y = 300;
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        wm.addView(button, lp);

這里注意,原書中代碼報錯,必須給窗口指定一個type,我這里指定的WindowManager.LayoutParams.TYPE_APPLICATION;不清楚是否是系統(tǒng)版本的問題。

8.1.2 Type

Type有三種類型:

  • 應(yīng)用Window
    應(yīng)用類Window對應(yīng)一個Activity。取值在1~99之間。

  • 子Window
    子Window附屬于一個特定的父Window,而不能單獨存在。取值在1000~1999之間。

  • 系統(tǒng)Window
    系統(tǒng)Window需要系統(tǒng)權(quán)限才能創(chuàng)建。取值在2000~2999之間。

8.1.3 Flag

Flag參數(shù)表示W(wǎng)indow的屬性,可以控制Window的顯示特性。

  • FLAG_NOT_FOCUSABLE
    表示W(wǎng)indow不需要獲取焦點,也不需要接受各種輸入事件。

  • FLAG_NOT_TOUCH_MODAL
    表示W(wǎng)indow會處理當(dāng)前區(qū)域以內(nèi)的單擊事件,同時把當(dāng)前Window區(qū)域以外的單擊事件傳遞到底層。

  • FLAG_SHOW_WHEN_LOCKED
    表示W(wǎng)indow可以顯示在鎖屏的界面上。

8.1.4 權(quán)限

想要顯示系統(tǒng)級的Window是需要申請權(quán)限的。例如當(dāng)type = TYPE_SYSTEM_ERROR的時候,需要在AndroidManifest中添加<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />。

在Android 6.0及以后,需要手動開啟懸浮窗權(quán)限:

    /**
     * 檢查是否有懸浮窗權(quán)限
     */
    @TargetApi(Build.VERSION_CODES.M)
    private void checkOverlay() {
        if (!Settings.canDrawOverlays(this)) {
            AlertDialog dialog = new AlertDialog.Builder(this)
                    .setTitle("權(quán)限禁止")
                    .setMessage("懸浮窗權(quán)限被禁止,點擊確定前往設(shè)置")
                    .setPositiveButton("確定", 
                        (dialog1, which) -> jumpToOverlaySetting())
                    .setCancelable(false)
                    .create();
            dialog.show();
        }
    }

    private void jumpToOverlaySetting() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
            startActivityForResult(intent, 1);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == 1) {
            checkOverlay();
        }
    }

然后在onActivityResult()中,再判斷是否開啟權(quán)限。

如果不想申請權(quán)限,使用type = TYPE_TOAST也可以創(chuàng)建一個懸浮窗。但是在Android 7.1.1(API 25)開始,TYPE_TOAST被限制使用[1]。如果想要在7.1.1以后使用懸浮窗,必須開啟權(quán)限。

8.2 Window的內(nèi)部機制

Window是一個抽象概念,而不是實際存在的。每一個Window都對應(yīng)著一個View和一個ViewRootImpl,Window和View是通過ViewRootImpl來建立聯(lián)系的。Window實際存在的形式是View。
對Window的操作只能通過WindowManager來進行。WindowManager主要提供了三個方法:

  • addView(View, LayoutParams)
  • removeView(View)
  • updateView(View, LayoutParams)

可以看到,WindowManager的操作對象是View。

8.2.1 Window的添加過程

在WindowManager的實現(xiàn)類WindowManagerImpl中,找到addView()方法:

    public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

    @Override
    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        mGlobal.updateViewLayout(view, params);
    }

    @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

而mGlobal的定義:

    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

這個單例類,在第四章[2]的時候已經(jīng)接觸過了。這里我們看到,WindowManager的添加、刪除、更新三個方法,其實都全權(quán)交給了WindowManagerGlobal。

轉(zhuǎn)到WindowManagerGlobal的addView()方法中:

    public void addView(View view, 
        ViewGroup.LayoutParams params, 
        Display display, 
        Window parentWindow) {
        // ... 省略若干代碼

        // 創(chuàng)建ViewRoot
        ViewRootImpl root;
        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        // 將View添加到列表中
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
         }
    }

這里主要是將待添加的View、ViewRoot、LayoutParams保存到列表中,創(chuàng)建了ViewRoot,以及調(diào)用了ViewRoot的setView方法。
接下來繼續(xù)跟蹤setView方法。

    public void setView(View view, 
          WindowManager.LayoutParams attrs, 
          View panelParentView) {
          // ...省略若干代碼

          // Schedule the first layout -before- adding to the window
          // manager, to make sure we do the relayout before receiving
          // any other events from the system.
          requestLayout();
          // ...
          res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                   getHostVisibility(), mDisplay.getDisplayId(),
                   mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                   mAttachInfo.mOutsets, mInputChannel);
          // ...
    }

requestLayout()之前有一個注釋:

// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.

大概意思是說,為了保證在接收到其他系統(tǒng)事件之前布局好,我們在將View添加到Window之前進行第一次的布局。

然后調(diào)用了WindowSession.addToDisplay()方法。mWindowSession是通過WindowManagerGlobal.getWindowSession()來獲取的,而跟蹤進去發(fā)現(xiàn)發(fā)現(xiàn)這是一個Binder對象,進行了和WindowManagerService之間的IPC。也就是說,Window的添加最后交給了WindowManagerService來進行。

8.2.2 Window的刪除過程

Window的刪除的過程和添加類似,通過WindowManager.removeView() -> ViewRoot.die() -> WindowSession.remove()最終交付WindowManagerService來處理。
在這個過程中,會調(diào)用View的onDetachedFromWindow()回調(diào)。然后會調(diào)用WindowManager.doRemoveView()來將之前添加到列表中的View、ViewRoot、LayoutParams(8.2.1)給刪掉。

8.2.3 Window的更新過程

更新過程相較添加和刪除比較簡單,WindowManager.updateViewLayout() -> ViewRoot.setLayoutParams() -> ViewRoot.scheduleTraversals()
最終會調(diào)用ViewRoot.performTraversals()方法,在這里會對View進行measure、layout、draw的步驟。同時會調(diào)用relayoutWindow -> WindowSession.relayoutWindow(),通過WindowSession來更新Window視圖,這個過程仍然是經(jīng)過IPC,由WindowManagerService來實現(xiàn)的。

8.2.* 小結(jié)

  • Window是一個抽象概念,實際上是不存在的,通過View來體現(xiàn)。View不能單獨存在,而必須依托于Window;而Window的具體呈現(xiàn)方式則是通過View。
  • 一個Window對應(yīng)一個ViewRoot對應(yīng)一個View,ViewRoot是WindowManager和View之間的紐帶。而ViewRoot所代表的意義,則是這個Window的ViewTree的根節(jié)點。
    WindowManager ----> ViewRoot --(IPC)-> WindowManagerService 這是操作Window的一般流程。而所有關(guān)于Window的操作,最終都是在WindowManagerService中實現(xiàn)的。

8.3 Window的創(chuàng)建過程

8.3.1 Activity的Window

在第四章里面,我分析過Activity的啟動流程:第4章 View的工作原理。

當(dāng)系統(tǒng)將要啟動一個Activity的時候,會調(diào)用ActivityThread.performLaunchActivity()方法,在這個方法中會創(chuàng)建Activity實例,并調(diào)用Activity的attach方法。在attach方法中,會賦予Activity上下文,創(chuàng)建Window并將其和Activity綁定。Activity實現(xiàn)了Window.Callback接口,所以Activity可以收到Window的若干回調(diào),比如onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent等。

Window實例的創(chuàng)建,是通過PolicyManager的工廠方法makeNewWindow(Context)來進行的。
當(dāng)我們在Activity調(diào)用setContentView方法時,會創(chuàng)建DecorView并將xml的布局加載進content中。DecorView雖然已經(jīng)創(chuàng)建完畢,但是他還沒有被WindowManager添加到Window中。Window的概念,更多表示的是一種抽象的功能集合。在Window和DecorView都創(chuàng)建完畢之后,ActivityThread會調(diào)用handleResumeActivity方法->makeVisible方法,在其中會調(diào)用WindowManager.addView(),這就回到了8.2.1的流程了,通過addView將DecorView添加到Window。
(有點吃驚,書中的內(nèi)容和我之前在第四章中分析的幾乎一模一樣,不過這也說明我沒有誤入歧途。)

8.3.2 Dialog的Window

Dialog和Activity創(chuàng)建Window的方式類似。

1、創(chuàng)建Window
2、初始化DecorView并將Dialog的視圖添加進去
3、將DecorView添加到Window中

在Dialog被關(guān)閉時,他會通過WindowManager來移除DecorView。

8.3.3 Toast的Window

Toast也是基于Window來實現(xiàn)的,不過過程和Dialog不同。
總的來說,Toast是通過IPC過程調(diào)用NotificationManagerService來進行顯示和隱藏的。

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容