Jetpack Compose Shape 基础使用

Jetpack Compose Shape 基础使用

目录

  1. [Shape 基础概念](#Shape 基础概念 "#%E4%B8%80shape-%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5")
  2. [内置 Shape 类型详解](#内置 Shape 类型详解 "#%E4%BA%8C%E5%86%85%E7%BD%AE-shape-%E7%B1%BB%E5%9E%8B%E8%AF%A6%E8%A7%A3")
  3. [Shape 的应用场景](#Shape 的应用场景 "#%E4%B8%89shape-%E7%9A%84%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF")
  4. [自定义 Shape 实现](#自定义 Shape 实现 "#%E5%9B%9B%E8%87%AA%E5%AE%9A%E4%B9%89-shape-%E5%AE%9E%E7%8E%B0")
  5. 高级自定义技巧
  6. [Shape 与动画结合](#Shape 与动画结合 "#%E5%85%ADshape-%E4%B8%8E%E5%8A%A8%E7%94%BB%E7%BB%93%E5%90%88")
  7. 性能优化与最佳实践
  8. 完整代码示例

一、Shape 基础概念

1.1 什么是 Shape

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

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

1.2 Shape 在 Compose 中的定位

css 复制代码
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(矩形)

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

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

使用场景:

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

2.2 CircleShape(圆形)

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

kotlin 复制代码
// 定义
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)
  • 圆形指示器
kotlin 复制代码
// 圆形头像
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 构造函数
kotlin 复制代码
// 统一圆角
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 类型
kotlin 复制代码
// 固定尺寸(dp)
val fixedCorner = CornerSize(16.dp)

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

// 零圆角
val zeroCorner = ZeroCornerSize
2.3.3 使用示例
kotlin 复制代码
// 统一圆角
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 的切角风格。

kotlin 复制代码
// 构造函数
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

使用示例:

kotlin 复制代码
// 统一切角
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 布局中保持相同视觉效果。

kotlin 复制代码
// 与 RoundedCornerShape 类似,但不随 layoutDirection 变化
AbsoluteRoundedCornerShape(16.dp)
AbsoluteCutCornerShape(16.dp)

区别说明:

scss 复制代码
LTR 布局:
┌─────────────────┐
│  topStart →     │
│                 │
│     ← bottomEnd │
└─────────────────┘

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

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

三、Shape 的应用场景

3.1 背景形状(background)

kotlin 复制代码
Box(
    modifier = Modifier
        .size(100.dp)
        .background(
            color = Color.Blue,
            shape = RoundedCornerShape(16.dp)
        )
)

3.2 边框形状(border)

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

3.3 内容裁剪(clip)

kotlin 复制代码
// 裁剪图片为圆形
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)

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

3.5 组件专用 Shape 参数

kotlin 复制代码
// 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 配置

kotlin 复制代码
// 定义 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 接口创建自定义形状:

kotlin 复制代码
// 三角形形状
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

kotlin 复制代码
// 可配置星形
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 气泡对话框形状

kotlin 复制代码
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 波浪形状

kotlin 复制代码
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 的创建:

kotlin 复制代码
// 使用 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 组合形状

kotlin 复制代码
// 圆角矩形 + 圆形缺口
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 渐变边框形状

kotlin 复制代码
@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 虚线边框形状

kotlin 复制代码
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 圆角动画

kotlin 复制代码
@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 形状变换动画

kotlin 复制代码
@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 波浪动画

kotlin 复制代码
@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 缓存

kotlin 复制代码
// ❌ 避免:每次重组都创建新的 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 复杂形状的优化

kotlin 复制代码
// ❌ 避免:在 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 避免不必要的重绘

kotlin 复制代码
// ✅ 使用 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 库

kotlin 复制代码
// 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 使用示例

kotlin 复制代码
// 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 主题配置

kotlin 复制代码
// 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)
}

参考资料

相关推荐
cxxcode2 小时前
ArrayBuffer / TypedArray / Blob / File 关系与操作指南
前端
We་ct2 小时前
React 更新触发原理详解
开发语言·前端·javascript·react.js·面试·前端框架·react
还是大剑师兰特2 小时前
Vue3 页面权限控制实战示例(路由守卫 + 权限判断)
开发语言·前端·javascript
冉冉同学2 小时前
Vibe Coding指南【道、法、术】
前端·人工智能·后端
枕布响丸辣2 小时前
Web 技术基础与 Nginx 网站环境部署超详细教程
运维·前端·nginx
又是忙碌的一天2 小时前
Java 面向对象三大特性:封装、继承、多态深度解析
java·前端·python
跟着珅聪学java3 小时前
Vue 2 + CommonJS 写法开发教程
前端·javascript·vue.js