00-android车载入门

如何选择页面展示的屏幕

  1. Presentation – 指定display的Dialog
1
2
3
4
5
public class Presentation extends Dialog {
public Presentation(Context outerContext, Display display) {
this(outerContext, display, 0);
}
}
  • 本质是dialog

  • 传入display

  • Presentation 的上下文与其宿主 Activity 的上下文相互独立。加载布局、资源时必须使用 Presentation 自身的上下文进行布局填充与资源加载,才能保证加载出适配目标显示器尺寸、屏幕密度的资源素材。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public Presentation(@NonNull Context outerContext, @NonNull Display display, int theme,
@WindowType int type) {
super(createPresentationContext(outerContext, display, theme, type), theme, false);

mDisplay = display;
mDisplayManager = getContext().getSystemService(DisplayManager.class);

final Window w = getWindow();
final WindowManager.LayoutParams attr = w.getAttributes();
w.setAttributes(attr);
w.setGravity(Gravity.FILL);
w.setType(getWindowType(type, display));
setCanceledOnTouchOutside(false);
}

private static Context createPresentationContext(
Context outerContext, Display display, int theme, @WindowType int type) {
if (outerContext == null) {
throw new IllegalArgumentException("outerContext must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}

Context windowContext = outerContext.createDisplayContext(display)
.createWindowContext(getWindowType(type, display), null /* options */);
if (theme == 0) {
TypedValue outValue = new TypedValue();
windowContext.getTheme().resolveAttribute(
com.android.internal.R.attr.presentationTheme, outValue, true);
theme = outValue.resourceId;
}
return new ContextThemeWrapper(windowContext, theme);
}
  • 当 Presentation 所依附的显示器被移除时,Presentation 会自动关闭(对应 Dialog.cancel())。宿主 Activity 在自身执行暂停 / 恢复生命周期时,需要自行控制 Presentation 内部正在播放的内容,同步执行暂停、恢复操作。
  1. ActivityOptions – 启动Activity时指定Display
1
2
3
4
5
6
7
8
@RequiresApi(Build.VERSION_CODES.O)
fun startActivity(context: Context, clz: Class<Activity>, displayId: Int) {
val activityOptions = ActivityOptions.makeBasic()
activityOptions.launchDisplayId = displayId

val intent = Intent(context, clz)
context.startActivity(intent, activityOptions.toBundle())
}
  1. adb am start -n <activity> --display

display的获取

通过媒体路由(MediaRouter)选择演示显示器

最简单的方式是使用 MediaRouter API。媒体路由服务会持续跟踪系统内可用的音视频路由。当路由选中、取消选中,或是某条路由的首选演示显示器发生变更时,媒体路由会发出通知。应用只需监听这类通知,即可自动在首选演示显示器上拉起或关闭 Presentation。
首选演示显示器:媒体路由推荐、应用可用来展示内容的副屏。若不存在首选演示显示器,应用应当直接在本机屏幕展示内容,无需启用 Presentation。
下面示例借助 android.media.MediaRouter.RouteInfo.getPresentationDisplay(),通过媒体路由在首选演示显示器上创建并展示 Presentation:

1
2
3
4
5
6
7
8
9
MediaRouter mediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_VIDEO);
if (route != null) {
Display presentationDisplay = route.getPresentationDisplay();
if (presentationDisplay != null) {
Presentation presentation = new MyPresentation(context, presentationDisplay);
presentation.show();
}
}

ApiDemos 提供示例代码,演示如何监听设备状态:有可用演示显示器时切换到 Presentation 展示,无可用屏幕时回退至主 Activity 界面展示内容。

直接调用 DisplayManager API。

显示管理器会记录系统全部显示器,但并非所有显示器都适合承载 Presentation。例如:如果尝试在主屏弹出 Presentation,会覆盖应用自身界面(相当于在 Activity 上层弹出对话框)。在主屏创建 Presentation 并调用 show () 时,会抛出 WindowManager.InvalidDisplayException 异常。
下方代码使用 DisplayManager.getDisplays(String) 配合分类常量 DisplayManager.DISPLAY_CATEGORY_PRESENTATION,筛选可用的演示显示器:

1
2
3
4
5
6
7
8
9
DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
Display[] presentationDisplays = displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
if (presentationDisplays.length > 0) {
// 如果存在多个可用演示显示器,可以提供选择给用户。
// 本例直接选取第一个显示器,也就是系统推荐的首选演示屏幕。
Display display = presentationDisplays[0];
Presentation presentation = new MyPresentation(context, presentationDisplay);
presentation.show();
}

WMS 四大核心容器:层级关系 + 作用(Android 12+ 通用容器树模型)

所有类全部继承自 WindowContainer,WMS 使用树形层级管理全部窗口;
先给最简层级链路(简化版,省略中间 DisplayArea/Task 分支):

1
2
3
4
5
RootWindowContainer(根)
↳ DisplayContent(一块屏幕)
↳ DisplayArea.Tokens
↳ WindowToken(窗口分组)
↳ WindowState(单个窗口实例)

补充:应用 Activity 窗口走另一条分支:
DisplayContent → TaskDisplayArea → Task → ActivityRecord(AppWindowToken) → WindowState

1. RootWindowContainer

定位:全局窗口树唯一根节点,WMS 内部单例
作用
管理系统所有屏幕:持有全部 DisplayContent,按 displayId 区分主屏、副屏、虚拟屏、Presentation 投屏屏;
全局统一调度:焦点分发、屏幕旋转、配置变更、壁纸管理、多屏之间交互协调;
对外提供查询入口:根据 displayId 查找对应 DisplayContent;
监听 DisplayManager 屏幕插拔事件,动态创建 / 销毁 DisplayContent。
关键特点
WMS 初始化时创建唯一实例;
子节点只能是 DisplayContent;
多屏场景所有 DisplayContent 的父容器。

2. DisplayContent

定位:代表一块逻辑显示屏(物理屏 / 虚拟屏 / Presentation 副屏),一屏一个实例
作用
隔离屏幕:不同 DisplayContent 的窗口互相独立,Z 序、布局、动画互不干扰(多屏核心基础);
维护当前屏幕属性:分辨率、density、旋转角度、cutout、显示模式;
内部划分多个 DisplayArea:
TaskDisplayArea:承载普通 App Activity 窗口栈(Task/ActivityRecord)
DisplayArea.Tokens:承载非 Activity 窗口:输入法、壁纸、状态栏、Dialog、Presentation
维护本屏幕内所有 WindowToken 映射表;
执行单屏内布局计算、窗口动画、输入命中查找。
和你前面代码关联:
setLaunchDisplayId(int displayId) 本质就是指定 Activity 投放至哪个 DisplayContent。
Presentation 内部窗口最终也归属副屏对应的 DisplayContent。

3. WindowToken

定位:窗口分组容器,一组同源窗口的 “令牌”,逻辑分组层
作用
分组管理多个 WindowState:同一个 Token 下的窗口具备相同归属、生命周期;
依靠 IBinder token 作为跨进程唯一标识;
两大分支:
AppWindowToken(子类 = ActivityRecord):一个 Activity 对应一个 AppWindowToken。
Activity 主窗口、Dialog、PopupWindow 全部归属同一个 AppWindowToken;Activity 销毁 → Token 销毁 → 所有窗口清理。
普通 WindowToken:不属于 Activity 的窗口组:输入法窗口、壁纸、系统弹窗、Presentation 窗口。
权限与生命周期控制:Token 销毁时,自动清理旗下全部 WindowState。
通俗理解
Token = “户口本”,一个户口本下可以有多个窗口(WindowState)。

4. WindowState

定位:WMS 服务端一个具体窗口实例 **,窗口最小管理单元 **
作用
一一对应应用侧 Window(PhoneWindow、Dialog 窗口、Presentation 窗口);
保存窗口全部属性:位置、大小、Z-order、窗口类型、flag、Insets、Surface、可见性;
和 SurfaceControl 绑定,最终交给 SurfaceFlinger 图层合成;
处理窗口添加、移除、布局、动画、焦点、输入点击命中;
父容器一定是某个 WindowToken。
通俗理解
WindowState = “户口本里单独一个人”,对应屏幕上真实渲染的一块窗口图层。

完整关系总结

RootWindowContainer:总管所有屏幕
└── DisplayContent:单块屏幕(主屏 / 外接副屏 / Presentation 投屏屏)
├── TaskDisplayArea → Task → ActivityRecord (AppWindowToken) → WindowState(Activity、Activity 内 Dialog)
└── DisplayArea.Tokens → WindowToken → WindowState(输入法、壁纸、Presentation、系统 Dialog)

Window / Surface / ViewRootImpl / DecorView 完整关系梳理

DecorView 是根 View;ViewRootImpl 是连接 View 树与 Window/Surface/WMS 的桥梁;Window 是窗口抽象;Surface 是图形缓冲区。
层级从上到下(应用进程客户端)

1
2
3
4
5
6
7
Window(PhoneWindow)
└── DecorView(FrameLayout,整个窗口最顶层View)
└── 我们写的布局(setContentView加载进来)
ViewRootImpl
←→ DecorView(setView()建立绑定)
←→ WindowManagerService(跨进程Binder通信)
←→ Surface(绘图缓冲区,最终交给SurfaceFlinger)

逐个类作用详解

  1. Window(窗口抽象,实现类 PhoneWindow)
    定位:逻辑窗口管理器,承载窗口策略、样式、主题
    每个 Activity 默认持有一个 PhoneWindow(Window 唯一实现类)
    职责:
    持有 DecorView;
    管理窗口特征:Title、ActionBar、WindowFlags、软键盘模式;
    setContentView() 最终委托 PhoneWindow 完成布局加载,把布局添加到 DecorView 的内容区域;
    ❗ Window 不是 View,也不负责绘制,只是容器策略对象。

  2. DecorView
    定位:整个窗口的根 View,FrameLayout 子类
    由 PhoneWindow 创建:installDecor()
    结构固定:
    DecorView 内部包含:
    标题栏区域(TitleBar/ActionBar)
    android.R.id.content(内容容器,setContentView 的布局就放进这里)
    特点:
    DecorView 在没有被 ViewRootImpl 绑定前,无法真正参与绘制、无法接收事件。
    Dialog、Presentation 内部同样拥有自己独立 PhoneWindow + DecorView。

  3. ViewRootImpl
    重中之重:View 体系与窗口系统的桥梁(中介)
    关键时机:

1
2
3
wm.addView(decorView, layoutParams)
→ new ViewRootImpl(display)
→ viewRoot.setView(decorView, ...)

作用:
建立 DecorView 和 WMS 的跨进程连接
通过 IWindow Binder 对象,WMS 服务端对应 WindowState。
创建 Surface,管理 Surface 生命周期
请求 WMS 分配 Surface,Surface 是一块图形缓冲区,所有 View 绘制最终输出到 Surface。
驱动 View 绘制三大流程:测量→布局→绘制(performTraversals)
分发触摸事件、窗口事件(尺寸变化、旋转、Insets 变化)
控制窗口参数更新、添加 / 移除窗口
关键结论:一个窗口(Window)对应唯一 ViewRootImpl。
Activity 窗口、Dialog、Presentation 各自独立 ViewRootImpl。

  1. Surface
    定位:图形渲染缓冲区(生产者)
    不是 View,和 View 体系无关,属于图形栈层
    ViewRootImpl 向 WMS 申请得到 Surface;
    Canvas 绘制、OpenGL 渲染最终都输出到 Surface;
    SurfaceFlinger 消费多个 Surface,合成后输出到屏幕(对应前面 WMS 的 DisplayContent 屏幕)。
    一个窗口通常对应一个 Surface(普通窗口);SurfaceView、TextureView 会额外拥有独立 Surface。
    完整启动链路(Activity 标准流程)
    Activity.attach() → new PhoneWindow
    window.setContentView ()
    → PhoneWindow.installDecor () 创建 DecorView
    → 将业务布局 add 进 DecorView 的 content 容器
    Activity.makeVisible()
    → WindowManager.addView(DecorView, lp)
    WindowManagerGlobal 创建 ViewRootImpl,调用 viewRoot.setView(DecorView)
    ViewRootImpl.setView:
    通过 Binder 调用 WMS,注册窗口,服务端生成 WindowToken + WindowState
    请求分配 Surface
    触发第一次 performTraversals,开始测量布局绘制
    几组极易混淆的边界区分

  2. Window vs DecorView
    Window:逻辑窗口管理对象(策略、Flag、主题)
    DecorView:View 树根节点,真正承载 UI 视图

  3. DecorView vs ViewRootImpl
    DecorView:UI 控件;
    ViewRootImpl:控制器、桥梁,本身不是 View,持有 DecorView 引用。

  4. ViewRootImpl vs Surface
    ViewRootImpl 管理 Surface 生命周期;
    Surface 只是一块画布缓冲区。
    结合你之前关心的 Presentation 场景补充
    Presentation 本质是一个独立 Dialog:
    拥有独立 PhoneWindow、独立 DecorView
    addView 时新建独立 ViewRootImpl
    在目标副屏 DisplayContent 下创建 WindowToken / WindowState
    拥有独立 Surface,输出到外接显示屏
    和 Activity 使用 setLaunchDisplayId 启动到副屏,两套窗口载体,但底层都走这套客户端架构。
    极简记忆口诀
    PhoneWindow 管窗口样式;
    DecorView 是视图树根;
    ViewRootImpl 连通 WMS、驱动绘制;
    Surface 是绘图画布缓冲区。

作者

Meow Meow Liu

发布于

2024-03-13

更新于

2026-07-24

许可协议

评论