文章已授权『鸿洋』公众号发布
前言
你好!
我是一只修仙的猿,欢迎阅读我的文章。
Window,读者可能更多的认识是windows系统的窗口。在windows系统上,我们可以多个窗口同时运行,每个窗口代表着一个应用程序。但在安卓上貌似并没有这个东西,但读者可以马上想到,不是有小窗口模式吗,像米UI最新的系统,不就是可以随意创建一个小窗口,然后两个应用同时操作?是的,那是属于android中,window的一种表现方式。但是手机屏幕终究不能和电脑相比,因为屏幕太小了,小到只能操作一款应用,多个窗口就显得非常不习惯,所以Android上关于窗口方面的知识读者可能接触不多。那window的意思就只是小米系统中那种小窗口吗?
当然不是。Android框架层意义上的window和我们认识的window其实是有点不一样的。我们日常最直观的,每个应用界面,都有一个应用级的window。再例如popupWindow、Toast、dialog、menu都是需要通过创建window来实现。所以其实window我们一直都见到,只是不知道那就是window。了解window的机制原理,可以更好地了解window,进而更好地了解android是怎么管理屏幕上的view。这样,当我们需要使用dialog或者popupWindow的时候,可以懂得他背后究竟做了什么,才能够更好的运用dialog、popupWindow等。
当然,到此如果你有很多的疑问,甚至质疑我的理论,那就希望你可以阅读完这一篇文章。我会从window是什么,有什么用,内部机制是什么,各种组件是如何创建window等等方面来阐述Android中的window。文章内容非常多,读者可自选章节阅读。
什么是window机制
先假设如果没有window,会发生什么:
我们看到的界面ui是view,如我们的应用布局,更简单是一个button。假如屏幕上现在有一个Button,如图1,现在往屏幕中间添加一个TextView,那么最终的结果是图2,还是图3:
![示例图](https://i-blog.csdnimg.cn/blog_migrate/6e82c59d022b3db548dd52c7d058b8d7.png)
在上图的图2中,如果我要实现点击textView执行他的监听事件逻辑,点击不是textView的区域让textView消失,需要怎么实现呢?读者可能会说,我们可以在Activity中添加这部分的逻辑,那如果我们需要让一个悬浮窗在所有界面显示呢,如上文我讲到的小米悬浮窗,两个不用应用的view,怎么确定他们的显示次序?又例如我们需要弹出一个dialog来提示用户,怎么样可以让dialog永远处于最顶层呢,包括显示dialog期间应用弹出的如popupWindow必须显示在dialog的低下,但toast又必须显示在dialog上面。
很明显,我们的屏幕可以允许多个应用同时显示非常多的view,他们的显示次序或者说显示高度是不一样的,如果没有一个统一的管理者,那么每一家应用都想要显示在最顶层,那么屏幕上的view会非常乱。
同时,当我们点击屏幕时,这个触摸事件应该传给哪个view?很明显我们都知道应该传给最上层的view,但是接受事件的是屏幕,是另一个系统服务,他怎么知道触摸位置的最上层是哪个view呢?即时知道,他又怎么把这个事件准确地传给他呢?
为了解决等等这些问题,急需有一个管理者来统一管理屏幕上的显示的view,才能让程序有条不紊地走下去。而这,就是Android中的window机制。
window机制就是为了管理屏幕上的view的显示以及触摸事件的传递问题。
什么是window?
那什么是window,在Android的window机制中,每个view树都可以看成一个window。为什么不是每个view呢?因为view树中每个view的显示次序是固定的,例如我们的Activity布局,每一个控件的显示都是已经安排好的,对于window机制来说,属于“不可再分割的view”。
什么是view树?例如你在布局中给Activity设置了一个布局xml,那么最顶层的布局如LinearLayout就是view树的根,他包含的所有view就都是该view树的节点,所以这个view树就对应一个window。
举几个具体的例子:
- 我们在添加dialog的时候,需要给他设置view,那么这个view他是不属于antivity的布局内的,是通过WindowManager添加到屏幕上的,不属于activity的view树内,所以这个dialog是一个独立的view树,所以他是一个window。
- popupWindow他也对应一个window,因为它也是通过windowManager添加上去的,不属于Activity的view树。
- 当我们使用使用windowManager在屏幕上添加的任何view都不属于Activity的布局view树,即使是只添加一个button。
view树(后面使用view代称,后面我说的view都是指view树)是window机制的操作单位,每一个view对应一个window,view是window的存在形式,window是view的载体,我们平时看到的应用界面、dialog、popupWindow以及上面描述的悬浮窗,都是window的表现形式。注意,我们看到的不是window,而是view。**window是view的管理者,同时也是view的载体。他是一个抽象的概念,本身并不存在,view是window的表现形式。**这里的不存在,指的是我们在屏幕上是看不到window的,他不像windows系统,如下图:
![windows系统窗口](https://i-blog.csdnimg.cn/blog_migrate/2232d27d80b41c794eb297197852a272.png)
有一个很明显的标志:看,我就是window。但在Android中我们是无法感知的,我们只能看到view无法看到window,window是控制view需要怎么显示的管理者。每个成功的男人背后都有一个女人,每个view背后都有一个window。
window本身并不存在,他只是一个概念。举个栗子:如班集体,就是一个概念,他的存在形式是这整个班的学生,当学生不存在那么这个班集体也就不存在。但是他的好处是得到了一个新的概念,我们可以以班为单位来安排活动。因他不存在,所以也很难从源码中找到他的痕迹,window机制的操作单位都是view,如果要说他在源码中的存在形式,笔者目前的认知就是在WindowManagerService中每一个view对应一个windowStatus。WindowManagerService是什么如果没了解过可以先忽略后面会讲到。读者可以慢慢思考一下这个抽象的概念,后面会慢慢深入讲源码帮助理解。
- view是window的存在形式,window是view的载体
- window是view的管理者,同时也是view的载体。他是一个抽象的概念,本身并不存在,view是window的表现形式
思考:Android中不是有一个抽象类叫做window还有一个PhoneWindow实现类吗,他们不就是window的存在形式,为什么说window是抽象不存在的?读者可自行思考,后面会讲到。
Window的相关属性
在了解window的操作流程之前,先补充一下window的相关属性。
window的type属性
前面我们讲到window机制解决的一个问题就是view的显示次序问题,这个属性就决定了window的显示次序。window是有分类的,不同类别的显示高度范围不同,例如我把1-1000m高度称为低空,1001-2000m高度称为中空,2000以上称为高空。window也是一样按照高度范围进行分类,他也有一个变量Z-Order,决定了window的高度。window一共可分为三类:
- 应用程序窗口:应用程序窗口一般位于最底层,Z-Order在1-99
- 子窗口:子窗口一般是显示在应用窗口之上,Z-Order在1000-1999
- 系统级窗口:系统级窗口一般位于最顶层,不会被其他的window遮住,如Toast,Z-Order在2000-2999。如果要弹出自定义系统级窗口需要动态申请权限。
Z-Order越大,window越靠近用户,也就显示越高,高度高的window会覆盖高度低的window。
window的type属性就是Z-Order的值,我们可以给window的type属性赋值来决定window的高度。系统为我们三类window都预设了静态常量,如下(以下常用参数介绍转自参考文献第一篇文章):
-
应用级window
// 应用程序 Window 的开始值 public static final int FIRST_APPLICATION_WINDOW = 1; // 应用程序 Window 的基础值 public static final int TYPE_BASE_APPLICATION = 1; // 普通的应用程序 public static final int TYPE_APPLICATION = 2; // 特殊的应用程序窗口,当程序可以显示 Window 之前使用这个 Window 来显示一些东西 public static final int TYPE_APPLICATION_STARTING = 3; // TYPE_APPLICATION 的变体,在应用程序显示之前,WindowManager 会等待这个 Window 绘制完毕 public static final int TYPE_DRAWN_APPLICATION = 4; // 应用程序 Window 的结束值 public static final int LAST_APPLICATION_WINDOW = 99;
-
子window
// 子 Window 类型的开始值 public static final int FIRST_SUB_WINDOW = 1000; // 应用程序 Window 顶部的面板。这些 Window 出现在其附加 Window 的顶部。 public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW; // 用于显示媒体(如视频)的 Window。这些 Window 出现在其附加 Window 的后面。 public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1; // 应用程序 Window 顶部的子面板。这些 Window 出现在其附加 Window 和任何Window的顶部 public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2; // 当前Window的布局和顶级Window布局相同时,不能作为子代的容器 public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3; // 用显示媒体 Window 覆盖顶部的 Window, 这是系统隐藏的 API public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4; // 子面板在应用程序Window的顶部,这些Window显示在其附加Window的顶部, 这是系统隐藏的 API public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5; // 子 Window 类型的结束值 public static final int LAST_SUB_WINDOW = 1999;
-
系统级window
// 系统Window类型的开始值 public static final int FIRST_SYSTEM_WINDOW = 2000; // 系统状态栏,只能有一个状态栏,它被放置在屏幕的顶部,所有其他窗口都向下移动 public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW; // 系统搜索窗口,只能有一个搜索栏,它被放置在屏幕的顶部 public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1; // 已经从系统中被移除,可以使用 TYPE_KEYGUARD_DIALOG 代替 public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4; // 系统对话框窗口 public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8; // 锁屏时显示的对话框 public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9; // 输入法窗口,位于普通 UI 之上,应用程序可重新布局以免被此窗口覆盖 public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11; // 输入法对话框,显示于当前输入法窗口之上 public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12; // 墙纸 public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13; // 状态栏的滑动面板 public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14; // 应用程序叠加窗口显示在所有窗口之上 public static final int TYPE_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 38; // 系统Window类型的结束值 public static final int LAST_SYSTEM_WINDOW = 2999;
Window的flags参数
flag标志控制window的东西比较多,很多资料的描述是“控制window的显示”,但我觉得不够准确。flag控制的范围包括了:各种情景下的显示逻辑(锁屏,游戏等)还有触控事件的处理逻辑。控制显示确实是他的很大部分功能,但是并不是全部。下面看一下一些常用的flag,就知道flag的功能了(以下常用参数介绍转自参考文献第一篇文章):
// 当 Window 可见时允许锁屏
public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001;
// Window 后面的内容都变暗
public static final int FLAG_DIM_BEHIND = 0x00000002;
// Window 不能获得输入焦点,即不接受任何按键或按钮事件,例如该 Window 上 有 EditView,点击 EditView 是 不会弹出软键盘的
// Window 范围外的事件依旧为原窗口处理;例如点击该窗口外的view,依然会有响应。另外只要设置了此Flag,都将会启用FLAG_NOT_TOUCH_MODAL
public static final int FLAG_NOT_FOCUSABLE = 0x00000008;
// 设置了该 Flag,将 Window 之外的按键事件发送给后面的 Window 处理, 而自己只会处理 Window 区域内的触摸事件
// Window 之外的 view 也是可以响应 touch 事件。
public static final int FLAG_NOT_TOUCH_MODAL = 0x00000020;
// 设置了该Flag,表示该 Window 将不会接受任何 touch 事件,例如点击该 Window 不会有响应,只会传给下面有聚焦的窗口。
public static final int FLAG_NOT_TOUCHABLE = 0x00000010;
// 只要 Window 可见时屏幕就会一直亮着
public static final int FLAG_KEEP_SCREEN_ON = 0x00000080;
// 允许 Window 占满整个屏幕
public static final int FLAG_LAYOUT_IN_SCREEN = 0x00000100;
// 允许 Window 超过屏幕之外
public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200;
// 全屏显示,隐藏所有的 Window 装饰,比如在游戏、播放器中的全屏显示
public static final int FLAG_FULLSCREEN = 0x00000400;
// 表示比FLAG_FULLSCREEN低一级,会显示状态栏
public static final int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;
// 当用户的脸贴近屏幕时(比如打电话),不会去响应此事件
public static final int FLAG_IGNORE_CHEEK_PRESSES = 0x00008000;
// 则当按键动作发生在 Window 之外时,将接收到一个MotionEvent.ACTION_OUTSIDE事件。
public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;
@Deprecated
// 窗口可以在锁屏的 Window 之上显示, 使用 Activity#setShowWhenLocked(boolean) 方法代替
public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
// 表示负责绘制系统栏背景。如果设置,系统栏将以透明背景绘制,
// 此 Window 中的相应区域将填充 Window#getStatusBarColor()和 Window#getNavigationBarColor()中指定的颜色。
public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000;
// 表示要求系统壁纸显示在该 Window 后面,Window 表面必须是半透明的,才能真正看到它背后的壁纸
public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
window的solfInputMode属性
这一部分就是当软件盘弹起来的时候,window的处理逻辑,这在日常中也经常遇到,如:我们在微信聊天的时候,点击输入框,当软键盘弹起来的时候输入框也会被顶上去。如果你不想被顶上去,也可以设置为被软键盘覆盖。下面介绍一下常见的属性(以下常见属性介绍选自参考文献第一篇文章):
// 没有指定状态,系统会选择一个合适的状态或者依赖于主题的配置
public static final int SOFT_INPUT_STATE_UNCHANGED = 1;