您可以使用 Compose 建立由多邊形構成的圖形。舉例來說,您可以製作下列形狀:
如要在 Compose 中建立自訂圓角多邊形,請將 graphics-shapes
依附元件新增至 app/build.gradle
:
implementation "androidx.graphics:graphics-shapes:1.0.0-rc01"
這個程式庫可讓你透過多邊形建立形狀。雖然多邊形形狀只有直線和銳角,但這些形狀允許選用的圓角。這可讓您輕鬆在兩個不同形狀之間變形。在任意形狀之間變形的難度很高,而且通常是設計階段的問題。但這個程式庫可透過類似多邊形結構的形狀轉換,簡化這項作業。
建立多邊形
以下程式碼片段會在繪圖區域的中心建立基本多邊形形狀,其中含有 6 個路徑點:
Box( modifier = Modifier .drawWithCache { val roundedPolygon = RoundedPolygon( numVertices = 6, radius = size.minDimension / 2, centerX = size.width / 2, centerY = size.height / 2 ) val roundedPolygonPath = roundedPolygon.toPath().asComposePath() onDrawBehind { drawPath(roundedPolygonPath, color = Color.Blue) } } .fillMaxSize() )
在這個範例中,程式庫會建立 RoundedPolygon
,該物件會保留代表要求形狀的幾何圖形。如要在 Compose 應用程式中繪製該形狀,您必須從中取得 Path
物件,將形狀轉換為 Compose 瞭解如何繪製的形式。
將多邊形的邊角設為圓角
如要將多邊形的邊角設為圓角,請使用 CornerRounding
參數。這個方法會採用兩個參數:radius
和 smoothing
。每個圓角都由 1 到 3 個立方曲線組成,其中圓心為圓弧形狀,而兩側的曲線則從形狀邊緣轉換為圓心曲線。
Radius
radius
是用於圓滑頂點的圓形半徑。
例如,下方圓角三角形的建構方式如下:
平滑程度
平滑度是決定從圓形圓角到邊緣的時間長短的因素。平滑因子為 0 (未平滑,CornerRounding
的預設值) 會產生純圓形的圓角。非零平滑因數 (上限為 1.0) 會導致邊角被四捨五入為三個獨立的曲線。
舉例來說,下列程式碼片段說明將平滑度設為 0 與 1 之間的細微差異:
Box( modifier = Modifier .drawWithCache { val roundedPolygon = RoundedPolygon( numVertices = 3, radius = size.minDimension / 2, centerX = size.width / 2, centerY = size.height / 2, rounding = CornerRounding( size.minDimension / 10f, smoothing = 0.1f ) ) val roundedPolygonPath = roundedPolygon.toPath().asComposePath() onDrawBehind { drawPath(roundedPolygonPath, color = Color.Black) } } .size(100.dp) )
大小和位置
根據預設,系統會在中心 (0, 0
) 周圍建立半徑為 1
的形狀。此半徑代表形狀所依據的多邊形中心和外側頂點之間的距離。請注意,圓角的形狀會縮小,因為圓角與中心的邊角接近中心的頂點。如要調整多邊形大小,請調整 radius
值。如要調整位置,請變更多邊形的 centerX
或 centerY
。或者,您也可以使用標準 DrawScope
轉換函式 (例如 DrawScope#translate()
) 轉換物件,以變更其大小、位置和旋轉角度。
變形形狀
Morph
物件是新的形狀,代表兩個多邊形形狀之間的動畫。如要在兩個形狀之間轉換,請建立兩個 RoundedPolygons
和一個採用這兩個形狀的 Morph
物件。如要計算起始形狀和結束形狀之間的形狀,請提供介於 0 和 1 之間的 progress
值,以決定起始形狀 (0) 和結束形狀 (1) 之間的形式:
Box( modifier = Modifier .drawWithCache { val triangle = RoundedPolygon( numVertices = 3, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f, rounding = CornerRounding( size.minDimension / 10f, smoothing = 0.1f ) ) val square = RoundedPolygon( numVertices = 4, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f ) val morph = Morph(start = triangle, end = square) val morphPath = morph .toPath(progress = 0.5f).asComposePath() onDrawBehind { drawPath(morphPath, color = Color.Black) } } .fillMaxSize() )
在上述範例中,進度在兩個形狀 (圓角三角形和正方形) 的一半就好,會產生下列結果:
在大多數情況下,變形是動畫的一部分,而非靜態算繪。如要為這兩者之間的變化加上動畫效果,您可以使用 Compose 中的 Animation API 標準,隨著時間變化來變更進度值。舉例來說,您可以無限地為這兩種形狀之間的變形建立動畫,如下所示:
val infiniteAnimation = rememberInfiniteTransition(label = "infinite animation") val morphProgress = infiniteAnimation.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( tween(500), repeatMode = RepeatMode.Reverse ), label = "morph" ) Box( modifier = Modifier .drawWithCache { val triangle = RoundedPolygon( numVertices = 3, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f, rounding = CornerRounding( size.minDimension / 10f, smoothing = 0.1f ) ) val square = RoundedPolygon( numVertices = 4, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f ) val morph = Morph(start = triangle, end = square) val morphPath = morph .toPath(progress = morphProgress.value) .asComposePath() onDrawBehind { drawPath(morphPath, color = Color.Black) } } .fillMaxSize() )
使用多邊形做為剪輯片段
通常會在 Compose 中使用 clip
修飾符,以變更可組合項的算繪方式,並利用在裁剪區域周圍繪製的陰影:
fun RoundedPolygon.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) } class RoundedPolygonShape( private val polygon: RoundedPolygon, private var matrix: Matrix = Matrix() ) : Shape { private var path = Path() override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { path.rewind() path = polygon.toPath().asComposePath() matrix.reset() val bounds = polygon.getBounds() val maxDimension = max(bounds.width, bounds.height) matrix.scale(size.width / maxDimension, size.height / maxDimension) matrix.translate(-bounds.left, -bounds.top) path.transform(matrix) return Outline.Generic(path) } }
接著,您可以使用多邊形做為剪輯片段,如以下程式碼片段所示:
val hexagon = remember { RoundedPolygon( 6, rounding = CornerRounding(0.2f) ) } val clip = remember(hexagon) { RoundedPolygonShape(polygon = hexagon) } Box( modifier = Modifier .clip(clip) .background(MaterialTheme.colorScheme.secondary) .size(200.dp) ) { Text( "Hello Compose", color = MaterialTheme.colorScheme.onSecondary, modifier = Modifier.align(Alignment.Center) ) }
這會產生以下結果:
這可能看起來與先前算繪的內容沒有太大差異,但可讓您在 Compose 中運用其他功能。舉例來說,這項技巧可用於裁剪圖片,並在裁剪區域周圍套用陰影:
val hexagon = remember { RoundedPolygon( 6, rounding = CornerRounding(0.2f) ) } val clip = remember(hexagon) { RoundedPolygonShape(polygon = hexagon) } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .graphicsLayer { this.shadowElevation = 6.dp.toPx() this.shape = clip this.clip = true this.ambientShadowColor = Color.Black this.spotShadowColor = Color.Black } .size(200.dp) ) }
點按後變形按鈕
您可以使用 graphics-shape
程式庫建立按鈕,在按下兩個形狀時變形。首先,請建立可擴充 Shape
的 MorphPolygonShape
,並進行適當翻譯,並適當轉譯這個物件。請注意,您必須傳入進度,才能為形狀製作動畫:
class MorphPolygonShape( private val morph: Morph, private val percentage: Float ) : Shape { private val matrix = Matrix() override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y. matrix.scale(size.width / 2f, size.height / 2f) matrix.translate(1f, 1f) val path = morph.toPath(progress = percentage).asComposePath() path.transform(matrix) return Outline.Generic(path) } }
如要使用這項變形形狀,請建立兩個多邊形:shapeA
和 shapeB
。建立並記住 Morph
。接著,將變形效果套用至按鈕做為剪輯邊框,並使用按下時的 interactionSource
做為動畫背後的驅動力:
val shapeA = remember { RoundedPolygon( 6, rounding = CornerRounding(0.2f) ) } val shapeB = remember { RoundedPolygon.star( 6, rounding = CornerRounding(0.1f) ) } val morph = remember { Morph(shapeA, shapeB) } val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val animatedProgress = animateFloatAsState( targetValue = if (isPressed) 1f else 0f, label = "progress", animationSpec = spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium) ) Box( modifier = Modifier .size(200.dp) .padding(8.dp) .clip(MorphPolygonShape(morph, animatedProgress.value)) .background(Color(0xFF80DEEA)) .size(200.dp) .clickable(interactionSource = interactionSource, indication = null) { } ) { Text("Hello", modifier = Modifier.align(Alignment.Center)) }
使用者輕觸方塊時會看到以下動畫:
以無限循環的動畫呈現形狀變形
如要為變形形狀建立無限動畫,請使用 rememberInfiniteTransition
。以下是個人資料相片隨著時間無限變換形狀 (和旋轉) 的範例。這種做法會對上述 MorphPolygonShape
進行微調:
class CustomRotatingMorphShape( private val morph: Morph, private val percentage: Float, private val rotation: Float ) : Shape { private val matrix = Matrix() override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y. matrix.scale(size.width / 2f, size.height / 2f) matrix.translate(1f, 1f) matrix.rotateZ(rotation) val path = morph.toPath(progress = percentage).asComposePath() path.transform(matrix) return Outline.Generic(path) } } @Preview @Composable private fun RotatingScallopedProfilePic() { val shapeA = remember { RoundedPolygon( 12, rounding = CornerRounding(0.2f) ) } val shapeB = remember { RoundedPolygon.star( 12, rounding = CornerRounding(0.2f) ) } val morph = remember { Morph(shapeA, shapeB) } val infiniteTransition = rememberInfiniteTransition("infinite outline movement") val animatedProgress = infiniteTransition.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( tween(2000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "animatedMorphProgress" ) val animatedRotation = infiniteTransition.animateFloat( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable( tween(6000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "animatedMorphProgress" ) Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .clip( CustomRotatingMorphShape( morph, animatedProgress.value, animatedRotation.value ) ) .size(200.dp) ) } }
這個程式碼會產生以下有趣的結果:
自訂多邊形
如果使用一般多邊形建立的形狀不適用於您的用途,您可以利用端點清單進一步建立自訂形狀。例如,您可能想要建立下列心形圖案:
您可以使用 RoundedPolygon
超載 (會接收 x、y 座標的浮點陣列),指定此形狀的個別頂點。
如要分解心形多邊形,請注意,使用極座標系統指定座標點比使用笛卡爾 (x,y) 座標系統更容易,因為 0°
會從右側開始,並順時針前進,270°
則位於 12 點鐘位置:
您現在可以透過更簡單的方式定義形狀,方法是指定角度 (𝜭?) 和從各個點到中心的半徑:
您現在可以建立頂點並傳遞至 RoundedPolygon
函式:
val vertices = remember { val radius = 1f val radiusSides = 0.8f val innerRadius = .1f floatArrayOf( radialToCartesian(radiusSides, 0f.toRadians()).x, radialToCartesian(radiusSides, 0f.toRadians()).y, radialToCartesian(radius, 90f.toRadians()).x, radialToCartesian(radius, 90f.toRadians()).y, radialToCartesian(radiusSides, 180f.toRadians()).x, radialToCartesian(radiusSides, 180f.toRadians()).y, radialToCartesian(radius, 250f.toRadians()).x, radialToCartesian(radius, 250f.toRadians()).y, radialToCartesian(innerRadius, 270f.toRadians()).x, radialToCartesian(innerRadius, 270f.toRadians()).y, radialToCartesian(radius, 290f.toRadians()).x, radialToCartesian(radius, 290f.toRadians()).y, ) }
頂點需要使用此 radialToCartesian
函式轉換為笛卡兒座標:
internal fun Float.toRadians() = this * PI.toFloat() / 180f internal val PointZero = PointF(0f, 0f) internal fun radialToCartesian( radius: Float, angleRadians: Float, center: PointF = PointZero ) = directionVectorPointF(angleRadians) * radius + center internal fun directionVectorPointF(angleRadians: Float) = PointF(cos(angleRadians), sin(angleRadians))
上述程式碼會提供心形圖案的原始頂點,但您需要將特定角落圓弧化,才能取得所選的心形圖案。90°
和 270°
的邊角沒有圓角,但其他邊角卻可以。如要針對個別邊角進行自訂圓角,請使用 perVertexRounding
參數:
val rounding = remember { val roundingNormal = 0.6f val roundingNone = 0f listOf( CornerRounding(roundingNormal), CornerRounding(roundingNone), CornerRounding(roundingNormal), CornerRounding(roundingNormal), CornerRounding(roundingNone), CornerRounding(roundingNormal), ) } val polygon = remember(vertices, rounding) { RoundedPolygon( vertices = vertices, perVertexRounding = rounding ) } Box( modifier = Modifier .drawWithCache { val roundedPolygonPath = polygon.toPath().asComposePath() onDrawBehind { scale(size.width * 0.5f, size.width * 0.5f) { translate(size.width * 0.5f, size.height * 0.5f) { drawPath(roundedPolygonPath, color = Color(0xFFF15087)) } } } } .size(400.dp) )
這會產生粉紅心形圖示:
如果上述形狀無法涵蓋您的用途,建議您使用 Path
類別繪製自訂形狀,或從磁碟載入 ImageVector
檔案。graphics-shapes
程式庫並非用於任意形狀,而是專門用於簡化圓角多邊形的建立作業,以及這些形狀之間的轉換動畫。
其他資源
如需更多資訊和範例,請參閱下列資源: