Jetpack Compose Shape 基础使用
目录
一、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)
}