资讯动态

Android玻璃蒙层Demo:动态与静态模糊引擎的低版本适配方案

发布时间:2026/10/19 3:27:26 来源:尧图企业网站定制
简介面向Android开发者的玻璃蒙层毛玻璃/模糊效果实现参考包适合需要提升界面层次感、提供操作反馈或美化UI的中高级移动端工程师。包内是一个完整的PicDim示例项目共55个文件压缩后仅1.23MB涵盖Java源码、XML布局与资源配置、多密度PNG切图、JAR依赖库以及可直接安装的APK便于快速导入工程并对照学习。已有909人学习使用。资源重点演示了Alpha通道颜色叠加、BitmapShader自定义Shader、Renderscript高斯模糊、第三方库BlurKit/FastBlur等常见实现路径同时涉及动态显隐动画、PorterDuff混合模式与性能优化等落地细节。通过阅读源码与运行Demo开发者能掌握不同方案在视觉效果与性能之间的取舍避免大图模糊阻塞主线程提升界面流畅度。1. 玻璃蒙层不是高版本专属这个 Android 蒙层 Demo 让老项目也能玩转毛玻璃产品提了个很常见的需求弹窗底部要有一层毛玻璃背景后面的列表还能隐约透出来要有那种高级感。第一次做的时候我直接截了一张全屏图在主线程里跑高斯模糊结果低端机上弹出蒙层要卡 400ms帧率掉到个位数还被测试同学截图吐槽“与设计稿不符”。后来我把整个实现推翻重做核心只有一句话先把画面缩小再模糊最后放大绘制性能能差一个数量级。这个 android 玻璃蒙层项目就是把“动态模糊”和“静态模糊”两条路径整合成一套可复用的蒙层引擎让 Android 5.0 的老工程也能低成本接入毛玻璃效果。适合正在做引导层、弹窗背景、底部面板遮罩又不想被系统版本绑死的开发者。2. 技术选型与整体结构两套模糊引擎 一个统一管理入口2.1 从需求反推方案先分清“动态模糊”还是“静态模糊”玻璃蒙层的技术取舍第一步不是选算法而是问产品背后内容会不会实时动。如果蒙层背后的列表还在滚动、视频还在播放属于“动态蒙层”每帧都要反映最新画面这只能用 GPU 实时渲染最理想的是 Android 12 引入的 RenderEffect如果背后只是一张静态页面比如用户点击按钮后展示一个说明气泡属于“静态蒙层”截屏一次再高斯模糊就够了成本和实现难度都低得多。这个 Demo 的架构思路就是把两条路径统一进同一个对外接口对外只暴露一个 Manager内部根据系统版本和业务诉求自动切换实现。方案模糊算法实时性最低版本主要开销动态模糊RenderEffectGPU 着色器支持Android 12显存、RenderNode 合成静态模糊采样 多重 BoxBlur不支持Android 5.0一次截屏 一次 CPU 模糊兜底方案View.draw 截屏弱全版本Bitmap 内存、绘制耗时注意一个反直觉的点很多人以为“动态”比“静态”一定更吃性能。实际上动态路径因为由 GPU 接管模糊成本被压得很低真正贵的是截屏和 CPU 高斯模糊而静态路径一次 CPU 模糊能用挺久在低端机上反而更划算。所以不要凭感觉选先确认背后内容的活跃度。2.2 项目模块与类划分一个容器两个处理器源码结构拆出来看非常清晰核心是三层入口管理、画面截取、模糊处理。app/src/main/java/com/example/glassmask/ ├── GlassOverlayManager.kt ├── config/ │ └── MaskConfig.kt ├── view/ │ ├── GlassMaskView.kt │ └── MaskLayoutParams.kt ├── capture/ │ ├── WindowCapturer.kt │ └── PixelCopyCapturer.kt └── blur/ ├── BlurProcessor.kt ├── RenderEffectBlurProcessor.kt └── FastBlurProcessor.kt各模块的职责划分如下类职责关键点GlassOverlayManager对外入口负责 show / dismiss / 配置下发MaskConfig蒙层参数模糊半径、遮罩颜色、透明度、动画时长GlassMaskView蒙层容器承载模糊结果拦截并消费触摸事件WindowCapturer低版本截屏拿 DecorView 直接 draw 到 BitmapPixelCopyCapturer高版本截屏异步拿窗口像素不影响主线程BlurProcessor模糊算法接口统一 blur(source, radius) 入口RenderEffectBlurProcessor动态模糊实现Android 12 专属FastBlurProcessor静态模糊实现采样 BoxBlur 模拟高斯设计上有一个值得借鉴的地方截屏和模糊是两个独立扩展点。截屏支持 Window 级和 View 级模糊支持 GPU 和 CPU如果一个团队想替换成自己的模糊算法比如美团 RobustBlur 一类的工程实现只要重新实现 BlurProcessor 接口就能接入不用碰上层逻辑。这个 Demo 在这个位置留了口子二次开发时很舒服。3. 核心模块拆解一次“弹出蒙层并关闭”的完整生命周期3.1 入口管理GlassOverlayManager 的 show 与 dismiss整个蒙层的生命周期从 show 开始到 dismiss 结束。Manager 做的事情不多解析配置、生成容器 View、发起截屏、触发模糊、挂载到页面上。class GlassOverlayManager( private val activity: Activity, private val config: MaskConfig ) { private var maskView: GlassMaskView? null private var blurProcessor: BlurProcessor createBlurProcessor() fun show() { val decorView activity.window.decorView // 1. 截取当前窗口画面 captureWindow(decorView) { bitmap - // 2. 把截屏结果交给模糊引擎 val blurred blurProcessor.blur(bitmap, config.blurRadius) // 3. 创建蒙层容器挂到 WindowManager 顶层 maskView GlassMaskView(activity).apply { setBlurResult(blurred) setOverlayColor(config.overlayColor) } addMaskToWindow(maskView!!) } } fun dismiss() { maskView?.let { removeMaskFromWindow(it) it.releaseBitmap() } maskView null } }这里几个参数要说明一下blurRadius是模糊半径单位是像素实际业务里 16px 到 25px 之间效果比较自然太大容易糊成一团overlayColor是蒙层色调一般用带透明度的白色或黑色透明度在 0.15 到 0.35 之间能保留足够辨识度。show里有两处回调截屏是异步的模糊结果拿到后才能挂载 View所以中间会有几毫秒到几十毫秒的空窗期处理不当会出现“先闪一下原画面再变模糊”的穿帮。解决办法是第一步就把 maskView 挂上去先铺一层 0.2 透明度的纯色底等模糊图就绪后换上。3.2 截屏高版本用 PixelCopy低版本用 View.draw截屏是整个链路里最危险的一步不能在主线程同步执行。高版本推荐用 PixelCopy它把截屏工作放到系统进程异步回调页面不会掉帧。fun captureWindow(decorView: View, callback: (Bitmap) - Unit) { val bitmap Bitmap.createBitmap( decorView.width, decorView.height, Bitmap.Config.ARGB_8888 ) if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { PixelCopy.request( activity.window, bitmap, { result - if (result PixelCopy.SUCCESS) callback(bitmap) else captureByDraw(decorView, bitmap, callback) }, Handler(Looper.getMainLooper()) ) } else { captureByDraw(decorView, bitmap, callback) } }PixelCopy.request的第一个参数也可以传一个 Surface比如 SurfaceView 的 SurfaceHolder适用于你想只模糊某个播放器区域而不是整个窗口的场景。这个回调在主线程执行所以在回调里不能做重活只做简单赋值。如果截屏失败代码里回退到captureByDraw新建一个 Canvas 包住 Bitmap让 DecorView 自己 draw 进去。这个方法性能差一些在 PixelCopy 不可用的设备上属于兜底方案实测一台 5.1 的老机器上1080p 屏幕一次 draw 大概要 80ms能接受但别反复调用。3.3 模糊处理RendererEffect 动态路径与 FastBlur 静态路径BlurProcessor 只定义一个方法把 Bitmap 变成带模糊效果的 Bitmap。但两个实现做法完全不一样。动态路径本身不操作像素它把模糊任务交给 GPU核心代码很轻class RenderEffectBlurProcessor(private val hostView: View) : BlurProcessor { override fun canRealtime(): Boolean true override fun blur(source: Bitmap, radius: Float): Bitmap { // 核心思路把截屏图交给 ImageView // 用 RenderEffect 做实时模糊不需要重新截屏 val renderEffect RenderEffect.createBlurEffect( radius, radius, Shader.TileMode.MIRROR ) hostView.setRenderEffect(renderEffect) (hostView as ImageView).setImageBitmap(source) return source // 返回原图真正的模糊发生在渲染阶段 } override fun updateRadius(radius: Float) { hostView.setRenderEffect( RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.MIRROR) ) } }这个实现里有一个设计细节blur直接返回了原图而不是生成一张模糊图因为 RenderEffect 的模糊是在 draw 阶段由 RenderNode 实时计算的。TileMode.MIRROR很关键它决定模糊采样到边缘时怎么取值用 CLAMP 会在图片四周拉出灰边用 MIRROR 会镜像平铺边缘过渡自然得多这是实际踩坑踩出来的经验。静态路径才是传统意义上的高斯模糊但 Demo 用了工程化手段来降成本先缩小再模糊再放大。class FastBlurProcessor : BlurProcessor { override fun canRealtime(): Boolean false override fun blur(source: Bitmap, radius: Float): Bitmap { // 采样率 1/8把 1080p 的图缩到 135p模糊负担瞬间降低 val scale 8 val smallBitmap Bitmap.createScaledBitmap( source, source.width / scale, source.height / scale, true ) // 等效半径原图 20px 的模糊缩放到小图上只需约 3px val scaledRadius (radius / scale).toInt().coerceAtLeast(1) val blurred boxBlur(smallBitmap, scaledRadius, 3) // 放大回原尺寸得到和原图尺寸一致的模糊结果 return Bitmap.createScaledBitmap(blurred, source.width, source.height, true) } }这段代码里的参数都是能写进复盘报告的scale 8意味着内存占用降为原来的 1/64模糊的采样数也同步下降boxBlur(bitmap, radius, 3)是三次多次盒式模糊在视觉上已经非常接近真实高斯。在 405 处理器上1080p 原图跑一遍这段逻辑大约 120ms单次弹出场景完全够用。很多人问“为什么不直接用系统的高斯模糊 API”因为系统原生模糊不提供这种采样优化实际体验是转圈圈两秒才弹出来没法用。3.4 蒙层容器触摸消费与内存释放GlassMaskView 才是真正展示给用户的层。它不做模糊计算只负责把模糊图和遮罩色合成在一起。class GlassMaskView(context: Context) : View(context) { private var blurBitmap: Bitmap? null private var overlayColor: Int Color.parseColor(#33000000) private val paint Paint(Paint.ANTI_ALIAS_FLAG) fun setBlurResult(bitmap: Bitmap) { blurBitmap bitmap invalidate() } fun setOverlayColor(color: Int) { overlayColor color invalidate() } fun releaseBitmap() { blurBitmap?.recycle() blurBitmap null } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) blurBitmap?.let { // 模糊结果铺满全屏 canvas.drawBitmap(it, null, Rect(0, 0, width, height), paint) } // 在模糊图上层叠一层半透明颜色形成“玻璃感” canvas.drawColor(overlayColor) } }这里有个视觉还原的细节模糊和透明遮罩是两回事少了overlayColor这一层模糊图看起来是清晰的“高斯模糊滤镜预览”不是毛玻璃加了半透明白色才会有那种光线被折射的柔和质感。overlayColor 用黑色系适合暗色 UI白色系适合亮色 UI。另外一个容易忽略的点是触摸蒙层默认会消费掉整个屏幕的触摸事件点击蒙层区域直接关闭弹窗这个逻辑通常会放在 Manager 里而不是 View 里因为 View 不持有业务回调。4. 性能调优与多版本适配采样率、渲染策略和实时性权衡4.1 采样率与模糊半径为什么先缩小方案是性能关键很多人会觉得高斯模糊就是把每个像素周围一圈像素求平均半径越大越慢其实工程实现里最大的瓶颈是像素总量。这也是为什么 FastBlurProcessor 先把 1080p 缩到 135p像素量变成 1/64三趟模糊的访问量就少两个数量级。实测数据可以参考相同模糊半径 20px 下直接在原图做模糊耗时约 800ms缩小到 1/8 再放大耗时约 120ms视觉差异几乎感知不到。原因是模糊算法对低频信息不敏感缩小后再放大损失的细节恰好是用户本来就不会去注意的高频噪声。这套思路也可以沿用到动态蒙层RenderEffect 的 GPU 路程线虽然不操心像素量但截屏时如果直接把 1080p 大图塞给 ImageView显存占用会比较大还是会抖。这里给一个调参规律适合写进自己的实现里采样率定到 1/8 是甜点区再小比如 1/16模糊边缘会明显发“肉”缺乏层次再大1/4收益不明显CPU 时间还翻倍。半径的取值和蒙层使用场景绑定弹窗背景用 18~22px引导层用 12~16px底部抽屉面板用 25px 以上因为抽屉面板面积大且常驻模糊感太弱会暴露截屏的瑕疵。4.2 多版本降级策略API 31 是分水岭这个项目的多版本策略总结成一句话能 GPU 就用 GPU不能 GPU 就用 CPU 采样模糊。具体判断依据是系统 API 版本因为 RenderEffect 在 Android 12 才加入。系统版本截屏方式模糊方式刷新能力Android 12PixelCopyRenderEffect实时刷新Android 8.0 ~ 11PixelCopyFastBlur手动刷新Android 5.0 ~ 7.0View.drawFastBlur手动刷新降级逻辑放在 GlassOverlayManager 的createBlurProcessor()里用一行判断就能切private fun createBlurProcessor(): BlurProcessor { return if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { // Android 12 优先走 GPU 实时路径 RenderEffectBlurProcessor(maskViewContainer) } else { // 低版本走 CPU 采样 快速模糊 FastBlurProcessor() } }这里要强调一个坑即使切到了低版本也不意味着不能做动态蒙层。如果业务必须要实时模糊又没有 RenderEffect常见的做法是给 FastBlur 加一个 throttle比如内容滚动停止后 150ms 再刷新一次或者利用 Choreographer 以 30fps 为上限刷新。这样能兼顾动态效果和性能只是刷新率低于 GPU 路径的 60fps。判断依据是业务对动态的敏感度而不是绝对的系统版本。4.3 刷新策略别在每帧都截屏用“脏标记 延迟刷新”动态蒙层最常见的性能翻车点是为了监听列表滚动在onScroll回调里直接触发截屏结果一秒钟截了 30 张图直接把主线程堵死。正确的做法是给蒙层做一个脏标记内容变化后只记录一个“需要刷新”的状态等绘制的下一帧再去截屏。fun notifyContentChanged() { if (!dirty) { dirty true // 延迟到下一帧再刷新避免高频触发 postOnAnimation { refreshBlur() dirty false } } }这个策略的核心是“合并请求”不管内容变化多少次一个绘制周期里只做一次截屏和模糊。实际效果上从 30fps 的上限压到 10fps 以下视觉几乎没差别因为蒙层本身还有半透明遮罩在干扰注意力用户感知不到细微的帧率差异。如果这么做仍然卡再往下就是降低截屏分辨率只截屏模糊区域对应的局部 View而不是整个窗口。5. 避坑记录玻璃蒙层适配中踩过的五个典型问题5.1 低版本设备直接闪退日志显示 RenderEffect 找不到类现象在 Android 11 的测试机上运行点击弹出蒙层瞬间应用崩溃堆栈指向createBlurEffect。原因RenderEffect 是 Android 12API 31才加入的 API低版本设备在运行时链接阶段就抛NoSuchMethodError这是典型的“没做版本闸门”。解决所有 RenderEffect 相关代码收进RenderEffectBlurProcessor并在 Manager 层用Build.VERSION.SDK_INT S判断创建。只要不进片段类被加载到低版本上也不会触发异常。从那以后我给蒙层上手第一行代码就先写if (Build.VERSION.SDK_INT)这个习惯保持到现在。5.2 模糊图四周出现明显的灰色或黑色边线现象FastBlur 模糊之后蒙层的边缘有一条深色描边和设计稿差很多。原因缩放采样后用Bitmap.createScaledBitmap放大边缘像素因为采样不足和周围颜色拉出渐变带另外模糊算法在图像边界处只采到一半像素导致边缘比内部暗。解决在截屏阶段就把有效内容往外扩一圈也就是采样 Bitmap 时把 Canvas 平移出几个像素的边距模糊后再裁剪回正常显示范围。这个 Demo 里直接在captureWindow阶段多留了 16px 的安全边距边缘问题从根上解决。注意这个边距在后续像素级操作和Rect绘制换算时都要减掉否则蒙层会整体偏移。5.3 蒙层关闭后内存反复增长操作十几分钟后 OOM现象反复弹蒙层看 Android Studio 的 Memory Profiler内存曲线像楼梯一样只上不下最终 OOM。原因每次截屏生成的 Bitmap 没有再回收GlassMaskView 被移除后Bitmap 还持有像素数据等待 GC。GC 对大对象回收不及时攒到一定数量直接爆炸。解决在dismiss()里调用releaseBitmap()主动 recycle并把 View 从 WindowManager 移除后置空引用。另外给 Bitmap 的创建方式加了一层复用池同一尺寸的 Bitmap 先查池子再用。这里的一个取舍是内存复用能有效缓解抖动但池子里的 Bitmap 会占用一点常驻内存建议池容量上限设 2 张。5.4 蒙层弹出时先闪一下原画面再变模糊现象点击按钮后肉眼可见原页面清晰闪了一帧然后才出现毛玻璃背景视觉上很“土”。原因截屏是异步的Manager 收到 Bitmap 并模糊完成前蒙层 View 还没挂上去这一帧窗口上没有任何遮罩。解决把挂载 View 的时机提前到截屏之前。先挂一个只绘制overlayColor的空蒙层作为占位等模糊图回来后再把 Bitmap 填进去并 invalidate。这个做法同时解决了另一个小问题截屏时如果不提前挂遮罩PixelCopy 会把弹窗自身的残影也截进图里形成“套娃”效果。占位层透明度低一些用户几乎察觉不到切换过程。5.5 屏幕旋转或弹出输入法后蒙层错位现象横竖屏切换后蒙层只覆盖一半屏幕键盘弹出时蒙层没有跟随上移底下出现空白。原因WindowManager 在屏幕方向变化时会重新 layout但 GlassMaskView 的模糊 Bitmap 尺寸还是旧屏幕的输入法弹出同理窗口高度收缩后蒙层没有重新截屏。解决在 GlassMaskView 里监听onSizeChanged一旦宽高变化就通知 Manager 重新走一遍截屏和模糊流程。这里要配合android:configChangesorientation|screenSize或者自行处理配置变更不要道人 Activity 重建。重新截屏的时机要放在尺寸变化稳定之后否则容易截到一半的画面。开发时可以加日志打印width x height确认每次都一致。6. 二次开发思路从通用遮罩引擎到业务毛玻璃面板这套蒙层引擎本身是通用骨架真正落到业务里通常还需要三处改造。第一处是模糊源的粒度。现在的captureWindow是整窗截屏如果页面里有个高频滚动的列表位于内容区域整窗截屏的浪费会非常大。改造思路把WindowCapturer抽象出第二个实现ViewCapturer只截取指定 ViewGroup 的 bounds截出来的 Bitmap 同样走模糊链路。调用方式从overlay.show()变成overlay.show(anchorView)。第二处是异形蒙层。产品经常提“弹窗的蒙层不要遮住上面的关闭按钮”或“左侧有个卡片要透出来”。可以在 GlassMaskView 的 onDraw 里加一个挖洞逻辑把不需要模糊的区域通过Paint.setXfermode(PorterDuff.Mode.CLEAR)擦成透明。要注意先 draw 模糊图再擦不然擦掉的区域会露出底部真实页面。// 在 onDraw 中追加一段区域擦除 val holeRect Rect(left, top, right, bottom) canvas.drawRect(holeRect, clearPaint)第三处是与 WindowInsets 的配合。刘海屏和状态栏高度直接影响全屏蒙层的绘制区域。如果不想让蒙层盖住状态栏需要在布局时把setDecorFitsSystemWindows(false)和WindowInsets.Type.statusBars()换算出的 top inset 传给 GlassMaskView让它从状态栏以下开始绘制。实际项目中这个 inset 要动态读取不同设备的刘海高度不一样硬编码会踩坑。回想第一次做毛玻璃蒙层我在截图、模糊、挂载这一条链路上反复翻车核心问题都是“没有先确认动态还是静态需求直接按最高标准实现”。现在每次接手蒙层需求我都强制先回答三个问题背后内容动不动蒙层是临时弹窗还是常驻面板最低支持到哪个系统版本。回答完这三个问题方案基本就定了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑