写完 3.2/3.3 的几百行样板,来看现代路线:同样的效果,AGSL 只要几十行,而且能直接作用在你已有的 Compose UI 上——这是 GLSurfaceView 给不了的能力。因为管线概念你已经懂了,这一章会快得像开挂。
1AGSL 从哪来,和 GLSL 差在哪
Android 的整个 UI(View 和 Compose)由 Skia 图形引擎渲染,Skia 内部的着色语言叫 SkSL。Android 13 把它开放给开发者,起名 AGSL(Android Graphics Shading Language)。所以 AGSL Shader 不是「另开一块画布」,而是作为一个环节插进系统本来就在跑的渲染流程——这就是它能拿 UI 内容当输入的原因。
语言差异一张表(完整心智模型不变,只是改口音):
| GLSL 300 es(本书 Demo) | AGSL | |
|---|---|---|
| 文件头 | #version 300 es + precision | 不需要,直接写 |
| 入口 | void main() + out vec4 | half4 main(float2 fragCoord) |
| 坐标来源 | gl_FragCoord(y 向上!) | 参数 fragCoord(y 向下,同 Canvas) |
| 类型名 | vec2 / mat3 | float2 / float3x3(vec2 也兼容) |
| 颜色精度 | mediump/highp 声明 | half(颜色)/ float(坐标)类型自带 |
| 采样输入图 | texture(uTex, uv) — uv 是 0~1 | content.eval(coord) — coord 是像素 |
| 装载方式 | glCreateShader / 编译 / 链接… | RuntimeShader("源码字符串") |
① y 轴终于顺了:AGSL 的 fragCoord 原点在左上、y 向下,和 Android 坐标一致——从本书 Demo 移植时,所有依赖「y 向上」的效果要翻一次 uv.y = 1.0 - uv.y(或干脆接受镜像)。② eval 用像素坐标:GLSL 的 texture() 吃 0~1 的 UV,AGSL 的 eval() 吃真实像素——归一化和反归一化要自己换算。
2用法一:ShaderBrush 画东西
把 Shader 当画笔,适合动态背景、进度装饰、艺术卡片。以 1.4 章的 IQ 调色板为例,完整移植:
private val FLOW_SHADER = """
uniform float2 uResolution;
uniform float uTime;
half3 palette(float t) {
return half3(0.5) + half3(0.5) * cos(6.28318 * (t + half3(0.0, 0.33, 0.67)));
}
half4 main(float2 fragCoord) {
float2 uv = fragCoord / uResolution; // 归一化,自己来
float t = uv.x * 0.6 + uv.y * 0.2 + uTime * 0.08;
half3 col = palette(t);
// 一点渐晕(y 向下,但渐晕对称,无需翻转)
col *= half(smoothstep(1.1, 0.35, length(uv - 0.5)));
return half4(col, 1.0);
}
"""
@Composable
fun FlowCard(modifier: Modifier = Modifier) {
val shader = remember { RuntimeShader(FLOW_SHADER) }
val time by rememberInfiniteTime() // 见下
Box(
modifier
.clip(RoundedCornerShape(24.dp))
.drawWithCache {
shader.setFloatUniform("uResolution", size.width, size.height)
shader.setFloatUniform("uTime", time)
val brush = ShaderBrush(shader)
onDrawBehind { drawRect(brush) }
}
) { /* 卡片内容照常叠在上面 */ }
}
/** 每帧递增的时间源:Compose 版的 uTime */
@Composable
fun rememberInfiniteTime(): State = produceState(0f) {
val start = withFrameNanos { it }
while (true) {
withFrameNanos { now -> value = (now - start) / 1e9f }
}
}
对比 3.2 章:没有 Surface、没有 Renderer、没有线程军规——Compose 的绘制系统全包了。setFloatUniform 随便在哪个线程调都安全,重组时自动重绘。
3用法二:RenderEffect 处理 UI
王牌功能:声明一个 uniform shader 类型的输入,系统会把这个 Modifier 所修饰的整块 UI 的渲染结果作为纹理喂进来——2.1 章「三把刀」的输入图,现在是你的真实界面。以 2.2 章马赛克为例:
private val PIXELATE = """
uniform shader content; // ← 被修饰的 UI,系统自动喂
uniform float uCell; // 格子大小(像素)
half4 main(float2 fragCoord) {
// 2.2 章的量子化,直接用像素坐标做,连归一化都省了
float2 cellCoord = floor(fragCoord / uCell) * uCell + uCell * 0.5;
return content.eval(cellCoord);
}
"""
fun Modifier.pixelate(cellPx: Float): Modifier = this.graphicsLayer {
val shader = RuntimeShader(PIXELATE)
shader.setFloatUniform("uCell", cellPx)
renderEffect = RenderEffect
.createRuntimeShaderEffect(shader, "content") // "content" 对应声明名
.asComposeRenderEffect()
}
// 用法:任何东西都能打码
Image(painter, null, Modifier.pixelate(24f))
Column(Modifier.pixelate(cellSize)) { /* 整块 UI 一起马赛克 */ }
把 cellPx 接上 animateFloatAsState,就是「内容逐渐清晰」的解锁动画;换成 2.3 章毛玻璃的随机偏移,就是会员内容的磨砂遮罩;换成 2.4 章 glitch,就是错误页面的故障艺术。Phase 2 的每个效果在这里都有一个 UI 岗位。
不用 Compose 的项目同样吃得到:view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(...))(API 33+)。另外系统自带的 RenderEffect.createBlurEffect() 从 API 31 就有——只要模糊不要花活时,优先用它,系统实现有降采样优化。
4GLSL → AGSL 移植清单
把本书 Demo(或 Shadertoy 作品)搬进 AGSL 时,顺着这张清单机械操作即可:
- 删掉
#version 300 es和precision行; out vec4 fragColor; void main()→half4 main(float2 fragCoord),所有fragColor = …改为return …;gl_FragCoord.xy→fragCoord;如效果不对称,补uv.y = 1.0 - uv.y;texture(uTex, uv)→uTex.eval(uv * uTexSize)(记得传一张图的尺寸,或用uniform shader+ 像素坐标);- 颜色相关变量类型尽量改成
half/half3/half4(性能),坐标保持 float; - Kotlin 侧:
RuntimeShader(src)+setFloatUniform,uTime 用withFrameNanos驱动。
5边界与性能
- API 33+。低版本降级方案:效果性内容直接不加(优雅降级),或关键场景走 GLSurfaceView。
- 单 pass:一个 RuntimeShader 就是一趟 Fragment Shader,没有多 pass、没有自定义顶点、不能反馈循环(前一帧的输出当下一帧输入要绕道 Bitmap)。复杂管线仍是路线 A 的地盘。
- 编译时机:RuntimeShader 构造时同步编译,源码有错直接抛异常——所以别在滚动回调里 new,老老实实
remember。 - 性能直觉照旧:AGSL 跑的还是那块 GPU,1.5 章「octave 别堆太多」、2.1 章「大模糊要降采样」的告诫原样适用。graphicsLayer 的 renderEffect 会强制离屏合成一层,别给列表里每个 item 都挂一个重效果。
本章小结
- AGSL = 开放给开发者的 Skia 着色语言(API 33+),插在系统渲染流程里,所以能吃 UI 当纹理。
- 语法差异口诀:头没了、main 收坐标返颜色、y 向下、eval 吃像素、颜色用 half。
- 两种用法:ShaderBrush 当画笔画新内容;RenderEffect +
uniform shader加工已有 UI。 - Phase 2 的所有效果按第 4 节清单机械移植即可;uTime 用 withFrameNanos 驱动。
- 边界:单 pass、33+、构造即编译;多 pass 和相机流回路线 A。
动手练习
- 按移植清单把 2.4 章 CRT 效果搬进 AGSL,套在一张
Image上,做一个「老电视相框」组件。 - 做「磨砂付费墙」:RenderEffect 版毛玻璃(2.3)盖住文章内容,强度接
animateFloatAsState,点按钮后 600ms 内变清晰。 - 压测一次:把你的效果挂到 LazyColumn 的每个 item 上滚动,打开 GPU 渲染分析条,亲眼看看离屏合成的代价,然后改成「只挂在可见的头图上」。