尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
用BoxBlur实现Android玻璃蒙层:兼容API21的自定义View完整指南
简介这是一份面向安卓开发者的玻璃蒙层实现工程聚焦于半透明蒙层、高斯模糊及毛玻璃效果适用于需要增强界面层次感与交互反馈的场景技能上覆盖从初级到中级的界面特效实现。压缩包共55个文件仅1.23MB包含Java源码、编译产物、XML布局与主题资源、图片素材和第三方依赖库并附可直接安装的APK结构简洁可快速导入查看。工程围绕“PicDim”示例展开演示了Alpha通道颜色叠加、BitmapShader、Renderscript及第三方模糊库的多套方案并处理了动态蒙层显示隐藏动画与异步模糊、硬件加速等性能优化能帮助开发者理解不同实现路线的取舍与适用场景。已有909人学习下载对于想在自己应用中复现毛玻璃对话框、模糊背景或过渡蒙层的开发者而言是一份实用且轻量的参考样本。1. 玻璃蒙层一个“半透明遮罩”背后藏着一整条渲染链路平时用第三方库做个毛玻璃遮罩只花五分钟可真到要把「android玻璃蒙层」接进弹窗、引导页和侧边抽屉这三类场景时问题就冒出来了低版本设备上 RenderScript 被废弃之后带不动、截出来的背景是一块黑屏、列表滚动时模糊区域跟不上手势。这个项目把整条链路拆成“背景采集 → 降采样 → 模糊 → 蒙层合成”四个环节用 Kotlin 封装成一个可以直接丢进工程的自定义 View兼容 API 21核心逻辑不依赖任何第三方库。适合正在做弹出层、新手引导、局部高亮遮罩以及想搞懂模糊到底怎么算出来的 Android 开发者。2. 技术选型与架构设计三层结构与渲染管线的取舍2.1 三条实现路线对比为什么没有选 RenderScriptAndroid 上做“把背景模糊掉”这件事成熟思路其实只有三条我先把结论放在前面这条项目走的是“离屏截屏 三次 BoxBlur Canvas 合成”路线。方案兼容范围性能表现实现成本后续维护坑RenderScript(ScriptIntrinsicBlur)API 19~3031 起被官方废弃原生调度整体较快低打包要带 support 库部分新设备驱动行为不一致已被官方移出依赖链WindowManager 系统级背景模糊仅 API 31由系统合成器完成耗电低极低Android 12 以下完全不可用部分厂商 ROM 会忽略模糊参数离屏截屏 高斯模糊算法API 21 全兼容取决于采样率与算法复杂度中需要自己管内存、线程和 Bitmap 复用之前我一度想用 RenderScript因为代码最短两行就能把 Bitmap 模糊掉。但后来在一个 targetSdk 31 的项目里发现RenderScript 相关类在部分设备上直接抛异常官方文档也明确标记废弃继续用等于给线上埋雷。而 WindowManager 那条路虽然清爽但只服务 Android 12一个做通用组件的项目不可能接受这个限制。所以最后走的是第三条路截当前 Window 的 DecorView 到一张小尺寸 Bitmap做模糊再盖一层半透明颜色。这样做的最大好处是把“模糊成什么样”完全控制在自己手里后面要支持动态跟手、局部挖空、圆角遮罩都只需要在 Canvas 层做文章。2.2 三层架构采集、模糊、渲染互相解耦我拆项目时习惯先把“会变化的部分”和“相对稳定的部分”分开。玻璃蒙层里最容易变的是三个点背景从哪来、模糊算法用哪种、蒙层怎么画。所以这个源码包内部被拆成了三个层次采集层BackgroundCapturer负责从 DecorView 拿到背景 Bitmap内部做降采样避免 1080P 屏幕全尺寸截屏导致 OOM。模糊层BoxBlur负责把 Bitmap 做近似高斯模糊只暴露一个 blur(src, radius) 方法将来想换 StackBlur 或接入 RenderEffect只动这一层。渲染层BlurMaskView最终用户看到的 View持有模糊后的 Bitmap在 onDraw 里合成背景和半透明遮罩色同时负责触摸事件穿透。三者之间的调用链是这样的伪代码fun attachTo(decorView: View) { // 1. 监听布局变化等 DecorView 拿到有效的宽高 decorView.viewTreeObserver.addOnGlobalLayoutListener { // 2. 采集层截取背景按 scaleFactor 缩成小图 val captured backgroundCapturer.capture(decorView, scaleFactor) // 3. 模糊层做三遍 BoxBlur val blurred BoxBlur.blur(captured, blurRadius) // 4. 渲染层持有结果并触发重绘 blurBitmap blurred invalidate() } }这个链路里每一步的输入输出都是 Bitmap 或 View彼此不持有内部引用。比如某一天你想让背景来源换成“当前页面下方某个列表的截图”只需要替换采集层的实现模糊层和渲染层完全不用碰。这也是我建议你在二次开发时遵守的边界别把截屏逻辑写进 onDraw。2.3 目录结构与核心类职责源码包里的代码不算多去掉注释和空行大概六百行左右结构上按职责分了五个文件glassmask/src/main/java/com/example/glassmask/ ├── mask/ │ ├── BlurMaskView.kt // 自定义 View对外入口 │ ├── BackgroundCapturer.kt // 背景采集与降采样 │ ├── BoxBlur.kt // 三遍 BoxBlur 近似高斯模糊 │ ├── BlurRender.kt // 蒙层合成与圆形挖空绘制 │ └── MaskConfig.kt // 统一参数配置可被 XML 属性覆盖BlurMaskView 是所有逻辑的对外门面持有另外三个类MaskConfig 用 data class 收拢 blurRadius、scaleFactor、maskColor、passCount 这几个可调参数。这种写法让单元测试变得很好做想验证模糊算法直接 new 一个 BoxBlur 丢一张测试图进去不需要起模拟器。3. 核心代码拆解从 DecorView 截屏到模糊上屏的完整链路3.1 BlurMaskView 骨架与 attachTo 调用链BlurMaskView 是项目里唯一需要被业务代码直接触碰的类。构造函数不直接拿 Activity而是暴露一个 attachTo 方法让调用方决定“模糊哪块内容”。这个设计是有意为之如果你在构造函数里通过 context 强转 Activity 拿 DecorView那这个组件就没法用在 Dialog 的 Window 上也没法只对某个 Fragment 区域做模糊。class BlurMaskView JvmOverloads constructor( context: Context, attrs: AttributeSet? null, defStyleAttr: Int 0 ) : View(context, attrs, defStyleAttr) { private val config MaskConfig() private val capturer BackgroundCapturer() private var blurBitmap: Bitmap? null private val drawPaint Paint(Paint.ANTI_ALIAS_FLAG) private val scope CoroutineScope(SupervisorJob() Dispatchers.Main.immediate) fun attachTo(decorView: View) { decorView.viewTreeObserver.addOnGlobalLayoutListener { val w decorView.width val h decorView.height if (w 0 || h 0) returnaddOnGlobalLayoutListener scope.launch { val captured withContext(Dispatchers.Default) { capturer.capture(decorView, config.scaleFactor, config) } val blurred withContext(Dispatchers.Default) { BoxBlur.blur(captured, config.blurRadius, config.passCount) } blurBitmap blurred invalidate() } } } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) blurBitmap?.let { bmp - canvas.drawBitmap( bmp, null, Rect(0, 0, width, height), drawPaint ) } canvas.drawColor(config.maskColor) BlurRender.drawHighlight(canvas, config.highlightRect, config.highlightRadius) } }逻辑说明整个模糊链路由 attachTo 触发核心跳过了主线程。capture 和 blur 都丢到 Dispatchers.Default只有在最后把 Bitmap 赋给成员变量并 invalidate 时才回到主线程。这里用 CoroutineScope 而不是直接在 onGlobalLayout 里同步算是为了避免打开弹窗瞬间卡住主线程。参数说明config.scaleFactor 控制采样倍率数值越大背景图越小、模糊越快但颗粒感越重config.blurRadius 是模糊半径单位是“小图上的像素”不是原始屏幕像素这个换算关系在 3.3 节会展开讲config.maskColor 用 ARGB 格式比如 0x66000000 表示半透明黑。highlightRect 和 highlightRadius 是给引导页高亮用的默认 null 时不绘制。3.2 背景采集降采样和 Canvas.draw 的正确用法采集层是整个项目最容易翻车的地方。新手通常会调用 view.getDrawingCache()这个 API 有两个问题一是它会强制触发一次完整的 measure 和 layout性能开销不可控二是它不支持自定义采样率拿回来的永远是全尺寸图后续模糊压力很大。正确做法是手动创建一张小尺寸 Bitmap用 Canvas 把 DecorView 画进去画之前对 Canvas 做 scale。这样一次 draw 调用既完成了截屏又完成了降采样。object BackgroundCapturer { fun capture(decorView: View, scaleFactor: Int, config: MaskConfig): Bitmap { val targetW (decorView.width / scaleFactor).coerceAtLeast(1) val targetH (decorView.height / scaleFactor).coerceAtLeast(1) val bitmap Bitmap.createBitmap(targetW, targetH, Bitmap.Config.ARGB_8888) val canvas Canvas(bitmap) canvas.scale(1f / scaleFactor, 1f / scaleFactor) canvas.translate(0f, -config.statusBarHeight.toFloat()) decorView.draw(canvas) return bitmap } }逻辑说明先算目标宽高再创建 Bitmap然后用 Canvas.scale 把 DecorView 的内容“压”进小图。注意这里 translate 了 statusBarHeight是因为 DecorView 包含状态栏区域而业务蒙层通常从状态栏下方开始如果不平移模糊出来的画面会整体错位。参数说明scaleFactor 一般取 6 到 10。取 8 意味着 1080x2400 的屏幕会被压成 135x300 的小图像素量只有原来的 1/64后续做三遍 BoxBlur 的耗时基本可以控制在个位数毫秒。数值再小比如 4模糊质量会更高但内存和耗时都会线性上涨再大比如 16背景会糊成色块蒙层一盖基本看不出原本内容。3.3 三次 BoxBlur 实现的高斯近似模糊模糊算法的选择上我没有直接用 StackBlur而是写了更精简的三遍 BoxBlur。BoxBlur 的原理是滑动窗口平均值每个像素取周围 2r1 个像素的平均值。单次 BoxBlur 的效果偏“生硬”但连续做三次之后曲线会逼近高斯模糊视觉上足够干净。object BoxBlur { fun blur(src: Bitmap, radius: Int, passCount: Int 3): Bitmap { var current src repeat(passCount) { current apply(current, radius) } return current } private fun apply(src: Bitmap, r: Int): Bitmap { val w src.width val h src.height val srcPixels IntArray(w * h) val tmpPixels IntArray(w * h) src.getPixels(srcPixels, 0, w, 0, 0, w, h) val div 2 * r 1 // 水平方向滑动窗口 for (y in 0 until h) { var sumA 0 var sumR 0 var sumG 0 var sumB 0 for (x in -r..r) { val px srcPixels[y * w x.coerceIn(0, w - 1)] sumA (px ushr 24) and 0xFF sumR (px ushr 16) and 0xFF sumG (px ushr 8) and 0xFF sumB px and 0xFF } for (x in 0 until w) { val idx y * w x tmpPixels[idx] ((sumA / div) shl 24) or ((sumR / div) shl 16) or ((sumG / div) shl 8) or (sumB / div) val xOut (x - r).coerceIn(0, w - 1) val xIn (x r 1).coerceIn(0, w - 1) val outPx srcPixels[y * w xOut] val inPx srcPixels[y * w xIn] sumA ((inPx ushr 24) and 0xFF) - ((outPx ushr 24) and 0xFF) sumR ((inPx ushr 16) and 0xFF) - ((outPx ushr 16) and 0xFF) sumG ((inPx ushr 8) and 0xFF) - ((outPx ushr 8) and 0xFF) sumB (inPx and 0xFF) - (outPx and 0xFF) } } // 垂直方向滑动窗口结果写回 srcPixels for (x in 0 until w) { var sumA 0 var sumR 0 var sumG 0 var sumB 0 for (y in -r..r) { val srcIdx y.coerceIn(0, h - 1) * w x val px tmpPixels[srcIdx] sumA (px ushr 24) and 0xFF sumR (px ushr 16) and 0xFF sumG (px ushr 8) and 0xFF sumB px and 0xFF } for (y in 0 until h) { val idx y * w x srcPixels[idx] ((sumA / div) shl 24) or ((sumR / div) shl 16) or ((sumG / div) shl 8) or (sumB / div) val yOut (y - r).coerceIn(0, h - 1) val yIn (y r 1).coerceIn(0, h - 1) val outPx tmpPixels[yOut * w x] val inPx tmpPixels[yIn * w x] sumA ((inPx ushr 24) and 0xFF) - ((outPx ushr 24) and 0xFF) sumR ((inPx ushr 16) and 0xFF) - ((outPx ushr 16) and 0xFF) sumG ((inPx ushr 8) and 0xFF) - ((outPx ushr 8) and 0xFF) sumB (inPx and 0xFF) - (outPx and 0xFF) } } return Bitmap.createBitmap(srcPixels, w, h, Bitmap.Config.ARGB_8888) } }逻辑说明滑动窗口的关键技巧是不必每个像素重新遍历 2r1 个邻居只需要在窗口向右移动时把新进入窗口的像素加到 sum把滑出窗口的像素从 sum 里减掉。这段代码先水平扫一遍把结果存在 tmpPixels再垂直扫一遍写回 srcPixels最后用 createBitmap 生成新图。ARGB 四个通道分别累加再分别求平均最后用位运算拼回一个像素。参数说明r 是模糊半径在组件里它作用于降采样后的小图所以视觉上的等效模糊范围约等于 r * scaleFactor。比如 scaleFactor 为 8r 配 18等效原始分辨率下 144 像素的模糊范围这也是为什么很多时候小图半径设 15~25 就已经能出很明显的毛玻璃感。passCount 默认 3大于 3 边际收益递减小于 3 会有明显“糖块感”。3.4 蒙层合成、点击穿透与异步更新蒙层合成在 onDraw 里完成先画模糊 Bitmap再盖一层 maskColor。点击穿透则是重写 onTouchEvent判断触摸点是否落在“允许穿透”的区域。这块逻辑单拎出来说是因为 Android 的触摸事件是在 dispatchTouchEvent 阶段就往下走的onTouchEvent 返回 false 只代表当前 View 不消费并不能直接把事件“送回”给下方兄弟视图所以组件选择用 Region 判定来决定是否拦截。class BlurMaskView JvmOverloads constructor(...) : View(...) { private val touchRegion Region() private val highlightRect RectF() fun setTouchThroughBounds(bounds: ListRectF) { touchRegion.setEmpty() for (rect in bounds) { val scaled Rect( rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt() ) touchRegion.op(scaled, Region.Op.UNION) } invalidate() } override fun onTouchEvent(event: MotionEvent): Boolean { val x event.x.toInt() val y event.y.toInt() // 点在允许穿透的区域里放行给下面 return !touchRegion.contains(x, y) } }逻辑说明setTouchThroughBounds 接收一组 RectF把它们合并进 Region。onTouchEvent 里做一次 contains 判断如果点在穿透区域内就返回 false否则返回 true 消费事件。背面那层模糊背景本身不需要响应点击返回 true 会阻止点击蔓延到蒙层之下的列表避免出现“想关弹窗却点到列表项”的误触。参数说明bounds 里的坐标系是蒙层 View 的本地坐标调用方如果拿的是屏幕坐标记得先做 View 坐标换算。Region.Op.UNION 是并集操作多个矩形可以直接叠加成一块不规则点击区。4. 性能与避坑记录黑屏、掉帧、错位、OOM 的排查实战4.1 现象一打开蒙层瞬间弹窗掉帧明显现象在低端测试机上点击按钮弹出蒙层时掉到每秒 10 帧弹窗动画明显卡顿。原因第一版实现把 capture 和 blur 全放在主线程里。1080P 屏幕全尺寸截图本来就耗时再做三遍模糊每一遍都在遍历几十万个像素主线程被阻塞了 200 多毫秒。解决把模糊链路拆进 Dispatchers.Default同时在采集阶段把 scaleFactor 提到 8。我在接入项目里验证过一组数据同样做三遍半径 18 的模糊全尺寸耗时约 180ms降采样后降到 6ms。注意不要在 capture 和 blur 之间切回主线程否则 Bitmap 对象跨线程传递没有可见性问题但调度开销会白费。4.2 现象二截出来的背景是黑屏现象蒙层弹出来背景区域全黑只能看到前景文案和按钮。原因这个现象有两个来源。一是目标屏幕上如果有 SurfaceView比如视频播放器SurfaceView 的内容是绘制在独立 Surface 上的DecorView.draw() 根本拿不到它的像素只能拿到黑色占位。二是开发者习惯在 capture 之后手动 recycle 掉 source Bitmap结果 Canvas 还没画完就报错表现为花屏而不是黑屏但初学者容易混在一起排查。解决第一个来源要么要求业务方把播放器改成 TextureView要么在蒙层对应区域单独截取 SurfaceTexture 的纹理通用组件内部只能做检测并打日志提示。第二个来源很简单模糊函数返回的是新建 Bitmap原图交给组件内部统一管理onDetachedFromWindow 里再统一回收。4.3 现象三状态栏那块区域模糊错位现象蒙层整体下移状态栏区域露出的背景明显没有经过模糊和下面内容有一条分界线。原因DecorView 的坐标系是从屏幕最顶端开始的而业务上蒙层通常配在 setContentView 的布局里起始点已经在状态栏下方。截屏时不做偏移模糊出来的位图最上面一段是状态栏内容绘制时却被对齐到了内容区顶部。解决capture 时把 statusBarHeight 作为 Canvas 偏移量传入也就是 3.2 节代码里的 canvas.translate(0f, -config.statusBarHeight.toFloat())。statusBarHeight 用 ViewCompat.getRootWindowInsets(decorView) 读取不要自己写反射获取不同厂商返回值的单位偶尔有坑。4.4 现象四反复开关蒙层后内存陡增现象连续开关弹窗十几次Android Profiler 里看到一个明显的内存爬坡每个都是几 MB 的 Byte 数组。原因每次 capture 都创建一个新 Bitmapblur 过程里又产生两个 IntArraysrcPixels 和 tmpPixels加起来单次峰值内存大约是原图的 12 倍字节量。用户开关十次GC 还没来得及回收堆就被打满。解决组件内部维护一个极简 Bitmap 复用池宽高不变时直接复用上一次的 Bitmap 画 Canvas同时把 srcPixels 和 tmpPixels 改成对象池。注意 createBitmap 不支持 inBitmap 复用所以池子是自己用 ArrayDeque 包的取出来前检查宽高是否一致。这个池子生命周期跟随 BlurMaskViewonDetachedFromWindow 时清空。5. 接入与运行把蒙层组件装进业务工程并调通5.1 导入组件与 Gradle 依赖补齐接入方式很直接把上述五个 Kotlin 文件拷进你的工程保持包名一致即可。这个组件没有用任何 AndroidX 专属 API唯一的外部依赖是 kotlinx-coroutines-android因为异步链路里用到了 withContext(Dispatchers.Default)。如果你的工程本身已经在用协程直接复用不需要额外加。implementation org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3如果你的工程是纯 Java 写的也只有一个额外成本把 attachTo 里的协程改写成 HandlerThread Handler 即可模糊函数本身不依赖协程。其实我建议你把 BoxBlur 和 BackgroundCapturer 当成普通 Java 类保留将来任何业务都能直接调用。5.2 XML 布局与 Activity 里的接入代码以最常见的“弹窗蒙层”为例蒙层包在 FrameLayout 里压在所有内容之上前景是一个居中的卡片。注意 BlurMaskView 的宽高必须是 match_parent否则截出来的图没有足够区域容纳背景偏移。FrameLayout android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.recyclerview.widget.RecyclerView android:idid/bottom_list android:layout_widthmatch_parent android:layout_heightmatch_parent / com.example.glassmask.mask.BlurMaskView android:idid/mask_view android:layout_widthmatch_parent android:layout_heightmatch_parent app:blur_radius18 app:scale_factor8 app:mask_color#66000000 / LinearLayout android:idid/dialog_card android:layout_width300dp android:layout_heightwrap_content android:layout_gravitycenter !-- 弹窗内容 -- /LinearLayout /FrameLayoutActivity 里的逻辑分三步找到 View、attach 到 DecorView、按下返回键时让蒙层消失。attach 的时机很关键必须在 DecorView 测量完成之后否则宽高为 0capture 里 coerceAtLeast(1) 会截出 1x1 的图蒙层会是一块纯色。class MainActivity : AppCompatActivity() { private lateinit var maskView: BlurMaskView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) maskView findViewById(R.id.mask_view) // onGlobalLayout 内部会兜底宽高为 0 的情况 maskView.attachTo(window.decorView) maskView.setTouchThroughBounds(listOf( RectF(0f, 0f, 500f, 200f) )) } }逻辑说明attachTo 不是在 onCreate 里立刻截屏而是注册了 ViewTreeObserver.OnGlobalLayoutListener等 DecorView 完成第一次布局后再触发。所以你在 onCreate 里调用也安全。setTouchThroughBounds 把屏幕顶部 500x200 区域设为可穿透事件会漏到下面的列表。参数说明stage 判断实际项目中通常会由“是否已显示过引导”之类状态控制这块属于业务逻辑组件不掺和。5.3 核心参数调优表与运行时报错排查调整毛玻璃效果时我建议按“先采样率、再模糊半径、最后透明度”的顺序来不要三个一起改否则某个参数出问题你根本不知道是谁的锅。参数推荐范围调大效果调小效果scaleFactor6~10更省内存速度更快颗粒感变强画面更细腻耗时和内存上升blurRadius12~25背景更糊看不清具体文字偏透明玻璃能隐约辨认文字maskColor 透明度0x40~0x8C前景内容更突出背景压制更深背景透出更多层次感更强passCount2~4更接近高斯平滑耗时线性增加回退到“窗口平均”的粗糙感运行时报错最常见的三个我在接入时都踩过一是日志出现 “Canvas: trying to use a recycled bitmap”原因是 Bitmap 在子线程被 recycle 了二是 “Only the original thread that created a view hierarchy can touch its views”原因是模糊结果在主线程之外调用了 invalidate三是 “RenderScript not supported” 这行那是历史版本用过时 API 留下的当前组件里已经没有 RenderScript换用 BoxBlur 后不会再有这条日志。6. 二次开发把静态蒙层升级为跟随手势的实时动态遮罩6.1 动态模糊半径让手感像系统通知栏一样跟手目前组件只在布局变化时算一次模糊。要做成“手指下拉、模糊跟着变强”的效果需要公开一个 setBlurRadius 方法内部用 ValueAnimator 把半径从 0 平滑过渡到目标值。fun animateBlurRadiusTo(targetRadius: Int, durationMs: Long 300) { val start config.blurRadius ValueAnimator.ofInt(start, targetRadius).apply { duration durationMs addUpdateListener { anim - setBlurRadius(anim.animatedValue as Int) } start() } }每帧回调 setBlurRadius 都会触发一次背景重截和模糊所以这里必须做节流。我一般会让 capture 和 blur 复用 3.2 节那个小图并且只在 radius 变化超过 2 个像素时才真正重算否则直接沿用上一帧的 Bitmap。6.2 滚动监听更新与圆形挖空高亮静态截图有一个短板底部列表滚动时蒙层里的背景不会跟着动看起来像贴了一张壁纸。改造方法是给目标 RecyclerView 注册 OnScrollListener在 onScrolled 回调里调用 BlurMaskView.updateNow()让组件重新走一遍 capture 链路。注意滚动监听里事件频率很高建议配合跳跃缩放列表每滚动超过 30 个像素才触发一次。圆形挖空则是 BlurRender.drawHighlight 里要做的事先把模糊 Bitmap 画到 Canvas再用 PorterDuff.Mode.DST_OUT 画一个圆把圆内的透明度清零露出清晰的原始内容。fun drawHighlight(canvas: Canvas, rect: RectF, radius: Float) { canvas.saveLayer(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), null) canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) paint.xfermode PorterDuff.Mode.DST_OUT canvas.drawOval(rect, paint) canvas.restore() }6.3 验证方法改完后别只看着顺眼上一台中端机在开发者选项里打开“GPU 渲染模式分析”观察蒙层出现那一帧的条形图高度。三遍 BoxBlur 加全屏合成小图场景下整帧耗时应该压在 8ms 以内超过这个数先检查是不是又有人把 scaleFactor 调回 4。我还保留一个习惯每轮改动都强制把关掉硬件加速的路径也跑一遍。因为很多自定义 View 只在软件绘制时才会暴露 Canvas 层级的错误硬件加速下 GPU 自动帮你兜住了。那次我没关硬件加速在几台低端机线上翻车之后这个流程就成了固定动作。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Java连接MySQL与数据交互:从JDBC到连接池的完整实战指南

Java连接MySQL与数据交互:从JDBC到连接池的完整实战指南

用 Java 连接 MySQL 数据库,看起来是个老掉牙的话题,但真正自己动手写过一遍的人都知道,这里面的坑比想象中多得多。尤其是刚接触 Java 后端的朋友,第一次调通 JDBC 代码,心情往往不是“终于学会了”,而是“…

📅 2026/10/19 19:05:49
元宝    LeetCode 675. Cut Off Trees for Golf Event Python3 Implement

元宝 LeetCode 675. Cut Off Trees for Golf Event Python3 Implement

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/19 19:05:49
3D视觉Infra核心痛点与落地实践:从数据采集到线上监控

3D视觉Infra核心痛点与落地实践:从数据采集到线上监控

我是在一个自动化设备项目里,第一次被“3D视觉Infra”这个词击中。当时团队里算法、工程、产品各干各的,模型训练出来效果也不差,可一到现场测试就翻车。后来排查发现,问题根本不在模型,而在于数据采集时的标定乱了、标…

📅 2026/10/19 19:05:49
MORE NEWS

更多资讯

📰

Python+大模型+微博舆情分析:从数据采集到可视化全链路实战指南

每年到毕业设计季,“微博舆情分析”这个方向都会被大量同学翻牌,尤其是加上Python和大模型之后,听起来既有技术含量又紧跟热点。但真正动手之后很多人会发现:微博数据怎么拿、情感分析怎么做准、大模型怎么接、预测结果怎么才能不…

📰

浅层模型与深度神经网络的选型之道:从特征工程到工程成本的全面对比

做算法这些年,我参加过的技术选型会里,争论最热烈的永远是同一个话题:传统机器学习(浅层模型)和深度学习(深度神经网络)到底该选哪个。上周项目评审,我提的方案是先上XGBoost搭基线&…

📰

Qt通过COM操作Word生成文档:完整实战指南

1. 为什么项目里总会冒出"Qt输出Word"的需求做了几年Qt桌面端开发,你会发现一个规律:凡是涉及业务系统、办公自动化的项目,十有八九会碰到"在程序里生成Word文档"这种需求。比如项目里要定期输出月度报告、按模板生成合同…

📰

Spring Boot四层XSS防御实战:过滤器+包装器+CSP+输出编码

我处理项目里那个"搜索框变成按钮"的Bug,才真正理解XSS不是文档里的一句名词,而是能在几分钟内把Cookie、页面内容、用户操作全部劫走的入口。当时用Spring Boot做的后台管理系统,前端同学随手往标题栏输入了一段带尖括号的内容&am…

📰

U2-Flash 1M上下文实测:从能塞到能用的工程实践

1. 当"1M上下文"不再是PPT上的数字第一次看到"1M上下文"这个说法,我的反应是:又一个拿理论值说事的营销话术。做过大模型应用的人都知道,上下文窗口标称值和实际可用值之间,往往隔着一道叫"注意力衰减&q…

📰

双麦克风语音分离:基于GCC-PHAT与IVA的轻量级实时声源解耦方案

简介:本资源是一套基于MATLAB实现的双麦克风语音分离完整源码工程,面向音频信号处理初学者、语音算法研究者及高校相关课程实践者,聚焦于利用TDOA与空间滤波技术从混合语音中分离目标声源,适用于噪声抑制、会议系统增强与语音识别…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬