compose-graphics
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCompose Graphics
Compose 图形开发
Advanced Compose visuals: M3 Expressive motion physics, AGSL shaders (Android 13+), Canvas / DrawScope, graphicsLayer effects. Loaded for advanced thesis (shader, expressive, M3 Expressive, AGSL, Canvas, holographic). Foundation:covers basics. Concise rules here. Deep-dive in../compose-motion/SKILL.md.references/
高级Compose视觉效果:M3动态物理动效、AGSL着色器(Android 13+)、Canvas/DrawScope绘图、graphicsLayer特效。 专为高级主题研究设计(着色器、动态效果、M3动态、AGSL、Canvas、全息效果)。 基础内容:涵盖基础知识。本文为精简规则,深度内容见../compose-motion/SKILL.md目录。references/
Decision Tree: Which API for Which Need
决策树:按需选择合适的API
| Need | API |
|---|---|
| Spring physics with bounce / overshoot | |
| Pixel-level shader | |
| Generative drawing (paths, particles, fractals) | |
| GPU effects (blur, shadows, color filters) | |
| Adaptive system materials (Material You glassmorphism) | |
| Liquid glass on Android | AGSL shader recipe (no native API like iOS yet) |
| 需求 | API |
|---|---|
| 带回弹/过冲的弹簧物理动效 | |
| 像素级着色器 | |
| 生成式绘图(路径、粒子、分形) | |
| GPU特效(模糊、阴影、颜色滤镜) | |
| 自适应系统材质(Material You毛玻璃效果) | |
| Android液态玻璃效果 | AGSL着色器方案(目前无iOS类似原生API) |
Domain 1: Material 3 Expressive
领域1:Material 3动态动效
What It Is
什么是Material 3动态动效
The 2025 Material 3 evolution introduces spring-based motion physics replacing fixed-duration tweens. New shape morphing API via . New selectable on the theme. Aimed at hero moments, key interactions, brand-defining UI.
androidx.graphics.shapesMotionScheme2025年Material 3演进版本引入基于弹簧的物理动效,替代固定时长的补间动画。新增基于的形状变形API,主题中可选择新的。适用于核心交互场景、关键操作、品牌化UI设计。
androidx.graphics.shapesMotionSchemeMotionScheme
MotionScheme
| Scheme | Personality | Use For |
|---|---|---|
| Calmer, less overshoot | Default for chrome, lists, navigation |
| More overshoot, longer settle | Hero reveals, FABs, primary CTAs |
Apply on the theme:
kotlin
MaterialTheme(motionScheme = MotionScheme.expressive()) {
// children read tokens via MaterialTheme.motionScheme.*
}Tokens exposed:
| Token | Domain | Speed |
|---|---|---|
| Position / size | < 200ms |
| Position / size | ~ 350ms |
| Position / size | ~ 600ms |
| Opacity / color | < 150ms |
| Opacity / color | ~ 250ms |
| Opacity / color | ~ 400ms |
Spatial vs Effects: spatial = anything physical (height, offset, scale). Effects = visual properties without inertia (alpha, color, elevation). Springs feel natural for spatial; tweens feel right for effects. The tokens encode this for you.
| 方案 | 特性 | 适用场景 |
|---|---|---|
| 更平缓,过冲更少 | 导航栏、列表、通用界面默认方案 |
| 过冲更明显,动画时长更长 | 核心内容展示、悬浮按钮(FAB)、主要操作按钮(CTA) |
在主题中应用:
kotlin
MaterialTheme(motionScheme = MotionScheme.expressive()) {
// 子组件通过MaterialTheme.motionScheme.*获取动效参数
}暴露的参数:
| 参数 | 适用领域 | 速度 |
|---|---|---|
| 位置/尺寸变化 | < 200ms |
| 位置/尺寸变化 | ~ 350ms |
| 位置/尺寸变化 | ~ 600ms |
| 透明度/颜色变化 | < 150ms |
| 透明度/颜色变化 | ~ 250ms |
| 透明度/颜色变化 | ~ 400ms |
空间动效vs视觉特效: 空间动效指物理属性变化(高度、偏移、缩放);视觉特效指无惯性的视觉属性变化(透明度、颜色、阴影)。弹簧动效更适合空间变化,补间动画更适合视觉特效。上述参数已封装好对应逻辑。
Hero Card Expand (Expressive Springs)
核心卡片展开效果(动态弹簧动效)
kotlin
@Composable
fun ExpressiveHero() {
var expanded by remember { mutableStateOf(false) }
MaterialTheme(motionScheme = MotionScheme.expressive()) {
val transition = updateTransition(targetState = expanded, label = "expand")
val height by transition.animateDp(
transitionSpec = { MaterialTheme.motionScheme.slowSpatialSpec() },
label = "height"
) { if (it) 400.dp else 100.dp }
val alpha by transition.animateFloat(
transitionSpec = { MaterialTheme.motionScheme.defaultEffectsSpec() },
label = "alpha"
) { if (it) 1f else 0f }
Card(
modifier = Modifier
.fillMaxWidth()
.height(height)
.clickable { expanded = !expanded }
) {
Box(modifier = Modifier.alpha(alpha)) {
Text("Detail content", modifier = Modifier.padding(24.dp))
}
}
}
}kotlin
@Composable
fun ExpressiveHero() {
var expanded by remember { mutableStateOf(false) }
MaterialTheme(motionScheme = MotionScheme.expressive()) {
val transition = updateTransition(targetState = expanded, label = "expand")
val height by transition.animateDp(
transitionSpec = { MaterialTheme.motionScheme.slowSpatialSpec() },
label = "height"
) { if (it) 400.dp else 100.dp }
val alpha by transition.animateFloat(
transitionSpec = { MaterialTheme.motionScheme.defaultEffectsSpec() },
label = "alpha"
) { if (it) 1f else 0f }
Card(
modifier = Modifier
.fillMaxWidth()
.height(height)
.clickable { expanded = !expanded }
) {
Box(modifier = Modifier.alpha(alpha)) {
Text("详情内容", modifier = Modifier.padding(24.dp))
}
}
}
}Spring Tuning Recipes
弹簧动效调优方案
| Mood | Spec |
|---|---|
| Hero reveal | |
| Snappy expressive | |
| Calm spatial | |
| Critical (no overshoot) | |
| 风格 | 参数配置 |
|---|---|
| 核心内容展示 | |
| 轻快动态效果 | |
| 平缓空间动效 | |
| 严谨无过冲 | |
Shape Morphing (M3 Expressive 1.3+)
形状变形(M3动态动效1.3+)
androidx.graphics.shapesMaterialShapes.CirclePentagonCookie4SidedSunnyHeartMorph(start, end)kotlin
val morph = remember { Morph(MaterialShapes.Circle, MaterialShapes.Cookie4Sided) }
val progress by animateFloatAsState(
targetValue = if (active) 1f else 0f,
animationSpec = MaterialTheme.motionScheme.slowSpatialSpec(),
label = "morph"
)
Box(
modifier = Modifier
.size(96.dp)
.clip(GenericShape { size, _ ->
addPath(
morph.toPath(progress).asAndroidPath().asComposePath()
)
})
.background(MaterialTheme.colorScheme.primary)
)androidx.graphics.shapesMaterialShapes.CirclePentagonCookie4SidedSunnyHeartMorph(start, end)kotlin
val morph = remember { Morph(MaterialShapes.Circle, MaterialShapes.Cookie4Sided) }
val progress by animateFloatAsState(
targetValue = if (active) 1f else 0f,
animationSpec = MaterialTheme.motionScheme.slowSpatialSpec(),
label = "morph"
)
Box(
modifier = Modifier
.size(96.dp)
.clip(GenericShape { size, _ ->
addPath(
morph.toPath(progress).asAndroidPath().asComposePath()
)
})
.background(MaterialTheme.colorScheme.primary)
)When to Use Expressive vs Standard
何时使用动态动效vs标准动效
- Expressive: hero moments, key interactions, FABs, primary CTAs. 1-3% of UI.
- Standard: default for the rest of the app. Mixing too much Expressive feels chaotic - every element fighting for attention.
- 动态动效: 核心内容展示、关键交互、悬浮按钮、主要操作按钮。占UI总量的1-3%。
- 标准动效: 应用其余界面的默认方案。过度使用动态动效会导致界面混乱,所有元素都在争夺注意力。
Domain 2: AGSL Shaders (Android 13+)
领域2:AGSL着色器(Android 13+)
What It Is
什么是AGSL着色器
AGSL is Android's shader language. Similar to GLSL with simplifications ( instead of , restricted feature set, sandbox-safe). compiles your AGSL source. Bind to a Compose modifier via .
half4vec4RuntimeShaderModifier.graphicsLayer { renderEffect = ... }AGSL是Android专属着色器语言,与GLSL类似但做了简化(用替代,功能集受限,沙箱安全)。可编译AGSL源码,通过绑定到Compose修饰符。
half4vec4RuntimeShaderModifier.graphicsLayer { renderEffect = ... }Setup
基础配置
kotlin
@Composable
fun ShaderEffect(content: @Composable () -> Unit) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// Android 12 and below: skip the shader, render content as-is.
content()
return
}
val shader = remember { RuntimeShader(AGSL_SOURCE) }
val time by produceState(0f) {
while (true) {
withFrameMillis { ms ->
value = ms / 1000f
}
}
}
Box(
modifier = Modifier
.onSizeChanged {
shader.setFloatUniform("resolution", it.width.toFloat(), it.height.toFloat())
}
.graphicsLayer {
shader.setFloatUniform("time", time)
renderEffect = RenderEffect
.createRuntimeShaderEffect(shader, "image")
.asComposeRenderEffect()
}
) { content() }
}Always gate on. Pre-Android 13 fallback: render the content unmodified, or useBuild.VERSION.SDK_INT >= 33/ a static gradient overlay.Modifier.blur()
kotlin
@Composable
fun ShaderEffect(content: @Composable () -> Unit) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// Android 12及以下版本:跳过着色器,直接渲染内容
content()
return
}
val shader = remember { RuntimeShader(AGSL_SOURCE) }
val time by produceState(0f) {
while (true) {
withFrameMillis { ms ->
value = ms / 1000f
}
}
}
Box(
modifier = Modifier
.onSizeChanged {
shader.setFloatUniform("resolution", it.width.toFloat(), it.height.toFloat())
}
.graphicsLayer {
shader.setFloatUniform("time", time)
renderEffect = RenderEffect
.createRuntimeShaderEffect(shader, "image")
.asComposeRenderEffect()
}
) { content() }
}务必通过做版本判断。 Android 13之前的版本降级方案:直接渲染内容,或使用Build.VERSION.SDK_INT >= 33/静态渐变覆盖。Modifier.blur()
Recipe: Touch Ripple
示例:触摸波纹效果
AGSL source:
glsl
// ripple.agsl
uniform float2 resolution;
uniform float2 origin;
uniform float time;
uniform shader image;
half4 main(float2 fragCoord) {
float2 toOrigin = fragCoord - origin;
float dist = length(toOrigin);
float wave = sin(dist * 0.05 - time * 8.0) * 0.05;
float falloff = 1.0 / max(dist * 0.01, 1.0);
float2 dir = toOrigin / max(dist, 0.0001);
float2 displaced = fragCoord + dir * wave * falloff * 50.0;
return image.eval(displaced);
}Compose binding (state-driven , animated via ):
timeLaunchedEffectkotlin
@Composable
fun RippleSurface(content: @Composable BoxScope.() -> Unit) {
if (Build.VERSION.SDK_INT < 33) { Box { content() }; return }
val shader = remember { RuntimeShader(RIPPLE_AGSL) }
var origin by remember { mutableStateOf(Offset.Zero) }
var startMs by remember { mutableStateOf<Long?>(null) }
val time by produceState(0f, startMs) {
if (startMs == null) { value = 0f; return@produceState }
val begin = startMs!!
while (true) {
withFrameMillis { now ->
value = (now - begin) / 1000f
}
}
}
Box(
modifier = Modifier
.onSizeChanged {
shader.setFloatUniform("resolution", it.width.toFloat(), it.height.toFloat())
}
.pointerInput(Unit) {
detectTapGestures { tap ->
origin = tap
startMs = System.currentTimeMillis()
}
}
.graphicsLayer {
shader.setFloatUniform("origin", origin.x, origin.y)
shader.setFloatUniform("time", time)
renderEffect = RenderEffect
.createRuntimeShaderEffect(shader, "image")
.asComposeRenderEffect()
},
content = content
)
}AGSL源码:
glsl
// ripple.agsl
uniform float2 resolution;
uniform float2 origin;
uniform float time;
uniform shader image;
half4 main(float2 fragCoord) {
float2 toOrigin = fragCoord - origin;
float dist = length(toOrigin);
float wave = sin(dist * 0.05 - time * 8.0) * 0.05;
float falloff = 1.0 / max(dist * 0.01, 1.0);
float2 dir = toOrigin / max(dist, 0.0001);
float2 displaced = fragCoord + dir * wave * falloff * 50.0;
return image.eval(displaced);
}Compose绑定(状态驱动,通过实现动画):
timeLaunchedEffectkotlin
@Composable
fun RippleSurface(content: @Composable BoxScope.() -> Unit) {
if (Build.VERSION.SDK_INT < 33) { Box { content() }; return }
val shader = remember { RuntimeShader(RIPPLE_AGSL) }
var origin by remember { mutableStateOf(Offset.Zero) }
var startMs by remember { mutableStateOf<Long?>(null) }
val time by produceState(0f, startMs) {
if (startMs == null) { value = 0f; return@produceState }
val begin = startMs!!
while (true) {
withFrameMillis { now ->
value = (now - begin) / 1000f
}
}
}
Box(
modifier = Modifier
.onSizeChanged {
shader.setFloatUniform("resolution", it.width.toFloat(), it.height.toFloat())
}
.pointerInput(Unit) {
detectTapGestures { tap ->
origin = tap
startMs = System.currentTimeMillis()
}
}
.graphicsLayer {
shader.setFloatUniform("origin", origin.x, origin.y)
shader.setFloatUniform("time", time)
renderEffect = RenderEffect
.createRuntimeShaderEffect(shader, "image")
.asComposeRenderEffect()
},
content = content
)
}Recipe: Holographic Gradient
示例:全息渐变效果
glsl
uniform float time;
uniform shader image;
half4 main(float2 fragCoord) {
half4 color = image.eval(fragCoord);
float n = fragCoord.x * 0.01 + fragCoord.y * 0.005 + time * 0.3;
half3 rainbow = half3(
sin(n * 2.0) * 0.5 + 0.5,
sin(n * 2.0 + 2.094) * 0.5 + 0.5,
sin(n * 2.0 + 4.188) * 0.5 + 0.5
);
half luma = dot(color.rgb, half3(0.299, 0.587, 0.114));
return half4(mix(color.rgb, rainbow * luma * 2.0, 0.5), color.a);
}Multiplies the underlying view's luminance by a phase-shifted RGB sine wave - shimmer over text or icons. animates the wave drift.
timeglsl
uniform float time;
uniform shader image;
half4 main(float2 fragCoord) {
half4 color = image.eval(fragCoord);
float n = fragCoord.x * 0.01 + fragCoord.y * 0.005 + time * 0.3;
half3 rainbow = half3(
sin(n * 2.0) * 0.5 + 0.5,
sin(n * 2.0 + 2.094) * 0.5 + 0.5,
sin(n * 2.0 + 4.188) * 0.5 + 0.5
);
half luma = dot(color.rgb, half3(0.299, 0.587, 0.114));
return half4(mix(color.rgb, rainbow * luma * 2.0, 0.5), color.a);
}将底层视图的亮度与相位偏移的RGB正弦波相乘,为文字或图标添加闪烁效果。参数控制波浪的漂移动画。
timeRecipe: Liquid Glass / Glassmorphism
示例:液态玻璃/毛玻璃效果
Android lacks a native iOS-26-style . Compose this with blur + chromatic aberration + soft edge tint:
glassEffectglsl
uniform float2 resolution;
uniform shader image;
half4 main(float2 fragCoord) {
float2 uv = fragCoord / resolution;
// Chromatic aberration: sample R, G, B at slightly different offsets
float2 ca = (uv - 0.5) * 0.004;
half r = image.eval(fragCoord + ca * resolution).r;
half g = image.eval(fragCoord).g;
half b = image.eval(fragCoord - ca * resolution).b;
half4 base = half4(r, g, b, 1.0);
// Soft edge tint: brighten near borders for that frosted look
float edge = smoothstep(0.45, 0.5, max(abs(uv.x - 0.5), abs(uv.y - 0.5)));
half3 tinted = base.rgb + half3(edge * 0.08);
return half4(tinted, 0.85);
}Pair with upstream for the actual blur (cheaper than computing it in AGSL).
Modifier.blur(20.dp, BlurredEdgeTreatment.Unbounded)Android没有iOS 26风格的原生。可通过模糊+色差+边缘柔化色调组合实现:
glassEffectglsl
uniform float2 resolution;
uniform shader image;
half4 main(float2 fragCoord) {
float2 uv = fragCoord / resolution;
// 色差:在略微不同的偏移位置采样R、G、B通道
float2 ca = (uv - 0.5) * 0.004;
half r = image.eval(fragCoord + ca * resolution).r;
half g = image.eval(fragCoord).g;
half b = image.eval(fragCoord - ca * resolution).b;
half4 base = half4(r, g, b, 1.0);
// 边缘柔化色调:在边框附近提亮,实现磨砂效果
float edge = smoothstep(0.45, 0.5, max(abs(uv.x - 0.5), abs(uv.y - 0.5)));
half3 tinted = base.rgb + half3(edge * 0.08);
return half4(tinted, 0.85);
}配合上游的实现实际模糊效果(比在AGSL中计算模糊更高效)。
Modifier.blur(20.dp, BlurredEdgeTreatment.Unbounded)Performance Notes
性能注意事项
- Each is a render pass. Compounds at 60fps - benchmark via Macrobenchmark.
RuntimeShader - samples the underlying view as a texture. Heavy if the view is complex (long lists, nested layouts). Cache static parts in a parent that doesn't recompose.
image.eval(coord) - On Android 12 and below: graceful fallback (static image, no shader). Never hard-crash.
- Prefer 1 well-crafted shader over chaining 4 shaders in series.
- 每个对应一次渲染通道。60fps下叠加使用会增加负载,需通过Macrobenchmark进行性能测试。
RuntimeShader - 会将底层视图作为纹理采样。如果视图复杂(长列表、嵌套布局),性能开销较大。将静态部分缓存到不会重组的父组件中。
image.eval(coord) - Android 12及以下版本:优雅降级(静态图片、无着色器效果),绝对不能崩溃。
- 优先使用一个精心设计的着色器,而非串联4个着色器。
Domain 3: Canvas / DrawScope
领域3:Canvas/DrawScope
Canvas API
Canvas API
kotlin
Canvas(modifier = Modifier.size(200.dp)) {
// 'this' is DrawScope - size, drawCircle, drawPath, drawRect...
drawCircle(Color.Blue, radius = size.minDimension / 2)
drawPath(myPath, color = Color.White, style = Stroke(width = 4.dp.toPx()))
}| DrawScope method | Use |
|---|---|
| Filled or stroked circles |
| Rectangles, optionally with gradient brush |
| Arbitrary path with |
| Slice of an oval - useful for progress arcs |
| Straight segment between two |
| Text via |
| Escape hatch - access raw |
kotlin
Canvas(modifier = Modifier.size(200.dp)) {
// 'this'代表DrawScope,包含size、drawCircle、drawPath、drawRect等方法
drawCircle(Color.Blue, radius = size.minDimension / 2)
drawPath(myPath, color = Color.White, style = Stroke(width = 4.dp.toPx()))
}| DrawScope方法 | 用途 |
|---|---|
| 绘制填充或描边的圆形 |
| 绘制矩形,可搭配渐变画笔 |
| 使用描边或填充模式绘制任意路径 |
| 绘制椭圆切片,适用于进度圆弧 |
| 在两个 |
| 通过 |
| 底层接口,访问原生 |
Recipe: Animated Sine Wave
示例:动画正弦波
kotlin
@Composable
fun AnimatedWave() {
val infinite = rememberInfiniteTransition(label = "wave")
val phase by infinite.animateFloat(
initialValue = 0f,
targetValue = 2 * PI.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(3000, easing = LinearEasing)
),
label = "phase"
)
Canvas(modifier = Modifier.fillMaxWidth().height(80.dp)) {
val path = Path()
var x = 0
while (x <= size.width.toInt()) {
val y = size.height / 2 + sin(x * 0.05f + phase) * 20f
if (x == 0) path.moveTo(x.toFloat(), y) else path.lineTo(x.toFloat(), y)
x += 2
}
drawPath(path, color = Color.Blue, style = Stroke(width = 2.dp.toPx()))
}
}kotlin
@Composable
fun AnimatedWave() {
val infinite = rememberInfiniteTransition(label = "wave")
val phase by infinite.animateFloat(
initialValue = 0f,
targetValue = 2 * PI.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(3000, easing = LinearEasing)
),
label = "phase"
)
Canvas(modifier = Modifier.fillMaxWidth().height(80.dp)) {
val path = Path()
var x = 0
while (x <= size.width.toInt()) {
val y = size.height / 2 + sin(x * 0.05f + phase) * 20f
if (x == 0) path.moveTo(x.toFloat(), y) else path.lineTo(x.toFloat(), y)
x += 2
}
drawPath(path, color = Color.Blue, style = Stroke(width = 2.dp.toPx()))
}
}Recipe: Particle System (50 Particles)
示例:粒子系统(50个粒子)
kotlin
data class Particle(var x: Float, var y: Float, var vx: Float, var vy: Float, var life: Float)
@Composable
fun ParticleField() {
val particles = remember {
mutableStateListOf<Particle>().apply {
repeat(50) {
add(Particle(
x = Random.nextFloat() * 1000f,
y = Random.nextFloat() * 1000f,
vx = (Random.nextFloat() - 0.5f) * 4f,
vy = (Random.nextFloat() - 0.5f) * 4f,
life = 1f
))
}
}
}
LaunchedEffect(Unit) {
while (true) {
withFrameMillis { /* tick */ }
particles.forEachIndexed { i, p ->
p.x += p.vx
p.y += p.vy
p.life -= 0.01f
if (p.life <= 0f) {
p.x = Random.nextFloat() * 1000f
p.y = Random.nextFloat() * 1000f
p.life = 1f
}
}
}
}
Canvas(modifier = Modifier.fillMaxSize()) {
particles.forEach { p ->
drawCircle(
color = Color.White.copy(alpha = p.life),
radius = 2.dp.toPx(),
center = Offset(p.x, p.y)
)
}
}
}kotlin
data class Particle(var x: Float, var y: Float, var vx: Float, var vy: Float, var life: Float)
@Composable
fun ParticleField() {
val particles = remember {
mutableStateListOf<Particle>().apply {
repeat(50) {
add(Particle(
x = Random.nextFloat() * 1000f,
y = Random.nextFloat() * 1000f,
vx = (Random.nextFloat() - 0.5f) * 4f,
vy = (Random.nextFloat() - 0.5f) * 4f,
life = 1f
))
}
}
}
LaunchedEffect(Unit) {
while (true) {
withFrameMillis { /* 帧更新 */ }
particles.forEachIndexed { i, p ->
p.x += p.vx
p.y += p.vy
p.life -= 0.01f
if (p.life <= 0f) {
p.x = Random.nextFloat() * 1000f
p.y = Random.nextFloat() * 1000f
p.life = 1f
}
}
}
}
Canvas(modifier = Modifier.fillMaxSize()) {
particles.forEach { p ->
drawCircle(
color = Color.White.copy(alpha = p.life),
radius = 2.dp.toPx(),
center = Offset(p.x, p.y)
)
}
}
}Recipe: Generative Pattern (Flow Field)
示例:生成式图案(流场)
Quick mention: noise-driven particle motion. See for the full deep-dive (Perlin noise, particle pools, advection loop).
references/canvas-generative.md简要说明:基于噪声驱动的粒子运动。完整深度内容见(Perlin噪声、粒子池、平流循环)。
references/canvas-generative.mdPerformance Notes
性能注意事项
- Canvas redraws on every state change of any state it reads. Wrap volatile state in to gate redraws.
derivedStateOf - is fine for 60fps. Avoid
LaunchedEffect(Unit) { while(true) { withFrameMillis { ... } } }- it drifts.delay(16) - Heavy generative work: precompute paths once in , only animate transforms via
remember.translate { drawPath(...) } - 200 simple draws per frame = OK on most devices. 2000+ starts to lag.
- Canvas会在它读取的任何状态发生变化时重绘。将易变状态包装在中,控制重绘触发时机。
derivedStateOf - 适合60fps场景。避免使用
LaunchedEffect(Unit) { while(true) { withFrameMillis { ... } } },会导致帧漂移。delay(16) - 复杂的生成式工作:在中预计算路径,仅通过
remember动画变换。translate { drawPath(...) } - 每帧绘制200个简单图形:多数设备可正常运行。超过2000个会开始卡顿。
Anti-Patterns
反模式
1. Expressive Everywhere
1. 全局使用动态动效
kotlin
// BAD - every interaction overshoots, UI feels like a bouncy castle
MaterialTheme(motionScheme = MotionScheme.expressive()) {
AppRoot()
}
// GOOD - Standard for chrome, scope Expressive to hero moments
MaterialTheme(motionScheme = MotionScheme.standard()) {
NavigationScaffold {
// Hero detail screen overrides locally
MaterialTheme(motionScheme = MotionScheme.expressive()) {
HeroDetail()
}
}
}kotlin
// 错误 - 所有交互都有过冲,UI像蹦床一样混乱
MaterialTheme(motionScheme = MotionScheme.expressive()) {
AppRoot()
}
// 正确 - 通用界面用标准动效,仅在核心场景使用动态动效
MaterialTheme(motionScheme = MotionScheme.standard()) {
NavigationScaffold {
// 核心详情页局部覆盖动效方案
MaterialTheme(motionScheme = MotionScheme.expressive()) {
HeroDetail()
}
}
}2. RuntimeShader Without API Gate
2. 使用RuntimeShader不做版本判断
kotlin
// BAD - crashes on Android 12 and below
val shader = remember { RuntimeShader(AGSL_SOURCE) }
// GOOD - gate on SDK level, provide fallback
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ShaderEffect { content() }
} else {
Box(modifier = Modifier.background(fallbackGradient)) { content() }
}kotlin
// 错误 - 在Android 12及以下版本会崩溃
val shader = remember { RuntimeShader(AGSL_SOURCE) }
// 正确 - 做版本判断并提供降级方案
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ShaderEffect { content() }
} else {
Box(modifier = Modifier.background(fallbackGradient)) { content() }
}3. Canvas Reading State Directly Each Frame
3. Canvas直接读取状态(每帧)
kotlin
// BAD - any state change in the parent triggers a recompose of the Canvas
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(color = if (viewModel.isActive) Color.Red else Color.Blue, radius = 50f)
}
// GOOD - hoist the reads, derive a stable value
val color by remember { derivedStateOf { if (viewModel.isActive) Color.Red else Color.Blue } }
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(color = color, radius = 50f)
}kotlin
// 错误 - 父组件的任何状态变化都会触发Canvas重组
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(color = if (viewModel.isActive) Color.Red else Color.Blue, radius = 50f)
}
// 正确 - 提取状态读取逻辑,生成稳定值
val color by remember { derivedStateOf { if (viewModel.isActive) Color.Red else Color.Blue } }
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(color = color, radius = 50f)
}4. Chaining 4 AGSL Shaders in Series
4. 串联4个AGSL着色器
kotlin
// BAD - 4 render passes per frame, GPU melts on mid-range devices
.graphicsLayer { renderEffect = blurEffect }
.graphicsLayer { renderEffect = chromaticEffect }
.graphicsLayer { renderEffect = noiseEffect }
.graphicsLayer { renderEffect = vignetteEffect }
// GOOD - one shader doing all the math in a single pass
.graphicsLayer { renderEffect = combinedGlassEffect }kotlin
// 错误 - 每帧4次渲染通道,中端设备GPU负载过高
.graphicsLayer { renderEffect = blurEffect }
.graphicsLayer { renderEffect = chromaticEffect }
.graphicsLayer { renderEffect = noiseEffect }
.graphicsLayer { renderEffect = vignetteEffect }
// 正确 - 单个着色器完成所有计算,一次渲染通道
.graphicsLayer { renderEffect = combinedGlassEffect }5. Allocating in DrawScope
5. 在DrawScope中分配对象
kotlin
// BAD - new Path every frame, GC stutter
Canvas(modifier = Modifier.fillMaxSize()) {
val path = Path()
points.forEach { path.lineTo(it.x, it.y) }
drawPath(path, color = Color.Black)
}
// GOOD - reuse a remembered Path, rewind each frame
val path = remember { Path() }
Canvas(modifier = Modifier.fillMaxSize()) {
path.rewind()
points.forEach { path.lineTo(it.x, it.y) }
drawPath(path, color = Color.Black)
}kotlin
// 错误 - 每帧创建新Path,导致GC卡顿
Canvas(modifier = Modifier.fillMaxSize()) {
val path = Path()
points.forEach { path.lineTo(it.x, it.y) }
drawPath(path, color = Color.Black)
}
// 正确 - 复用已缓存的Path,每帧重置
val path = remember { Path() }
Canvas(modifier = Modifier.fillMaxSize()) {
path.rewind()
points.forEach { path.lineTo(it.x, it.y) }
drawPath(path, color = Color.Black)
}Quick Reference: Loading Sub-resources
快速参考:加载子资源
| Need | Load |
|---|---|
| AGSL recipes (7 working shaders with binding code) | |
| M3 Expressive choreography deep-dive | |
| Generative drawing patterns (flow fields, L-systems, particles) | |
| Base animations (animateAsState, AnimatedVisibility, Transition) | |
| CMP patterns (shared UI iOS / Android / Desktop) | |
| 需求 | 加载路径 |
|---|---|
| AGSL示例(7个可运行着色器及绑定代码) | |
| M3动态动效编排深度内容 | |
| 生成式绘图模式(流场、L系统、粒子) | |
| 基础动画(animateAsState、AnimatedVisibility、Transition) | |
| CMP跨平台UI模式(iOS/Android/桌面端通用) | |
Sources
参考来源
- drinkthestars/shady - AGSL shaders rendered in Compose
- Mortd3kay/liquid-glass-android
- JumpingKeyCaps/DynamicVisualEffectsAGSL
- Material 3 Expressive blog
- Material Design 3 Motion specs
- Android AGSL docs
- androidx.graphics.shapes - shape morphing