【SystemUI】基于 Android R 实现下拉状态栏毛玻璃背景

一、需求描述

基于 Android 11 平台,下拉状态栏的背景实现毛玻璃(Frosted Glass)效果,通知和控制中心的背景实现实时背景模糊效果。

【SystemUI】下拉通知和控制中心背景实现毛玻璃效果

二、需求分析

在 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)

相关推荐
vocal2 小时前
【我的AOSP第一课】Android bootanim 的启动
android
shenshizhong2 小时前
Compose + Mvi 架构的玩android 项目,请尝鲜
android·架构·android jetpack
Chuck_Chan2 小时前
Launcher3模块化-组件化
android
xuyin12042 小时前
Android内存优化
android
jzlhll1232 小时前
android kotlinx.serialization用法和封装全解
android
龚子亦2 小时前
【Unity开发】安卓应用开发中,用户进行权限请求
android·unity·安卓权限
共享家95273 小时前
MySQL-基础查询(下)
android·mysql
查克陈Chuck3 小时前
Launcher3模块化-组件化
android·launcher开发
千里马学框架3 小时前
google官方文档:深入剖析ProtoLog原理及Winscope的查看方式
android·车载系统·framework·perfetto·系统开发·winscope