ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android玻璃蒙层实现全攻略:从BlurView到RenderEffect的实践与踩坑

Android玻璃蒙层实现全攻略:从BlurView到RenderEffect的实践与踩坑 简介面向Android开发者的玻璃蒙层实现资源重点解决界面半透明、模糊与毛玻璃视觉效果问题覆盖高斯模糊、BitmapShader、Renderscript、第三方模糊库等关键技术点适合关注UI层次感与交互反馈的进阶开发者。压缩包共55个文件体积1.23MB内容包含java源码与class字节码、xml布局文件、png图片资源、jar依赖库以及可运行的apk和工程配置信息便于从源码到运行全方位研读其中xml配合alpha属性与PorterDuff混合模式控制透明度java等文件承载模糊算法与动态动画逻辑。资源结合PicDim图片处理工具完整演示了颜色透明度叠加、动态蒙层显隐动画和异步模糊性能优化等落地思路并说明了大图与复杂效果下的性能处理方法可直接迁移到实际项目。已有907人学习下载对希望提升Android界面质感、深入理解模糊算法工程化实现的开发者有很高的参考价值。 讲真Android上做玻璃蒙层这件事我踩过的坑比大多数人写过的代码都多。记得几年前第一次在项目里接到毛玻璃弹窗需求我天真地以为给View设个半透明背景再调下模糊半径就完事了结果一跑起来直接傻眼——背景是糊了但糊的是自己View里面的内容后面的界面该长什么样还长什么样。后来翻了一圈资料才发现Android的模糊和iOS完全不是一个路子你得自己想办法把后面的画面抓出来再做处理。这篇博文就把我做Android玻璃蒙层的完整思路、代码实现和踩坑记录都整理出来。不管你是要做弹窗背景、底部导航栏、还是那种沉浸式的状态栏毛玻璃效果这篇都能给你一个可以直接落地的方案。1. 玻璃蒙层的整体设计与方案选型1.1 核心需求拆解我们要的到底是什么先想清楚“玻璃蒙层”这个问题到底在解决什么。用户看到的玻璃效果本质上是前景半透明层对背景画面进行实时模糊产生一种“透过去但看不清”的视觉反馈。它和普通半透明遮罩最大的区别在于半透明遮罩只能改变背景的亮度或色相而玻璃蒙层会抹掉背景的细节让文字、图片等信息在蒙层下方无法被辨认同时保留大色块和轮廓光线看起来高级、干净。在Android里实现这个效果按技术路径分无非三种原生RenderEffect实时模糊、第三方库BlurView/模糊位图、以及静态模糊图兜底。各有各的适用场景我得先看看项目实际要什么。如果说你的目标页面是那种全屏弹窗背景是一张静态图片或者内容基本不动的界面直接用我后面讲的BlurView方案最省事它是通过抓取整个Window的Bitmap再模糊后塞到View里实时性和兼容性都比较均衡。如果App的最低版本是Android 12API 31以上那直接用系统自带的RenderEffect挂在View上做实时模糊性能和效果都是最好的代码量最少还没什么兼容性负担。如果只是想要一个“看起来像毛玻璃”的背景对实时性要求不高比如某个页面的头部区域、底部操作栏那用静态模糊图加渐变遮罩的组合就够了性能开销最低。我这次做的是一个带“玻璃蒙层”效果的全局弹窗组件要求是弹窗出现时背景内容被实时模糊同时弹窗本身是一个半透明的玻璃质感面板。技术选型上我直接选了BlurView做核心方案再用RenderEffect作为高版本设备的优化通道双轨并行后面我会细讲这么搭的原因。1.2 方案对比三种主流实现路径的取舍选择之前我先把三种方案的核心机制和利弊列出来对比一下方便后面按需选型。方案核心机制最低系统要求实时性性能开销局限RenderEffect基于RenderNode的GPU实时模糊Android 12完全实时低低版本不可用BlurView第三方库截取Window位图ScriptIntrinsicBlur模糊Android 4.1接近实时中高需控制刷新频率复杂页面可能出现截屏延迟静态模糊图预模糊的图片或渐变遮罩所有版本无几乎为零无法匹配动态背景看到这个表你可能会觉得既然RenderEffect性能好、代码简单直接只用它不就行了。问题就在于国内存量设备里Android 12以下的机型还占着不小比例产品说要兼容到Android 8那RenderEffect就只能当增强优化用不能当主力方案。BlurView这种“截屏-模糊-呈现”的思路虽然有点古老但它硬是能跑在Android 4.1以上的所有设备上兼容性就是它最大的价值。静态模糊图方案我没法把它作为核心因为它的表现力差了一个量级——背景一变静态图就露馅了。但在个别场景我会拿它做保底策略比如极低端设备上BlurView性能扛不住的时候自动降级成静态模糊图体验不至于完全缺失。1.3 为什么最终选择“BlurView RenderEffect”双轨结构标准库和自研我用的是自研的轻量实现架构上其实借鉴了一些开源库的思路。核心原因有三个。第一接口风格统一。自研组件对外暴露的是“模糊半径、蒙层颜色、蒙层透明度”三个参数内部根据系统版本来决定用RenderEffect还是BlurView这样上层业务完全不需要关心实现细节切换和降级都在内部完成。第二降级链路清晰。在主流的Android 10、11设备上跑BlurView在Android 12以上跑RenderEffect如果BlurView抓屏失败或者性能掉帧太厉害自动从模糊半径降到半透明遮罩。这条降级路径在自研组件里可以做得非常顺滑避免线上出现白屏或卡死。第三可控性更强。很多开源库的模糊效果在处理RecyclerView滚动、视频播放这类高频刷新场景时会出现明显的滞后感和锯齿感自研以后可以对刷新频率、缓存策略做精细控制这个后面在性能优化章节详细说。2. 核心细节解析模糊原理与关键参数2.1 Bitmap模糊处理的原理别再把模糊当滤镜先补一个基础点Android里做模糊本质上是对一张位图的像素点做卷积计算。每个像素的新值由它周围一定半径内的像素决定半径radius越大采样范围越大模糊效果越强。听起来不复杂但计算量非常大——一张1080×1920的图如果半径设为25那每个像素要计算大约1961个周边像素的加权和性能再好的手机也扛不住。所以BlurView这类方案在实际操作中的做法是先把要模糊的View区域截成Bitmap然后把这个Bitmap缩小到原来的1/8甚至1/16再进行模糊计算。因为模糊算法对小图的计算量是平方级下降的缩小的图算完以后再放大回原尺寸视觉效果上几乎看不出区别。这也是为什么网上很多模糊实现代码里都有scaleFactor这个参数它就是干这个用的。2.2 关键参数模糊半径、缩放因子、蒙层颜色自研玻璃蒙层组件我对外暴露了三个核心参数下面给出我实践下来比较稳的配置区间。参数作用推荐值备注blurRadius模糊强度10~25dp小于10几乎看不出效果大于25会糊成一片scaleFactor位图缩放比0.1~0.2越小性能越好但过小会显得模糊不均匀overlayColor蒙层颜色与透明度#66000000约40%黑深色背景建议透明度再低一点避免画面太黑这几个参数不是随便配的。模糊半径决定了“玻璃感”的强弱太小看着像普通半透明遮罩太大背景完全变成色块缩放因子是性能和效果之间的天平我在多台测试机上对比过0.125这个值比较平衡也就是先把图缩到1/8再模糊再放大蒙层颜色是决定“高级感”的关键很多新手直接把蒙层做成纯白半透明配浅色背景还好遇到深色背景就显脏建议用黑色或深灰色配低透明度风格更统一。2.3 对背景内容动态变化的处理思路玻璃蒙层最容易翻车的地方其实是背景在动。比如背景里有一个轮播Banner在自动切图或者用户在蒙层后面的列表还在滚动如果模糊是静态的画面就会穿帮。BlurView解决这个问题的思路是监听View树的绘制事件在每一帧绘制前判断背景区域是否发生变化如果变了就重新截屏、模糊、更新纹理。实现方式可以粗犷也可以精细。粗犷做法是每隔几十毫秒强制刷新一次精细做法是给需要模糊的View设置ViewTreeObserver.OnDrawListener只在真正发生绘制的时候才触发重算。我这边还做了一个小优化同一个Window里可能有多个玻璃蒙层组件比如顶部一个、底部一个它们是共享同一份截屏Bitmap的刷新的时候只截一次屏模糊计算也可以复用中间结果避免重复消耗性能。2.4 完整实现代码一个可直接运行的玻璃蒙层组件直接上代码这是我封装的一个自研玻璃蒙层View的核心实现做了简化但主流程完整你可以直接跑起来看效果。class GlassOverlayView JvmOverloads constructor( context: Context, attrs: AttributeSet? null ) : View(context, attrs) { private var blurRadius 20f private var scaleFactor 0.125f private var overlayColor Color.parseColor(#66000000) private val paint Paint(Paint.ANTI_ALIAS_FLAG) private var blurredBitmap: Bitmap? null private var renderEffect: RenderEffect? null // 是否使用 RenderEffect private val useRenderEffect: Boolean get() Build.VERSION.SDK_INT Build.VERSION_CODES.S override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (useRenderEffect) { // 高版本直接用 RenderEffect 做实时模糊 renderEffect RenderEffect.createBlurEffect( blurRadius, blurRadius, Shader.TileMode.CLAMP ) paint.renderEffect renderEffect canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) } else { // 低版本绘制抓取并模糊的 Bitmap再叠加半透明遮罩 blurredBitmap?.let { bitmap - canvas.drawBitmap(bitmap, null, Rect(0, 0, width, height), paint) } canvas.drawColor(overlayColor) } } /** * 刷新背景模糊需要从外部获取根视图引用 * param rootView 当前 Window 的根视图 */ fun refreshBlur(rootView: View) { if (useRenderEffect) { invalidate() return } val location IntArray(2) getLocationOnScreen(location) val screenBitmap grabScreenBitmap(rootView) if (screenBitmap null) { // 抓屏失败降级为纯色遮罩 blurredBitmap null invalidate() return } // 按缩放因子缩小再进行模糊 val scaledBitmap Bitmap.createScaledBitmap( screenBitmap, (width * scaleFactor).toInt().coerceAtLeast(1), (height * scaleFactor).toInt().coerceAtLeast(1), true ) // 回收大图避免内存飙升 if (screenBitmap ! scaledBitmap) { screenBitmap.recycle() } blurredBitmap blurBitmap(scaledBitmap, blurRadius * scaleFactor) scaledBitmap.recycle() invalidate() } private fun grabScreenBitmap(rootView: View): Bitmap? { val bitmap Bitmap.createBitmap( rootView.width, rootView.height, Bitmap.Config.ARGB_8888 ) val canvas Canvas(bitmap) rootView.draw(canvas) return bitmap } private fun blurBitmap(bitmap: Bitmap, radius: Float): Bitmap? { if (Build.VERSION.SDK_INT Build.VERSION_CODES.JELLY_BEAN_MR1) { return bitmap } val rs RenderScript.create(context) val input Allocation.createFromBitmap(rs, bitmap) 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(bitmap) input.destroy() output.destroy() script.destroy() rs.destroy() return bitmap } }这段代码有一个很关键的设计就是RenderEffect分支和BlurView分支共用同一个View入口上层只需要调用refreshBlur()就能触发模糊刷新。在Android 12以上refreshBlur()实际只做了一件事——invalidate()剩下的交给系统对View做实时模糊。而在低版本上会走“截屏-缩放-模糊-重绘”的老路子。2.5 在协调布局CoordinatorLayout Banner场景下的适配热词里有人提到“协调布局banner”的组合这个场景我正好专门调过。Banner轮播图所在的页面通常会有一个ImageView或者ViewPager2玻璃蒙层往往要覆盖一部分Banner区域形成沉浸式视觉。这里要注意的一个坑是截屏不能用View的draw()方法因为部分硬件加速下的View并不会把内容画进你手动创建的Canvas里尤其是视频、SurfaceView这些基于独立窗口的组件抓出来是黑块。解决办法有两个。一个是改用PixelCopy从Window层面做截屏这样能拿到GPU合成后的真实画面视频和Banner都能正确显示另一个是把轮播图的当前帧单独画到Bitmap里作为模糊源。前者通用性强但代码多一点后者只适用于已知内容源的场景。我这边实际项目里用的PixelCopy方案具体实现下一节一起来说。3. 实操过程完整接入与关键环节实现3.1 第一步确定模糊源的获取方式要把后面的内容变模糊首先得拿到后面的画面。在Android里实际操作可以分两种方式第一种是旧的View.draw方式直接把根View画进Bitmap优点是兼容API 19缺点是遇到硬件加速层比如SurfaceView、部分WebView内容会抓到黑块或者空白。第二种是用PixelCopy它从系统层面抓取指定窗口的像素能拿到屏幕真正显示出来的画面和手动draw的结果差别很大。下面是PixelCopy的关键代码从一个Window获取像素fun captureWindowPixelCopy( window: Window, onResult: (Bitmap) - Unit ) { val bitmap Bitmap.createBitmap( window.decorView.width, window.decorView.height, Bitmap.Config.ARGB_8888 ) val location IntArray(2) window.decorView.getLocationOnScreen(location) PixelCopy.request(window, bitmap, { copyResult - if (copyResult PixelCopy.SUCCESS) { onResult(bitmap) } else { // 失败则走备选逻辑 } }, Handler(Looper.getMainLooper())) }这段代码可以放在Activity或者Dialog里调用拿到Bitmap以后再裁剪出玻璃蒙层所在的区域交给模糊管线处理。实测下来PixelCopy的效率和稳定性都高出View.draw一截但注意它要求Android 8.0API 26以上所以低版本还得保留View.draw作为兼容路径。3.2 第二步模糊运算的性能优化策略拿到屏幕Bitmap之后不要直接对它做模糊运算必须先缩小。我用的缩放因子是0.125也就是1/8把1080×1920的大图直接缩到135×240这个尺寸下的模糊运算量比原图小了64倍时间从几百毫秒降到几毫秒。模糊半径也要做联动调整。如果原图模糊半径是25缩到1/8之后传进ScriptIntrinsicBlur的半径就必须是25×0.125≈3。这个不调整的话模糊效果会过强背景会糊成色块。还有个容易翻车的细节RenderScript在Android 12以上已经废弃而且在个别厂商ROM上有兼容性问题。如果你要长期维护建议在API 31以上直接走RenderEffectAPI 31以下再走RenderScript。如果是API 33以上的设备甚至可以直接用RenderEffect.createBlurEffect挂到根View上连PixelCopy都不用做。RenderScript和RenderEffect之间怎么切换参考下面这个版本判断逻辑if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { // 使用 RenderEffect } else if (Build.VERSION.SDK_INT Build.VERSION_CODES.JELLY_BEAN_MR1) { // 使用 RenderScript } else { // 直接降级为半透明遮罩 }3.3 第三步把GlassOverlayView嵌入到页面接入的时候GlassOverlayView作为覆盖层放在页面布局的顶层下面是布局示例FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent !-- 真正的页面内容 -- androidx.viewpager2.widget.ViewPager2 android:idid/banner android:layout_widthmatch_parent android:layout_height200dp / com.example.widget.GlassOverlayView android:idid/glassOverlay android:layout_widthmatch_parent android:layout_height200dp android:layout_gravitybottom / /FrameLayout在代码里需要监听页面绘制完成后再触发模糊刷新否则可能出现蒙层先显示一片黑的情况glassOverlay.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { glassOverlay.viewTreeObserver.removeOnGlobalLayoutListener(this) refreshOverlay() } }) fun refreshOverlay() { val activity context as? Activity ?: return activity.window?.let { window - window.decorView.post { glassOverlay.refreshBlur(window.decorView) } } }这里用post是为了确保布局已经完成防止拿到的是一个0×0的区域。3.4 第四步与Banner自动轮播的联动刷新Banner场景的特殊性在于背景内容在动玻璃蒙层如果不动就显得背景是静止的。所以Banner每次切换图片以后都需要重新触发模糊刷新。比较常见的做法是在Banner的监听回调里调用refreshOverlay()。但要注意如果Banner每3秒切一次图每次都走一次“截屏-缩小-模糊-重绘”全链路内存和CPU压力都不小。我实测下来4秒切一次图的中低端手机还能流畅跑2秒以内就会肉眼可见掉帧。所以我在组件里加了一个模糊结果缓存如果背景内容和上一帧比变化不大就跳过重算画面仍然沿用旧的模糊结果视觉上基本察觉不到差别。override fun refreshBlur(rootView: View) { if (useRenderEffect) { invalidate() return } // 如果距离上次刷新时间小于 100ms直接跳过避免频繁截图 val now System.currentTimeMillis() if (now - lastRefreshTime 100) { return } lastRefreshTime now // 其余逻辑保持不变 }这个100毫秒的节流在Banner快速切换时能挡住大量重复计算亲测有效。4. 常见问题与排查技巧实录4.1 模糊蒙层显示黑色或空白怎么办这是新手最常遇到的问题大部分是因为用View.draw手动创建Canvas时部分内容没有被画出来。检查方向有两个一是确认是不是用了SurfaceView、TextureView、VideoView这类独立渲染的组件如果是改用PixelCopy抓图二是确认GlassOverlayView是否真的在DecorView的顶层如果被遮挡了抓出来的区域自然不是你想要的样子。另一个可能就是截屏拿到的Bitmap本身是空的。可以加一个调试开关把模糊前的Bitmap存到本地一眼就能看出是截屏出问题还是模糊出问题if (BuildConfig.DEBUG) { File(/sdcard/Download/glass_screen.png).apply { FileOutputStream(this).use { out - screenBitmap.compress(Bitmap.CompressFormat.PNG, 100, out) } } }4.2 低端机型卡顿和内存抖动问题中低端机型上玻璃蒙层最容易引发的问题就是内存抖动和掉帧。原因很简单——截一张1080×1920的ARGB_8888位图大约占8MB内存缩放后的Bitmap和模糊后的Bitmap又是两份新对象再加上各种临时变量一次模糊操作轻松吃掉20MB以上内存。如果频繁触发GC就会频繁执行掉帧顺理成章。我的解决思路是“复用Bitmap”。一个是在Activity启动时就创建好缩放后的目标Bitmap截完屏直接画进去不再重复创建另一个是给模糊管线维护一个对象池同一个尺寸的Bitmap重复使用。实测下来内存抖动减少了70%左右。还有一个容易被忽略的问题BlurView刷新频率要控制在每秒不超过10次操作太快时做降级。也就是用户快速滑动或快速操作时把模糊半径临时降低或者暂停模糊刷新等操作停稳了再恢复体验比一直卡顿好得多。4.3 状态栏和刘海屏适配问题做沉浸式玻璃蒙层的时候状态栏区域的适配是个老大难。如果直接把GlassOverlayView铺满全屏状态栏区域也会被模糊掉但Android的状态栏图标是系统绘制的遮罩盖上去图标还是会透过来颜色重叠容易变得很难看。我通常的做法是给蒙层的上下区域预留状态栏高度把模糊效果只应用在内容区域状态栏区域用纯色半透明遮罩代替。这样视觉上依然是整体玻璃感但不会和系统图标打架val statusBarHeight getStatusBarHeight() glassOverlay.setPadding(0, statusBarHeight, 0, 0)获取状态栏高度的写法已经烂大街了这里不再赘述。注意刘海屏设备在不同厂商ROM上的高度值可能不同建议用WindowInsets方式读取比直接读status_bar_height资源更可靠。4.4 常见问题速查表问题可能原因解决方案蒙层黑屏View.draw没抓到内容改用PixelCopy抓取蒙层模糊效果过强缩放后半径未调整半径乘以缩放因子卡顿严重大图直接模糊、频繁刷新缩小后再模糊加节流背景内容不更新Banner切换后没触发刷新监听页面切换回调调refreshBlur状态栏区域模糊后发黑遮罩覆盖了系统图标给蒙层加状态栏避让5. 升级方向实时模糊在更多场景中的应用玻璃蒙层做完以后我发现这套架构可以顺手迁移到另外几个场景里。一个是底部弹窗面板的背景模糊。传统做法是弹窗底下加一层黑色半透明遮罩换成玻璃蒙层以后质感提升非常明显。实现方式就是弹窗show出来的时候获取DecorView的Bitmap模糊后作为弹窗背景注意在弹窗dismiss的时候要及时回收Bitmap。另一个是Tab切换或者页面切换时的过渡模糊。在ViewPager切换过程中给两个页面的背景加一层玻璃质感切换动画会显得非常柔和。这个场景可以利用RenderEffect的实时性直接挂到目标View上配合属性动画调整模糊半径代码量很少效果却很出彩。如果你要长期维护这类组件我建议把模糊这一层单独拆成一个工具类因为未来系统版本可能会继续更新模糊APIAPI 34上用RenderEffectAPI 31上用RenderScript兼容工具类把这些差异封装起来上层业务就不需要跟着系统更新反复改代码了。我在实际做的时候遇到的一个比较让人崩溃的坑是PixelCopy在某些定制ROM的Dialog窗口上会失败返回PixelCopy.ERROR_TIMEOUT。这个问题没有通用的源码级解法最后我是在失败后自动重试两次重试还不行就降级成纯色遮罩至少用户看到的不是一块黑屏白屏。分享出来提醒一下遇到这种问题别死磕降级策略才是保命手段。本文还有配套的精品资源点击获取
返回列表