27、Launcher的Overlay(悬浮层):GameMode与浮动窗口、分屏模式的触发、悬浮小部件
各位好,今天我们来聊聊Launcher里一个很有意思的话题——Overlay,也就是悬浮层。
说实话,我刚开始做Launcher开发时,对Overlay的理解很浅。觉得不就是个浮在桌面上的View嘛,有什么难的?后来踩了不少坑才明白,Overlay的设计直接关系到整个桌面的交互流畅度和系统稳定性。你想想看,用户每天在桌面上划来划去,如果悬浮层处理不好,轻则卡顿,重则ANR,那体验可就全毁了。
Overlay的本质:一个特殊的Window
在Android系统里,Overlay本质上是一个WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY类型的窗口。它悬浮在所有Activity之上,但又不同于系统级的Toast或Dialog。
我个人习惯把Overlay分为三类:
- 系统级Overlay:比如状态栏、导航栏,由SystemUI管理
- 应用级Overlay:比如Launcher的GameMode悬浮窗、分屏触发把手
- 小部件Overlay:比如桌面上的时钟小部件、天气小部件
我们今天重点讲的是第二类和第三类,也就是Launcher自己管理的Overlay。
核心要点:Launcher的Overlay不是随便new一个View就往上丢的。它需要遵循Launcher自身的窗口层级管理,否则会出现触摸事件冲突、焦点抢占等问题。
GameMode与浮动窗口
先说说GameMode。这个功能在手机厂商的定制ROM里很常见,比如游戏时自动屏蔽手势、显示帧率、开启免打扰等。
在Launcher层面,GameMode的Overlay其实是一个半透明的悬浮面板。它通常包含几个元素:
- 游戏帧率显示
- 免打扰开关
- 性能模式切换
- 快捷截图/录屏按钮
我在项目中遇到过一个问题:游戏启动时,Launcher的GameMode Overlay和游戏自身的UI发生了触摸事件冲突。用户点击游戏内的按钮,结果触发了Overlay上的关闭按钮。排查下来发现是Overlay的WindowManager.LayoutParams设置了FLAG_NOT_TOUCH_MODAL,但没有正确处理事件传递。
正确的做法是这样的:
// 创建GameMode悬浮窗
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
PixelFormat.TRANSLUCENT
);
// 关键:设置触摸事件监听
mOverlayView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
// 触摸事件发生在Overlay之外,交给下层处理
return false;
}
// 处理Overlay内部的触摸
return handleOverlayTouch(event);
});
避坑指南:我曾经在GameMode Overlay上忘记设置FLAG_WATCH_OUTSIDE_TOUCH,结果导致用户点击游戏界面时,Overlay收不到任何事件回调,也就无法自动隐藏。嗯,这个坑我记了好几年。
分屏模式的触发
分屏模式是Android多任务的核心功能之一。在Launcher中,分屏的触发通常有两种方式:
- 长按最近任务卡片:弹出分屏选项
- 拖拽应用图标到屏幕顶部:直接进入分屏
这里涉及到一个关键的Overlay——分屏触发把手。当用户长按任务卡片时,Launcher会在屏幕顶部显示一个半透明的拖拽区域,这就是一个Overlay。
它的实现逻辑其实不复杂:
// 显示分屏触发把手
private void showSplitScreenHandle() {
View handleView = LayoutInflater.from(mContext)
.inflate(R.layout.split_screen_handle, null);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
dpToPx(48), // 把手高度48dp
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
PixelFormat.TRANSLUCENT
);
// 把手显示在屏幕顶部
params.gravity = Gravity.TOP;
params.y = 0;
mWindowManager.addView(handleView, params);
}
但这里有个细节:把手Overlay不能挡住状态栏。我见过有些ROM的实现,把手直接盖住了状态栏,用户想下拉通知栏都点不到。说白了,这就是Overlay的层级没算对。
注意:分屏触发把手的Overlay必须设置FLAG_LAYOUT_IN_SCREEN,但不要设置FLAG_LAYOUT_NO_LIMITS,否则会超出屏幕边界。另外,把手的高度建议不超过48dp,这是Material Design的推荐值。
悬浮小部件
悬浮小部件是Launcher Overlay里最灵活的一种。它不像GameMode那样固定位置,也不像分屏把手那样临时出现。悬浮小部件可以随意拖动,甚至可以跨桌面停留。
实现悬浮小部件的核心是处理拖拽事件和位置持久化。我给大家看一个简化版的实现:
public class FloatingWidget extends FrameLayout {
private int mLastX, mLastY;
private boolean isDragging;
public FloatingWidget(Context context) {
super(context);
init();
}
private void init() {
// 设置触摸监听
setOnTouchListener((v, event) -> {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = (int) event.getRawX();
mLastY = (int) event.getRawY();
isDragging = false;
break;
case MotionEvent.ACTION_MOVE:
int dx = (int) event.getRawX() - mLastX;
int dy = (int) event.getRawY() - mLastY;
// 更新Overlay位置
WindowManager.LayoutParams params =
(WindowManager.LayoutParams) getLayoutParams();
params.x += dx;
params.y += dy;
mWindowManager.updateViewLayout(this, params);
mLastX = (int) event.getRawX();
mLastY = (int) event.getRawY();
isDragging = true;
break;
case MotionEvent.ACTION_UP:
if (isDragging) {
// 保存位置到SharedPreferences
savePosition(getLayoutParams().x, getLayoutParams().y);
} else {
// 执行点击操作
performClick();
}
break;
}
return true;
});
}
}
这里有个容易忽略的点:拖拽和点击的区分。用户可能只是想点一下小部件,结果因为手指轻微移动就被判定为拖拽了。我建议设置一个阈值,比如移动超过10像素才认为是拖拽,否则视为点击。
Overlay的生命周期管理
Overlay不是创建了就完事了。它需要跟随Launcher的生命周期进行管理。比如:
- Launcher进入后台时,Overlay应该自动隐藏
- Launcher回到前台时,Overlay恢复显示
- 配置变化(如横竖屏切换)时,Overlay需要重新计算位置
我见过最糟糕的实现是:Overlay创建后就不管了,用户切换了桌面主题,Overlay还停留在原来的位置,结果和新的桌面布局重叠在一起。嗯,这种问题在用户反馈里特别常见。
最佳实践:在Launcher的onPause()中移除Overlay,在onResume()中重新创建。如果Overlay状态需要保持,可以序列化到SharedPreferences中。
知识体系总览
下面这张图是我自己整理的Launcher Overlay知识结构,方便大家理解各个模块之间的关系:
从这张图可以看出,Overlay管理的核心就是三个维度:窗口类型、触摸事件、生命周期。不管你是做GameMode、分屏还是小部件,最终都要回到这三个基础问题上来。
总结
Launcher的Overlay说难不难,说简单也不简单。关键是要理解它在整个窗口体系中的位置,以及它和用户触摸事件的交互方式。我个人建议,如果你刚开始接触这块,可以先从悬浮小部件入手,因为它逻辑最独立,不容易影响到其他模块。等熟悉了Overlay的基本套路,再去做GameMode和分屏触发,会顺手很多。
好了,今天就聊到这里。如果你在实际开发中遇到了Overlay相关的问题,欢迎随时交流。
公众号:蓝海资料掘金营,微信deep3321