Android 开发4 min2 次浏览

液态玻璃与性能降级:toCode 的视觉系统

用 AGSL 手写 SDF 边缘光着色器、RenderEffect 渐进模糊顶栏、二阶阻尼液态底栏,并以 Full/Degraded/Fallback 三级降级覆盖低端机,减动效模式下自动退场。

液态玻璃与性能降级:toCode 的视觉系统
目录(展开)

「码达 toCode」系列第三篇:在 Compose 里手写"液态玻璃"——SDF 边缘光着色器、渐进模糊顶栏、阻尼拖拽,以及一套面向低端机的三级视觉降级策略。

液态玻璃与性能降级

为什么自研特效层

取件码工具的用户什么设备都有——从旗舰机到三年前的千元机。想要"液态玻璃"这种高感知的视觉品质,又不想在低端机上掉帧,就不能只依赖现成组件库。码达在 Miuix(MIUI 风格 Compose 组件库)之上自建了一层特效系统:ui/effects + ui/glass 两个包,负责玻璃卡片、边缘光、渐进模糊、液态底栏四种效果,并且每一种都内置降级路径。

先看依赖组合:

miuix-ui        = "top.yukonga.miuix.kmp"   # MIUI 风格组件
backdrop        = "io.github.kyant0:backdrop"  # 玻璃折射的"背景采样"
shapes          = "io.github.kyant0:shapes"    # 超椭圆连续圆角

backdrop 库的思路是把"背景内容"建模为可采样的图层(layerBackdrop),玻璃组件再对该图层做模糊与折射——比传统 BlurMaskFilter 方案多出真实的"折射边缘"。码达在此基础上叠加了两层自研能力。

边缘光:用 AGSL 写 SDF

EdgeLight 是最核心的自研特效:沿圆角卡片边缘渲染一圈可调的光,模拟玻璃边缘对环境光的反射。实现在 EdgeLightShaders.kt,用 Android 的 AGSL(RuntimeShader,API 33+)手写了几何着色:

// 圆角矩形的 SDF(有向距离场)
float sdRoundedRect(float2 coord, float2 halfSize, float radius) {
    float2 cornerCoord = abs(coord) - (halfSize - float2(radius));
    float outside = length(max(cornerCoord, 0.0)) - radius;
    float inside  = min(max(cornerCoord.x, cornerCoord.y), 0.0);
    return outside + inside;
}
// 距离场梯度:光沿边缘法线方向衰减
float2 gradSdRoundedRect(float2 coord, float2 halfSize, float radius) { ... }

基于 SDF 与其梯度,三种光样式被统一进一个着色器参数集:

  • Uniform:均匀描边,默认 0.28dp 超细亮线;
  • Directional:定向光,按角度与衰减系数模拟单侧受光;
  • Glow:泛光,光晕沿边缘向外扩散。

使用侧是一个标准 Modifier 扩展(EdgeLightModifier),参数全部 @Immutable + @Stable 预置实例(EdgeLight.Subtle / Prominent),避免重组时反复分配。宽度以半像素为单位调(0.28dp),在暗色背景下"似有若无"——这正是玻璃质感的关键:边缘光不该被看见,应该被感觉到。

渐进模糊顶栏与液态底栏

ProgressiveBlurTopBar 用 RenderEffect 对顶栏背景做渐进模糊——从上到下模糊半径递减,内容滚动时上边缘"融化"进模糊里。为了避免整帧模糊的性能开销,实现按行分段采样背景图层(rememberLayerBackdrop),只模糊顶栏矩形区域。

LiquidBottomTabs 则是交互重头:底部标签以"液滴"形态在选中项之间流动,配合 DampedDragAnimation 实现二阶阻尼——手指拖动时液滴带惯性跟随,松手后以临界阻尼回弹,DragGestureInspector 负责把原始手势解析成速度向量。全部动画走 withFrameNanos 帧驱动,滚动列表时与页面滑动共享同一时间轴。

三级降级策略:Full / Degraded / Fallback

重头戏是 VisualEffectsPolicy。它把设备能力收敛成一个 GlassCapability 值对象,所有特效组件只看 tier,不直接探测硬件:

enum class GlassTier { Full, Degraded, Fallback }

data class GlassCapability(
    val runtimeShaderSupported: Boolean,   // AGSL (API 33+)
    val renderEffectSupported: Boolean,    // RenderEffect (API 31+)
    val topGradientEnabled: Boolean,
    val reduceMotion: Boolean,             // 系统减动效
    val lowRamDevice: Boolean,             // ActivityManager.isLowRamDevice
) {
    val fullLiquidGlass: Boolean get() = runtimeShaderSupported
    val blurGlass: Boolean get() = renderEffectSupported && !lowRamDevice
    val progressiveBlur: Boolean get() = fullLiquidGlass && !lowRamDevice
    val tier: GlassTier
        get() = when {
            progressiveBlur -> GlassTier.Full      // SDF 边缘光 + 渐进模糊
            blurGlass       -> GlassTier.Degraded  // 保留模糊,去着色器
            else            -> GlassTier.Fallback  // 纯色渐变近似
        }
}

决策树一目了然:

flowchart TD
    A["设备能力探测"] --> B{"AGSL 可用且非低内存?"}
    B -- 是 --> C["Tier Full<br/>SDF 边缘光 + 渐进模糊 + 液态底栏"]
    B -- 否 --> D{"RenderEffect 可用?"}
    D -- 是 --> E["Tier Degraded<br/>保留模糊,特效退化"]
    D -- 否 --> F["Tier Fallback<br/>顶栏渐变 + 实色底栏"]

两个值得强调的细节:

  1. 减动效无障碍:rememberReduceMotionEnabled() 监听系统 ANIMATOR_DURATION_SCALE(开发者开启"动画时长为 0"即减动效模式),并注册 ContentObserver 实时响应变化。减动效开启时所有弹性动画退化为直接切换——这是很多特效实现会漏掉的无障碍义务;
  2. 能力值全局缓存:rememberGlassCapability() 以 Application Context 为 key 记忆结果,探测(反射 + ActivityManager 查询)只发生一次,不会在每次重组中重复执行。

小结

这套特效层的经验可以浓缩成三句话:用 SDF 而不是贴图做边缘光(任意圆角、任意尺寸都精确);把"能不能开特效"收敛成一个纯函数(组件只看 tier,探测只做一次);每一种高级效果都准备一个体面的退路(模糊 → 渐变 → 实色)。视觉的华丽程度应该由设备决定,而视觉的一致性由架构保证——低端机用户看到的依然是同一个设计语言,只是少了几层光。

相 关 文 章

3 篇

已运行 —北京时间 --:--:--

© 2026 风起|永远相信美好的事情即将发生!