17、车载窗口系统定制:WindowManagerService定制、多窗口模式、分屏与画中画、窗口动画与过渡效果
车载系统的窗口管理,说白了就是屏幕上的“地盘划分”。
我刚开始接触车载项目时,以为窗口系统就是Android原生的那一套。后来发现,车机上多屏交互、分屏、画中画的需求,远比手机复杂得多。今天我们就来聊聊,怎么把WMS(WindowManagerService)玩出花来。
17.1 WindowManagerService定制:从底层掌控窗口
WMS是Android窗口系统的核心。它管理着所有窗口的创建、显示、层级、焦点等。车载场景下,我们经常需要定制WMS,比如限制某些应用的窗口大小、强制全屏、或者调整窗口层级。
核心思路:通过修改WMS的窗口策略(WindowPolicy)或直接注入自定义逻辑,来控制窗口行为。
17.1.1 定制窗口策略
WMS中的WindowManagerPolicy接口,定义了窗口的布局、焦点、按键分发等策略。我们可以继承PhoneWindowManager,重写关键方法。
// 自定义窗口策略
public class CarWindowPolicy extends PhoneWindowManager {
@Override
public int getWindowLayerFromTypeLw(int type) {
// 强制系统窗口(如导航栏)始终在最上层
if (type == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR) {
return 30; // 比默认更高
}
return super.getWindowLayerFromTypeLw(type);
}
@Override
public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
// 强制所有应用窗口全屏,去除状态栏区域
if (attrs.type == WindowManager.LayoutParams.TYPE_APPLICATION) {
attrs.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
}
}
}
嗯,这里要注意:修改窗口层级时,一定要考虑系统UI的可见性。我曾经在项目中把导航栏层级调得太高,结果导致弹窗被导航栏挡住,用户点不到“确定”按钮。后来加了个条件判断,只在特定场景下提升层级。
17.1.2 注入自定义窗口
有时候我们需要在系统启动时,就显示一个自定义窗口(比如车载Logo或安全提示)。可以通过WMS的addWindow方法直接注入。
// 在SystemServer中注入自定义窗口
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
params.gravity = Gravity.CENTER;
wm.addView(yourCustomView, params);
警告:注入系统窗口时,务必设置FLAG_NOT_FOCUSABLE,否则会抢走焦点,导致用户无法操作其他应用。
17.2 多窗口模式:车机上的“多任务”艺术
车载场景下,多窗口模式非常实用。比如导航和音乐同时显示,或者地图和车辆设置分屏。Android原生支持多窗口,但车机上需要更精细的控制。
17.2.1 启用多窗口模式
在config.xml中启用多窗口支持:
<!-- 在 frameworks/base/core/res/res/values/config.xml 中 -->
<bool name="config_supportsMultiWindow">true</bool>
<bool name="config_supportsSplitScreenMultiWindow">true</bool>
但仅仅开启还不够。车机上,我们通常希望某些应用(如导航)始终占据固定区域,不允许用户随意调整。这就需要定制ActivityManagerService中的多窗口策略。
17.2.2 限制窗口大小和位置
我建议在WindowManagerService中,重写getDefaultDisplayInfo或setForcedDisplaySize,来限制分屏时每个窗口的可用区域。
// 强制分屏时,左侧窗口占60%,右侧占40%
public void setSplitScreenBounds(int leftWidth, int rightWidth) {
DisplayInfo info = new DisplayInfo();
mDisplayManager.getDisplayInfo(Display.DEFAULT_DISPLAY, info);
int totalWidth = info.logicalWidth;
int left = 0;
int right = (int) (totalWidth * 0.6);
// 更新窗口边界
mSplitScreenBounds.set(left, 0, right, info.logicalHeight);
}
避坑指南:我曾经在调整分屏边界时,忘记考虑导航栏的高度,结果右侧窗口底部被导航栏遮挡。后来统一用getStableInsets获取系统UI占用的区域,再计算可用空间。
17.3 分屏与画中画:车载场景的“黄金搭档”
分屏和画中画(PiP)是车载窗口系统的两大核心功能。分屏适合同时显示两个应用,画中画则适合视频或导航的小窗显示。
17.3.1 分屏模式定制
分屏模式下,我们需要处理两个关键问题:
- 应用兼容性:不是所有应用都支持分屏。可以在
AndroidManifest.xml中为应用设置android:resizeableActivity="true"。 - 窗口切换:用户点击分屏按钮时,需要弹出应用选择器。可以定制
RecentTasksActivity,只显示支持分屏的应用。
// 在RecentTasksActivity中过滤应用
List<ActivityManager.RecentTaskInfo> tasks = activityManager.getRecentTasks(...);
List<ActivityManager.RecentTaskInfo> filteredTasks = new ArrayList<>();
for (ActivityManager.RecentTaskInfo task : tasks) {
if (isResizeableTask(task)) {
filteredTasks.add(task);
}
}
17.3.2 画中画模式定制
画中画在车机上常用于导航或视频。Android原生支持PiP,但车机上需要调整窗口大小和位置。
// 在Activity中进入画中画模式
public void enterPictureInPictureMode() {
PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder();
// 设置画中画窗口的宽高比
Rational aspectRatio = new Rational(16, 9);
builder.setAspectRatio(aspectRatio);
// 设置源矩形(可选)
builder.setSourceRectHint(new Rect(0, 0, 200, 200));
enterPictureInPictureMode(builder.build());
}
个人经验:画中画窗口的位置,我建议固定在屏幕右下角,避免遮挡导航地图的关键信息。可以在WindowManagerService中重写getPictureInPictureBounds,强制设置固定位置。
17.4 窗口动画与过渡效果:让交互更流畅
窗口动画直接影响用户体验。车机上,动画不仅要流畅,还要符合驾驶场景——不能太花哨,避免分散驾驶员注意力。
17.4.1 定制窗口动画
Android的窗口动画定义在WindowManagerService的mAnimator中。我们可以通过修改WindowStateAnimator来定制动画效果。
// 自定义窗口打开动画
public class CarWindowAnimator extends WindowStateAnimator {
@Override
public void applyAnimationLocked(int transit, boolean enter) {
if (enter) {
// 使用淡入动画
Animation anim = AnimationUtils.loadAnimation(mContext, R.anim.car_fade_in);
setAnimation(anim);
} else {
// 使用淡出动画
Animation anim = AnimationUtils.loadAnimation(mContext, R.anim.car_fade_out);
setAnimation(anim);
}
}
}
17.4.2 过渡效果优化
过渡效果包括Activity切换、窗口打开/关闭等。我建议使用Transition框架,实现平滑的共享元素过渡。
// 在Activity中设置过渡效果
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置进入和退出过渡
getWindow().setEnterTransition(new Fade());
getWindow().setExitTransition(new Fade());
// 设置共享元素过渡
getWindow().setSharedElementEnterTransition(new ChangeBounds());
}
注意:车机上不要使用过于复杂的动画(如3D旋转、缩放等),容易导致帧率下降。我建议使用简单的淡入淡出或滑动,动画时长控制在200-300ms之间。
17.5 知识体系总览
下面这张图,概括了本章的核心内容:
17.6 实战建议与避坑指南
最后,分享几个我在项目中踩过的坑:
- 窗口层级冲突:多个系统窗口(如导航栏、状态栏、弹窗)层级重叠时,容易导致触摸事件穿透。我建议统一管理窗口层级,使用
WindowManager.LayoutParams.TYPE常量,不要随意自定义。 - 分屏时应用崩溃:有些应用没有适配分屏,强行分屏会导致崩溃。可以在
ActivityManagerService中捕获ActivityNotFoundException,并回退到全屏模式。 - 画中画窗口闪烁:画中画窗口在切换时,偶尔会出现闪烁。这是因为窗口动画和PiP动画冲突。我建议在进入PiP时,禁用窗口动画。
个人习惯:每次修改WMS后,我都会用dumpsys window命令检查窗口层级和状态。这个命令能帮你快速定位问题,强烈推荐。
好了,关于车载窗口系统定制,今天就聊到这里。记住,窗口系统的核心是“平衡”——平衡多应用的需求,平衡动画的流畅与安全,平衡定制的灵活与稳定。希望这些内容对你有帮助。
公众号:蓝海资料掘金营,微信deep3321