ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android玻璃蒙层实现方案:RenderEffect与低版本兼容实战

Android玻璃蒙层实现方案:RenderEffect与低版本兼容实战 简介面向Android初中级开发者的玻璃蒙层实战工程解决在对话框、页面背景及滑动交互中实现半透明模糊界面层的需求重点覆盖颜色透明度控制、BitmapShader与Renderscript的高斯模糊、毛玻璃质感、动态显隐动画以及大图模糊时的性能优化等关键实现方式。整套资源打包了完整Android项目工程共55个文件约1.23MB包含Java源码、XML布局、多分辨率PNG切图、依赖JAR库以及可直接安装的APK目录按project/classes/res等结构组织便于从源码、资源到最终产物对照学习。目前已有907人学习下载。借助这套工程开发者能快速掌握从Shader绘制到第三方模糊库选型的完整思路同时参考其动画、菜单和多屏幕适配写法减少自行摸索的成本适合需要同时顾及界面层次感与运行流畅度的实际App开发场景。资源属于可直接运行的Demo不依赖特定业务环境可在Android Studio中打开并调整模糊参数用于验证效果和二次开发。1. 玻璃蒙层的真实应用场景与方案选型做Android开发这几年被产品经理追着要“iOS那种毛玻璃效果”的次数两只手都数不过来。弹窗背景要模糊底部抽屉要模糊引导蒙层要模糊甚至播放器控制栏也要一层带模糊的遮罩。把这种半透明、带模糊效果的覆盖层做到Android上就是标题里说的“玻璃蒙层”。它解决的底层问题其实很简单当一层UI盖在另一层UI上面时通过模糊和半透明让底层内容保持“可感知但不可读”的状态视觉上高级信息层级也清晰。适合看这篇内容的朋友有两类一类是刚接触自定义View和Window的新手想搞清楚玻璃蒙层到底有哪几种做法另一类是已经用某种方案实现过但遇到性能瓶颈或低版本兼容问题想看看有没有更好的替代方案。这篇我把自己从API 21到API 34都跑过的实现方案、踩过的坑、调参的心得全部整理出来可以直接当参考手册用。1.1 哪些场景真正需要玻璃蒙层不是所有半透明层都配叫玻璃蒙层我一般按需求把场景分成三类信息遮罩型引导蒙层、新手提示。要求底层内容大范围可见但细节不可读模糊半径可以很大对实时性要求不高。层级过渡型底部弹窗BottomSheet、侧滑抽屉。用户能看到底层在“滑动变化”模糊层必须跟随更新实时性要求高。氛围营造型播放器控制层、悬浮按钮展开面板。这类对帧率敏感模糊叠加在动态画面上稍卡一下就能感觉到。这三类场景对技术方案的要求完全不一样。信息遮罩型只需要静态模糊层级过渡型需要实时模糊氛围营造型既要实时又得省电。搞清楚需求再选方案别一上来就上最重的。1.2 四代技术方案的演进路线Android上做模糊从来没有官方的一键API直到Android 12才补上。我按演进顺序理了一下每一代都有明显的优势和硬伤方案出现时期原理优点硬伤XML半透明覆盖远古时期纯色alpha零成本没有模糊只有透明截图模糊API 14截取底层View再模糊后作为背景效果可控兼容老版本静态实时性差RenderScript模糊API 17GPU/CPU并行计算高斯模糊速度快可实时API 31后被废弃RenderEffectAPI 31RenderNode原生模糊真正实时系统级调优仅支持Android 12补充一个关键点API 31就是Android 12现在新设备基本都覆盖到了但存量老设备仍然不少。我的建议是主方案用RenderEffect低版本降级用截图模糊具体怎么切换下面章节会讲。1.3 我的选型结论直接给结论省得你们纠结App最低版本 API 31闭眼用RenderEffect无论是Window级别还是View级别都有原生支持。App最低版本在 API 23 ~ 30 之间主用截图模糊方案配合RenderScript提速但要做好RenderScript被废弃后的迁移预案。App需要支持 API 21/22别想实时模糊了老老实实预模糊一张静态背景图或者用FastBlur这种纯Java算法做低速模糊。这个选型结论不是拍脑袋定的。我在真机上一台一台测过RenderScript在API 25以下的机器上偶尔会崩纯Java模糊在低端机上模糊一张1080P的图耗时能超过200ms体验很差。方案选型比写代码本身更重要选错了后面坑连坑。2. 核心前置概念模糊本质与RenderEffect原理很多人上来就抄代码抄完发现效果不对或者性能拉胯根本原因是不理解底层机制。这一节我把两个最重要的概念讲透理解了再写代码会顺手很多。2.1 模糊的本质卷积、半径和采样高斯模糊的本质是卷积运算把每个像素点周围一定范围内的像素按高斯权重求平均得到这个点的新颜色值。模糊半径Radius决定了卷积窗口的大小半径越大参与运算的像素越多画面就越“糊”。这里有个性能公式要记住模糊耗时约等于 像素总数 × 卷积核大小。卷积核大小又是半径的平方级增长。所以1080P的图半径从10加到20计算量不是翻倍而是接近四倍。这就是为什么不能盲目调大模糊半径后面讲参数选择时还会细说。还有一个概念是采样Sampling。真正高效的模糊不是对每个像素都做完整卷积而是先缩小图片再模糊再放大效果几乎一样但计算量小一个数量级。早期很多库号称“快速模糊”核心就是干这个事。2.2 RenderEffect API 与 RenderNode 的工作机制RenderEffect是Android 12引入的一套渲染效果API支持模糊、颜色滤镜、链式组合等。它跟老方案最大的区别在于它作用在RenderNode上由系统渲染管线在合成阶段完成模糊不需要把位图读回CPU也不需要你手动做任何像素运算。RenderNode是View树绘制的基础节点每个View都对应一个RenderNode。当你调用view.renderEffect blurEffect时系统在硬件加速渲染的合成阶段对这个View的图层做一次高斯模糊再把模糊后的图层和其他图层合成显示。整个过程在GPU上完成所以速度极快而且支持实时更新——底层内容变化时模糊结果自动跟着变。2.3 为什么说实时模糊很难像素读取与GPU回读老方案实现实时模糊之所以难核心问题是“像素读取与GPU回读”。硬件加速渲染下View的内容都在GPU显存里。要做到模糊传统办法是先截屏但这个截屏要把GPU里的像素拷贝回CPU内存这个操作叫回读Readback。回读一张1080P的图在部分中低端机型上就要几十毫秒做完模糊再传回GPU一帧时间就超了16ms直接掉帧。RenderEffect完全绕开了回读模糊在合成阶段原地完成所以能扛住实时场景。理解了这个区别你就明白为什么我强烈建议API 31直接上RenderEffect——这是架构上的代差不是简单的API封装差异。3. 高版本实时方案RenderEffect 实现动态玻璃蒙层这一节上实操。环境前提很简单Android Studio最新稳定版compileSdk 31minSdk按你们项目实际来但RenderEffect相关代码外面要套版本判断。Gradle配置里加上android { compileSdk 34 defaultConfig { minSdk 23 targetSdk 34 } }3.1 Window级别模糊的代码实现Window级别模糊是最接近iOS毛玻璃效果的做法它把整个窗口后面的内容都模糊掉适合Dialog、弹窗、底部抽屉这类场景。API 31提供了一个开关两行代码搞定if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { window.addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND) window.attributes window.attributes.apply { blurBehindRadius 50 // 模糊半径单位是像素 } }注意几个细节。必须是addFlags直接setFlags会把其他flag覆盖掉。模糊半径建议从30开始调太小效果不明显太大低端机可能扛不住。这个flag生效范围是整个窗口如果你只想要窗口上半部分模糊Window级别就做不到了得换View级别。我实测下来的经验Dialog里配合圆角背景把blurBehindRadius设为40视觉效果跟iOS的UIBlurEffectStyleLight非常接近。但有个坑是部分国产ROM对Dialog的Window flag处理有兼容差异同一套代码在不同机型上模糊程度不一样所以上线前要多机型截图对比。3.2 View级别玻璃蒙层的具体代码View级别的灵活性高很多可以让某个View自身产生模糊效果也可以模糊它背后的兄弟View。核心代码// 对某个View自身做模糊 if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { val blurEffect RenderEffect.createBlurEffect( 30f, 30f, // x和y方向的模糊半径像素单位 Shader.TileMode.CLAMP // 边缘处理模式 ) view.renderEffect blurEffect }这里需要解释一下三个参数xRadius和yRadius横向和纵向的模糊半径可以不一样。做水平动态模糊效果比如列表滑动时的标签时可以把xRadius调大、yRadius调小。TileMode决定模糊边缘之外的像素怎么采样。CLAMP是拉伸边缘像素MIRROR是镜像采样REPEAT是重复采样。做玻璃蒙层几乎都用CLAMP因为边缘不会出现奇怪的光晕。Dialog上加模糊背景的完整做法是这样class GlassDialog(context: Context) : Dialog(context) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { // 把Dialog根布局做模糊显示出来的就是毛玻璃效果 window?.decorView?.renderEffect RenderEffect.createBlurEffect( 30f, 30f, Shader.TileMode.CLAMP ) } else { // 低版本降级逻辑见第4章 setBackgroundDrawable(createBlurredBitmapBackground()) } } }有一种常见需求是“只模糊背景弹窗上的文字和按钮清晰”。这时候不能把decorView整体模糊而是把弹窗之外的区域做成一个View只对这个View设置renderEffect。具体做法是用一个全屏的FrameLayout底层放承载模糊效果的蒙层View上层放内容View层级互不干扰。3.3 参数调节的实测经验参数直接抄我的实测值能少走很多弯路场景模糊半径蒙层颜色颜色透明度底部弹窗背景30 ~ 40#FFFFFF60% ~ 70%新手引导蒙层50 ~ 80#00000030% ~ 40%播放器控制栏20 ~ 25#00000040%悬浮按钮展开面板35#FFFFFF50%模糊半径加颜色叠加是玻璃蒙层的“灵魂”。纯模糊会显得画面发灰发闷叠加一层半透明白色会立刻通透起来叠加半透明黑色则更沉稳。这两种叠加不是随便拍的白色叠加模拟的是光线散射黑色叠加模拟的是暗化聚焦跟设计上的“玻璃拟态”规范是对应的。我自己的调参心得是先用最小半径跑通再逐步加大到视觉舒适点最后再降回来一档——因为模糊半径越大底层文字越不可读但性能开销也在涨找到那个“刚好看不出来又来了一点点”的平衡点就行。4. 低版本兼容方案静态模糊图与动态遮罩如果你要支持API 30以下的机型RenderEffect用不了得回到截图模糊方案。核心思路把底层View的内容截成一张图模糊后作为蒙层的背景。虽然做不到每帧实时模糊但对大多数弹窗和引导场景完全够用。4.1 截图模糊方案的核心思路整体流程分四步找到底层内容所在的View一般是Activity的content节点。view.draw(Canvas(bitmap))把View内容画到一张Bitmap上。对Bitmap做高斯模糊。把模糊后的Bitmap设置到蒙层View的background上。这个方案的关键在于什么时候截、截多大。截图的时机决定了模糊内容的时效性内容变化后需要重新截图模糊截图尺寸决定了内存占用如果直接截全屏原始大小一张1080P的Bitmap就占8MB内存再加上模糊过程中的中间缓存很容易OOM。4.2 用 RenderScript 做高斯模糊RenderScript是官方比较推荐的模糊方案虽然API 31被标记废弃但在API 23到30之间依然是最稳的选择。用法如下fun blurBitmap(context: Context, source: Bitmap, radius: Float): Bitmap { val rs RenderScript.create(context) val input Allocation.createFromBitmap(rs, source) val output Allocation.createTyped(rs, input.type) val script ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)) script.setRadius(radius) script.setInput(input) script.forEach(output) output.copyTo(source) rs.destroy() return source }几个坑必须提醒setRadius的合法范围是0到25超过25会直接抛异常。需要更大模糊效果时先缩小Bitmap再模糊效果等同大半径。传入的source会被原地修改如果后面还要用原图记得先copy()一份。RenderScript.create(context)很耗资源用完后必须destroy()否则会内存泄漏。4.3 纯Java的FastBlur降级方案部分低端机连RenderScript都用不了极少数API 23以下的设备驱动有问题我留了一个纯Java的后备方案就是网上常见的FastBlur基于StackBlur思想实现。虽然性能不如RenderScript但兼容性最好写法也简单fun fastBlur(source: Bitmap, radius: Int): Bitmap { val w source.width val h source.height val pixels IntArray(w * h) source.getPixels(pixels, 0, w, 0, 0, w, h) // 这里做StackBlur核心运算代码较长按需引入完整实现 source.setPixels(pixels, 0, w, 0, 0, w, h) return source }FastBlur的实战技巧先缩小图片到原图的1/4再做模糊最后放大回来。因为模糊本质是低频信息的保留缩小再放大几乎无损但计算量直接降到原来的1/16。我用这个技巧在低端机上把耗时从300ms降到了40ms左右。4.4 静态与动态结合的架构设计低版本方案虽然做不到实时模糊但可以做到“伪实时”。我的做法是把截屏模糊抽成一个独立方法在需要更新模糊时调用同时加一个节流控制避免频繁触发class GlassBackgroundHelper(private val targetView: View) { private var lastBlurTime 0L fun updateBlur(force: Boolean false) { val now System.currentTimeMillis() if (!force now - lastBlurTime 300) return // 300ms节流 lastBlurTime now val root targetView.rootView val bitmap Bitmap.createBitmap(root.width, root.height, Bitmap.Config.ARGB_8888) root.draw(Canvas(bitmap)) val scaled Bitmap.createScaledBitmap(bitmap, root.width / 4, root.height / 4, false) val blurred blurBitmap(context, scaled, 20f) targetView.background BitmapDrawable(resources, blurred) bitmap.recycle() scaled.recycle() } }这套架构在BottomSheet滑动监听里调用每300ms最多截一次滑动过程中用户基本感知不到模糊层在刷新但视觉上又是跟手的。如果你的场景要求完美跟手那只能升级到API 31用RenderEffect低版本方案做不到完美的逐帧模糊。5. 性能分析与常见坑位排查玻璃蒙层最大的敌人是性能和兼容性。这一节把我压测的数据、踩过的坑和排查思路全部倒出来你们直接对号入座。5.1 性能瓶颈在哪里玻璃蒙层的性能消耗有三个主要瓶颈按权重排序Bitmap内存占用截图模糊方案中全屏Bitmap在1080P下占用约8.3MB如果模糊过程中产生多份拷贝内存轻松超过30MB。必须及时复用和回收。模糊计算耗时纯Java模糊1080P图耗时200msRenderScript约30msRenderEffect约5ms。差距就是这么大这也是为什么我一直强调能用RenderEffect就用。GPU合成开销RenderEffect虽然计算快但会增加GPU合成负担多个View同时模糊时低端机的帧率会明显下降。5.2 实测数据与调优建议我拿三台真机跑过同一个BottomSheet模糊场景数据如下机型方案模糊耗时帧率影响内存增量骁龙8 Gen 2RenderEffect约5ms基本无感极低骁龙765GRenderScript约35ms偶发掉帧约20MB麒麟710FastBlur缩小4倍约45ms掉帧明显约15MB调优建议按优先级排列降分辨率模糊前把图缩到1/2或1/4肉眼几乎看不出差别性能翻倍。限频滑动监听里做节流控制在300ms左右一次刷新。减少模糊View数量同一个页面上最多保留1到2个模糊层多了叠加效果其实并不好视觉还会发灰。复用Bitmap不要每次截图都新建用Bitmap.createBitmap配合Canvas重复绘制。5.3 高频问题的排查速查表我把群里和评论区高频出现的问题整理成一张表方便直接查问题现象可能原因解决方案模糊层是黑色的RenderEffect未生效检查是否在API 31分支里设置检查View是否在硬件加速环境下模糊层模糊了整个页面包括弹窗内容renderEffect设置在了根节点分隔为独立蒙层View只对蒙层设置弹窗打开时闪烁一下截图时机太早底层还没绘制完postDelayed到绘制完成后再截低端机OOMBitmap没回收或截了全屏原图缩小截图尺寸及时recycle用复用池模糊部分区域出现白边TileMode设置不对改用CLAMP模式RenderScript在API 30崩溃已废弃导致驱动问题加版本判断API 31走RenderEffect模糊层不随底层内容更新用了静态截图方案在内容变化监听里重新调用updateBlur5.4 我踩过的几个坑第一个坑是VIew的setRenderEffect和setAlpha一起用的时候部分骁龙机型上模糊会失效表现为蒙层变成半透明但没有模糊。排查了半天最后发现是View的layerType被改成了LAYER_TYPE_SOFTWARE。设置RenderEffect时确保View用的是LAYER_TYPE_HARDWARE否则模糊效果不生效。第二个坑是Dialog的blurBehindRadius在部分国产ROM上不生效。这个flag依赖系统WindowManager的合成支持部分ROM会忽略。我的应对方案是优先用Dialog但上线前在主要目标机型上做自动化截图对比发现不生效的机型就降级到View级方案。第三个坑比较隐蔽模糊蒙层导致触摸事件穿透。用RenderEffect实现蒙层后蒙层View本身是“存在”的但在某些场景下需要点击底层内容这时候蒙层会拦截事件。解决方案是给蒙层设置isClickable false只保留视觉效果不拦截交互。6. 结尾留个经验最后说一个我自己团队已经落地的经验玻璃蒙层这个效果代码只占三成剩下七成是调参和适配。同一个模糊半径在不同分辨率的屏幕上视觉差异很大同一套代码在不同厂商的ROM上性能差距也很明显。我的做法是在项目里建一个GlassConfig类把模糊半径、颜色、透明度、降级策略全部参数化按屏幕密度和机型档位动态选择配置每次上版本前跑一轮真机截图对比。效果稳定后这套配置基本不用再动。如果你们团队也遇到类似问题建议直接抄这个模式能省掉不少联调扯皮的时间。本文还有配套的精品资源点击获取
返回列表