mcpField
C
compose-graphics

compose-graphics

github.com/athevon设计与 UI698934 次安装3 周前更新
代码仓库

关于 compose-graphics

面向 Claude Code、claude.ai 和 Cowork 的创意编码技能,涵盖 Android Jetpack Compose 图形与动效。它强调先提出交互设计主张并等待确认,再编写代码。

主要功能

  1. 01Material 3 Expressive 弹簧动效
  2. 02AGSL 着色器效果
  3. 03Canvas 与生成式绘图
  4. 04图形层效果与模糊
  5. 05交互设计主张先行

适用场景

  1. 01为关键界面设计弹性动效
  2. 02制作触控涟漪或全息渐变
  3. 03通过 Canvas 绘制粒子与图案

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/athevon/genjutsu --skill compose-graphics

安装后在 Claude Code 中输入/compose-graphics确认已加载。

详细介绍

版本敏感。 以下所有 API 名称、SDK 门槛和浏览器支持声明均已在 2026-09-08 对照一手资料核实。核实依据和时间见 _jutsu/VERSIONS.md。如果该日期已久远,请在依据版本号采取行动前重新核实。

Compose 图形

高级 Compose 视觉效果:M3 Expressive 运动物理、AGSL 着色器(Android 13+)、Canvas / DrawScope、graphicsLayer 效果。 为高级主题(着色器、expressive、M3 Expressive、AGSL、Canvas、全息效果)加载。 基础内容:../compose-motion/SKILL.md 涵盖基础知识。此处提供简明规则。深入内容见 references/。


决策树:根据需求选择 API

需求API
带弹跳 / 过冲的弹簧物理效果MotionScheme.expressive()(M3 Expressive)
像素级着色器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)

领域 1:Material 3 Expressive

定义

2025 年 Material 3 的演进引入了基于弹簧的运动物理效果,取代固定时长的补间动画。通过 androidx.graphics.shapes 提供新的形状变换 API。主题中可选择新的 MotionScheme。适用于重要时刻、关键交互和定义品牌的 UI。

MotionScheme
方案特性适用场景
MotionScheme.standard()更平缓,过冲较少默认用于界面装饰、列表、导航
MotionScheme.expressive()过冲更多,稳定时间更长主视觉揭示、FAB、主要 CTA

应用到主题:

MaterialTheme(motionScheme = MotionScheme.expressive()) {
    // children read tokens via MaterialTheme.motionScheme.*
}

提供的 Token:

Token类别弹簧(阻尼 / 刚度)- standard... expressive
fastSpatialSpec()位置 / 尺寸0.9 / 14000.6 / 800
defaultSpatialSpec()位置 / 尺寸0.9 / 7000.8 / 380
slowSpatialSpec()位置 / 尺寸0.9 / 3000.8 / 200
fastEffectsSpec()不透明度 / 颜色1.0 / 38001.0 / 3800
defaultEffectsSpec()不透明度 / 颜色1.0 / 16001.0 / 1600
slowEffectsSpec()不透明度 / 颜色1.0 / 8001.0 / 800

空间属性与效果属性: 空间属性 = 任何物理属性(高度、偏移、缩放)。效果属性 = 不带惯性的视觉属性(alpha、颜色、海拔)。这两类 都使用弹簧;效果属性的弹簧只是采用临界阻尼(dampingRatio = 1.0),刚度也高得多,因此不会过冲。只有空间弹簧会因 standard() 和 expressive() 而不同。

可用性: MotionScheme、MaterialTheme(motionScheme = ...) 和 MaterialExpressiveTheme 在 material3 1.4.0 中已稳定(无需 opt-in)。MaterialShapes 和形状变换辅助函数尚未稳定——需要 1.5.0-alpha 和 @OptIn(ExperimentalMaterial3ExpressiveApi::class)。

主视觉卡片展开(Expressive 弹簧)
@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))
            }
        }
    }
}
弹簧调校方案
风格规格
主视觉揭示spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
灵动 expressivespring(stiffness = Spring.StiffnessMediumLow, dampingRatio = 0.7f)
平缓的空间效果MaterialTheme.motionScheme.defaultSpatialSpec()(使用 Token)
临界阻尼(无过冲)spring(stiffness = Spring.StiffnessHigh, dampingRatio = 1f)
形状变形(MaterialShapes + Morph)

这里涉及两个不同的库,区分它们很重要:

  • androidx.graphics.shapes:graphics-shapes(稳定版 1.1.0)提供 RoundedPolygon、CornerRounding 和 Morph(start, end)。其 Morph.toPath(progress) 返回 android.graphics.Path。
  • 预定义的 Material 形状(MaterialShapes.Circle、Pentagon、Cookie4Sided、Sunny、Heart 等)属于 material3,而不是 graphics-shapes。它们仍标注为 @ExperimentalMaterial3ExpressiveApi,且只在 androidx.compose.material3:material3:1.5.0-alpha* 中提供——不包含在稳定版 material3 1.4.0 中。material3 还添加了返回 Compose Path 的 Morph.toPath(progress),以及 RoundedPolygon.toShape()。

绝不要写 .asAndroidPath().asComposePath():使用 graphics-shapes 的导入时,这段代码无法编译(android.graphics.Path 没有 asAndroidPath());使用 material3 的导入时,这只是一次无意义的往返转换。另外,由于 MaterialShapes 以 0..1 的方框为基准归一化,必须将路径缩放到绘制尺寸——直接把原始路径传给 GenericShape,会在左上角得到一个约 1px 大小的形状。

import androidx.compose.material3.MaterialShapes
import androidx.compose.material3.toPath          // Morph.toPath -> androidx.compose.ui.graphics.Path
import androidx.graphics.shapes.Morph

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MorphingBadge(active: Boolean) {
    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(MorphShape(morph, progress))
            .background(MaterialTheme.colorScheme.primary)
    )
}

/** MaterialShapes are normalized to 0..1: scale the path to the draw size, then centre it. */
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private class MorphShape(private val morph: Morph, private val progress: Float) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density,
    ): Outline {
        val path = morph.toPath(progress = progress)
        path.transform(Matrix().apply { scale(x = size.width, y = size.height) })
        path.translate(size.center - path.getBounds().center)
        return Outline.Generic(path)
    }
}

对于静态(不变形)形状,跳过上述所有步骤:MaterialShapes.Cookie4Sided.toShape() 会返回一个可直接使用的 Shape,并且已处理缩放。

何时使用 Expressive,何时使用 Standard
  • Expressive: 用于重点展示时刻、关键交互、FAB 和主要 CTA。占 UI 的 1–3%。
  • Standard: 应用其余部分的默认选择。过多混用 Expressive 会显得混乱——每个元素都在争夺注意力。

领域 2:AGSL 着色器(Android 13+)

简介

AGSL 是 Android 的着色器语言。它与 GLSL 类似,但经过简化(使用 half4 而不是 vec4,功能集受限,并且可在沙箱中安全运行)。RuntimeShader 会编译 AGSL 源码。通过 Modifier.graphicsLayer { renderEffect = ... } 绑定到 Compose 修饰符。

设置
@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() }
}

务必检查 Build.VERSION.SDK_INT >= 33。 Android 13 之前的回退方案:原样渲染内容,或使用 Modifier.blur() / 静态渐变叠加层。

示例:触摸波纹

AGSL 源码:

// 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 动画处理):

@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
    )
}
示例:全息渐变
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 会为波浪漂移制作动画。

示例:液态玻璃 / 玻璃拟态

Android 没有原生的 iOS 26 风格 glassEffect。可通过模糊 + 色差 + 柔和的边缘色调,在 Compose 中组合实现:

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);
}

在上游搭配 Modifier.blur(20.dp, BlurredEdgeTreatment.Unbounded) 实现实际模糊(比在 AGSL 中计算开销更低)。

性能说明
  • 每个 RuntimeShader 都会产生一次渲染过程。在 60fps 下会产生叠加开销——使用 Macrobenchmark 进行基准测试。
  • image.eval(coord) 会将底层视图作为纹理采样。如果视图较复杂(长列表、嵌套布局),开销会很大。将静态部分缓存在不会重新组合的父级中。
  • Android 12 及以下版本:提供平滑回退方案(静态图像、不使用着色器)。绝不要导致应用崩溃。
  • 优先使用 1 个精心制作的着色器,而不是串联 4 个着色器。

领域 3:Canvas / DrawScope

Canvas API
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 方法用途
drawCircle填充或描边圆形
drawRect矩形,可选用渐变画笔
drawPath使用 Stroke 或 Fill 绘制任意路径
drawArc椭圆的一部分——适用于进度弧
drawLine两个 Offset 之间的直线段
drawText通过 TextMeasurer 绘制文本(推荐),或使用 drawIntoCanvas { it.nativeCanvas.drawText(...) }
drawIntoCanvas { }备用方案——访问原始 android.graphics.Canvas
示例:动画正弦波
@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()))
    }
}
示例:粒子系统(50 个粒子)
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)
            )
        }
    }
}
示例:生成式图案(流场)

简要介绍:由噪声驱动的粒子运动。完整深入讲解(Perlin 噪声、粒子池、平流循环)请参阅 references/canvas-generative.md。

性能说明
  • Canvas 会在其读取的任何状态发生变化时重绘。使用 derivedStateOf 包装易变状态,以控制重绘。
  • LaunchedEffect(Unit) { while(true) { withFrameMillis { ... } } } 适用于 60fps。避免使用 delay(16),因为它会逐渐产生偏差。
  • 密集的生成式绘制工作:在 remember 中预先计算路径,仅通过 translate { drawPath(...) } 为变换添加动画。
  • 每帧绘制 200 个简单图形,在大多数设备上都没问题。超过 2000 个时就会开始卡顿。

反模式

1. 到处使用 Expressive
// 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()
        }
    }
}
2. 使用 RuntimeShader 时未进行 API 检查
// 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() }
}
3. Canvas 每帧都直接读取状态
// 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)
}
4. 串联 4 个 AGSL 着色器
// 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 }
5. 在 DrawScope 中分配内存
// 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)
}

快速参考:加载子资源

需求加载
AGSL 配方(7 个可运行的着色器及其绑定代码)references/agsl-recipes.md
M3 Expressive 编排深入介绍references/m3-expressive-deep.md
生成式绘制模式(流场、L 系统、粒子)references/canvas-generative.md
基础动画(animateAsState、AnimatedVisibility、Transition)../compose-motion/SKILL.md
CMP 模式(iOS / Android / Desktop 共享 UI)../compose-multiplatform/SKILL.md

来源

评论

支持 Markdown