大师级 Compose 图形编程—AGSL 入门

随着 Android 12L (API 32) 的发布,Google 引入了一项强大的图形技术 ------ AGSL (Android Graphics Shading Language)。

AGSL 为使用 Jetpack Compose 构建动态、高性能、视觉震撼的 UI 效果打开了全新的大门。如果你曾经对 GLSLHLSL 或其他着色语言有所耳闻,那么 AGSL 将是你在 Android 原生开发中实现复杂视觉效果的瑞士军刀,不,是屠龙宝刀!

本篇博客将带你由浅入深、层层递进地掌握 AGSL 的基本概念,并结合 Compose 实现一个简单案例,助你拿到这张大师级图形编程的入场券。

为什么需要 AGSL?

AGSL 出现之前,Android 开发者若想实现复杂的图形效果(如模糊、渐变动画、像素化、斜体文字等),通常有以下几种选择:

  1. Bitmap 操作:在主线程或后台线程中操作 Bitmap。该操作性能差,如果在主线程还容易导致卡顿。因为操作实际上是在 CPU 上进行的,很多小伙伴可能认为图形相关的操作都是在 GPU 上运行的,其实不是的,只要是通过代码实现的逻辑,就是在 CPU 上运行的。
  2. OpenGL / Vulkan :功能强大,但学习曲线陡峭,代码复杂,与 Compose 集成困难。很多游戏引擎都是采用的这种方法,几乎是纯 OpenGL 方案。
  3. 第三方库:如 RenderScript(已废弃)、Lottie (矢量动画库,结合设计师做矢量动画,非常好用)或 SVGA。但灵活性有限,适用的场景也比较局限。

那么,AGSL 的出现解决了哪些痛点:

  • 原生集成:专为 Android 设计,与 CanvasShaderBrush 深度集成。
  • 高性能:运行在 GPU 上,效率远超 CPU 计算。
  • 简单易用:语法类似 GLSL,但更轻量,无需管理上下文或缓冲区。
  • Compose 友好:可直接在 CanvasModifier.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 的入口函数,每一次像素的计算,都是从这个函数开始的。

该函数接受一个输入,类型是 float2float2 表示两个浮点数,也就是一个包含两个浮点数的数组。同理 float3 表示三个浮点数,float4 表示四个浮点数,float5 表示......没有 float5,哈哈!一般情况下,数组个数只能支持到 4

官方称法是矢量,不过这里我们理解称数组,逻辑是一样的。

如果你只需要一个 float,那就是 float 类型,没有 float1

该函数会返回一个 half4,也就是 4half 的数组。

half 表示中等精度的浮点数,通常是 16 位,也就是 2 个字节。而 float 是高精度浮点数,32 位,也就是 4 个字节。

同理,还有一些其他类型:int/int2/int3/int4short/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 本身无法直接绘制内容,它需要通过 RuntimeShaderShaderBrush 集成到 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 上绘制一个正方形,就需要使用 drawRectdrawRect 方法需要一个笔刷 ------ 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 的坐标系:

AGSLView 的坐标系保持一致,左上角是原点,x 轴和 y 轴的正方向分别向右、向下。

那么上述代码,会形成从左往右,从 01 的渐变,也就是颜色上从黑色到白色。

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 的方法,这里 iResolutionfloat2 类型,所以使用 setFloatUniform,并传递两个 float 值,分别是画布的宽高。

我们看下效果:


关于元素的访问,我们这里使用了 xyzw------矢量方式进行访问,实际上 AGSL 也支持 rgba 的颜色访问方式。

以下表达式是等价的:

  • fragCoord.x / iResolution.x
  • fragCoord.r / iResolution.x
  • fragCoord.r / iResolution.r
  • fragCoord.x / iResolution.r

如果你想同时访问多个元素,就是:fragCoord.xyfragCoord.yzfragCoord.xyz

你甚至可以调换顺序:fragCoord.zxyfragCoord.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,这个与 AGSLuniform shader imageimage 是一致的,表示将当前的纹理设置给 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 结合的神奇用法,点个关注,不迷路!

AGSL 参考:developer.android.com/develop/ui/...


欢迎各位关注公众号,文章同步更新:

相关推荐
wayne21411 小时前
「原生 + RN 混合工程」一条命令启动全攻略:解密 react-native.config.js
android·react native
一个CCD12 小时前
MySQL主从复制之进阶延时同步、GTID复制、半同步复制完整实验流程
android·mysql·adb
小池先生13 小时前
docker中的mysql变更宿主机映射端口
android·mysql·docker
小孔龙15 小时前
ANR定位手册
android
用户20187928316715 小时前
🎨 Android View背景选择:Shape、PNG与SVG的奥秘
android
大马力拖拉机15 小时前
经验之谈-Fragment中监听返回键
android
张可15 小时前
Kotlin 函数式编程思想
android·前端·kotlin
悟乙己15 小时前
如何区分 Context Engineering 与 Prompt Engineering
android·java·prompt
4Forsee16 小时前
【Android】从复用到重绘的控件定制化方式
android