本文基于 BlurView 3.2.0 源码,用通俗故事 + 源码 + 时序图,带你彻底搞懂 Android 端实时高斯模糊是怎么炼成的。
📖 一、从一个生活场景说起
想象一下:你正在用手机看一个很美的风景照片,突然底部弹出一个半透明的导航栏,它能透出后面照片的内容,但是像隔了一层毛玻璃,朦胧又高级。
这种效果,就是大名鼎鼎的 毛玻璃效果(Frosted Glass / Gaussian Blur) 。
iOS 玩得炉火纯青,Android 开发者看着眼馋。于是有了 BlurView 这个开源库(GitHub 星标 3k+),它让你可以在任何 Android View 上实现动态实时模糊。
今天,我们就化身"源码侦探",钻进 BlurView 的肚子里,看看这层"魔法毛玻璃"到底是怎么贴上去的。
🎭 二、先认识剧中的几位"主角"
在深入源码前,先用一个 "照相馆" 的比喻,认识一下 BlurView 的几位核心角色:
| 角色 | 源码对应类 | 在故事中的身份 |
|---|---|---|
| 🧑🎨 摄影师 | BlurView | 对外的"门面",承接你的所有调用请求 |
| 🎯 被拍摄对象 | BlurTarget | 你想要模糊的背景内容(API 31+ 才有) |
| 📷 老式相机 | PreDrawBlurController | 老版本(API < 31)的摄影师助理,用软件拍照 |
| 📸 数码相机 | RenderNodeBlurController | 新版本(API 31+)的摄影师助理,用硬件拍照 |
| 🖌️ PS 滤镜 | BlurAlgorithm / RenderScriptBlur | 把照片变模糊的"后期滤镜" |
| 📐 缩放师 | SizeScaler | 把大照片缩小,省内存省时间 |
| ✨ 化妆师 | Noise | 给模糊后的照片加点"噪点颗粒",更自然 |
| 🏷️ 标签纸 | BlurViewCanvas | 贴在画布上的标记,防止"自己拍自己"死循环 |
记住这个比喻,后面看代码时你会发现,每一行都在演绎这个故事。
🗺️ 三、整体架构:一图看懂
BlurView 根据手机系统版本,走两条完全不同的路:
┌─────────────────────────────────┐
│ BlurView (门面) │
│ setupWith(target, ...) │
└───────────────┬─────────────────┘
│ 选择哪条路?
┌───────────────┴───────────────┐
│ │
API >= 31 (S) API < 31
│ │
┌───────────▼──────────┐ ┌──────────▼──────────┐
│ RenderNodeBlurCtrl │ │ PreDrawBlurCtrl │
│ (硬件渲染路线) │ │ (软件渲染路线) │
│ BlurTarget 记录快照 │ │ OnPreDrawListener │
│ RenderEffect 模糊 │ │ rootView.draw() │
│ 在 RenderThread 执行 │ │ RenderScriptBlur │
└──────────────────────┘ └─────────────────────┘
这个分流逻辑就在 BlurView.setupWith() 里,只有寥寥几行:
public BlurViewFacade setupWith(@NonNull BlurTarget target, BlurAlgorithm algorithm,
float scaleFactor, boolean applyNoise) {
blurController.destroy();
if (BlurTarget.canUseHardwareRendering) {
// API 31+:忽略传入的算法,强制用 RenderEffect(硬件模糊)
blurController = new RenderNodeBlurController(this, target, overlayColor, scaleFactor, applyNoise);
} else {
// API < 31:走老路,软件绘制 + RenderScript
blurController = new PreDrawBlurController(this, target, overlayColor, algorithm, scaleFactor, applyNoise);
}
return blurController;
}
canUseHardwareRendering 的判断也很直白:
// BlurTarget.java
static final boolean canUseHardwareRendering = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
💡 为什么是 API 31(Android 12)? 因为 Android 12 才正式开放了
RenderEffect和完善的RenderNodeAPI,这两样是硬件模糊的"杀手锏"。
接下来,我们分两条路深入讲解。先讲新路线(推荐重点关注),再讲老路线(理解原理)。
🚀 四、新路线(API 31+):硬件渲染的"数码相机"
4.1 故事篇:数码相机是怎么工作的
在新路线里,流程是这样的:
BlurTarget是一个相框,你把要模糊的内容放进这个相框。- 每次界面要重绘时,
BlurTarget就会用一块叫RenderNode的"数码底片"把自己的内容拍下来,然后正常画到屏幕上。 - 真正的
BlurView(毛玻璃)登场时,它会拿来这块"数码底片" ,套上一个RenderEffect模糊滤镜,然后贴在自己身上。 - 由于
RenderNode和RenderEffect都是硬件加速的,整个模糊过程跑在系统的 Render Thread(渲染线程) ,几乎不占用主线程,性能爆表!
最妙的是:BlurView 从来不主动调用 invalidate() 刷新自己,因为系统每次绘制 View 树时会自动更新 RenderNode 的内容,BlurView 只需要"读取"最新的快照即可。这就是官方说的 "near zero overhead of snapshotting"(快照几乎零开销) 。
4.2 源码篇:四步走
第一步:BlurTarget 拍快照(数码底片)
// BlurTarget.java
@Override
protected void dispatchDraw(@NonNull Canvas canvas) {
if (canUseHardwareRendering && canvas.isHardwareAccelerated()) {
renderNode.setPosition(0, 0, getWidth(), getHeight());
RecordingCanvas recordingCanvas = renderNode.beginRecording(); // 开始录制
super.dispatchDraw(recordingCanvas); // 把子 View 全画进去
renderNode.endRecording(); // 结束录制
canvas.drawRenderNode(renderNode); // 正常显示到屏幕
} else {
super.dispatchDraw(canvas);
}
}
🎯 关键点:RenderNode 就像一个"显示列表(Display List)",它记录绘制指令而不是直接画像素。所以 BlurTarget 画一遍自己,既显示到了屏幕,又把"绘制指令"存了一份在 renderNode 里,供 BlurView 复用。一次绘制,两处使用,这就是零开销的秘密。
第二步:BlurView.draw() 被调用
// BlurView.java
@Override
public void draw(@NonNull Canvas canvas) {
boolean shouldDraw = blurController.draw(canvas); // 委托给控制器
if (shouldDraw) {
super.draw(canvas); // 再画自己的子 View(比如 TabLayout)
}
}
第三步:RenderNodeBlurController 把快照贴上来 + 套滤镜
// RenderNodeBlurController.java
@Override
public boolean draw(Canvas canvas) {
if (!enabled) return true;
saveOnScreenLocation(); // 记录 BlurView 在屏幕上的位置
if (canvas.isHardwareAccelerated()) {
if (!target.renderNode.hasDisplayList()) {
blurView.invalidate(); // 快照还没准备好,下一帧再试
}
hardwarePath(canvas); // ✅ 主力路线
} else {
softwarePath(canvas); // 软件兜底(截图、转场动画时)
}
return true;
}
private void hardwarePath(Canvas canvas) {
blurNode.setPosition(0, 0, target.getWidth(), target.getHeight());
updateRenderNodeProperties(); // 调整位移(让快照对齐 BlurView 位置)
drawSnapshot(); // 把 target 的快照画到自己的 blurNode 上
canvas.save();
canvas.clipRect(0, 0, blurView.getWidth(), blurView.getHeight()); // 不画到边界外
canvas.drawRenderNode(blurNode); // 🎯 把带模糊滤镜的 blurNode 画到屏幕
if (applyNoise) Noise.apply(...); // 加噪点
if (overlayColor != TRANSPARENT) canvas.drawColor(overlayColor); // 叠加颜色
canvas.restore();
}
private void drawSnapshot() {
RecordingCanvas recordingCanvas = blurNode.beginRecording();
if (frameClearDrawable != null) frameClearDrawable.draw(recordingCanvas);
recordingCanvas.drawRenderNode(target.renderNode); // 把 target 的快照画进来
applyBlur(); // 给 blurNode 套上模糊滤镜
blurNode.endRecording();
}
private void applyBlur() {
float realBlurRadius = blurRadius * scaleFactor;
RenderEffect blur = RenderEffect.createBlurEffect(
realBlurRadius, realBlurRadius, Shader.TileMode.CLAMP);
blurNode.setRenderEffect(blur); // ✨ 关键:设置硬件模糊滤镜
}
第四步:跟着 BlurView 滚动/移动
当 BlurView 在可滚动容器里移动时,需要不断更新它的位置,让快照"对齐"到正确的位置:
// RenderNodeBlurController.java
private final ViewTreeObserver.OnPreDrawListener drawListener = () -> {
saveOnScreenLocation();
updateRenderNodeProperties(); // 重新设置 blurNode 的位移/旋转/缩放
return true;
};
private void updateRenderNodeProperties() {
float layoutTranslationX = -getLeft(); // BlurView 相对 target 的偏移
float layoutTranslationY = -getTop();
blurNode.setTranslationX(layoutTranslationX);
blurNode.setTranslationY(layoutTranslationY);
// ... API 31 有 bug,需要重新 applyBlur() 触发重绘
}
💡 这里
getLeft() = blurViewLocation[0] - targetLocation[0],算的是 BlurView 相对于 target 的偏移。模糊后,要把快照"反向位移"过来,对齐到 BlurView 当前位置,看起来就像透过毛玻璃看到了正下方的真实内容。
4.3 时序图:硬件渲染路线全流程
mermaid
RenderThread(渲染线程)blurNode(毛玻璃底片)RenderNodeBlurControllerBlurViewtarget.renderNode(数码底片)BlurTargetAndroid系统(View树遍历)RenderThread(渲染线程)blurNode(毛玻璃底片)RenderNodeBlurControllerBlurViewtarget.renderNode(数码底片)BlurTargetAndroid系统(View树遍历)每一帧绘制周期开始真正的模糊计算在GPU/RenderThread上完成主线程几乎零负担!dispatchDraw(canvas)beginRecording()记录子View绘制指令endRecording()drawRenderNode(node1) 正常显示draw(canvas)draw(canvas)saveOnScreenLocation()记录屏幕坐标beginRecording()drawRenderNode(node1)复用target快照setRenderEffect(模糊滤镜)endRecording()drawRenderNode(node2)叠加噪点+颜色提交所有 RenderNode渲染完成,毛玻璃呈现
📷 五、老路线(API < 31):软件渲染的"老式相机"
5.1 故事篇:老式相机的笨办法
在没有 RenderEffect 的旧手机上,BlurView 只能用"笨办法":
- 每次界面要重绘前(
OnPreDrawListener触发),用一个软件 Canvas 把整个背景内容画到一张Bitmap上(这就是"老式胶卷")。 - 把这张 Bitmap 交给 RenderScript 这个 GPU 滤镜去模糊。
- BlurView 显示时,把这张模糊后的 Bitmap 贴出来。
这个"拍照"过程(rootView.draw())是软件渲染,比较慢,而且每帧都要重拍。所以官方建议老机型用较大的 scaleFactor(默认 4),把 Bitmap 缩小再模糊,牺牲精度换性能。
5.2 源码篇:核心三步
第一步:监听绘制,每帧拍照
// PreDrawBlurController.java
private final ViewTreeObserver.OnPreDrawListener drawListener = new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
// 每次系统准备绘制前,先更新模糊Bitmap
updateBlur();
return true;
}
};
第二步:把背景画到 Bitmap(拍照)
// PreDrawBlurController.java
void updateBlur() {
if (!blurEnabled || !initialized) return;
// 1. 清空"胶卷"
if (frameClearDrawable == null) {
internalBitmap.eraseColor(Color.TRANSPARENT);
} else {
frameClearDrawable.draw(internalCanvas);
}
// 2. 调整矩阵:让快照对齐 BlurView 的位置
internalCanvas.save();
setupInternalCanvasMatrix();
try {
rootView.draw(internalCanvas); // 🎯 把整个背景画到 Bitmap
} catch (Exception e) {
Log.e("BlurView", "Error during snapshot capturing", e);
}
internalCanvas.restore();
// 3. 模糊处理
blurAndSave();
}
这里有个非常巧妙的设计——BlurViewCanvas:
// BlurViewCanvas.java —— 只是一个"标记"类!
public class BlurViewCanvas extends Canvas {
public BlurViewCanvas(@NonNull Bitmap bitmap) { super(bitmap); }
}
它没有任何新方法,纯粹是个标签。它的作用在 draw() 方法里:
// PreDrawBlurController.java
@Override
public boolean draw(Canvas canvas) {
if (canvas instanceof BlurViewCanvas) {
return false; // 🚫 正在拍背景,不要把"自己"拍进去!
}
// ... 正常画模糊内容
}
💡 为什么需要这个? 想象一下:
BlurView也是rootView的子 View。当rootView.draw(internalCanvas)执行时,会递归绘制所有子 View,包括BlurView自己!如果不跳过,就会无限递归(BlurView 画背景→背景里有 BlurView→BlurView 又画背景……)。BlurViewCanvas这个"标签"就是用来识别"这是内部拍照画布,跳过自己"。
第三步:矩阵对齐 + RenderScript 模糊
// PreDrawBlurController.java —— 矩阵对齐
private void setupInternalCanvasMatrix() {
rootView.getLocationOnScreen(rootLocation);
blurView.getLocationOnScreen(blurViewLocation);
int left = blurViewLocation[0] - rootLocation[0];
int top = blurViewLocation[1] - rootLocation[1];
// 因为 Bitmap 被缩小了 scaleFactor 倍,所以位置也要相应换算
float scaleFactorH = (float) blurView.getHeight() / internalBitmap.getHeight();
float scaleFactorW = (float) blurView.getWidth() / internalBitmap.getWidth();
internalCanvas.translate(-left / scaleFactorW, -top / scaleFactorH);
internalCanvas.scale(1 / scaleFactorW, 1 / scaleFactorH);
}
// RenderScriptBlur.java —— GPU 模糊
@Override
public Bitmap blur(@NonNull Bitmap bitmap, float blurRadius) {
Allocation inAllocation = Allocation.createFromBitmap(renderScript, bitmap); // 输入
outAllocation = Allocation.createTyped(renderScript, inAllocation.getType()); // 输出
blurScript.setRadius(min(blurRadius, 25f)); // RenderScript 最大半径 25
blurScript.setInput(inAllocation);
blurScript.forEach(outAllocation); // 🎯 GPU 并行计算模糊
outAllocation.copyTo(bitmap); // 结果写回 Bitmap
return bitmap;
}
5.3 时序图:软件渲染路线全流程
mermaid
internalBitmap(模糊后的胶卷)BlurViewRenderScript(GPU滤镜)internalCanvas(软件画布/胶卷)PreDrawBlurControllerrootView(ViewTreeObserver)Android系统internalBitmap(模糊后的胶卷)BlurViewRenderScript(GPU滤镜)internalCanvas(软件画布/胶卷)PreDrawBlurControllerrootView(ViewTreeObserver)Android系统每一帧绘制前软件渲染整个背景遇到 BlurViewCanvas 自动跳过自己触发 OnPreDrawListeneronPreDraw()updateBlur()eraseColor(清空胶卷)设置对齐矩阵rootView.draw(internalCanvas)拍照完成blur(bitmap, radius)GPU并行计算高斯模糊返回模糊BitmaponPreDraw return truedraw(canvas)draw(canvas)将Bitmap放大画到屏幕+噪点 + 颜色叠加毛玻璃呈现
🔬 六、两个"幕后功臣":SizeScaler 和 Noise
6.1 SizeScaler:缩放师的智慧
老路线里,Bitmap 要缩小 4 倍再模糊。但这里有个坑:
// SizeScaler.java
// Bitmap 的宽度必须能被 64 整除(三星设备的 stride 要求)
private static final int ROUNDING_VALUE = 64;
private int roundSize(int value) {
if (value % ROUNDING_VALUE == 0) return value;
return value - (value % ROUNDING_VALUE) + ROUNDING_VALUE; // 向上取整到 64 的倍数
}
💡 什么是 stride(步长)? RenderScript 处理 Bitmap 时,每行像素在内存里要按固定长度(64)对齐。如果 Bitmap 宽度不是 64 的倍数,系统会偷偷再分配一个对齐的 Bitmap,白白浪费内存和时间。
SizeScaler提前帮你对齐好,避免这个隐藏开销。这就是高手写库的细节!
6.2 Noise:化妆师的"颗粒感"
纯高斯模糊的画面会过于平滑、有塑料感。Noise 类在模糊后叠加一层"蓝噪声"纹理,模拟真实磨砂玻璃的颗粒质感:
// Noise.java
private static void getNoiseBitmap(Context context) {
Bitmap bitmap = BitmapFactory.decodeResource(..., R.drawable.blue_noise);
Paint paint = new Paint();
paint.setAlpha(38); // 15% 透明度,很淡
canvas.drawBitmap(bitmap, 0, 0, paint);
}
// 使用 PorterDuff.Mode.SRC_ATOP,只在已有内容上叠加,不覆盖透明区域
noisePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
noisePaint.setShader(new BitmapShader(alphaBitmap, TileMode.REPEAT, TileMode.REPEAT));
⚡ 七、BlurView 为什么这么快?三大性能秘籍
读完源码,我总结出 BlurView 高性能的三个关键设计:
秘籍 1:从不主动 invalidate(避免无限绘制循环)
很多老库(BlurKit、RealtimeBlurView)的做法是自己 invalidate 自己,触发下一帧重绘,形成循环。这会导致 CPU 持续满载。
BlurView 的做法是被动响应:
- 老路线:监听系统的
OnPreDrawListener,系统自己要重绘时顺便更新 Bitmap。 - 新路线:完全依赖
RenderNode的自动更新,0 次 invalidate。
源码里 PreDrawBlurController.onPreDraw() 的注释说得很清楚:
// Not invalidating a View here, just updating the Bitmap.
// This relies on the HW accelerated bitmap drawing behavior in Android
秘籍 2:硬件加速 + RenderNode(新路线零开销)
新路线用 RenderNode 记录绘制指令而不是像素,BlurTarget 画一遍自己就同时完成了"显示"和"录制快照"两件事。模糊计算交给 RenderEffect,跑在 Render Thread,主线程无感。
秘籍 3:Allocation 复用 + Bitmap 对齐(老路线优化)
// RenderScriptBlur.java —— 复用 Allocation,避免重复创建
if (!canReuseAllocation(bitmap)) {
if (outAllocation != null) outAllocation.destroy();
outAllocation = Allocation.createTyped(renderScript, inAllocation.getType());
}
配合 SizeScaler 提前对齐尺寸,老路线也做到了性能榨干。
🛠️ 八、实战:5 行代码接入 BlurView
8.1 XML 布局
<!-- 1. 用 BlurTarget 包裹要模糊的内容 -->
<eightbitlab.com.blurview.BlurTarget
android:id="@+id/target"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 你的主内容:图片、列表、滚动视图等 -->
<ImageView android:src="@drawable/bg" ... />
</eightbitlab.com.blurview.BlurTarget>
<!-- 2. 放置 BlurView,它会透出下方模糊内容 -->
<eightbitlab.com.blurview.BlurView
android:id="@+id/blurView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:blurOverlayColor="#66FFFFFF">
<!-- BlurView 的子 View 不会被模糊,比如导航栏 -->
<com.google.android.material.tabs.TabLayout ... />
</eightbitlab.com.blurview.BlurView>
8.2 Java 代码
BlurTarget target = findViewById(R.id.target);
BlurView blurView = findViewById(R.id.blurView);
Drawable windowBackground = getWindow().getDecorView().getBackground();
blurView.setupWith(target) // 绑定目标
.setFrameClearDrawable(windowBackground) // 可选:设置背景(透明背景时建议加)
.setBlurRadius(25f); // 模糊半径(实际半径 = radius × scaleFactor)
就这么简单!API 31+ 自动走硬件路线,旧机器自动走软件路线,你完全不用操心。
📊 九、两条路线对比总结
| 维度 | RenderNodeBlurController(API 31+) | PreDrawBlurController(API < 31) |
|---|---|---|
| 快照方式 | RenderNode 录制指令(硬件) | rootView.draw() 软件绘制 |
| 模糊方式 | RenderEffect(硬件,GPU/RenderThread) | RenderScript(GPU 加速) |
| 更新触发 | RenderNode 自动更新 | OnPreDrawListener 每帧检查 |
| invalidate | ❌ 几乎不调用 | ❌ 不调用(更新 Bitmap 即可) |
| 性能开销 | 极低(近乎零开销) | 较高(每帧软件绘制) |
| 支持 TextureView | ✅ 支持 | ❌ 不支持 |
| 主线程影响 | 极小 | 有一定影响 |
| scaleFactor 作用 | 仅放大模糊半径 | 缩小 Bitmap 省性能 |
🎓 十、源码设计亮点回顾(写给想进阶的你)
读完整个 BlurView,我被它几个设计细节深深折服:
- 门面模式(Facade) :
BlurView+BlurViewFacade接口对外提供统一 API,内部用不同 Controller 实现,对用户透明。 - 空对象模式(Null Object) :
NoOpController在未初始化或编辑模式下提供安全默认实现,杜绝 NPE。 - 标记类技巧:
BlurViewCanvas这个"空类"巧妙解决了递归绘制问题,是设计模式的活教材。 - 策略模式:
BlurAlgorithm接口让模糊算法可替换(RenderScript 只是一个实现)。 - 版本自适应:同一个 API,底层根据
Build.VERSION自动切换最优实现,向后兼容。
🏁 结语
毛玻璃效果看似神秘,拆开 BlurView 的源码后,你会发现它的核心思想朴素而精妙:
"先拍一张背景的快照 → 加模糊滤镜 → 贴到目标 View 上"
新旧两条路线的差异,本质上就是 "怎么拍快照" 和 "怎么加滤镜" 的技术演进——从 CPU 软件绘制到 GPU 硬件渲染,从 RenderScript 到 RenderEffect。这背后折射出的,正是 Android 图形系统这些年飞速进步的缩影。
希望这篇文章能帮你彻底搞懂 BlurView。下次产品经理再说"加个毛玻璃效果",你不仅能 5 分钟接入,还能在 Code Review 时给同事讲一讲它背后的故事。😎
📚 参考资料
- BlurView GitHub 仓库
- BlurView 3.0 迁移指南
- Android RenderEffect 官方文档
- Android RenderNode 官方文档
如果觉得本文对你有帮助,欢迎 点赞 + 收藏 + 关注,后续会带来更多 Android 源码深度解析!你有用过哪些毛玻璃方案?欢迎评论区交流~ 💬