Jetpack Compose Shape 基础使用

8 阅读11分钟

Jetpack Compose Shape 基础使用

目录

  1. Shape 基础概念
  2. 内置 Shape 类型详解
  3. Shape 的应用场景
  4. 自定义 Shape 实现
  5. 高级自定义技巧
  6. Shape 与动画结合
  7. 性能优化与最佳实践
  8. 完整代码示例

一、Shape 基础概念

1.1 什么是 Shape

在 Jetpack Compose 中,Shape 是一个接口,用于定义组件的轮廓形状。它是 Compose 图形系统的核心组件之一,广泛应用于背景裁剪、边框绘制、阴影形状、点击涟漪效果等场景。

// Shape 接口定义
interface Shape {
    fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline
}

1.2 Shape 在 Compose 中的定位

Compose 图形渲染层级:
┌─────────────────────────────────────┐
│           UI 组件层                  │
│    (Box, Card, Button, Surface...)  │
├─────────────────────────────────────┤
│           Modifier 层                │
│    (background, border, clip...)    │
├─────────────────────────────────────┤
│           Shape 层                   │
│    (RoundedCornerShape, CircleShape)│
├─────────────────────────────────────┤
│           Outline 层                 │
│    (Rectangle, RoundRect, Path)     │
├─────────────────────────────────────┤
│           Canvas/Path 层             │
│    (Android 底层图形 API)            │
└─────────────────────────────────────┘

1.3 Shape 的核心特性

特性说明
延迟计算Shape 在布局测量后才根据实际尺寸创建 Outline
尺寸自适应自动适应目标组件的尺寸变化
方向感知支持 RTL(从右到左)布局方向
密度感知自动处理不同屏幕密度的单位转换
可复用性同一个 Shape 实例可应用于多个组件

二、内置 Shape 类型详解

2.1 RectangleShape(矩形)

最基础的形状,表示标准的直角矩形。

// 定义
object RectangleShape : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ) = Outline.Rectangle(
        Rect(0f, 0f, size.width, size.height)
    )
}

使用场景:

  • 默认背景形状
  • 需要直角边框的组件
  • 作为其他形状的基础
Box(
    modifier = Modifier
        .size(100.dp)
        .background(Color.Blue, shape = RectangleShape)
)

2.2 CircleShape(圆形)

将组件裁剪为圆形,适用于头像、浮动按钮等场景。

// 定义
object CircleShape : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val radius = min(size.width, size.height) / 2f
        return Outline.Generic(
            Path().apply {
                addOval(
                    Rect(
                        center = Offset(size.width / 2f, size.height / 2f),
                        radius = radius
                    )
                )
            }
        )
    }
}

使用场景:

  • 用户头像
  • 浮动操作按钮 (FAB)
  • 圆形指示器
// 圆形头像
Image(
    painter = painterResource(R.drawable.avatar),
    contentDescription = "Avatar",
    modifier = Modifier
        .size(80.dp)
        .clip(CircleShape)
)

// 圆形按钮
FloatingActionButton(
    onClick = { },
    shape = CircleShape
) {
    Icon(Icons.Default.Add, contentDescription = "Add")
}

2.3 RoundedCornerShape(圆角矩形)

最常用的形状类型,支持统一圆角和独立圆角设置。

2.3.1 构造函数
// 统一圆角
fun RoundedCornerShape(corner: CornerSize): RoundedCornerShape
fun RoundedCornerShape(size: Dp): RoundedCornerShape
fun RoundedCornerShape(percent: Int): RoundedCornerShape

// 独立圆角
fun RoundedCornerShape(
    topStart: CornerSize = ZeroCornerSize,
    topEnd: CornerSize = ZeroCornerSize,
    bottomEnd: CornerSize = ZeroCornerSize,
    bottomStart: CornerSize = ZeroCornerSize
): RoundedCornerShape

// 便捷构造函数
fun RoundedCornerShape(
    topStart: Dp = 0.dp,
    topEnd: Dp = 0.dp,
    bottomEnd: Dp = 0.dp,
    bottomStart: Dp = 0.dp
): RoundedCornerShape
2.3.2 CornerSize 类型
// 固定尺寸(dp)
val fixedCorner = CornerSize(16.dp)

// 百分比(相对于短边)
val percentCorner = CornerSize(50)  // 50%

// 零圆角
val zeroCorner = ZeroCornerSize
2.3.3 使用示例
// 统一圆角
Card(
    shape = RoundedCornerShape(16.dp)
) { }

// 百分比圆角(半圆形)
Box(
    modifier = Modifier
        .size(100.dp, 50.dp)
        .background(Color.Blue, RoundedCornerShape(50))
)

// 独立圆角(顶部圆角,底部直角)
Card(
    shape = RoundedCornerShape(
        topStart = 16.dp,
        topEnd = 16.dp,
        bottomStart = 0.dp,
        bottomEnd = 0.dp
    )
) { }

// 胶囊形状
Button(
    shape = RoundedCornerShape(50)  // 50% 圆角
) { }

2.4 CutCornerShape(切角矩形)

创建斜切角的矩形形状,常用于 Material Design 的切角风格。

// 构造函数
fun CutCornerShape(corner: CornerSize): CutCornerShape
fun CutCornerShape(size: Dp): CutCornerShape
fun CutCornerShape(percent: Int): CutCornerShape

fun CutCornerShape(
    topStart: CornerSize = ZeroCornerSize,
    topEnd: CornerSize = ZeroCornerSize,
    bottomEnd: CornerSize = ZeroCornerSize,
    bottomStart: CornerSize = ZeroCornerSize
): CutCornerShape

使用示例:

// 统一切角
Card(
    shape = CutCornerShape(16.dp)
) { }

// 独立切角
Surface(
    shape = CutCornerShape(
        topStart = 20.dp,
        topEnd = 0.dp,
        bottomEnd = 20.dp,
        bottomStart = 0.dp
    )
) { }

2.5 AbsoluteRoundedCornerShape / AbsoluteCutCornerShape

不考虑布局方向的绝对圆角/切角形状,在 RTL 布局中保持相同视觉效果。

// 与 RoundedCornerShape 类似,但不随 layoutDirection 变化
AbsoluteRoundedCornerShape(16.dp)
AbsoluteCutCornerShape(16.dp)

区别说明:

LTR 布局:
┌─────────────────┐
│  topStart →     │
│                 │
│     ← bottomEnd │
└─────────────────┘

RTL 布局(RoundedCornerShape):
┌─────────────────┐
│     ← topStart  │
│                 │
│  bottomEnd →    │
└─────────────────┘

RTL 布局(AbsoluteRoundedCornerShape):
┌─────────────────┐
│  topLeft →      │  (保持左上角圆角)
│                 │
│     ← bottomRight│
└─────────────────┘

三、Shape 的应用场景

3.1 背景形状(background)

Box(
    modifier = Modifier
        .size(100.dp)
        .background(
            color = Color.Blue,
            shape = RoundedCornerShape(16.dp)
        )
)

3.2 边框形状(border)

Box(
    modifier = Modifier
        .size(100.dp)
        .border(
            width = 2.dp,
            color = Color.Blue,
            shape = RoundedCornerShape(16.dp)
        )
)

3.3 内容裁剪(clip)

// 裁剪图片为圆形
Image(
    painter = painterResource(R.drawable.photo),
    contentDescription = null,
    modifier = Modifier
        .size(100.dp)
        .clip(CircleShape)
)

// 裁剪并添加边框
Box(
    modifier = Modifier
        .size(100.dp)
        .clip(RoundedCornerShape(16.dp))
        .background(Color.LightGray)
        .padding(4.dp)
) {
    Image(
        painter = painterResource(R.drawable.photo),
        contentDescription = null
    )
}

3.4 阴影形状(shadow)

Box(
    modifier = Modifier
        .size(100.dp)
        .shadow(
            elevation = 8.dp,
            shape = RoundedCornerShape(16.dp)
        )
        .background(Color.White)
)

3.5 组件专用 Shape 参数

// Card
Card(
    shape = RoundedCornerShape(16.dp)
) { }

// Button
Button(
    onClick = { },
    shape = RoundedCornerShape(24.dp)
) { }

// TextField
TextField(
    value = text,
    onValueChange = { },
    shape = RoundedCornerShape(8.dp)
)

// Surface
Surface(
    shape = CutCornerShape(topStart = 16.dp),
    shadowElevation = 4.dp
) { }

3.6 MaterialTheme 中的 Shape 配置

// 定义 Shape 主题
val Shapes = Shapes(
    small = RoundedCornerShape(4.dp),
    medium = RoundedCornerShape(8.dp),
    large = RoundedCornerShape(16.dp)
)

// 应用主题
MaterialTheme(
    shapes = Shapes
) {
    // 使用主题中的形状
    Card(shape = MaterialTheme.shapes.medium) { }
    Button(shape = MaterialTheme.shapes.small) { }
}

四、自定义 Shape 实现

4.1 基础自定义 Shape

通过实现 Shape 接口创建自定义形状:

// 三角形形状
class TriangleShape : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val path = Path().apply {
            // 从左上角开始
            moveTo(0f, size.height)
            // 画到顶部中间
            lineTo(size.width / 2f, 0f)
            // 画到右下角
            lineTo(size.width, size.height)
            // 闭合路径
            close()
        }
        return Outline.Generic(path)
    }
}

// 使用
Box(
    modifier = Modifier
        .size(100.dp)
        .background(Color.Blue, shape = TriangleShape())
)

4.2 带参数的自定义 Shape

// 可配置星形
class StarShape(
    private val points: Int = 5,
    private val innerRadiusRatio: Float = 0.5f
) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val path = Path()
        val centerX = size.width / 2f
        val centerY = size.height / 2f
        val outerRadius = min(size.width, size.height) / 2f
        val innerRadius = outerRadius * innerRadiusRatio
        
        val angleStep = (2 * PI / points).toFloat()
        
        for (i in 0 until points * 2) {
            val angle = i * angleStep / 2 - PI.toFloat() / 2
            val radius = if (i % 2 == 0) outerRadius else innerRadius
            val x = centerX + radius * cos(angle)
            val y = centerY + radius * sin(angle)
            
            if (i == 0) {
                path.moveTo(x, y)
            } else {
                path.lineTo(x, y)
            }
        }
        path.close()
        
        return Outline.Generic(path)
    }
}

// 使用
Box(
    modifier = Modifier
        .size(120.dp)
        .background(Color.Yellow, shape = StarShape(points = 6))
)

4.3 气泡对话框形状

class BubbleShape(
    private val cornerRadius: Dp = 16.dp,
    private val triangleWidth: Dp = 20.dp,
    private val triangleHeight: Dp = 12.dp,
    private val trianglePosition: BubblePosition = BubblePosition.BottomCenter
) : Shape {
    
    enum class BubblePosition {
        TopStart, TopCenter, TopEnd,
        BottomStart, BottomCenter, BottomEnd,
        LeftTop, LeftCenter, LeftBottom,
        RightTop, RightCenter, RightBottom
    }
    
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val radius = with(density) { cornerRadius.toPx() }
        val triWidth = with(density) { triangleWidth.toPx() }
        val triHeight = with(density) { triangleHeight.toPx() }
        
        val path = Path()
        
        // 根据位置绘制不同形状
        when (trianglePosition) {
            BubblePosition.BottomCenter -> {
                // 顶部圆角
                path.moveTo(radius, 0f)
                path.lineTo(size.width - radius, 0f)
                path.quadraticBezierTo(
                    size.width, 0f,
                    size.width, radius
                )
                // 右侧
                path.lineTo(size.width, size.height - radius - triHeight)
                path.quadraticBezierTo(
                    size.width, size.height - triHeight,
                    size.width - radius, size.height - triHeight
                )
                // 底部三角形
                path.lineTo(size.width / 2f + triWidth / 2, size.height - triHeight)
                path.lineTo(size.width / 2f, size.height)
                path.lineTo(size.width / 2f - triWidth / 2, size.height - triHeight)
                // 左侧
                path.lineTo(radius, size.height - triHeight)
                path.quadraticBezierTo(
                    0f, size.height - triHeight,
                    0f, size.height - radius - triHeight
                )
                path.lineTo(0f, radius)
                path.quadraticBezierTo(0f, 0f, radius, 0f)
            }
            // 其他位置类似实现...
            else -> {
                // 默认矩形
                path.addRect(Rect(0f, 0f, size.width, size.height))
            }
        }
        
        path.close()
        return Outline.Generic(path)
    }
}

// 使用
Box(
    modifier = Modifier
        .padding(16.dp)
        .shadow(4.dp, shape = BubbleShape())
        .background(Color.White, shape = BubbleShape())
        .padding(16.dp)
) {
    Text("这是一个气泡对话框")
}

4.4 波浪形状

class WaveShape(
    private val waveHeight: Dp = 20.dp,
    private val waveCount: Int = 2
) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val height = with(density) { waveHeight.toPx() }
        val path = Path()
        
        path.moveTo(0f, 0f)
        path.lineTo(size.width, 0f)
        path.lineTo(size.width, size.height - height)
        
        // 绘制波浪
        val waveWidth = size.width / (waveCount * 2)
        for (i in 0 until waveCount * 2) {
            val x = size.width - (i + 1) * waveWidth
            val controlY = if (i % 2 == 0) size.height else size.height - height * 2
            path.quadraticBezierTo(
                x + waveWidth / 2, controlY,
                x, size.height - height
            )
        }
        
        path.lineTo(0f, size.height - height)
        path.close()
        
        return Outline.Generic(path)
    }
}

五、高级自定义技巧

5.1 使用 GenericShape 简化自定义

Compose 提供了 GenericShape 来简化自定义 Shape 的创建:

// 使用 GenericShape 创建心形
val HeartShape = GenericShape { size, _ ->
    val width = size.width
    val height = size.height
    
    // 心形路径
    moveTo(width / 2f, height / 5f)
    
    // 左上半圆
    cubicTo(
        0f, 0f,
        0f, height * 3f / 5f,
        width / 2f, height
    )
    
    // 右上半圆
    cubicTo(
        width, height * 3f / 5f,
        width, 0f,
        width / 2f, height / 5f
    )
    
    close()
}

// 使用
Box(
    modifier = Modifier
        .size(100.dp)
        .background(Color.Red, shape = HeartShape)
)

5.2 组合形状

// 圆角矩形 + 圆形缺口
class NotchedShape(
    private val cornerRadius: Dp = 16.dp,
    private val notchRadius: Dp = 30.dp
) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val radius = with(density) { cornerRadius.toPx() }
        val notchR = with(density) { notchRadius.toPx() }
        
        val path = Path()
        
        // 左上角
        path.moveTo(0f, radius)
        path.quadraticBezierTo(0f, 0f, radius, 0f)
        
        // 顶部到缺口左侧
        path.lineTo(size.width / 2f - notchR, 0f)
        
        // 圆形缺口(使用弧线)
        path.arcTo(
            rect = Rect(
                left = size.width / 2f - notchR,
                top = -notchR,
                right = size.width / 2f + notchR,
                bottom = notchR
            ),
            startAngleDegrees = 180f,
            sweepAngleDegrees = -180f,
            forceMoveTo = false
        )
        
        // 缺口右侧到右上角
        path.lineTo(size.width - radius, 0f)
        path.quadraticBezierTo(size.width, 0f, size.width, radius)
        
        // 右侧
        path.lineTo(size.width, size.height - radius)
        path.quadraticBezierTo(
            size.width, size.height,
            size.width - radius, size.height
        )
        
        // 底部
        path.lineTo(radius, size.height)
        path.quadraticBezierTo(0f, size.height, 0f, size.height - radius)
        
        // 左侧
        path.lineTo(0f, radius)
        
        path.close()
        return Outline.Generic(path)
    }
}

5.3 渐变边框形状

@Composable
fun GradientBorderShape(
    modifier: Modifier = Modifier,
    shape: Shape = RoundedCornerShape(16.dp),
    borderWidth: Dp = 2.dp,
    gradientColors: List<Color> = listOf(Color.Cyan, Color.Magenta, Color.Yellow)
) {
    Box(
        modifier = modifier
            .padding(borderWidth)
            .background(
                brush = Brush.sweepGradient(gradientColors),
                shape = shape
            )
    ) {
        Box(
            modifier = Modifier
                .padding(borderWidth)
                .background(Color.White, shape)
                .fillMaxSize()
        )
    }
}

5.4 虚线边框形状

class DashedBorderShape(
    private val cornerRadius: Dp = 8.dp,
    private val dashLength: Dp = 10.dp,
    private val gapLength: Dp = 5.dp,
    private val strokeWidth: Dp = 2.dp
) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        // 虚线边框通常使用 drawBehind 实现
        // 这里返回基础形状
        return RoundedCornerShape(cornerRadius).createOutline(
            size, layoutDirection, density
        )
    }
}

// 实际使用方式
fun Modifier.dashedBorder(
    color: Color,
    shape: Shape,
    strokeWidth: Dp = 2.dp,
    dashLength: Dp = 10.dp,
    gapLength: Dp = 5.dp
) = this.then(
    Modifier.drawBehind {
        val stroke = Stroke(
            width = strokeWidth.toPx(),
            pathEffect = PathEffect.dashPathEffect(
                intervals = floatArrayOf(
                    dashLength.toPx(),
                    gapLength.toPx()
                )
            )
        )
        
        val outline = shape.createOutline(size, layoutDirection, this)
        drawOutline(
            outline = outline,
            color = color,
            style = stroke
        )
    }
)

六、Shape 与动画结合

6.1 圆角动画

@Composable
fun AnimatedRoundedCard() {
    var expanded by remember { mutableStateOf(false) }
    
    val cornerRadius by animateDpAsState(
        targetValue = if (expanded) 32.dp else 8.dp,
        animationSpec = tween(durationMillis = 300)
    )
    
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .height(if (expanded) 300.dp else 100.dp)
            .clickable { expanded = !expanded },
        shape = RoundedCornerShape(cornerRadius)
    ) {
        // 内容
    }
}

6.2 形状变换动画

@Composable
fun MorphingShape() {
    var isCircle by remember { mutableStateOf(true) }
    
    val shape by animateValueAsState(
        targetValue = if (isCircle) 50 else 0,
        typeConverter = Int.VectorConverter,
        animationSpec = tween(500)
    ) { percent ->
        // 根据百分比创建形状
        GenericShape { size, _ ->
            val radius = size.minDimension / 2f
            val cornerRadius = radius * percent / 50f
            
            addRoundRect(
                RoundRect(
                    rect = Rect(0f, 0f, size.width, size.height),
                    radiusX = cornerRadius,
                    radiusY = cornerRadius
                )
            )
        }
    }
    
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Color.Blue, shape = shape)
            .clickable { isCircle = !isCircle }
    )
}

6.3 波浪动画

@Composable
fun AnimatedWaveShape() {
    val infiniteTransition = rememberInfiniteTransition(label = "wave")
    
    val phase by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 2 * PI.toFloat(),
        animationSpec = infiniteRepeatable(
            animation = tween(2000, easing = LinearEasing)
        ),
        label = "phase"
    )
    
    val waveShape = remember(phase) {
        GenericShape { size, _ ->
            val waveHeight = 30f
            val waveCount = 3
            
            moveTo(0f, 0f)
            lineTo(size.width, 0f)
            lineTo(size.width, size.height - waveHeight)
            
            val waveWidth = size.width / waveCount
            for (i in waveCount downTo 0) {
                val x = i * waveWidth
                val controlY = size.height - waveHeight + 
                    sin(phase + i) * waveHeight
                quadraticBezierTo(
                    x + waveWidth / 2, controlY,
                    x, size.height - waveHeight
                )
            }
            
            lineTo(0f, size.height - waveHeight)
            close()
        }
    }
    
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(200.dp)
            .background(Color.Blue, shape = waveShape)
    )
}

七、性能优化与最佳实践

7.1 Shape 缓存

// ❌ 避免:每次重组都创建新的 Shape
@Composable
fun BadExample() {
    Card(
        shape = RoundedCornerShape(16.dp)  // 每次重组都创建新对象
    ) { }
}

// ✅ 推荐:使用 remember 缓存 Shape
@Composable
fun GoodExample() {
    val shape = remember { RoundedCornerShape(16.dp) }
    Card(shape = shape) { }
}

// ✅ 更好:使用静态常量
object AppShapes {
    val Card = RoundedCornerShape(16.dp)
    val Button = RoundedCornerShape(24.dp)
    val Small = RoundedCornerShape(8.dp)
}

@Composable
fun BestExample() {
    Card(shape = AppShapes.Card) { }
}

7.2 复杂形状的优化

// ❌ 避免:在 Shape 中进行复杂计算
class BadCustomShape : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        // 避免在这里进行复杂计算
        val complexPath = calculateComplexPath(size)  // 耗时操作
        return Outline.Generic(complexPath)
    }
}

// ✅ 推荐:预计算路径数据
class OptimizedShape(
    private val pathData: PathData  // 预计算的数据
) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val path = Path().apply {
            // 使用预计算数据快速构建路径
            pathData.segments.forEach { segment ->
                when (segment) {
                    is Segment.Move -> moveTo(segment.x, segment.y)
                    is Segment.Line -> lineTo(segment.x, segment.y)
                    is Segment.Curve -> cubicTo(...)
                }
            }
        }
        return Outline.Generic(path)
    }
}

7.3 避免不必要的重绘

// ✅ 使用 immutable 数据类
@Immutable
data class StarShapeConfig(
    val points: Int = 5,
    val innerRadiusRatio: Float = 0.5f
)

@Composable
fun StarShapeBox(config: StarShapeConfig) {
    val shape = remember(config) {
        StarShape(config.points, config.innerRadiusRatio)
    }
    
    Box(
        modifier = Modifier.background(Color.Yellow, shape)
    )
}

7.4 最佳实践总结

实践说明
缓存 Shape 实例使用 remember 或静态常量缓存 Shape
避免复杂计算在 Shape 创建时进行预计算
使用 @Immutable标记配置数据类避免不必要的重组
合理使用 GenericShape简单形状优先使用内置 Shape
注意 RTL 支持自定义 Shape 考虑布局方向
测试不同尺寸确保 Shape 在各种尺寸下表现正常

八、完整代码示例

8.1 自定义 Shape 库

// shapes/CustomShapes.kt

package com.example.ui.shapes

import androidx.compose.ui.geometry.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.unit.*
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import kotlin.math.*

// 三角形
val TriangleShape = GenericShape { size, _ ->
    moveTo(size.width / 2f, 0f)
    lineTo(size.width, size.height)
    lineTo(0f, size.height)
    close()
}

// 菱形
val DiamondShape = GenericShape { size, _ ->
    moveTo(size.width / 2f, 0f)
    lineTo(size.width, size.height / 2f)
    lineTo(size.width / 2f, size.height)
    lineTo(0f, size.height / 2f)
    close()
}

// 六边形
class HexagonShape : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val path = Path()
        val radius = min(size.width, size.height) / 2f
        val centerX = size.width / 2f
        val centerY = size.height / 2f
        
        for (i in 0 until 6) {
            val angle = (i * 60 - 30) * PI.toFloat() / 180f
            val x = centerX + radius * cos(angle)
            val y = centerY + radius * sin(angle)
            if (i == 0) path.moveTo(x, y) else path.lineTo(x, y)
        }
        path.close()
        
        return Outline.Generic(path)
    }
}

// 心形
val HeartShape = GenericShape { size, _ ->
    val width = size.width
    val height = size.height
    
    moveTo(width / 2f, height / 5f)
    cubicTo(
        0f, 0f, 0f, height * 3f / 5f,
        width / 2f, height
    )
    cubicTo(
        width, height * 3f / 5f, width, 0f,
        width / 2f, height / 5f
    )
    close()
}

// 消息气泡
class MessageBubbleShape(
    private val cornerRadius: Dp = 16.dp,
    private val triangleSize: Dp = 12.dp,
    private val isFromMe: Boolean = false
) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val radius = with(density) { cornerRadius.toPx() }
        val triangle = with(density) { triangleSize.toPx() }
        
        val path = Path()
        
        if (isFromMe) {
            // 右侧气泡
            path.moveTo(radius, 0f)
            path.lineTo(size.width - radius, 0f)
            path.quadraticBezierTo(size.width, 0f, size.width, radius)
            path.lineTo(size.width, size.height - triangle - radius)
            path.quadraticBezierTo(
                size.width, size.height - triangle,
                size.width - radius, size.height - triangle
            )
            path.lineTo(size.width - triangle, size.height - triangle)
            path.lineTo(size.width - triangle * 2, size.height)
            path.lineTo(size.width - triangle * 2, size.height - triangle)
            path.lineTo(radius, size.height - triangle)
            path.quadraticBezierTo(0f, size.height - triangle, 0f, size.height - triangle - radius)
            path.lineTo(0f, radius)
            path.quadraticBezierTo(0f, 0f, radius, 0f)
        } else {
            // 左侧气泡
            path.moveTo(radius, 0f)
            path.lineTo(size.width - radius, 0f)
            path.quadraticBezierTo(size.width, 0f, size.width, radius)
            path.lineTo(size.width, size.height - triangle - radius)
            path.quadraticBezierTo(
                size.width, size.height - triangle,
                size.width - radius, size.height - triangle
            )
            path.lineTo(radius, size.height - triangle)
            path.quadraticBezierTo(0f, size.height - triangle, 0f, size.height - triangle - radius)
            path.lineTo(0f, radius + triangle)
            path.quadraticBezierTo(0f, triangle, radius, triangle)
            path.lineTo(triangle * 2, triangle)
            path.lineTo(triangle, 0f)
            path.lineTo(radius, 0f)
        }
        
        path.close()
        return Outline.Generic(path)
    }
}

// 票券形状(带半圆缺口)
class TicketShape(
    private val cornerRadius: Dp = 8.dp,
    private val notchRadius: Dp = 10.dp
) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val radius = with(density) { cornerRadius.toPx() }
        val notch = with(density) { notchRadius.toPx() }
        
        val path = Path()
        
        // 左上
        path.moveTo(0f, radius)
        path.quadraticBezierTo(0f, 0f, radius, 0f)
        
        // 上边到缺口
        path.lineTo(size.width / 3f - notch, 0f)
        path.arcTo(
            Rect(
                size.width / 3f - notch, -notch,
                size.width / 3f + notch, notch
            ),
            180f, -180f, false
        )
        
        // 缺口到右上
        path.lineTo(size.width - radius, 0f)
        path.quadraticBezierTo(size.width, 0f, size.width, radius)
        
        // 右边
        path.lineTo(size.width, size.height - radius)
        path.quadraticBezierTo(size.width, size.height, size.width - radius, size.height)
        
        // 下边到缺口
        path.lineTo(size.width * 2f / 3f + notch, size.height)
        path.arcTo(
            Rect(
                size.width * 2f / 3f - notch, size.height - notch,
                size.width * 2f / 3f + notch, size.height + notch
            ),
            0f, -180f, false
        )
        
        // 缺口到左下
        path.lineTo(radius, size.height)
        path.quadraticBezierTo(0f, size.height, 0f, size.height - radius)
        
        // 左边
        path.lineTo(0f, radius)
        
        path.close()
        return Outline.Generic(path)
    }
}

8.2 使用示例

// examples/ShapeExamples.kt

@Composable
fun ShapeShowcase() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text("Shape 展示", style = MaterialTheme.typography.headlineMedium)
        
        // 基础形状
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            ShapeItem("三角形", TriangleShape, Color.Red)
            ShapeItem("菱形", DiamondShape, Color.Green)
            ShapeItem("心形", HeartShape, Color(0xFFE91E63))
        }
        
        // 六边形
        Box(
            modifier = Modifier
                .size(100.dp)
                .background(Color.Blue, shape = HexagonShape()),
            contentAlignment = Alignment.Center
        ) {
            Text("六边形", color = Color.White)
        }
        
        // 消息气泡
        MessageBubbleDemo()
        
        // 票券
        TicketDemo()
    }
}

@Composable
private fun ShapeItem(name: String, shape: Shape, color: Color) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Box(
            modifier = Modifier
                .size(80.dp)
                .background(color, shape)
        )
        Text(name, modifier = Modifier.padding(top = 4.dp))
    }
}

@Composable
private fun MessageBubbleDemo() {
    Column {
        Text("消息气泡", style = MaterialTheme.typography.titleMedium)
        Spacer(modifier = Modifier.height(8.dp))
        
        // 对方消息
        Box(
            modifier = Modifier
                .padding(end = 64.dp)
                .shadow(2.dp, shape = MessageBubbleShape(isFromMe = false))
                .background(Color(0xFFE0E0E0), shape = MessageBubbleShape(isFromMe = false))
                .padding(horizontal = 16.dp, vertical = 12.dp)
        ) {
            Text("你好!这是收到的消息。")
        }
        
        Spacer(modifier = Modifier.height(8.dp))
        
        // 我的消息
        Box(
            modifier = Modifier
                .padding(start = 64.dp)
                .fillMaxWidth(),
            contentAlignment = Alignment.CenterEnd
        ) {
            Box(
                modifier = Modifier
                    .shadow(2.dp, shape = MessageBubbleShape(isFromMe = true))
                    .background(Color(0xFF2196F3), shape = MessageBubbleShape(isFromMe = true))
                    .padding(horizontal = 16.dp, vertical = 12.dp)
            ) {
                Text("你好!这是发送的消息。", color = Color.White)
            }
        }
    }
}

@Composable
private fun TicketDemo() {
    Column {
        Text("票券样式", style = MaterialTheme.typography.titleMedium)
        Spacer(modifier = Modifier.height(8.dp))
        
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(120.dp)
                .shadow(4.dp, shape = TicketShape())
                .background(Color.White, shape = TicketShape())
                .padding(16.dp)
        ) {
            Row(
                modifier = Modifier.fillMaxSize(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Column {
                    Text("音乐会门票", style = MaterialTheme.typography.titleLarge)
                    Text("2024年12月31日", style = MaterialTheme.typography.bodyMedium)
                }
                
                // 虚线分隔
                Box(
                    modifier = Modifier
                        .width(1.dp)
                        .fillMaxHeight()
                        .background(Color.Gray, shape = RectangleShape)
                )
                
                Text("¥188", style = MaterialTheme.typography.headlineMedium)
            }
        }
    }
}

8.3 主题配置

// theme/Shape.kt

package com.example.ui.theme

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp

val AppShapes = Shapes(
    small = RoundedCornerShape(4.dp),
    medium = RoundedCornerShape(8.dp),
    large = RoundedCornerShape(16.dp),
    extraLarge = RoundedCornerShape(24.dp)
)

// 自定义形状集合
object CustomShapes {
    val Card = RoundedCornerShape(16.dp)
    val Button = RoundedCornerShape(24.dp)
    val Input = RoundedCornerShape(8.dp)
    val Avatar = RoundedCornerShape(50)
    val BottomSheet = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
}

参考资料