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() {
        @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里声明使用的权限


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 你还有疑惑吗

创建悬浮窗源码

你可能感兴趣的:(Window/WindowManager 不可不知之事)