Window/WindowManager 不可不知之事

前言

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

系列文章:

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

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

1、Window/WindowManager 創(chuàng)建、屬性及其使用
2、WindowManager.LayoutParams flag屬性之key/touch事件
3、View如何與Window關(guān)聯(lián)
4、WindowManager常用場(chǎng)景

Window/WindowManager 創(chuàng)建與使用

先看看一個(gè)簡(jiǎn)單的添加View到Window的過程

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

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

        //窗口標(biāo)記屬性
        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;
        }

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

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

效果如下:


gif2.gif

以上代碼分三個(gè)部分看:

1、獲取WindowManager對(duì)象
2、設(shè)置LayoutParams屬性
3、將View添加到Window里

I 獲取WindowManager對(duì)象

App是繼承自Application,App.getApplication()獲取當(dāng)前應(yīng)用的application實(shí)例,其本身也是Context。關(guān)于Context請(qǐng)移步:Android各種Context的前世今生

public interface WindowManager extends ViewManager

WindowManager是個(gè)接口,繼承了ViewManager,ViewManager也是個(gè)接口,來看看它的內(nèi)容:

    public interface ViewManager
    {
        //添加View, view 表示內(nèi)容本身,params表示對(duì)此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是個(gè)接口,那么必然有實(shí)現(xiàn)它的類,答案就在: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實(shí)現(xiàn)類是WindowManagerImpl

WindowManagerImpl 內(nèi)容并不多

    public final class WindowManagerImpl implements WindowManager {
        //WindowManagerImpl 代理類 WindowManagerGlobal單例
        private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
        //getSystemService傳進(jìn)來的Context
        private final Context mContext;
        //記錄構(gòu)造WindowManager父Window
        private final Window mParentWindow;
        //關(guān)聯(lián)Activity時(shí)會(huì)賦值
        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);
        }
        //省略
    }
  • 其實(shí)現(xiàn)的add/update/remove方法最終交由WindowManagerGlobal實(shí)現(xiàn)。
  • WindowManagerGlobal記錄著該App內(nèi)所有展示的Window一些相關(guān)信息

II 設(shè)置LayoutParams屬性

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

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

該例我們?cè)O(shè)置的type屬于系統(tǒng)窗口,系統(tǒng)窗口需要用戶開啟權(quán)限,對(duì)應(yīng)的是設(shè)置里的:“顯示在其他應(yīng)用的上層”
在Activity里檢查并獲取應(yīng)用的方法如下:

    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();
            }
        }
    }

當(dāng)然還需要在AndroidManifest.xml里聲明使用的權(quán)限

<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) {
            //調(diào)整LayoutParams
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            //省略
        }

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

真正實(shí)現(xiàn)窗口的添加是通過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展示請(qǐng)求(測(cè)量、布局、繪制),只是提交到隊(duì)列里
                //當(dāng)屏幕刷新信號(hào)到來之時(shí)從隊(duì)列取出執(zhí)行
                requestLayout();
                try {
                    //添加到窗口
                    //進(jìn)程間通信,告訴WindowManagerService為我們開辟一個(gè)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是其父控件
                //這參數(shù)是向上遍歷View Tree的關(guān)鍵
                view.assignParent(this);
                //輸入事件相關(guān) 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);
            }
        }
    }

關(guān)于requestLayout()請(qǐng)移步:Android Activity創(chuàng)建到View的顯示過程

使用Binder方式,ViewRootImpl與WindowManagerService建立Session進(jìn)行通信
mWindowSession.addToDisplay 簡(jiǎn)單來看看后續(xù)調(diào)用(有興趣的可以深入源碼看看)。

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實(shí)際上也是通過Window展示的,現(xiàn)在Activity之上添加了另一個(gè)Window,那么key/touch事件是如何決定分發(fā)給哪個(gè)Window呢?


image.png

如上圖所示,Window2 是在Window1之上,層級(jí)比Window1高,決定Window2 key/touch事件是否分發(fā)給Window1取決于WindowManager.LayoutParams flag 參數(shù),flag默認(rèn)為0。結(jié)合上圖來看看一些常用的值及其作用,當(dāng)Window2使用如下參數(shù)時(shí):

    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默認(rèn)為0,不對(duì)flag設(shè)置時(shí),Window2默認(rèn)接受所有的touch/key 事件,即使點(diǎn)擊區(qū)域不在Window2的范圍內(nèi)。

FLAG_NOT_TOUCHABLE

表示W(wǎng)indow 不接收所有的touch事件。此時(shí)無論點(diǎn)擊Window2 區(qū)域還是Window2之外的區(qū)域,touch事件都分發(fā)給了下一層Window1。而key事件則不受影響。

FLAG_NOT_FOCUSABLE

表示W(wǎng)indow不接收輸入焦點(diǎn),不和鍵盤交互。比如當(dāng)Window里使用editText時(shí),是無法彈出鍵盤的。另外一個(gè)作用就是:當(dāng)點(diǎn)擊Window2之外的區(qū)域時(shí),touch事件分發(fā)給了Window1,而點(diǎn)擊Window2區(qū)域是分發(fā)給了其自身,key事件也不會(huì)分發(fā)給Window2,而是給了Window1(該作用相當(dāng)于設(shè)置了FLAG_NOT_TOUCH_MODAL)。

FLAG_NOT_TOUCH_MODAL

表示當(dāng)點(diǎn)擊Window2之外的區(qū)域時(shí),touch事件分發(fā)給了Window1,而key事件不受影響。當(dāng)然此時(shí)Window2是能獲取焦點(diǎn)的,能和鍵盤交互。

FLAG_WATCH_OUTSIDE_TOUCH

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

FLAG_ALT_FOCUSABLE_IM

與鍵盤相關(guān)。當(dāng)FLAG_NOT_FOCUSABLE沒有設(shè)置且FLAG_ALT_FOCUSABLE_IM設(shè)置時(shí),表示無需與鍵盤交互。當(dāng)FLAG_NOT_FOCUSABLE/FLAG_ALT_FOCUSABLE_IM同時(shí)設(shè)置時(shí),表示需要與輸入法交互。FLAG_ALT_FOCUSABLE_IM單獨(dú)設(shè)置時(shí)不影響touch/key 事件。

View如何與Window關(guān)聯(lián)

通過前面的分析,并沒有發(fā)現(xiàn)View和Window的直接關(guān)聯(lián),那么View的內(nèi)容怎么顯示在Window上的呢?

Surface與Canvas

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

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

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

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三大流程時(shí),performTraversals->relayoutWindow,將Window與SurfaceControl關(guān)聯(lián),進(jìn)而關(guān)聯(lián)Surface。這樣,Window->Surface->Canvas就關(guān)聯(lián)起來了,通過Canvas將View繪制到Surface上,最終顯示出來。
而對(duì)于硬件加速來說
每個(gè)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關(guān)聯(lián),而ThreadedRenderer里持有:

protected RenderNode mRootNode;

該mRootNode是整個(gè)ViewTree的根node。這樣Surface和Canvas建立了關(guān)聯(lián)。
用圖表示View、Window、Surface關(guān)系:


image.png

Window內(nèi)容是通過Surface展示,而SurfaceFlinger將多個(gè)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;
        //設(shè)置View的params
        view.setLayoutParams(wparams);
        synchronized (mLock) {
            //找到目標(biāo)View在數(shù)組中的位置
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            //移除舊的params
            mParams.remove(index);
            //添加新的params
            mParams.add(index, wparams);
            //ViewRootImpl重新設(shè)置params
            //最終按需開啟View的三大流程
            root.setLayoutParams(wparams, false);
        }
    }

removeView(xx)

image.png

public void removeView(View view, boolean immediate)

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

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

    void doDie() {
        synchronized (this) {
            //通知View已經(jīng)移除
            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數(shù)組等
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }

WindowManager常用場(chǎng)景

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

Activity

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

Dialog

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

PopupWindow

與Dialog類似,只是沒有PhoneWindow

Toast

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

Dialog/PopupWindow/Toast 更詳細(xì)的差異請(qǐng)移步:
Dialog PopupWindow Toast 你還有疑惑嗎

創(chuàng)建懸浮窗源碼

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

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

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