
随着 Android 12L (API 32) 的发布,Google 引入了一项强大的图形技术 ------ AGSL (Android Graphics Shading Language)。
AGSL 为使用 Jetpack Compose 构建动态、高性能、视觉震撼的 UI 效果打开了全新的大门。如果你曾经对 GLSL 、HLSL 或其他着色语言有所耳闻,那么 AGSL 将是你在 Android 原生开发中实现复杂视觉效果的瑞士军刀,不,是屠龙宝刀!
本篇博客将带你由浅入深、层层递进地掌握 AGSL 的基本概念,并结合 Compose 实现一个简单案例,助你拿到这张大师级图形编程的入场券。
为什么需要 AGSL?
在 AGSL 出现之前,Android 开发者若想实现复杂的图形效果(如模糊、渐变动画、像素化、斜体文字等),通常有以下几种选择:
Bitmap
操作:在主线程或后台线程中操作Bitmap
。该操作性能差,如果在主线程还容易导致卡顿。因为操作实际上是在 CPU 上进行的,很多小伙伴可能认为图形相关的操作都是在 GPU 上运行的,其实不是的,只要是通过代码实现的逻辑,就是在 CPU 上运行的。- OpenGL / Vulkan :功能强大,但学习曲线陡峭,代码复杂,与 Compose 集成困难。很多游戏引擎都是采用的这种方法,几乎是纯 OpenGL 方案。
- 第三方库:如
RenderScript
(已废弃)、Lottie (矢量动画库,结合设计师做矢量动画,非常好用)或 SVGA。但灵活性有限,适用的场景也比较局限。
那么,AGSL 的出现解决了哪些痛点:
- 原生集成:专为 Android 设计,与
Canvas
和ShaderBrush
深度集成。 - 高性能:运行在 GPU 上,效率远超 CPU 计算。
- 简单易用:语法类似 GLSL,但更轻量,无需管理上下文或缓冲区。
- Compose 友好:可直接在
Canvas
或Modifier.graphicsLayer
中使用。
你的第一个 Shader
AGSL 是一种类 C 语言,用于编写运行在 GPU 上的小程序(Shader )。它的核心是 main
函数,接收输入并返回颜色:
c
half4 main(float2 fragCoord) {
return half4(1.0, 0.0, 0.0, 1.0); // 红色
}
half4 main(float2 fragCoord)
是一个 main
函数,也就是该 Shader 的入口函数,每一次像素的计算,都是从这个函数开始的。
该函数接受一个输入,类型是 float2
,float2
表示两个浮点数,也就是一个包含两个浮点数的数组。同理 float3
表示三个浮点数,float4
表示四个浮点数,float5
表示......没有 float5
,哈哈!一般情况下,数组个数只能支持到 4
。
官方称法是矢量,不过这里我们理解称数组,逻辑是一样的。
如果你只需要一个 float
,那就是 float
类型,没有 float1
。
该函数会返回一个 half4
,也就是 4
个 half
的数组。
half
表示中等精度的浮点数,通常是 16
位,也就是 2
个字节。而 float
是高精度浮点数,32
位,也就是 4
个字节。
同理,还有一些其他类型:int
/int2
/int3
/int4
,short
/short2
/short3
/short4
等。
Shader 的作用是:返回 fragCoord
对应的像素坐标的颜色。此时此刻,我们返回的是红色。
half4(1.0, 0.0, 0.0, 1.0)
表示颜色的rgba
分量,也就是颜色的红色,绿色,蓝色,透明度分量,取值范围是[0-1]
。
使用 uniform
uniform
是在 Shader 外部设置、在 Shader 内部保持不变的变量。
c
uniform half4 uColor; // uniform 颜色
half4 main(float2 fragCoord) {
return uColor;
}
此时我们可以暂时理解为,uColor
针对该函数是静态变量,也就是在运行期间 uColor
是全局可访问的。
因为 main
函数是无法更改其输入输出的。它只能接收一个像素坐标,然后返回该坐标的颜色。如果我们想告知 Shader 其他信息,例如时间、尺寸等,我们就需要使用 uniform
传递进去。
好的,Shader 的入门就到这里了,接下来,我们举几个例子实战一下。
Compose 实战
AGSL 本身无法直接绘制内容,它需要通过 RuntimeShader
和 ShaderBrush
集成到 Compose 中。
定义 AGSL 代码
我们就用上面用到的 AGSL 代码:
Kotlin
val simpleShader = """
half4 main(float2 fragCoord) {
return half4(1.0, 0.0, 0.0, 1.0); // 红色
}
""".trimIndent()
第一步我们需要将 AGSL 存入到 Kotlin 的 String
中。
创建 RuntimeShader
kotlin
val shader = remember { RuntimeShader(simpleShader) }
这一步非常简单,就是将这段字符串传递给 RuntimeShader
构造函数即可。考虑到 Compose 的重组问题,所以这里我们需要 remember
,防止 Compose 重组的时候创建新的 RuntimeShader
。
使用 Canvas 绘制
Kotlin
val shaderBrush = remember {
ShaderBrush(shader)
}
Canvas(
modifier = Modifier.size(200.dp)
) {
drawRect(shaderBrush)
}
使用 Canvas
绘制的时候,需要使用 ShaderBrush
来应用 RuntimeShader
。
稍安勿躁,这里概念突然变多了,我们花点时间解释一下这里的工作原理。
Canvas
实际上只是一张画布,他可以绘制任何东西,与 Shader 没有什么关系。
ShaderBrush
是一种笔刷,这个笔刷的如何绘制内容是通过 RuntimeShader
去定义的。
而 RuntimeShader
如何绘制像素,是通过 AGSL 去定义的。
现在,我们要在 Canvas
上绘制一个正方形,就需要使用 drawRect
,drawRect
方法需要一个笔刷 ------ Brush
。因为此处笔刷的实现我们需要使用 AGSL 去定义绘制逻辑,所以使用了 Brush
的一个实现之一 ShaderBrush
,而ShaderBrush
,需要 RuntimeShader
,于是顺理成章。
完整代码如下,其实非常简单:
Kotlin
// simpleShader 是一个全局变量,不存在于 Composable 函数中。
val simpleShader = """
half4 main(float2 fragCoord) {
return half4(1.0, 0.0, 0.0, 1.0); // 红色
}
""".trimIndent()
//...
val shader = remember { RuntimeShader(simpleShader) }
val shaderBrush = remember {
ShaderBrush(shader)
}
Canvas(
modifier = Modifier.size(200.dp)
) {
drawRect(shaderBrush)
}
//...
一睹为快,看看效果:

Nothing less, Nothing more.
一个简单的红色正方形。
渐变
好的,我们已经知道如何使用 AGSL 了,接下来,我们给这个纯色加一点不一样的效果,渐变。
我们修改一下 AGSL 代码:
c
uniform float2 iResolution; // 多了一个 float2 uniform 类型,这里用来表示画布大小
half4 main(float2 fragCoord) {
float gradient = fragCoord.x / iResolution.x;
return half4(gradient, gradient, gradient, 1.0); // 红色
}
这里多加了一个 uniform
类型,用于存储画布的大小,fragCoord.x / iResolution.x
会计算像素在 x
轴上的比率,靠左的像素,会接近 0
,靠右的像素,会接近 1
。
先说一下 AGSL 的坐标系:

AGSL 与 View
的坐标系保持一致,左上角是原点,x
轴和 y
轴的正方向分别向右、向下。
那么上述代码,会形成从左往右,从 0
到 1
的渐变,也就是颜色上从黑色到白色。
Kotlin
val shader = remember { RuntimeShader(simpleShader) }
val shaderBrush = remember {
ShaderBrush(shader)
}
Canvas(
modifier = Modifier.size(200.dp)
) {
shader.setFloatUniform("iResolution", size.width, size.height)
drawRect(shaderBrush)
}
仔细看是如何给 iResolution
赋值的,RuntimeShader
有多个设置 Uniform
的方法,这里 iResolution
是 float2
类型,所以使用 setFloatUniform
,并传递两个 float
值,分别是画布的宽高。
我们看下效果:

关于元素的访问,我们这里使用了 xyzw
------矢量方式进行访问,实际上 AGSL 也支持 rgba
的颜色访问方式。
以下表达式是等价的:
fragCoord.x / iResolution.x
fragCoord.r / iResolution.x
fragCoord.r / iResolution.r
fragCoord.x / iResolution.r
如果你想同时访问多个元素,就是:fragCoord.xy
、fragCoord.yz
、fragCoord.xyz
。
你甚至可以调换顺序:fragCoord.zxy
、fragCoord.wzy
。
这里我们可以用到的时候再说,大家先有个概念即可。
与 graphicsLayer 共舞
我们最后一个案例,将与 Modifier.graphicsLayer
进行结合。
我们先使用一个普通的组件 Image
显示一张普通的图片:
Kotlin
Image(
painter = painterResource(R.drawable.bkg_naruto),
contentDescription = null,
)

我们现在希望实现一个效果,就是将这个图片灰度化,也就是只有黑白之间的过渡,没有其他颜色的情况。
灰度化其实就是颜色的色值 rgb
三个分量是一样的。
我们先编写 AGSL :
c
uniform shader image; // 图片
half4 main(float2 fragCoord) {
half4 sample = image.eval(fragCoord); // 对图片进行采样
float gray = (sample.r + sample.g + sample.b) / 3.0; // 计算平均分量
return half4(gray, gray, gray, 1.0); // 灰度化
}
uniform shader image
定义了一个 shader
类型的全局变量。shader
类型就是一张纹理,这里我们直接理解为当前的显示效果。如果组件是 Image
,显示效果就是当前的图片,如果组件是 Text
,显示效果就是文字。
image.eval(fragCoord)
会对纹理进行采样,也就是获取 image
在坐标 fragCoord
的颜色色值,返回一个 half4
类型。
当我们拿到这个颜色色值的时候,就可以直接进行运算,返回平均分量了,在最后,将每个颜色分量,都设置为这个平均分量,我们的图片灰度化,就成功了!
注意看代码中,因为我们把 sample
当做颜色来对待,所以是用了 rgba
的访问方式,实际上如果用 xyzw
是一样的,但是 rgba
会更加直观,可读性高!
接下来,就是结合 graphicsLayer
使用了:
Kotlin
val shader = remember {
RuntimeShader(grayShader)
}
Image(
painter = painterResource(R.drawable.bkg_naruto),
contentDescription = null,
modifier = Modifier.graphicsLayer {
renderEffect = RenderEffect.createRuntimeShaderEffect(
shader,"image"
).asComposeRenderEffect() // 注意这里的使用方法,与 Canvas 不一样
},
)
我们依然是先创建一个 RuntimeShader
,之后在 Modifier.graphicsLayer
中,需要设置 renderEffect
来使用该效果。
RenderEffect.createRuntimeShaderEffect
会通过 RuntimeShader
创建一个 RenderEffect
,但是 RenderEffect
并不能直接在 Compose 中使用,不过转换也很简单,asComposeRenderEffect
即可。
注意 createRuntimeShaderEffect
这个方法,第一个参数是我们创建的 RuntimeShader
,第二个参数我们给入了 image
,这个与 AGSL 中 uniform shader image
的 image
是一致的,表示将当前的纹理设置给 image
。
官方文档对于
createRuntimeShaderEffect
第二个参数的注释:the uniform name defined in the RuntimeShader's program to which the contents of the RenderNode will be bound.
好,所有工作完毕,我们运行,看看效果:

完美,收工!
总结
AGSL 是 Android 图形编程的一次重大进化。它让原本复杂的 GPU 编程变得触手可及。通过本篇博客的学习,你已经掌握了从基础语法到实战应用的完整链条。
后续我会继续给大家分享关于 AGSL 与 Compose 结合的神奇用法,点个关注,不迷路!
欢迎各位关注公众号,文章同步更新:
