☰
Android玻璃蒙层实现:静态快照与动态模糊的选型与优化
2026/10/15 1:24:18 网站建设 项目流程

简介:面向需要在界面中实现半透明、模糊“玻璃蒙层”效果的Android开发者,这份资源结合“高斯模糊”“毛玻璃效果”等常见设计趋势,提供了一套可直接运行的示例工程。包内共55个文件,主要包含Java源码、class编译产物、XML布局与资源文件、多尺寸PNG图片、APK安装包及Android Support库等;压缩包约1.23MB,结构清晰,便于快速导入和对照学习。已有908人学习使用。内容包括颜色Alpha通道叠加、BitmapShader自定义着色器、Renderscript图像处理、第三方模糊库的对比思路,并涉及PorterDuff混合模式、ObjectAnimator动态蒙层动画及性能优化等实现细节;同时预览中显示了一个名为PicDim的完整Android项目,包含清单文件、res资源目录和libs依赖,读者可直接参考工程结构、查看关键代码,理解从半透明遮罩到复杂模糊效果的不同层级做法,从而在自己的应用中灵活选用合适方案。适合希望提升UI层次感、丰富交互反馈,或系统学习Android图像模糊处理的初中级开发者。

1. 玻璃蒙层是什么,以及它为什么比普通半透明蒙层更难做

玻璃蒙层在 Android 里指覆盖在页面内容上方、带模糊和半透明效果的一层遮罩,常见于弹窗、底部菜单、侧滑面板、音量面板和引导层。很多人以为它就是把背景调成半透明再加点模糊,真动手才发现完全不是一回事:Android 没有 iOS 那种系统级的 UIToolbar 毛玻璃,想做出“背后内容被糊掉”的效果,必须自己把下层内容离屏画成位图、缩小、高斯模糊,再作为蒙层背景铺上去。这一步离屏绘制、位图缩放和模糊计算只要有一个环节没处理好,表现就是卡顿、边缘发黑,甚至弹窗把自己也糊进了背景里。

这篇文章从一条完整的落地路线讲起:静态蒙层和动态蒙层的选型区别、核心代码怎么组织、三个必调参数怎么定、以及我实际踩过的五个坑。适合给弹窗、控制面板、引导层加玻璃效果的 Android 开发者,也适合从零开始做自定义控件的同学参考。先给结论:如果背后内容在弹窗打开后不会滚动,静态快照方案足够且最稳;只有需要跟手模糊时才上动态方案,那是另一套成本。

2. 玻璃蒙层的实现路线与选型:静态蒙层和动态蒙层分开考虑

玻璃蒙层的第一道分水岭不是代码,是需求:蒙层显示期间,背后的内容到底动不动。这个问题不先想清楚,后面所有性能优化都是瞎忙。

2.1 静态玻璃蒙层:弹窗弹出时的快照模糊

静态蒙层指蒙层显示期间,背后内容不发生变化或变化不影响观感。典型场景是暂停面板、设置弹窗、模式选择框、游戏内的确认弹层。打开弹窗的瞬间截一张背景,模糊后铺在蒙层上,用户看到的就是一张“糊好的底”,后面不再刷新。

实现上有个关键前提:必须先隐藏蒙层本身,等界面重绘一帧后再截图。否则离屏绘制时会把半透明的蒙层一起画进去,模糊背景里出现一块轮廓分明的矩形,视觉上像背景被挖了一个洞。这个问题我见过很多次,后面避坑章会展开。

静态方案的优点是计算量极小、内存一次性、兼容性好,API 21 以上都能跑。缺点是结构是死的:背后内容一旦滚动或切换,蒙层底图跟不上。如果产品只有在弹窗刚出现的那一瞬间需要玻璃效果,静态方案就是最优解,不需要为“可能发生”的滚动提前买单。

我一般会在接需求时先问一句:弹窗浮起来之后,底下页面还滚不滚。如果答案是“不滚”,一律静态方案;如果回答是“滚了会怎样”,我会把坑讲清楚再上动态。

2.2 动态玻璃蒙层:实时模糊的适用边界与实现思路

动态蒙层需要背后内容变化时,蒙层跟着刷新模糊。典型场景是列表上方悬浮的播放条、聊天页底部的输入面板、侧滑菜单跟手拉开时背后的页面模糊。这类效果要的是“跟手感”,背后列表每滚一帧,模糊内容都应该同步变化。

实现思路有两种常见做法。一种是在滚动监听里做防抖刷新,每隔 80 到 120 毫秒重新截一次图、重新模糊一次;另一种是把蒙层做成一个自定义 View,在父容器绘制完成后直接基于渲染结果做模糊。前者简单可控,适合绝大多数业务;后者是某个第三方模糊库的核心思路,它能在滑动的同时逐帧跟手,但实现复杂度高,对视图层级和硬件加速敏感,不推荐从零复刻。

动态方案的关键边界是性能。高斯模糊天生是重计算,全屏尺寸在低端机上做一次就需要几十毫秒,如果每帧都刷,卡顿是必然的。所以动态方案必须做两件事:降采样到 1/4 甚至 1/8,以及降低刷新频率。不要试图让模糊跟到 60fps,现实是大多数场景下,120 毫秒刷新一次,人眼已经觉得足够顺滑,帧率还能保住。

2.3 选型对比表:效果、兼容性、性能、接入成本

把三种常见做法放在一起对比,方便直接照着选:

方案视觉效果最低 API性能开销接入成本适用场景
半透明底色只透不糊,无玻璃质感API 21几乎为零极低对质感要求低的普通遮罩
静态快照模糊有玻璃质感,内容不可变API 21一次截图一次模糊低弹窗、引导层、暂停面板
动态防抖模糊玻璃质感跟随内容滚动API 21持续消耗 CPU/内存中滚动列表上方悬浮面板

选型上我的建议是:先用静态方案把业务跑通,再根据产品反馈决定要不要上动态。很多产品嘴上说要“实时模糊”,实际展示时根本分辨不出静态和动态的区别,尤其是弹窗打开后用户注意力已经集中到弹窗内容上。动态方案只有在“手指滑动时背景模糊必须跟着变”这种强交互场景才有必要,比如侧滑返回、仿 iOS 控制中心。为了一个用户几乎注意不到的细节去烧性能,是我见过最不值的投入。

3. 搭一个 android 玻璃蒙层:布局、离屏截图与模糊核心代码

选型定了,接下来是一套可复现的最小实现。这里以静态快照方案为主,因为它的代码量最小、最容易跑通,动态方案在最后一章补上。

3.1 布局结构:蒙层单独一层,不包住整页

玻璃蒙层应该是一个独立 View,放在页面根布局的最上层,默认 GONE。不要试图在弹窗 Dialog 的 DecorView 里做模糊,那样会把弹窗自身的标题、按钮全部糊进去,控制起来很难受。

常见的布局结构是:

<FrameLayout android:id="@+id/root_container" android:layout_width="match_parent" android:layout_height="match_parent"> <RecyclerView android:id="@+id/content_list" android:layout_width="match_parent" android:layout_height="match_parent" /> <!-- 业务内容区域 --> <!-- 玻璃蒙层,默认隐藏,代码里触发 --> <View android:id="@+id/glass_overlay" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" /> </FrameLayout>

把蒙层放在最后声明,它就在层级最上层。这里有个细节:蒙层本身不要设置任何背景,背景由代码在截图模糊后动态赋值。如果预先设置了半透明颜色,截图时容易把自己的轮廓带进去。

事件拦截上,蒙层 View 默认不消费触摸事件,但它会挡住下层内容的点击。弹窗打开时点蒙层关闭页面,这个点击事件落在蒙层上,再由蒙层所在 Activity 处理关闭逻辑是常见做法。我用的是给蒙层设一个 OnClickListener,触发弹窗消失动画,不额外整复杂的触摸分发。

3.2 生成模糊背景:离屏截图、降采样与模糊工具

这是整个玻璃蒙层的核心:把根布局内容离屏画到位图,降采样后模糊,再作为蒙层背景。

object GlassOverlayUtil { // 降采样倍率:4 表示宽高各缩小到 1/4,像素总量缩到 1/16 private const val DOWN_SAMPLE = 4 // 模糊半径:12f 是经验值,配合降采样后观感接近系统毛玻璃 private const val BLUR_RADIUS = 12f fun applyGlass(overlay: View, target: View) { // 第一步:先隐藏蒙层,避免把自己截进去 overlay.visibility = View.GONE // 等一帧,确保界面已经重新绘制完成 overlay.post { val width = target.width val height = target.height if (width <= 0 || height <= 0) return@post // 第二步:按 1/4 创建离屏位图,缩小绘制一次 val scaledW = width / DOWN_SAMPLE val scaledH = height / DOWN_SAMPLE val bitmap = Bitmap.createBitmap(scaledW, scaledH, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) val scaleFactor = 1f / DOWN_SAMPLE canvas.scale(scaleFactor, scaleFactor) // target.draw 会把 target 及其子树内容画到 canvas,不改变界面本身 target.draw(canvas) // 第三步:高斯模糊,见 blurBitmap 的实现说明 val blurred = blurBitmap(bitmap, BLUR_RADIUS) // 第四步:模糊结果直接作为蒙层背景铺满 overlay.setBackgroundDrawable(BitmapDrawable(overlay.resources, blurred)) overlay.visibility = View.VISIBLE } } }

代码里的 target 是根布局的 content 区域,我一般传activity.findViewById(android.R.id.content),它不包含状态栏,避免截图顶部出现黑条。截图前先 GONE 蒙层是必须的,否则 target.draw 会把蒙层本身也画进位图。

模糊这一步,社区里的常见做法是引用一个纯 Java 的 StackBlur 实现,核心是三次滑窗近似高斯模糊,效果足够,兼容所有 API 级别。如果你想省事,也可以用 RenderScript 的 ScriptIntrinsicBlur,它到 API 30 都表现良好,但注意 API 31 之后被标注废弃,新项目不建议再依赖。模糊半径 12f 是经验值,和你截图的尺寸、降采样倍数强相关,后面第 4 章会展开讲。

3.3 API 31 以上用 RenderEffect 替换软件模糊

StackBlur 是纯 CPU 计算,在低端机上全屏模糊一次可能耗掉 20 毫秒。API 31(Android 12)之后,系统提供了 RenderEffect,可以把模糊交给硬件渲染管线做。需要注意:RenderEffect 作用于 View 自身绘制的内容,它不能凭空模糊 View 下层的内容。所以要配合离屏截图,把模糊位图作为蒙层背景,再给蒙层 View 挂一个模糊效果。

@SuppressLint("NewApi") private fun applyRenderEffectIfSupported(overlay: View, radius: Float) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val blurEffect = RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP) overlay.setRenderEffect(blurEffect) } else { // 低版本靠软件模糊,这里保持背景位图不动 overlay.setRenderEffect(null) } }

这段代码的关键在于:setRenderEffect 会让 View 在绘制时对其整个输出再做一次模糊。因为蒙层的背景已经是模糊过的位图,再挂一次模糊相当于把模糊效果加强了一点,我在实际使用中会把 BLUR_RADIUS 降低到 8f,避免两层模糊叠加后糊成一团。

低版本分支一定要保留。我见过有人在 API 29 的设备上直接调 createBlurEffect,运行到那里直接 NoSuchMethodError,因为这个类是 API 31 才加入的。做兼容最好写成像上面这样按版本分支,不要把 RenderEffect 当主力模糊手段,它只是高版本上的一层增强。

4. 玻璃蒙层的 3 个必调参数:半径、降采样、刷新时机

玻璃蒙层好不好看、卡不卡,几乎全看三个参数:模糊半径、降采样倍率、刷新时机。这三个参数相互影响,调整顺序不对,效果就玄学了。

4.1 模糊半径与 Alpha 怎么配才像玻璃

模糊半径决定“糊”的程度,Alpha 决定蒙层整体的明暗基调。半径太小没有玻璃质感,太大则连内容的色块都认不出来,模糊就失去了意义。

使用场景模糊半径蒙层 Alpha背景色建议
弹窗遮罩120.4白色或黑色,依界面明暗
侧滑面板160.5深色,强化面板层次
引导层80.3深色,保留内容轮廓

我一般会以界面里最小的文字能否保留轮廓为判断标准:模糊后小字还能隐约看到笔画,但读不出内容,这个半径就是合适的。如果小字完全变成一团灰,说明半径过大。Alpha 上,白色蒙层更适合明亮界面,深色蒙层适合夜间模式;玻璃质感主要靠半透明和模糊带出来,不是靠背景色压出来的。

有个容易忽略的细节:同一套参数在不同分辨率屏幕上观感完全不同。小屏手机模糊半径 12 看着合适,换到大屏平板,同样的 12 会显得内容太清晰。所以参数最好以 dp 或按宽高比计算,而不是写死一个像素值。我的做法是半径与降采样后的位图宽度挂钩,用位图宽度的 1/60 作为基准半径,再微调。

4.2 降采样倍率与内存占用:从全尺寸到位图的取舍

降采样是玻璃蒙层性能优化的核心手段。原理很简单:高斯模糊本质是低通滤波,会把高频信息抹掉,所以根本不需要全尺寸位图去算。先把内容缩小到 1/4,模糊完再铺满屏幕,视觉效果几乎没差别,但计算量缩到 1/16。

以一台 1080x2400 的设备为例,全尺寸 ARGB_8888 位图占用约 8.3MB,缩小到 1/4 后不到 0.6MB。内存只是一方面,更大的收益在模糊计算时间:StackBlur 的耗时和像素总量成正比,缩小到 1/16 之后,一次模糊通常能把耗时压到 2 到 3 毫秒。

// 推荐固定 DOWN_SAMPLE = 4,再通过 BLUR_RADIUS 微调观感 private const val DOWN_SAMPLE = 4 // 2 也可以,内存和耗时显著上升,观感提升有限 private const val BLUR_RADIUS = 12f

不要试图把降采样倍率调成 1,也就是全尺寸模糊。观感确实有细微提升,但内存占用和计算时间成倍上涨,用户几乎感知不到那点差异。我踩过这个坑:为了所谓的“高清玻璃”,把降采样倍率设为 2,低端机打开弹窗明显卡一下,改回 4 后流畅了,用户没一个人看出区别。

模糊半径和降采样倍率是联动的:降采样到 1/4 后,同样的半径观感上会更糊,因为位图分辨率低了。所以降采样倍率越激进,半径可以适当调小。我会在固定 DOWN_SAMPLE 的前提下调半径,而不是同时动两个变量。

4.3 刷新时机:滚动监听里别直接触发模糊

动态方案里最容易犯的错误,是在 RecyclerView 的 onScrolled 回调里直接重新截图、重新模糊。onScrolled 一次滑动可能回调几十次,每次做一次全屏模糊,即使降采样到 1/4,低端机也会直接卡成幻灯片。

正确的做法是刷新时机做防抖:滚动过程中只触发一次“延迟刷新”,如果滚动还在继续,就不断重置延迟;当用户停下,或者延迟时间到了,才真正执行一次截图模糊。我后面第 6 章给的代码就是这个思路,这里先把时机讲清楚:模糊刷新的目标不是跟到每一帧,而是让内容变化后有足够的刷新率去更新。

刷新间隔的经验值是 80 到 120 毫秒。低于 60 毫秒,帧率压力大;高于 150 毫秒,滚动的跟手感明显变差。还要注意一点:RecyclerView 的滚动状态切到 IDLE 时,一定要强制刷新一次,保证松手后蒙层停在清晰的状态。

5. 玻璃蒙层避坑:兼容性、硬件加速与边缘发黑的 5 个踩坑记录

玻璃蒙层是一个很看细节的功能,我整理了自己和身边同事实际遇到过的 5 个高频问题。每一条都是血泪经验,照着排查能省半天时间。

5.1 低版本兼容:RenderEffect 不是后悔药

现象:在 Android 11 及以下的设备上,代码跑到RenderEffect.createBlurEffect时直接崩溃,报NoSuchMethodError;有的设备不崩,但蒙层区域变成一片空白。

原因:RenderEffect 是 Android 12 才加入的 API,低版本系统里根本没有这个类。即使你把它包在if (Build.VERSION.SDK_INT >= 31)里也会踩坑,因为某些国产 ROM 的 API level 是正常的,但系统字体缩放或沉浸式布局导致渲染管线兼容性异常,RenderEffect 挂上去后 View 整体绘制失败。

解决:唯一安全的路是在低版本分支走软件模糊。我现在的做法是把 RenderEffect 当成增强项,不把它当核心路径。核心路径永远是想办法拿到模糊位图,高版本再用 RenderEffect 锦上添花。代码里做好版本分支后,务必在 API 30 的真机上跑一遍弹窗流程,模拟器上很多问题复现不出来。

5.2 画面异常:把自己糊进去、边缘发黑、硬件加速失效

现象:模糊背景中间出现一块轮廓清晰的半透明矩形,像背景被人挖了个洞;或者蒙层顶部有一条黑色横带;或者整个蒙层区域的模糊效果消失了。

原因:这三类异常各有各的坑。把自己糊进去是截图时没有先隐藏蒙层,离屏绘制把蒙层自己的半透明矩形画进了位图。边缘发黑多半是截图的 target 包含了状态栏,状态栏区域在离屏绘制时是空像素,模糊后变成一团黑色。模糊效果完全消失则是因为所在 View 被强制关闭了硬件加速,RenderEffect 依赖硬件渲染管线,在软件渲染下直接失效。

解决:截图前蒙层先 GONE,再overlay.post等一帧,把状态栏从 target 里排除掉,截图用android.R.id.content而不是 DecorView。硬件加速问题排查起来比较隐蔽,我会在自定义 View 里检查isHardwareAccelerated,如果蒙层的父容器被设置了LAYER_TYPE_SOFTWARE,模糊效果就会全部失效。另一种情况是没有关闭硬件加速,但蒙层处于滚动容器中,滚出屏幕后系统回收了渲染资源,回来后效果丢失——这种情况把蒙层改成固定位置、不随容器滚动就解决了。

5.3 内存峰值:反复开关弹窗后 OOM

现象:App 打开弹窗十几次后,内存飙升几十 MB,低端机直接 OOM。从 Android Studio 的 Profiler 看,位图对象越来越多且没被回收。

原因:每次打开弹窗都 new 一张模糊位图,旧位图没有及时回收。静态快照方案最容易被忽略的就是这里:业务上觉得弹窗只开一次,但用户反复操作,每次都在堆上积累,GC 还没来得及处理就又来一张。

解决:缓存上一次的模糊位图。同一个页面、同样的窗口尺寸下,模糊结果可以直接复用,不要让重复打开同一弹窗触发多次模糊计算。蒙层关闭时,把顶层的 BitmapDrawable 置空并调用recycle(),确保位图能及时释放。我给工具类加了一个简单的 LRU 缓存,键是 target 的宽高和当前界面主题的 hash,命中缓存就直接设置背景,跳过截图和模糊两个重操作,内存问题基本解决。

6. 进阶:让玻璃蒙层跟随 RecyclerView 滚动而不卡顿

如果你的场景确实需要动态模糊,这一章给出一个能跑通的做法,以及怎么判断它是不是真的达标。

6.1 让滚动中的蒙层跟手:80ms 防抖

滚动过程中不要一帧一帧刷模糊,正确做法是防抖。核心思路:滚动中只记录“需要刷新”的状态,延迟 80ms 后真正执行一次;如果滚动还没停,延迟会被不断重置,相当于滚动过程中最多每秒执行 12 次模糊,既保住了跟手感,也不至于把 CPU 打满。

private var pendingRefresh = false private val fuzzyRefreshRunnable = Runnable { pendingRefresh = false GlassOverlayUtil.refreshGlassWithSnapshot() } recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (!pendingRefresh) { pendingRefresh = true // 80ms 是经验值,区间 60~120ms 都可以 recyclerView.postDelayed(fuzzyRefreshRunnable, 80) } } override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { // 停下来必须强制刷一次,保证清晰 recyclerView.removeCallbacks(fuzzyRefreshRunnable) pendingRefresh = false GlassOverlayUtil.refreshGlassWithSnapshot() } } })

这套代码的关键是 pendingRefresh 标志位:单次滑动过程中只保留一个延迟任务,不会把任务积压成队列。IDLE 时强制刷新一次,确保松手后背景是清晰的。refreshGlassWithSnapshot 内部复用前面applyGlass的截图模糊逻辑,只是这次不先 GONE 蒙层——因为蒙层需要一直显示,截图时要把蒙层排除在外。我给工具类加了一个参数,传 skipOverlay 的 View 列表,截图时把这些 View 临时设为 INVISIBLE,画完再恢复,这样才不会把自己截进去。

6.2 验证是否达标:帧率与观感的双重检查

做完了动态方案,别急着交付。我的验证习惯是先看帧率再看观感。开发者选项里打开“Surface 更新”或“GPU 渲染模式分析”,滚动列表时观察柱状图:如果超过 50% 的帧在 8ms 内完成,说明模糊刷新没有拖垮渲染;如果柱状图频繁超过 16ms,先调大刷新间隔到 120ms,再考虑降低降采样倍率。

观感检查要看三处:小字是否能保持模糊轮廓、蒙层边缘有没有与背景脱节、模糊内容滚动跟手会不会像“被拽着走”。最后一点很多文档不提,但实际体验最容易出问题:动态模糊如果刷新不及时,内容滚过去了,模糊还停留在上一帧,视觉上像蒙层自己在延迟。处理办法是把防抖间隔压到 80ms 以内,如果你感觉还是跟不上,说明需要更高刷新的实现,普通防抖方案到顶了。

我最早做动态方案时,把刷新间隔设到 16ms,滚动起来模糊非常跟手,但帧率从 55 掉到 20,割裂感更严重。后来换成 80ms 防抖,肉眼几乎分辨不出跟手差异,帧率回到 50 以上。这之后我养成了一个习惯:涉及模糊、阴影这类重渲染的效果,先在 600 元档位的低端机上验证一遍,模拟器上的流畅没有任何参考价值。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询