一、引言
在 Android 开发的庞大体系中,Window 和 WindowManager 如同幕后的关键角色,掌控着界面展示的核心逻辑,对构建丰富、交互性强的应用起着举足轻重的作用。
当你打开手机上的各类应用,无论是常规的 Activity 界面切换,还是弹出的 Dialog 提示框,又或是短暂出现的 Toast 消息,这些视觉呈现的背后都离不开 Window 的承载 。它就像是一块无形的画布,为 View 提供了展示的空间,是 View 得以在屏幕上呈现的基础容器。而 WindowManager 则如同一位指挥家,精准地控制着这些 Window 的创建、显示、隐藏以及层级关系等,决定着用户最终看到的界面效果。
例如,在实现一个悬浮窗功能时,我们就需要借助 WindowManager 来创建一个特殊类型的 Window,并通过精细调整其参数,如位置、大小、透明度等,使其能够悬浮在其他应用之上,为用户提供便捷的交互体验。又比如,在多窗口模式的应用开发中,WindowManager 负责管理各个窗口的布局和显示优先级,确保不同窗口之间能够协调工作,互不干扰。正是因为 Window 和 WindowManager 在这些实际场景中的深度参与,使得它们成为 Android 开发中不可或缺的部分,深入探究它们的原理和使用方法,对于提升应用的质量和用户体验具有至关重要的意义。
二、Window 详解
2.1 Window 概念与作用
在 Android 系统中,Window 是一个十分抽象的概念,它并不像我们日常接触的具体视图那样直观可见,但却实实在在地在幕后发挥着关键作用,是整个 Android 视图体系的重要基石。从本质上讲,Window 可以被看作是一个容器,是 View 的直接管理者 ,所有的 View 都必须依附于 Window 才能最终呈现在用户面前。
以我们最常用的 Activity 为例,当一个 Activity 启动时,系统会为其创建一个对应的 Window,这个 Window 就像是一个无形的舞台,而 Activity 中的各种布局和控件(即 View)则是在这个舞台上进行展示和交互的演员。我们在 Activity 中通过setContentView方法设置的布局文件,实际上就是将一个 View 树添加到了这个 Window 中。同样,Dialog 作为一种常见的弹窗组件,也是依赖于 Window 来显示的。当我们创建并显示一个 Dialog 时,其实是在当前 Activity 的 Window 之上创建了一个新的子 Window,并将 Dialog 的内容视图添加到这个子 Window 中,从而实现了弹窗效果。Toast 则是一种更为特殊的情况,它也是基于 Window 来实现短暂的提示信息展示,不过它的 Window 属于系统 Window,具有更高的层级,能够在任何应用界面之上显示,且无需用户交互,在显示一段时间后自动消失。
正是由于 Window 的存在,Android 系统才能够有条不紊地管理和展示各种视图,实现丰富多样的界面效果和交互体验。它不仅为 View 提供了展示的空间,还负责处理 View 的事件分发、焦点管理以及窗口的层级关系等重要任务,是 Android 应用开发中不可或缺的一部分。
2.2 Window 的类型
在 Android 中,Window 根据其用途和特性的不同,主要分为三大类:应用 Window、子 Window 和系统 Window,每一类都有其独特的特点、区别和用途。
- 应用 Window:这类 Window 对应着我们日常开发中的 Activity,是应用程序界面的主要承载者,其层级范围是 1 - 99。每个 Activity 在创建时都会关联一个应用 Window,它是应用与用户进行交互的主要窗口,包含了 Activity 的标题栏(如果有的话)、内容视图以及可能的状态栏等元素。应用 Window 可以获得焦点,能够接收用户的各种输入事件,如触摸、点击、键盘输入等。例如,当我们打开一个社交应用的聊天界面 Activity 时,这个 Activity 所对应的应用 Window 就展示了聊天列表、输入框、发送按钮等各种用于交互的 View,用户可以在这个窗口中进行聊天消息的查看、输入和发送等操作。多个应用 Window 之间的层级关系取决于它们的创建顺序和当前的 Activity 栈状态,新创建的 Activity 对应的应用 Window 通常会覆盖在旧的 Activity 的应用 Window 之上。
- 子 Window:子 Window 需要依附于父 Window 存在,不能独立显示,其层级范围是 1000 - 1999 。常见的子 Window 有 Dialog、PopupWindow 等。以 Dialog 为例,当我们在一个 Activity 中显示一个 Dialog 时,这个 Dialog 就是一个子 Window,它依赖于所在 Activity 的应用 Window。子 Window 通常用于展示一些临时性的、与当前 Activity 相关的信息或功能,比如在文件管理应用中,当我们点击某个文件进行操作时,弹出的 “打开”“删除”“重命名” 等选项的 Dialog,就是一个子 Window,它为用户提供了针对该文件的具体操作入口。子 Window 的大小、位置和显示效果通常会受到父 Window 的影响,并且在父 Window 被销毁或隐藏时,子 Window 也会随之消失。
- 系统 Window:这是一类比较特殊的 Window,用于实现系统级别的功能,如 Toast、状态栏、输入法窗口等,其层级范围是 2000 - 2999 。系统 Window 需要声明特定的权限才能创建和使用,例如,要创建一个类似 Toast 的系统 Window,需要在 AndroidManifest.xml 文件中声明android.permission.SYSTEM_ALERT_WINDOW权限,并且在 Android 6.0 及以上版本还需要动态申请该权限。系统 Window 的显示优先级较高,通常会覆盖在应用 Window 和子 Window 之上,以确保系统功能的正常展示和使用。比如,当手机电量低时,会弹出一个系统级别的电量低提示 Window,这个 Window 会直接显示在当前所有应用界面之上,提醒用户及时充电。又比如,输入法窗口作为系统 Window,在用户需要输入文字时,会自动弹出并覆盖在当前应用的相关输入区域之上,方便用户进行文字输入。
这三类 Window 通过不同的层级范围和特性,共同构建了 Android 丰富多样的界面展示和交互体系,开发者在实际开发中需要根据具体的需求选择合适的 Window 类型来实现相应的功能。
2.3 Window 的内部实现 - PhoneWindow
PhoneWindow 是 Window 的具体实现类,在 Android 应用的界面展示中扮演着至关重要的角色。当我们在 Activity 中调用setContentView方法来设置界面布局时,其背后的实现机制就与 PhoneWindow 密切相关。
在 Activity 的创建过程中,会通过attach方法创建一个 PhoneWindow 对象,并将其与 Activity 进行关联。PhoneWindow 内部包含一个重要的成员变量 DecorView,它是整个 Activity 界面的根视图,也是 Activity 窗口的顶级容器。DecorView 不仅包含了我们通过setContentView设置的内容视图,还包含了一些系统提供的装饰元素,如标题栏(如果有的话)、状态栏等。
具体来说,当调用setContentView(int layoutResID)时,PhoneWindow 会首先创建 DecorView(如果尚未创建)。DecorView 是一个继承自 FrameLayout 的 ViewGroup,它的布局结构是由系统定义的。接着,PhoneWindow 会通过 LayoutInflater 将我们指定的布局资源文件解析成 View 树,并将这个 View 树添加到 DecorView 的一个名为mContentParent的 ViewGroup 中。mContentParent是 DecorView 中专门用于存放内容视图的容器,通过这种方式,我们的布局内容就被嵌入到了 DecorView 中,最终呈现在用户面前。例如,在一个简单的登录界面 Activity 中,我们通过setContentView(R.layout.activity_login)设置了登录界面的布局文件,PhoneWindow 会将activity_login.xml解析成包含用户名输入框、密码输入框、登录按钮等 View 的 View 树,并将其添加到 DecorView 的mContentParent中,同时 DecorView 还会处理标题栏和状态栏的显示逻辑,从而组合成一个完整的登录界面展示给用户。
正是通过 PhoneWindow 和 DecorView 的协同工作,Android 系统实现了 Activity 界面的灵活定制和展示,开发者只需要关注setContentView中设置的内容视图,而无需过多关心底层的窗口创建和布局管理细节,大大提高了开发效率和界面开发的便捷性。
三、WindowManager 详解
3.1 WindowManager 概述
WindowManager 是 Android 系统中一个至关重要的服务,它充当着应用程序与窗口管理器之间交互的接口,为开发者提供了管理窗口的强大功能。从继承关系上看,WindowManager 继承自 ViewManager 接口 ,这使得它具备了 ViewManager 所定义的对 View 进行添加、更新和移除的基本操作能力。
在实际开发中,我们获取 WindowManager 实例的方式非常简单,通常是通过Context.getSystemService(Context.WINDOW_SERVICE)来获取。例如,在一个 Activity 中,我们可以这样获取 WindowManager:
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
通过这种方式获取到的 WindowManager 实例,就像是一把钥匙,能够开启对窗口和 View 的各种操作大门。它不仅可以用于在屏幕上添加自定义的 View,实现诸如悬浮窗、自定义弹窗等特殊效果,还能对已有的 View 进行布局更新,动态调整 View 的大小、位置等属性,以及在不再需要某个 View 时,将其从窗口中移除,释放资源。比如,在实现一个音乐播放器应用时,我们可以利用 WindowManager 创建一个悬浮的音乐控制窗口,让用户在操作其他应用时也能方便地控制音乐的播放、暂停和切换曲目,极大地提升了用户体验。
3.2 WindowManager 的功能方法
3.2.1 addView
addView方法是 WindowManager 中用于添加 View 到 Window 的重要方法,其完整定义为public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params),它接收两个参数:
- view:这是我们要添加到 Window 中的具体视图,它可以是一个简单的 Button、TextView,也可以是一个复杂的自定义 ViewGroup,例如包含多个子 View 的自定义布局。这个 View 将成为 Window 的一部分,并最终显示在屏幕上。
- params:该参数是 ViewGroup.LayoutParams 的实例,实际上在 WindowManager 中,我们通常会使用它的子类WindowManager.LayoutParams 。这个参数用于设置 View 在 Window 中的布局参数,包括 View 的宽度、高度、位置、窗口类型、标志位等重要属性。比如,我们可以通过params.width和params.height来设置 View 的宽高,使用params.x和params.y来指定 View 在屏幕上的坐标位置,通过params.type来设置窗口的类型(如应用窗口、子窗口或系统窗口),还可以通过params.flags来设置一些特殊的标志位,如是否可聚焦、是否可触摸等。
下面以添加一个 Button 到 Window 为例,展示具体的代码实现:
// 获取WindowManager实例
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
// 创建一个Button
Button button = new Button(this);
button.setText("点击我");
// 创建LayoutParams并设置参数
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSPARENT
);
layoutParams.gravity = Gravity.CENTER; // 设置Button在窗口中居中显示
// 将Button添加到Window
windowManager.addView(button, layoutParams);
在上述代码中,我们首先获取了 WindowManager 实例,然后创建了一个 Button,并为其设置了显示文本。接着,创建了WindowManager.LayoutParams对象,设置了 Button 的宽高为包裹内容,窗口类型为应用窗口,标志位为不可聚焦,像素格式为透明,同时通过layoutParams.gravity = Gravity.CENTER将 Button 设置为在窗口中居中显示。最后,调用windowManager.addView(button, layoutParams)将 Button 成功添加到 Window 中。运行这段代码后,我们会在屏幕上看到一个居中显示的 “点击我” 按钮,当用户点击该按钮时,就可以触发相应的点击事件逻辑。
3.2.2 updateViewLayout
updateViewLayout方法用于更新 Window 中已存在 View 的布局参数,其定义为public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params),接收两个参数:
- view:即需要更新布局的目标 View,这个 View 必须是已经通过addView方法添加到 Window 中的。
- params:新的布局参数,同样是ViewGroup.LayoutParams类型,在实际使用中一般为WindowManager.LayoutParams 。通过这个参数,我们可以对 View 的布局进行各种修改,如改变 View 的大小、位置、边距等属性。
例如,当我们想要改变一个已添加的 View 的大小和位置时,可以这样实现:
// 假设已经添加了一个View到Window,并保存了其引用
View myView =...;
// 获取WindowManager实例
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
// 获取当前的LayoutParams
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) myView.getLayoutParams();
// 修改布局参数,比如将宽度设置为屏幕宽度的一半,高度设置为200dp,位置移动到(100, 100)
DisplayMetrics displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
layoutParams.width = displayMetrics.widthPixels / 2;
layoutParams.height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());
layoutParams.x = 100;
layoutParams.y = 100;
// 更新View的布局
windowManager.updateViewLayout(myView, layoutParams);
在这段代码中,首先获取了 WindowManager 实例和已添加 View 的引用。然后通过myView.getLayoutParams()获取当前的布局参数,并进行修改。这里通过DisplayMetrics获取屏幕的宽度,将 View 的宽度设置为屏幕宽度的一半,通过TypedValue.applyDimension方法将 200dp 转换为实际的像素值作为 View 的高度,并设置了 View 的新位置坐标为 (100, 100)。最后,调用windowManager.updateViewLayout(myView, layoutParams)完成对 View 布局的更新。执行这段代码后,原本的 View 就会按照新的布局参数进行显示,大小和位置都发生了相应的改变。
3.2.3 removeView
removeView方法用于从 Window 中移除指定的 View,其定义为public void removeView(View view),只接收一个参数view,即要移除的 View 对象 。这个 View 必须是之前通过addView方法添加到 Window 中的,否则会抛出异常。
在实际使用中,removeView方法有异步和同步移除两种方式 ,这主要取决于内部的实现机制。在WindowManagerGlobal类的removeView方法中,有一个布尔类型的参数immediate来控制移除方式:
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
if (curView == view) {
return;
}
throw new IllegalStateException("Calling with view " + view + " but the ViewAncestor is attached to " + curView);
}
}
当immediate为false时,执行异步移除操作 。在这种情况下,ViewRootImpl的die方法会发送一个请求删除的消息后就立刻返回,此时 View 并没有真正完成删除操作,而是被添加到mDyingViews列表中,表示待删除的 View 列表,等待后续处理。例如,在一些对删除操作实时性要求不高的场景,如在 Activity 销毁时移除一些非关键的悬浮 View,使用异步移除可以避免在主线程中进行耗时的删除操作,保证界面的流畅性。
当immediate为true时,执行同步移除操作 。这种方式会直接调用ViewRootImpl的doDie方法,立即完成 View 的删除操作,包括清理相关资源、移除回调等。比如,在一些需要立即响应 View 移除的场景,如用户点击关闭按钮时,需要立即移除某个提示 View,就可以使用同步移除,确保用户操作的即时反馈。
在实际开发中,我们通常使用removeView(View view)方法进行异步移除,因为它更为常用且能较好地平衡性能和操作的即时性。例如:
// 假设已经添加了一个View到Window,并保存了其引用
View myView =...;
// 获取WindowManager实例
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
// 移除View
windowManager.removeView(myView);
这样就可以将指定的 View 从 Window 中移除,释放相关资源。
3.3 WindowManager 的内部机制
WindowManager 作为一个接口,其具体实现是由WindowManagerImpl类来完成的。然而,WindowManagerImpl并没有直接实现所有的窗口管理功能,而是将大部分实际的功能逻辑委托给了WindowManagerGlobal类,这种设计模式被称为桥接模式,它有效地分离了接口和实现,提高了代码的可维护性和扩展性。
WindowManagerGlobal是一个单例类,在整个进程中只有一个实例 ,它就像是一个中央控制器,维护着与窗口管理相关的各种重要信息。在WindowManagerGlobal内部,主要维护了以下几个关键的集合:
- mViews:类型为ArrayList,用于存储所有添加到 Window 中的 View 对象 。每一个通过addView方法添加的 View 都会被添加到这个集合中,方便进行统一管理和查找。
- mRoots:类型为ArrayList,存储了与每个 View 对应的ViewRootImpl对象 。ViewRootImpl是连接 WindowManager 和 View 的关键纽带,它负责管理 View 的绘制、事件处理等重要操作,通过这个集合,WindowManagerGlobal可以方便地对每个 View 的绘制和更新进行控制。
- mParams:类型为ArrayList<WindowManager.LayoutParams>,用于保存每个 View 对应的布局参数 。这些参数定义了 View 在窗口中的位置、大小、层级等属性,在添加、更新和移除 View 时,都会涉及到对这些参数的操作和管理。
- mDyingViews:类型为ArraySet,用于存储正在被删除或即将被删除的 View 对象 。当执行异步删除操作时,View 会先被添加到这个集合中,等待后续的实际删除操作,避免在删除过程中对其他操作产生干扰。
以addView方法为例,其实现流程如下:
- 参数检查:在WindowManagerGlobal的addView方法中,首先会对传入的参数进行严格检查,确保view不为空,display不为空,并且params是WindowManager.LayoutParams类型。如果参数不合法,会抛出相应的异常,如IllegalArgumentException。
- 布局参数调整:如果添加的是子 Window(即parentWindow不为空),则会调用parentWindow.adjustLayoutParamsForSubWindow(wparams)方法,对布局参数进行相应的调整,以适应子 Window 的特性和需求。如果是普通的应用 Window,则会根据应用的硬件加速设置,为LayoutParams设置FLAG_HARDWARE_ACCELERATED标志位。
- 创建 ViewRootImpl:创建一个新的ViewRootImpl对象,它是 View 的顶级容器,负责管理 View 的绘制、布局和事件处理等核心操作。ViewRootImpl的构造函数接收view.getContext()和display作为参数,用于初始化相关的上下文和显示信息。
- 添加对象到集合:将传入的view、新创建的ViewRootImpl对象以及布局参数wparams分别添加到mViews、mRoots和mParams集合中,建立起 View、ViewRootImpl和布局参数之间的关联,方便后续的管理和操作。
- 完成界面更新和 Window 添加:通过调用ViewRootImpl的setView方法,将 View 绑定到ViewRootImpl上,并完成一系列的界面更新和 Window 添加操作。在setView方法内部,会首先调用requestLayout方法,触发异步渲染流程,通过scheduleTraversals方法安排一次遍历,最终完成 View 的测量、布局和绘制,将 View 显示在屏幕上。
通过这样的内部机制,WindowManager 能够高效地管理窗口和 View,实现复杂的界面展示和交互功能,为 Android 应用的开发提供了坚实的基础。
四、Window 与 WindowManager 的关系
WindowManager 是外界访问 Window 的入口,在 Android 的视图体系中扮演着桥梁的关键角色,连接着应用与底层的窗口管理服务。从本质上讲,WindowManager 为开发者提供了一系列操作 Window 的方法,通过这些方法,我们能够实现对 Window 的创建、显示、更新和删除等核心操作,使得 Window 能够按照我们的需求呈现在用户面前。
Window 和 WindowManager 之间通过 ViewRootImpl 建立起紧密的联系 。ViewRootImpl 是整个视图绘制和事件处理的核心枢纽,它不仅是 View 树的顶级容器,还负责与 WindowManagerService 进行通信,协调 Window 的各种操作。在实际的操作过程中,当我们使用 WindowManager 的addView方法添加一个 View 到 Window 时,背后的实现机制如下:
- 创建 ViewRootImpl:WindowManagerGlobal会创建一个新的ViewRootImpl对象,这个对象将作为 View 与 WindowManager 之间通信的桥梁,负责管理 View 的绘制、布局和事件处理等重要任务。
- 关联 View 与 ViewRootImpl:将需要添加的 View 与新创建的ViewRootImpl进行关联,通过view.setLayoutParams(wparams)方法为 View 设置布局参数,并将 View 添加到WindowManagerGlobal的mViews列表中,同时将ViewRootImpl添加到mRoots列表,将布局参数添加到mParams列表,建立起三者之间的紧密联系。
- 界面更新与绘制:调用ViewRootImpl的setView方法,这个方法是整个添加过程的关键步骤。在setView内部,首先会调用requestLayout方法,触发异步渲染流程,通过scheduleTraversals方法安排一次遍历,这个遍历会依次执行performMeasure、performLayout和performDraw方法,完成 View 的测量、布局和绘制,最终将 View 显示在屏幕上。在这个过程中,ViewRootImpl会与 WindowManagerService 进行交互,完成窗口的添加和相关属性的设置,确保 View 能够正确地显示在对应的 Window 中。
同样,在使用 WindowManager 的updateViewLayout方法更新 View 的布局时,WindowManagerGlobal会找到对应的ViewRootImpl,通过ViewRootImpl的setLayoutParams方法更新布局参数,并调用scheduleTraversals方法重新触发 View 的测量、布局和绘制流程,从而实现 View 布局的更新 。而在执行removeView方法删除 View 时,WindowManagerGlobal会先通过findViewLocked方法找到待删除 View 的索引,然后调用removeViewLocked方法,通过ViewRootImpl的die方法发送删除请求,最终在ViewRootImpl的doDie方法中完成 View 的删除操作,包括清理相关资源、移除回调等,同时将 View 从mViews列表中移除,将ViewRootImpl从mRoots列表移除,将布局参数从mParams列表移除,完成整个删除流程。
通过这样的机制,WindowManager 借助 ViewRootImpl 实现了对 Window 中 View 的高效管理,使得开发者能够方便地操作 Window 和 View,构建出丰富多样的用户界面。
五、WindowManager 的使用场景与案例
5.1 悬浮窗的实现
悬浮窗是一种在手机屏幕上始终显示,且能够悬浮在其他应用界面之上的特殊窗口,它为用户提供了便捷的操作方式,例如音乐播放器的迷你控制窗口、聊天应用的快捷回复窗口等。在 Android 中,使用 WindowManager 创建悬浮窗主要包含以下几个关键步骤:
- 添加权限:首先,需要在 AndroidManifest.xml 文件中添加android.permission.SYSTEM_ALERT_WINDOW权限,以允许应用在其他应用之上绘制悬浮窗。这是创建悬浮窗的基本前提,没有该权限,应用将无法创建悬浮窗。代码如下:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
- 检查并请求权限:在 Android 6.0 及以上版本,除了在 Manifest 中声明权限外,还需要动态请求悬浮窗权限。因为从这个版本开始,系统对应用的权限管理更加严格,用户需要在设置中手动授予悬浮窗权限。在应用启动时,我们可以通过以下代码检查并请求权限:
if (!Settings.canDrawOverlays(this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION);
}
这里的REQUEST_OVERLAY_PERMISSION是自定义的请求码,用于在onActivityResult方法中处理权限请求的结果。
- 创建悬浮窗口:创建一个服务类,例如FloatingWindowService,在服务中利用 WindowManager 将悬浮窗添加到窗口层级中。以下是一个完整的服务类示例:
public class FloatingWindowService extends Service {
private WindowManager windowManager;
private View floatingView;
@Override
public void onCreate() {
super.onCreate();
windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
LayoutInflater inflater = LayoutInflater.from(this);
floatingView = inflater.inflate(R.layout.floating_window, null);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
);
// 设置悬浮窗初始位置
params.gravity = Gravity.TOP | Gravity.LEFT;
params.x = 0;
params.y = 100;
windowManager.addView(floatingView, params);
floatingView.setOnTouchListener((v, event) -> {
// 处理悬浮窗的触摸事件,例如拖动
return false;
});
}
@Override
public void onDestroy() {
super.onDestroy();
if (floatingView != null) {
windowManager.removeView(floatingView);
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
在上述代码中,首先获取 WindowManager 实例,然后通过 LayoutInflater 加载悬浮窗的布局文件R.layout.floating_window。接着创建WindowManager.LayoutParams对象,设置悬浮窗的宽高为包裹内容,窗口类型根据 Android 版本进行设置(Android 8.0 及以上使用TYPE_APPLICATION_OVERLAY,否则使用TYPE_PHONE),标志位设置为不可聚焦,像素格式为透明。通过params.gravity和params.x、params.y设置悬浮窗的初始位置。最后将悬浮窗添加到 WindowManager 中,并为其设置触摸事件监听器,用于处理用户的触摸操作,如拖动悬浮窗。
- 启动服务:在应用中启动悬浮窗口服务,首先检查权限是否已授予,如果已授予则直接启动服务,否则请求权限。代码如下:
if (Settings.canDrawOverlays(this)) {
startService(new Intent(this, FloatingWindowService.class));
} else {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION);
}
- 请求权限后的处理:在onActivityResult方法中处理用户的权限请求结果,如果用户授予了悬浮窗权限,则启动服务,否则提示用户权限未授予。代码如下:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_OVERLAY_PERMISSION) {
if (Settings.canDrawOverlays(this)) {
startService(new Intent(this, FloatingWindowService.class));
} else {
Toast.makeText(this, "Overlay permission is required.", Toast.LENGTH_SHORT).show();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
运行效果展示:当应用成功获取悬浮窗权限并启动服务后,屏幕上会出现一个悬浮的窗口,用户可以自由拖动该窗口,并且它会始终显示在其他应用界面之上,不会被其他应用遮挡,为用户提供了便捷的交互体验,就像常见的音乐播放器悬浮控制窗一样,用户在操作其他应用时也能随时控制音乐的播放状态。
5.2 全屏模式的实现
在 Android 开发中,实现全屏效果是一个常见的需求,特别是在视频播放、游戏等应用场景中,全屏模式能够为用户提供更加沉浸式的体验。利用 WindowManager 实现全屏效果主要通过设置窗口布局参数来实现,具体步骤如下:
- 获取 WindowManager 实例:首先,在需要实现全屏效果的 Activity 或其他 Context 中获取 WindowManager 实例,这是操作窗口的入口。代码如下:
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
- 设置窗口布局参数:创建WindowManager.LayoutParams对象,并设置其属性,将窗口的宽度和高度设置为MATCH_PARENT,表示填充整个屏幕,同时添加FLAG_FULLSCREEN标志位,以实现全屏效果。示例代码如下:
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_FULLSCREEN,
PixelFormat.TRANSLUCENT
);
在上述代码中,MATCH_PARENT确保窗口占据整个屏幕的宽度和高度,TYPE_APPLICATION指定窗口类型为应用窗口,FLAG_FULLSCREEN标志位告诉系统该窗口需要以全屏模式显示,PixelFormat.TRANSLUCENT设置窗口的像素格式为半透明,这在一些需要与底层界面有一定交互或显示效果的场景中比较有用,如果不需要半透明效果,也可以根据需求选择其他像素格式。
- 创建并显示窗口:根据设置好的布局参数创建窗口,并将需要显示的 View 添加到窗口中,然后显示窗口。这里以创建一个简单的红色背景 View 并添加到窗口为例:
View view = new View(this);
view.setBackgroundColor(Color.RED);
windowManager.addView(view, params);
上述代码创建了一个简单的 View,并设置其背景颜色为红色,然后通过windowManager.addView(view, params)将该 View 添加到窗口中,按照之前设置的全屏布局参数进行显示,最终实现全屏显示红色背景的效果。
实现流程图如下:
st=>start: 开始
getWM=>inputoutput: 获取WindowManager实例
createLP=>inputoutput: 创建LayoutParams
setLP=>inputoutput: 设置LayoutParams属性(宽度MATCH_PARENT、高度MATCH_PARENT、添加FLAG_FULLSCREEN标志位)
createView=>inputoutput: 创建View并设置相关属性(如背景颜色)
addView=>inputoutput: 将View添加到WindowManager中
e=>end: 结束
st->getWM->createLP->setLP->createView->addView->e
通过以上步骤和代码示例,我们可以看到使用 WindowManager 实现全屏效果是一个相对简单的过程。但在实际开发中,可能还需要考虑窗口的动画效果、与系统状态栏的交互等问题,以提供更加丰富和流畅的用户体验。例如,在进入全屏模式时,可以添加一个渐变动画,使窗口从非全屏状态平滑过渡到全屏状态,增强视觉效果;同时,还需要处理用户在全屏模式下可能进行的操作,如调出系统状态栏进行截屏、调整亮度等操作,确保用户操作的便捷性和应用的稳定性。
5.3 屏幕常亮的实现
在一些特定的应用场景中,如导航应用、视频播放应用等,需要保持屏幕常亮,以避免因屏幕自动熄灭而影响用户使用。通过 WindowManager 设置屏幕常亮可以通过添加或清除FLAG_KEEP_SCREEN_ON标记来实现,具体实现方法如下:
- 添加屏幕常亮标记:在 Activity 的onCreate方法中,调用getWindow方法获取 Window 对象,然后通过addFlags方法添加WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON标记,即可使屏幕保持常亮。示例代码如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
在上述代码中,getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)这行代码向当前 Activity 的窗口添加了FLAG_KEEP_SCREEN_ON标记,告诉系统在该 Activity 显示期间保持屏幕常亮。这样,当用户打开这个 Activity 时,屏幕将不会因为系统默认的屏幕超时设置而熄灭,直到用户离开该 Activity 或手动关闭屏幕。
- 移除屏幕常亮标记:当不需要屏幕常亮时,例如在 Activity 暂停或销毁时,可以调用clearFlags方法清除FLAG_KEEP_SCREEN_ON标记,使屏幕恢复正常的自动熄灭机制。示例代码如下:
@Override
protected void onPause() {
super.onPause();
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
在onPause方法中,getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)这行代码移除了屏幕常亮标记,当 Activity 进入暂停状态时,屏幕将按照系统默认设置在一段时间无操作后自动熄灭,从而节省电量。
通过以上简单的代码示例,我们可以方便地通过 WindowManager 实现屏幕常亮和恢复正常熄灭的功能,满足不同应用场景下对屏幕显示状态的需求。但在实际应用中,需要注意电量管理和用户体验的平衡,因为长时间保持屏幕常亮会消耗更多电量,在不需要屏幕常亮时应及时移除标记,避免不必要的电量消耗,同时也要确保在用户需要屏幕常亮时,能够及时有效地进行设置,提供良好的用户体验。
六、总结
在 Android 开发中,Window 和 WindowManager 是构建用户界面的重要基石,它们的核心要点贯穿于整个应用开发过程。Window 作为一个抽象概念,承载着 View 的展示,其丰富的类型体系 —— 应用 Window、子 Window 和系统 Window,依据不同的层级范围和特性,为开发者提供了多样化的界面展示方式。应用 Window 对应 Activity,是应用交互的主要窗口;子 Window 依附于父 Window,用于展示临时性信息;系统 Window 则实现系统级功能,如 Toast 和状态栏等。PhoneWindow 作为 Window 的具体实现类,通过 DecorView 将我们设置的内容视图与系统装饰元素相结合,完成了 Activity 界面的构建。
WindowManager 则是管理 Window 的关键服务,它通过继承 ViewManager 接口,为开发者提供了addView、updateViewLayout和removeView等强大功能方法。这些方法允许我们动态地添加、更新和移除 View,实现诸如悬浮窗、全屏模式等复杂的界面效果。在内部机制上,WindowManagerImpl 将大部分功能委托给单例的 WindowManagerGlobal,后者通过维护mViews、mRoots、mParams和mDyingViews等集合,实现了对 View 和 Window 的高效管理,同时借助 ViewRootImpl 完成了 View 的绘制、布局和事件处理等核心操作。