Window/WindowManager 不可不知之事

前言

從Android app的視角看,Window是比較抽象的概念,它是View的承載者。而WindowManager顧名思義是Window的管理者,通過addView方法將View添加到Window里最終展示到屏幕上。

系列文章:

Window/WindowManager 不可不知之事
Android Window 如何確定大小/onMeasure()多次執行原因

通過本篇文章,你將了解到:

1、Window/WindowManager 創建、屬性及其使用
2、WindowManager.LayoutParams flag屬性之key/touch事件
3、View如何與Window關聯
4、WindowManager常用場景

Window/WindowManager 創建與使用

先看看一個簡單的添加View到Window的過程

    private void showView() {
        //獲取WindowManager實例,這里的App是繼承自Application
        WindowManager wm = (WindowManager) App.getApplication().getSystemService(Context.WINDOW_SERVICE);

        //設置LayoutParams屬性
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        layoutParams.height = 400;
        layoutParams.width = 400;
        layoutParams.format = PixelFormat.RGBA_8888;

        //窗口標記屬性
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

        //Window類型
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        } else {
            layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        }

        //構造TextView
        TextView textView = new TextView(this);
        textView.setBackground(new ColorDrawable(Color.WHITE));
        textView.setText("hello windowManager");

        //將textView添加到WindowManager
        wm.addView(textView, layoutParams);
    }

效果如下:


gif2.gif

以上代碼分三個部分看:

1、獲取WindowManager對象
2、設置LayoutParams屬性
3、將View添加到Window里

I 獲取WindowManager對象

App是繼承自Application,App.getApplication()獲取當前應用的application實例,其本身也是Context。關于Context請移步:Android各種Context的前世今生

public interface WindowManager extends ViewManager

WindowManager是個接口,繼承了ViewManager,ViewManager也是個接口,來看看它的內容:

    public interface ViewManager
    {
        //添加View, view 表示內容本身,params表示對此view位置、大小等屬性的限制
        public void addView(View view, ViewGroup.LayoutParams params);
        //更新view
        public void updateViewLayout(View view, ViewGroup.LayoutParams params);
        //移除View
        public void removeView(View view);
    }

既然WindowManager是個接口,那么必然有實現它的類,答案就在:getSystemService(Context.WINDOW_SERVICE)里。

ContextImpl.java
    @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }
SystemServiceRegistry.java
    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }

    registerService(Context.WINDOW_SERVICE, WindowManager .class,
                new CachedServiceFetcher<WindowManager>() {
        @Override
        public WindowManager createService (ContextImpl ctx){
            return new WindowManagerImpl(ctx);
        }
    });
  • WindowManager實現類是WindowManagerImpl

WindowManagerImpl 內容并不多

    public final class WindowManagerImpl implements WindowManager {
        //WindowManagerImpl 代理類 WindowManagerGlobal單例
        private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
        //getSystemService傳進來的Context
        private final Context mContext;
        //記錄構造WindowManager父Window
        private final Window mParentWindow;
        //關聯Activity時會賦值
        private IBinder mDefaultToken;

        public WindowManagerImpl(Context context) {
            this(context, null);
        }

        private WindowManagerImpl(Context context, Window parentWindow) {
            mContext = context;
            mParentWindow = parentWindow;
        }

        public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
            return new WindowManagerImpl(mContext, parentWindow);
        }
        //省略
    }
  • 其實現的add/update/remove方法最終交由WindowManagerGlobal實現。
  • WindowManagerGlobal記錄著該App內所有展示的Window一些相關信息

II 設置LayoutParams屬性

WindowManager.LayoutParams 繼承自ViewGroup.LayoutParams,來看看一些我們關注的屬性:

width : 指定Window的寬度
height : 指定Window的高度
x : Window在屏幕X軸的偏移(偏移的起點是gravity設置的位置)
y : Window在屏幕Y軸的偏移(偏移的起點是gravity設置的位置)
flags :控制Window一些行為,比如能否讓下層的Window獲得點擊事件,Window能否超出屏幕展示等
type :Window類型,分為三種:
FIRST_APPLICATION_WINDOW ~ LAST_APPLICATION_WINDOW(1~99)應用窗口
FIRST_SUB_WINDOW ~ LAST_SUB_WINDOW (1000 ~ 1999)子窗口
FIRST_SYSTEM_WINDOW ~ LAST_SYSTEM_WINDOW (2000 ~ 2999)系統窗口
數值越大,層級越高,也就是層級越高的就能顯示在層級低的上邊。
gravity : Window的位置,取值自Gravity
windowAnimations : Window動畫

該例我們設置的type屬于系統窗口,系統窗口需要用戶開啟權限,對應的是設置里的:“顯示在其他應用的上層”
在Activity里檢查并獲取應用的方法如下:

    public void onClick(View view) {
        if (checkPermission(this)) {
            showView();
        } else {
            Intent intent = getPermissionIntent(this);
            if (intent != null) {
                try {
                    startActivityForResult(intent, 100);
                } catch (Exception e) {
                    Log.d("hello", "error");
                }
            } else {
            }
        }
    }

    public static boolean checkPermission(@NonNull Context context) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            return Settings.canDrawOverlays(context);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            int op = 24;
            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            try {
                Class clazz = AppOpsManager.class;
                Method method = clazz.getDeclaredMethod("checkOp", int.class, int.class, String.class);
                return AppOpsManager.MODE_ALLOWED == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
            } catch (Exception e) {
                return false;
            }
        } else {
            return true;
        }
    }

    public static Intent getPermissionIntent(@NonNull Context context) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            return new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName()));
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            String brand = Build.BRAND;
            if (TextUtils.isEmpty(brand)) {
                return null;
            }
            return null;
        } else {
            return null;
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == 100) {
            if (checkPermission(this)) {
                showView();
            }
        }
    }

當然還需要在AndroidManifest.xml里聲明使用的權限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

III 將View添加到Window里

wm.addView(textView, layoutParams)

WindowManagerGlobal.java
    public void addView(View view, ViewGroup.LayoutParams params,
                        Display display, Window parentWindow) {
        
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            //調整LayoutParams
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            //省略
        }

        ViewRootImpl root;
        View panelParentView = null;
        synchronized (mLock) {
            //構造ViewRootImpl
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            //用數組記錄
            //mViews 存放添加到Window的view
            //mRoots 存放ViewRootImpl
            //mParams 存放Window參數
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            try {
                //調用ViewRootImpl setView
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
            }
        }
        //省略
    }

真正實現窗口的添加是通過ViewRootImpl setView(xx)方法

ViewRootImpl.java
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                //省略
                int res;
                //提交View展示請求(測量、布局、繪制),只是提交到隊列里
                //當屏幕刷新信號到來之時從隊列取出執行
                requestLayout();
                try {
                    //添加到窗口
                    //進程間通信,告訴WindowManagerService為我們開辟一個Window空間
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                            mTempInsets);
                } catch (RemoteException e) {
                } finally {
                }
                if (res < WindowManagerGlobal.ADD_OKAY) {
                    //窗口添加失敗拋出各種異常
                }
                //Window根View的mParent是ViewRootImpl 而其他View的mParent是其父控件
                //這參數是向上遍歷View Tree的關鍵
                view.assignParent(this);
                //輸入事件相關 touch、key事件接收
                CharSequence counterSuffix = attrs.getTitle();
                mSyntheticInputStage = new SyntheticInputStage();
                InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
                InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
                        "aq:native-post-ime:" + counterSuffix);
                InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
                InputStage imeStage = new ImeInputStage(earlyPostImeStage,
                        "aq:ime:" + counterSuffix);
                InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
                InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
                        "aq:native-pre-ime:" + counterSuffix);
            }
        }
    }

關于requestLayout()請移步:Android Activity創建到View的顯示過程

使用Binder方式,ViewRootImpl與WindowManagerService建立Session進行通信
mWindowSession.addToDisplay 簡單來看看后續調用(有興趣的可以深入源碼看看)。

Session.java
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
            Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
                outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel,
                outInsetsState);
    }
WindowManagerService.java
    public int addWindow(Session session, IWindow client, int seq,
            LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState) {
            //省略
}

WindowManager.LayoutParams flag屬性之key/touch事件

我們知道Activity實際上也是通過Window展示的,現在Activity之上添加了另一個Window,那么key/touch事件是如何決定分發給哪個Window呢?


image.png

如上圖所示,Window2 是在Window1之上,層級比Window1高,決定Window2 key/touch事件是否分發給Window1取決于WindowManager.LayoutParams flag 參數,flag默認為0。結合上圖來看看一些常用的值及其作用,當Window2使用如下參數時:

    public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;
    public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;
    public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;
    public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;
    public static final int FLAG_ALT_FOCUSABLE_IM = 0x00020000;

flag默認為0,不對flag設置時,Window2默認接受所有的touch/key 事件,即使點擊區域不在Window2的范圍內。

FLAG_NOT_TOUCHABLE

表示Window 不接收所有的touch事件。此時無論點擊Window2 區域還是Window2之外的區域,touch事件都分發給了下一層Window1。而key事件則不受影響。

FLAG_NOT_FOCUSABLE

表示Window不接收輸入焦點,不和鍵盤交互。比如當Window里使用editText時,是無法彈出鍵盤的。另外一個作用就是:當點擊Window2之外的區域時,touch事件分發給了Window1,而點擊Window2區域是分發給了其自身,key事件也不會分發給Window2,而是給了Window1(該作用相當于設置了FLAG_NOT_TOUCH_MODAL)。

FLAG_NOT_TOUCH_MODAL

表示當點擊Window2之外的區域時,touch事件分發給了Window1,而key事件不受影響。當然此時Window2是能獲取焦點的,能和鍵盤交互。

FLAG_WATCH_OUTSIDE_TOUCH

該值配合FLAG_NOT_TOUCH_MODAL才會生效。意思就是當設置了FLAG_NOT_TOUCH_MODAL時,點擊Window2外部區域其收不到touch事件,但是這個時候Window2想要收到外部點擊的事件,同時又不影響事件分發給Window1,此時FLAG_WATCH_OUTSIDE_TOUCH標記就發揮其作用了。此Window2接收到ACTION_OUTSIDE類型的事件,而touch事件(down/move/up)則分發給了Window1。key事件不受影響。

FLAG_ALT_FOCUSABLE_IM

與鍵盤相關。當FLAG_NOT_FOCUSABLE沒有設置且FLAG_ALT_FOCUSABLE_IM設置時,表示無需與鍵盤交互。當FLAG_NOT_FOCUSABLE/FLAG_ALT_FOCUSABLE_IM同時設置時,表示需要與輸入法交互。FLAG_ALT_FOCUSABLE_IM單獨設置時不影響touch/key 事件。

View如何與Window關聯

通過前面的分析,并沒有發現View和Window的直接關聯,那么View的內容怎么顯示在Window上的呢?

Surface與Canvas

平時我們都是重寫View onDraw(Canvas canvas),通過Canvas繪制我們想要的效果,來看看Canvas是怎么來的:
對于軟件繪制

ViewRootImpl.java
public final Surface mSurface = new Surface();
final Canvas canvas = mSurface.lockCanvas(dirty);

可以看出,Canvas是從Surface獲取的,那自然想到Surface和Window是否有關系呢,是怎么關聯呢?

ViewRootImpl.java
    private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
                               boolean insetsPending) throws RemoteException {
        //省略
        //傳入SurfaceControl,在WindowManagerService里處理
        int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
                (int) (mView.getMeasuredWidth() * appScale + 0.5f),
                (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
                insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
                mTmpFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
                mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
                mPendingMergedConfiguration, mSurfaceControl, mTempInsets);
        if (mSurfaceControl.isValid()) {
            //返回App層的Surface
            mSurface.copyFrom(mSurfaceControl);
        } else {
            destroySurface();
        }
        //省略
        return relayoutResult;
    }

在View開啟ViewTree三大流程時,performTraversals->relayoutWindow,將Window與SurfaceControl關聯,進而關聯Surface。這樣,Window->Surface->Canvas就關聯起來了,通過Canvas將View繪制到Surface上,最終顯示出來。
而對于硬件加速來說
每個View都有RenderNode

RenderNode.java
    public @NonNull RecordingCanvas beginRecording(int width, int height) {
        if (mCurrentRecordingCanvas != null) {
            throw new IllegalStateException(
                    "Recording currently in progress - missing #endRecording() call?");
        }
        mCurrentRecordingCanvas = RecordingCanvas.obtain(this, width, height);
        return mCurrentRecordingCanvas;
    }

繪制該View的Canvas通過beginRecording獲取,Canvas繪制的操作封裝在DisplayList。
在ViewRootImpl->performTraversals

hwInitialized = mAttachInfo.mThreadedRenderer.initialize(
                    mSurface);

建立ThreadedRenderer和Surface關聯,而ThreadedRenderer里持有:

protected RenderNode mRootNode;

該mRootNode是整個ViewTree的根node。這樣Surface和Canvas建立了關聯。
用圖表示View、Window、Surface關系:


image.png

Window內容是通過Surface展示,而SurfaceFlinger將多個Surface合成顯示在屏幕上。

ViewManager 其他方法

上面說了添加View到Window的addView(xx)方法,接下來看看updateViewLayout(xx)和removeView(xx)方法
updateViewLayout(xx)

WindowManagerGlobal.java
    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
        //設置View的params
        view.setLayoutParams(wparams);
        synchronized (mLock) {
            //找到目標View在數組中的位置
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            //移除舊的params
            mParams.remove(index);
            //添加新的params
            mParams.add(index, wparams);
            //ViewRootImpl重新設置params
            //最終按需開啟View的三大流程
            root.setLayoutParams(wparams, false);
        }
    }

removeView(xx)

image.png

public void removeView(View view, boolean immediate)

immediate 表示是否立即移除View,如果是false,那么通過Handler發送Message,等待下次Looper輪詢后執行。

具體工作是在ViewRootImpl里的doDie()。

    void doDie() {
        synchronized (this) {
            //通知View已經移除
            if (mAdded) {
                dispatchDetachedFromWindow();
            }
            if (mAdded && !mFirst) {
                destroyHardwareRenderer();
                if (mView != null) {
                        try {
                            //通知WindowManagerService重新布局
                            if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
                                    & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
                                mWindowSession.finishDrawing(mWindow);
                            }
                        } catch (RemoteException e) {
                        }
                    }
                    //移除surface
                    destroySurface();
                }
            }
            mAdded = false;
        }
        //移除WindowManagerGlobal記錄的信息,比如ViewRootImpl、View數組等
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }

WindowManager常用場景

Android里的界面展示都是通過WindowManager.addView(xx),也就是說我們看到的界面都是有個Window的。只是Window比較抽象,我們更多接觸的是View。

Activity

Activity實際上也是通過Window展示界面的,只是系統封裝好了addView的過程。我們只需要setContentView(resId),將我們的布局傳入即可。
關于setContentView(resId),請移步:Android DecorView 一窺全貌(上)

Dialog

Dialog內部也是通過addView(xx)展示

PopupWindow

與Dialog類似,只是沒有PhoneWindow

Toast

Toast與其他的系統彈框等...只要界面展示都會用到addView(xx)

Dialog/PopupWindow/Toast 更詳細的差異請移步:
Dialog PopupWindow Toast 你還有疑惑嗎

創建懸浮窗源碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容