
如果你想在 Compose 上实现两个几何形状之间做形变,那么 Morph
,你一定不要错过!
24 年八月份的时候,Androidx
发布了 androidx.graphics:graphics-shapes
的第一个正式版本,该库可让开发者创建由多边形构成的图形。
虽然多边形图形仅包含直线边和尖角,但该库支持圆角。
传统意义上在任意图形之间进行变形较为困难,当然,工具是一方面,另一方面,设计的表达很多时候也不够清晰。
该库通过在具有相似多边形结构的图形之间进行变形,使这一过程变得简单。
闲言少叙,我们立即开始体验。
创建多边形
下面的代码片段展示了如何创建一个简单的多边形:
Kotlin
Box(
modifier = Modifier
.size(200.dp)
.drawWithCache {
val roundedPolygon = RoundedPolygon(
numVertices = 5,
radius = size.minDimension / 2,
centerX = size.width / 2,
centerY = size.height / 2
)
val roundedPolygonPath = roundedPolygon.toPath().asComposePath()
onDrawBehind {
drawPath(roundedPolygonPath, color = Color.Blue)
}
}
)
上述代码,我们会创建一个简单的五边形,

如果你喜欢圆角,可以这样做:
Kotlin
//...
val roundedPolygon = RoundedPolygon(
numVertices = 5,
radius = size.minDimension / 2,
centerX = size.width / 2,
centerY = size.height / 2,
rounding = CornerRounding(40f) // 在这里设置圆角
)
//...

如果仔细查看 CornerRounding
,你会发现它还有第二个参数,smoothing
------平滑度。
Kotlin
@FloatRange(from = 0.0, to = 1.0) val smoothing: Float = 0f
平滑度决定角的圆角部分如何过渡到边缘。它的设置返回从 0.0
到 1.0
。
这里五边形不够明显,我们使用三角形举例子:
Kotlin
val roundedPolygon = RoundedPolygon(
numVertices = 3,
radius = size.minDimension / 2,
centerX = size.width / 2,
centerY = size.height / 2,
rounding = CornerRounding(60f, smoothing = 0f) // 默认的 0 平滑度
)
Kotlin
val roundedPolygon = RoundedPolygon(
numVertices = 3,
radius = size.minDimension / 2,
centerX = size.width / 2,
centerY = size.height / 2,
rounding = CornerRounding(60f, smoothing = 1f)// 最大的平滑度
)

仔细查看圆角的过渡部分,还是有区别的。smoothing
值越大,感觉圆角更加钝一点,整个图像也更加的圆滑。
形状裁剪
如果形状只能用来显示,那确实鸡肋。我们可以使用形状,对现有的 UI 元素进行裁剪。
为了更好的使用新装,我们先来优化一下代码:
Kotlin
val roundedPolygon = RoundedPolygon(
numVertices = 3,
radius = size.minDimension / 2,
centerX = size.width / 2,
centerY = size.height / 2,
rounding = CornerRounding(60f, smoothing = 1f)
)
这段代码使我们不得不使用当前元素的尺寸信息,导致我们一开始不能定义形状,而必须在 drawWithCache
或者 graphicsLayer
等能够获取尺寸的作用域中使用。
为了更加广泛的使用形状,我们定义一个形状转换的方式,能够将 RoundedPolygon
这样的形状应用在任何尺寸的元素上。
Kotlin
class PathShape(
private val path: android.graphics.Path, // 接受一个 path
private var matrix: Matrix = Matrix() // 自定义 matrix
) : Shape {
private var composePath = androidx.compose.ui.graphics.Path()
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
composePath.rewind()
composePath = path.asComposePath() // path 转换,内部逻辑非常简单
matrix.reset()
matrix.translate(size.width / 2, size.height / 2) // 移动到元素的中间
matrix.scale(size.minDimension, size.minDimension) // 按照元素的最小尺寸进行缩放
composePath.transform(matrix) // 应用变换
return Outline.Generic(composePath) // 创建尺寸
}
}
现在,我们展示一段代码,让形状可以作用在一个 Image
元素上:
Kotlin
val roundedPolygon = remember {
RoundedPolygon(
numVertices = 3,
radius = 0.5f, // 这里不再使用具体尺寸,而是使用元素大小的比例
rounding = CornerRounding(radius = 0.12f, smoothing = 1f)// 同样,radius 也是比例
)
}
val clipShape = remember(roundedPolygon) {
PathShape(roundedPolygon.toPath())
}
Image(
painter = painterResource(R.drawable.plant), modifier = Modifier
.size(200.dp)
.graphicsLayer {
clip = true
shape = clipShape // 应用形状
},
contentDescription = null
)

非常棒!
形变动画
如果只能定义形状,还不足以吸引开发者。
真正让我们感到惊叹的其实是动画------从形状到另一个形状。
首先,我们定义两个形状:
Kotlin
// 五边形
val startPolygon = remember {
RoundedPolygon(
numVertices = 5,
radius = 0.5f,
rounding = CornerRounding(1f / 6f)
)
}
// 十边形
val endPolygon = remember {
RoundedPolygon(
numVertices = 10,
radius = 0.5f,
rounding = CornerRounding(1f / 6f)
)
}
然后,定义形变:
Kotlin
val morph = remember {
Morph(startPolygon, endPolygon)
}
此时,我们就拥有了从五边形变化到十边形的能力。
然后定义一个 android.graphics.Path()
,用于存储形变后的 Path
,也就是路径。
Morph
在形变的时候,需要使用 toPath
方法。该方法第一个参数是进度------progress
,它是一个 [0-1]
的浮点数类型,0
就表示开始的形状,1
表示结束的形状,中间的任何值,表示开始形状到结束形状的过渡。
形变的基本要素已经到齐了,接下来,开始你的表演:
Kotlin
val startPolygon = remember {
RoundedPolygon(
numVertices = 5,
radius = 0.5f,
rounding = CornerRounding(1f / 6f)
)
}
val endPolygon = remember {
RoundedPolygon(
numVertices = 10,
radius = 0.5f,
rounding = CornerRounding(1f / 6f)
)
}
val morph = remember {
Morph(startPolygon, endPolygon)
}
val androidPath = remember {
android.graphics.Path()
}
val interactionSource = remember { MutableInteractionSource() }
val pressed by interactionSource.collectIsPressedAsState() // 使用按下作为触发机制
val progress by animateFloatAsState(if (pressed) 1f else 0f) // 按下的时候变成十边形,默认状态是五边形
Image(
painter = painterResource(R.drawable.plant), modifier = Modifier
.size(200.dp)
.graphicsLayer {
clip = true
shape = PathShape(morph.toPath(progress, androidPath)) // 形变
}
.clickable(enabled = true, indication = null, interactionSource = interactionSource) { },
contentDescription = null
)

当我们按下的时候,该图片会变成十边形,当我们松开的时候,会回到五边形。
一个简单的录制按钮
参考上面的用法,我们可以简单的做一个拍照功能的录像按钮。
按钮默认状态是圆形,当开始录制的时候,是四边形。
Kotlin
// 圆形
val startPolygon = remember {
RoundedPolygon.circle(numVertices = 12, radius = 0.4f)
}
// 四边形
val endPolygon = remember {
RoundedPolygon.rectangle(0.28f,0.28f, rounding = CornerRounding(radius = 0.1f))
}
// 形变
val morph = remember {
Morph(startPolygon, endPolygon)
}
val androidPath = remember {
android.graphics.Path()
}
// 记录当前状态是否在播放
var recording by remember { mutableStateOf(false) }
// 形变进度
val progress by animateFloatAsState(if (recording) 1f else 0f)
Spacer(
modifier = Modifier.graphicsLayer {
clip = true
shape = PathShape(morph.toPath(progress, androidPath))
}
.size(200.dp)
.background(Brush.linearGradient(0f to Color(red = 137, green = 247, blue = 197), 1f to Color(red = 192, green = 255, blue = 122)))// 添加一个渐变色,让图形更好看
.clickable {
recording = !recording
}
)

总结
全新的图形库为安卓系统带来了一系列全新的形状应用可能性。在本文的示例中,我们通过基本形状,展示了裁剪以及形变动画。
借助这些创建形状和形变的新 API,还有许多其他用法等待开发者们去探索。