一、需求描述
基于 Android 11 平台,下拉状态栏的背景实现毛玻璃(Frosted Glass)效果,通知和控制中心的背景实现实时背景模糊效果。
二、需求分析
在 Android 的 SystemUI(状态栏、下拉通知栏、音量面板等)开发中,实现高斯模糊(Blur)是一个非常经典且复杂的话题。
在 Android 11 (API Level 30) 上实现高斯模糊(Gaussian Blur)与 Android 12+ 有很大不同。Android 12 引入了原生的 RenderEffect 和窗口模糊(Window Blur)API,但这些在 Android 11 上都无法直接使用。
使用 RenderScript,配合"缩小-模糊-放大"的技巧,这是 Android 11 时代的标准做法,对于 Android 11 及更低版本,RenderScript 仍然是性能最好且兼容性最强的原生方案。
Android 11 不支持 setRenderEffect 或 setBlurBehindRadius(毛玻璃窗体),这些代码只能在 Build.VERSION.SDK_INT >= 31 (Android 12) 时运行,否则会崩溃。
三、实现方案
截图 + RenderScript (传统高性能损耗方案):这是早期 SystemUI(如 Android 5.0 - 8.0 时代)和部分第三方 ROM 的做法。
- 监听下拉事件:在 NotificationPanelView 的 onLayout 或触摸事件中。
- 截取屏幕:使用 SurfaceControl.screenshot() 截取当前屏幕内容。
- 模糊处理:使用 RenderScript 处理 Bitmap。
- 设置背景:将模糊后的 Bitmap 设为 NotificationPanel 的背景。
内存开销大,无法做到 60fps 丝滑跟随手指。
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
先定义一个背景更新方法 updatePanelBackground
java
private void updatePanelBackground() {
if (mBarState == StatusBarState.SHADE) {
// 如果是固定的 drawable 背景可以直接使用接口
//mView.setBackgroundResource(R.drawable.panel_background);
if(mPanelExpanded){
//captureScreenshotBitmap 是用于截图并生成高斯模糊背景的方法
mView.setBackground(new BitmapDrawable(mView.getResources(), captureScreenshotBitmap()));
} else {
mView.setBackground(null);
}
} else {
mView.setBackgroundColor(Color.TRANSPARENT);
}
}
然后在三个场景中用于更新背景时调用,一是界面初始化时候
java
private class OnAttachStateChangeListener implements View.OnAttachStateChangeListener {
@Override
public void onViewAttachedToWindow(View v) {
...
updatePanelBackground();
}
}
二是状态改变时候,如:解锁状态 StatusBarState.SHADE、锁屏状态 StatusBarState.KEYGUARD
java
private class StatusBarStateListener implements StateListener {
@Override
public void onStateChanged(int statusBarState) {
...
updatePanelBackground();
}
}
三是下拉状态栏或收起时
java
private void updatePanelExpanded() {
boolean isExpanded = !isFullyCollapsed() || mExpectingSynthesizedDown;
if (mPanelExpanded != isExpanded) {
...
updatePanelBackground();
}
}
在 captureScreenshotBitmap 时直接传入缩小的宽高 (targetW, targetH) 给 SurfaceControl.screenshot,而不是截全屏后再 createScaledBitmap,这能提升 5-10 倍的性能。
java
private Bitmap captureScreenshotBitmap() {
// 获取真实屏幕尺寸
mDisplay.getRealMetrics(mDisplayMetrics);
int displayWidth = mDisplayMetrics.widthPixels;
int displayHeight = mDisplayMetrics.heightPixels;
// 修正:确保宽高为正数且合理
if (displayWidth <= 0 || displayHeight <= 0) return null;
// 1. 缩小比例 (Sampling):为了性能,建议缩小 4-8 倍
// 缩小倍数越大,模糊越快,且模糊半径即使很小看起来也很大
float scaleFactor = 0.25f; // 相当于缩小 4 倍 (1/4)
int targetW = (int) (displayWidth * scaleFactor);
int targetH = (int) (displayHeight * scaleFactor);
// 2. 截取屏幕
// 注意:SurfaceControl.screenshot 在不同 Android 版本参数略有不同,
// 这里假设你的 sourceCrop 是全屏
Rect sourceCrop = new Rect(0, 0, displayWidth, displayHeight);
// 使用 screenshot 尝试截取原始屏幕,并进行缩小
Bitmap screenshot = SurfaceControl.screenshot(sourceCrop, targetW, targetH, mDisplay.getRotation());
if (screenshot != null) {
// 因为 screenshot 已经是缩小后的了 (targetW, targetH),
// 所以不需要再调用 createScaledBitmap 了,直接去模糊
// 这样节省了一次 createBitmap 的巨大开销
// 执行模糊
return blurAndDarkenBitmap(mView.getContext(), screenshot, 15, 0.3f);
// 注意:因为图片缩小了,模糊半径(Radius)也要相应减小,不然会糊成一团
}
return null;
}
使用 RenderScript实现 高斯模糊并通过 Canvas 绘制遮罩降低亮度
为了让背景上的白色文字清晰可见,我们需要在高斯模糊的基础上叠加一层半透明的黑色遮罩(或者直接降低图片亮度)。最简单且性能开销最小的方法是在模糊完成后,利用 Canvas 在图片上画一层黑色的半透明蒙版。
java
public static Bitmap blurAndDarkenBitmap(Context context, Bitmap inputBitmap, float radius, float brightnessFactor) {
if (inputBitmap == null) return null;
// 1. 准备 inputBitmap
// 如果 inputBitmap 是不可变的(Hardware/Immutable),必须复制一份 Config.ARGB_8888
// 如果本来就是 Software 且 Mutable,直接复用可以省内存,但为了安全通常还是 Copy 一份
Bitmap outputBitmap;
if (inputBitmap.isMutable() && inputBitmap.getConfig() == Bitmap.Config.ARGB_8888) {
outputBitmap = inputBitmap; // 直接复用
} else {
outputBitmap = inputBitmap.copy(Bitmap.Config.ARGB_8888, true);
// 如果 inputBitmap 不是外部传入需要保留的,可以在这里 recycle inputBitmap (视调用逻辑而定)
}
try {
//初始化 RenderScript
RenderScript rs = RenderScript.create(context);
// 创建 Allocation (内存分配)
Allocation input = Allocation.createFromBitmap(rs, outputBitmap);
Allocation output = Allocation.createTyped(rs, input.getType()); // 直接根据 input 创建 output Allocation
// 执行高斯模糊
ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setRadius(Math.max(0.1f, Math.min(radius, 25.0f)));
script.setInput(input);
script.forEach(output);
// 将模糊后的数据填回 outputBitmap
output.copyTo(outputBitmap);
rs.destroy();
} catch (Exception e) {
e.printStackTrace();
// 如果 RS 失败,至少返回原图
}
// 2. 绘制黑色遮罩
//我们没有使用复杂的矩阵计算来修改像素值(那样比较慢),而是利用 Canvas 在图片上画一层黑色的半透明蒙版。
// 0.1 - 0.2: 轻微变暗,适合深色图片。0.3 - 0.5: 推荐值。这是 Material Design 中常见的遮罩浓度,上面的白色文字对比度最佳。
Canvas canvas = new Canvas(outputBitmap);
// 计算 alpha 值 (0-255): 0 表示完全透明,255 表示全黑
int alpha = (int) (brightnessFactor * 255);
int blackColor = Color.argb(alpha, 0, 0, 0);
// 绘制遮罩: 使用 SRC_ATOP 模式:只在图片存在的像素上画黑色
canvas.drawColor(blackColor, PorterDuff.Mode.SRC_ATOP);
return outputBitmap;
}
SurfaceControl.screenshot 获取到的 Bitmap 是 HARDWARE 类型,不可被处理,需要 copy 副本12-16 11:14:43.755 2532 2532 E AndroidRuntime: FATAL EXCEPTION: main
12-16 11:14:43.755 2532 2532 E AndroidRuntime: Process: com.android.systemui, PID: 2532
12-16 11:14:43.755 2532 2532 E AndroidRuntime: android.renderscript.RSInvalidStateException: Bad bitmap type: HARDWARE
12-16 11:14:43.755 2532 2532 E AndroidRuntime: at android.renderscript.Allocation.elementFromBitmap(Allocation.j
ava:2767)
12-16 11:14:43.755 2532 2532 E AndroidRuntime: at android.renderscript.Allocation.typeFromBitmap(Allocation.java
:2772)
12-16 11:14:43.755 2532 2532 E AndroidRuntime: at android.renderscript.Allocation.createFromBitmap(Allocation.ja
va:2811)