compose-graphics

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose 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:
../compose-motion/SKILL.md
covers basics. Concise rules here. Deep-dive in
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

NeedAPI
Spring physics with bounce / overshoot
MotionScheme.expressive()
(M3 Expressive)
Pixel-level shader
RuntimeShader
+
Modifier.graphicsLayer { renderEffect = ... }
(Android 13+)
Generative drawing (paths, particles, fractals)
Canvas { drawScope -> ... }
GPU effects (blur, shadows, color filters)
Modifier.graphicsLayer { renderEffect = ... }
or
Modifier.blur(...)
Adaptive system materials (Material You glassmorphism)
Modifier.background(MaterialTheme.colorScheme.surfaceContainerHighest)
Liquid glass on AndroidAGSL shader recipe (no native API like iOS yet)

需求API
带回弹/过冲的弹簧物理动效
MotionScheme.expressive()
(M3动态动效)
像素级着色器
RuntimeShader
+
Modifier.graphicsLayer { renderEffect = ... }
(Android 13+)
生成式绘图(路径、粒子、分形)
Canvas { drawScope -> ... }
GPU特效(模糊、阴影、颜色滤镜)
Modifier.graphicsLayer { renderEffect = ... }
Modifier.blur(...)
自适应系统材质(Material You毛玻璃效果)
Modifier.background(MaterialTheme.colorScheme.surfaceContainerHighest)
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
androidx.graphics.shapes
. New
MotionScheme
selectable on the theme. Aimed at hero moments, key interactions, brand-defining UI.
2025年Material 3演进版本引入基于弹簧的物理动效,替代固定时长的补间动画。新增基于
androidx.graphics.shapes
的形状变形API,主题中可选择新的
MotionScheme
。适用于核心交互场景、关键操作、品牌化UI设计。

MotionScheme

MotionScheme

SchemePersonalityUse For
MotionScheme.standard()
Calmer, less overshootDefault for chrome, lists, navigation
MotionScheme.expressive()
More overshoot, longer settleHero reveals, FABs, primary CTAs
Apply on the theme:
kotlin
MaterialTheme(motionScheme = MotionScheme.expressive()) {
    // children read tokens via MaterialTheme.motionScheme.*
}
Tokens exposed:
TokenDomainSpeed
fastSpatialSpec()
Position / size< 200ms
defaultSpatialSpec()
Position / size~ 350ms
slowSpatialSpec()
Position / size~ 600ms
fastEffectsSpec()
Opacity / color< 150ms
defaultEffectsSpec()
Opacity / color~ 250ms
slowEffectsSpec()
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.
方案特性适用场景
MotionScheme.standard()
更平缓,过冲更少导航栏、列表、通用界面默认方案
MotionScheme.expressive()
过冲更明显,动画时长更长核心内容展示、悬浮按钮(FAB)、主要操作按钮(CTA)
在主题中应用:
kotlin
MaterialTheme(motionScheme = MotionScheme.expressive()) {
    // 子组件通过MaterialTheme.motionScheme.*获取动效参数
}
暴露的参数:
参数适用领域速度
fastSpatialSpec()
位置/尺寸变化< 200ms
defaultSpatialSpec()
位置/尺寸变化~ 350ms
slowSpatialSpec()
位置/尺寸变化~ 600ms
fastEffectsSpec()
透明度/颜色变化< 150ms
defaultEffectsSpec()
透明度/颜色变化~ 250ms
slowEffectsSpec()
透明度/颜色变化~ 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

弹簧动效调优方案

MoodSpec
Hero reveal
spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
Snappy expressive
spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = 0.7f)
Calm spatial
MaterialTheme.motionScheme.defaultSpatialSpec()
(use the token)
Critical (no overshoot)
spring(stiffness = Spring.StiffnessHigh, dampingRatio = 1f)
风格参数配置
核心内容展示
spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
轻快动态效果
spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = 0.7f)
平缓空间动效
MaterialTheme.motionScheme.defaultSpatialSpec()
(使用封装参数)
严谨无过冲
spring(stiffness = Spring.StiffnessHigh, dampingRatio = 1f)

Shape Morphing (M3 Expressive 1.3+)

形状变形(M3动态动效1.3+)

androidx.graphics.shapes
ships predefined morphable shapes (
MaterialShapes.Circle
,
Pentagon
,
Cookie4Sided
,
Sunny
,
Heart
, etc.) and a
Morph(start, end)
interpolator.
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.shapes
库提供预定义可变形形状(
MaterialShapes.Circle
Pentagon
Cookie4Sided
Sunny
Heart
等),以及
Morph(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 (
half4
instead of
vec4
, restricted feature set, sandbox-safe).
RuntimeShader
compiles your AGSL source. Bind to a Compose modifier via
Modifier.graphicsLayer { renderEffect = ... }
.
AGSL是Android专属着色器语言,与GLSL类似但做了简化(用
half4
替代
vec4
,功能集受限,沙箱安全)。
RuntimeShader
可编译AGSL源码,通过
Modifier.graphicsLayer { renderEffect = ... }
绑定到Compose修饰符。

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
Build.VERSION.SDK_INT >= 33
.
Pre-Android 13 fallback: render the content unmodified, or use
Modifier.blur()
/ a static gradient overlay.
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() }
}
务必通过
Build.VERSION.SDK_INT >= 33
做版本判断。
Android 13之前的版本降级方案:直接渲染内容,或使用
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
time
, animated via
LaunchedEffect
):
kotlin
@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绑定(状态驱动
time
,通过
LaunchedEffect
实现动画):
kotlin
@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.
time
animates the wave drift.
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);
}
将底层视图的亮度与相位偏移的RGB正弦波相乘,为文字或图标添加闪烁效果。
time
参数控制波浪的漂移动画。

Recipe: Liquid Glass / Glassmorphism

示例:液态玻璃/毛玻璃效果

Android lacks a native iOS-26-style
glassEffect
. Compose this with blur + chromatic aberration + soft edge tint:
glsl
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
Modifier.blur(20.dp, BlurredEdgeTreatment.Unbounded)
upstream for the actual blur (cheaper than computing it in AGSL).
Android没有iOS 26风格的原生
glassEffect
。可通过模糊+色差+边缘柔化色调组合实现:
glsl
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);
}
配合上游的
Modifier.blur(20.dp, BlurredEdgeTreatment.Unbounded)
实现实际模糊效果(比在AGSL中计算模糊更高效)。

Performance Notes

性能注意事项

  • Each
    RuntimeShader
    is a render pass. Compounds at 60fps - benchmark via Macrobenchmark.
  • image.eval(coord)
    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.
  • 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.

  • 每个
    RuntimeShader
    对应一次渲染通道。60fps下叠加使用会增加负载,需通过Macrobenchmark进行性能测试。
  • 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 methodUse
drawCircle
Filled or stroked circles
drawRect
Rectangles, optionally with gradient brush
drawPath
Arbitrary path with
Stroke
or
Fill
drawArc
Slice of an oval - useful for progress arcs
drawLine
Straight segment between two
Offset
drawText
Text via
TextMeasurer
(preferred) or
drawIntoCanvas { it.nativeCanvas.drawText(...) }
drawIntoCanvas { }
Escape hatch - access raw
android.graphics.Canvas
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方法用途
drawCircle
绘制填充或描边的圆形
drawRect
绘制矩形,可搭配渐变画笔
drawPath
使用描边或填充模式绘制任意路径
drawArc
绘制椭圆切片,适用于进度圆弧
drawLine
在两个
Offset
之间绘制直线段
drawText
通过
TextMeasurer
绘制文字(推荐)或
drawIntoCanvas { it.nativeCanvas.drawText(...) }
drawIntoCanvas { }
底层接口,访问原生
android.graphics.Canvas

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
references/canvas-generative.md
for the full deep-dive (Perlin noise, particle pools, advection loop).
简要说明:基于噪声驱动的粒子运动。完整深度内容见
references/canvas-generative.md
(Perlin噪声、粒子池、平流循环)。

Performance Notes

性能注意事项

  • Canvas redraws on every state change of any state it reads. Wrap volatile state in
    derivedStateOf
    to gate redraws.
  • LaunchedEffect(Unit) { while(true) { withFrameMillis { ... } } }
    is fine for 60fps. Avoid
    delay(16)
    - it drifts.
  • Heavy generative work: precompute paths once in
    remember
    , only animate transforms via
    translate { drawPath(...) }
    .
  • 200 simple draws per frame = OK on most devices. 2000+ starts to lag.

  • Canvas会在它读取的任何状态发生变化时重绘。将易变状态包装在
    derivedStateOf
    中,控制重绘触发时机。
  • LaunchedEffect(Unit) { while(true) { withFrameMillis { ... } } }
    适合60fps场景。避免使用
    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

快速参考:加载子资源

NeedLoad
AGSL recipes (7 working shaders with binding code)
references/agsl-recipes.md
M3 Expressive choreography deep-dive
references/m3-expressive-deep.md
Generative drawing patterns (flow fields, L-systems, particles)
references/canvas-generative.md
Base animations (animateAsState, AnimatedVisibility, Transition)
../compose-motion/SKILL.md
CMP patterns (shared UI iOS / Android / Desktop)
../compose-multiplatform/SKILL.md

需求加载路径
AGSL示例(7个可运行着色器及绑定代码)
references/agsl-recipes.md
M3动态动效编排深度内容
references/m3-expressive-deep.md
生成式绘图模式(流场、L系统、粒子)
references/canvas-generative.md
基础动画(animateAsState、AnimatedVisibility、Transition)
../compose-motion/SKILL.md
CMP跨平台UI模式(iOS/Android/桌面端通用)
../compose-multiplatform/SKILL.md

Sources

参考来源