Với Compose, bạn có thể tạo các hình dạng được tạo từ đa giác. Ví dụ: bạn có thể tạo các loại hình dạng sau:

Để tạo một đa giác bo tròn tuỳ chỉnh trong Compose, hãy thêm phương thức
Phần phụ thuộc graphics-shapes
vào của bạn
app/build.gradle
:
implementation "androidx.graphics:graphics-shapes:1.0.1"
Thư viện này cho phép bạn tạo các hình dạng được tạo từ đa giác. Mặc dù các hình đa giác chỉ có cạnh thẳng và góc nhọn, nhưng các hình này cho phép góc bo tròn không bắt buộc. Điều này giúp bạn dễ dàng biến đổi giữa hai hình dạng khác nhau. Việc biến đổi giữa các hình dạng tuỳ ý là rất khó và thường là vấn đề xảy ra trong thời gian thiết kế. Tuy nhiên, thư viện này trở nên đơn giản bằng cách chuyển đổi giữa có cấu trúc đa giác tương tự nhau.
Tạo đa giác
Đoạn mã sau tạo một hình đa giác cơ bản có 6 điểm ở giữa của vùng vẽ:
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() )

Trong ví dụ này, thư viện tạo một RoundedPolygon
chứa hình học đại diện cho hình dạng được yêu cầu. Để vẽ hình dạng đó trong ứng dụng Compose,
bạn phải lấy đối tượng Path
từ đối tượng đó để biến hình dạng này thành biểu mẫu mà Compose
biết cách vẽ.
Bo tròn các góc của đa giác
Để bo tròn các góc của đa giác, hãy dùng tham số CornerRounding
. Phương thức này có hai tham số là radius
và smoothing
. Mỗi góc bo tròn được tạo thành từ 1 đến 3 đường cong khối lập phương, trong đó tâm có hình vòng cung tròn trong khi hai đường cong bên ("bên cạnh") chuyển đổi từ cạnh của hình dạng đến đường cong trung tâm.
Bán kính
radius
là bán kính của hình tròn dùng để làm tròn một đỉnh.
Ví dụ: tam giác bo tròn góc sau đây được tạo như sau:


r
xác định kích thước bo tròn hình tròn của các góc bo tròn.Độ nhẵn
Độ mượt là một yếu tố xác định thời gian cần để
phần bo tròn tròn của góc đến cạnh. Hệ số làm mượt là 0 (không làm mượt, giá trị mặc định cho CornerRounding
) sẽ làm tròn góc hoàn toàn theo hình tròn. Hệ số làm mượt khác 0 (lên đến tối đa 1.0) dẫn đến
góc được làm tròn bởi 3 đường cong riêng biệt.


Ví dụ: đoạn mã dưới đây minh hoạ sự khác biệt nhỏ về chế độ cài đặt làm mượt 0 so với 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) )

Kích thước và vị trí
Theo mặc định, một hình dạng được tạo với bán kính 1
xung quanh tâm (0, 0
). Bán kính này thể hiện khoảng cách giữa tâm và các đỉnh bên ngoài của đa giác mà hình dạng dựa trên đó. Lưu ý rằng bo tròn các góc
dẫn đến hình dạng nhỏ hơn vì các góc bo tròn sẽ gần với
chính giữa so với các đỉnh được làm tròn. Để định kích thước đa giác, hãy điều chỉnh giá trị radius
. Để điều chỉnh vị trí, hãy thay đổi centerX
hoặc centerY
của đa giác.
Ngoài ra, hãy biến đổi đối tượng để thay đổi kích thước, vị trí và hướng xoay của đối tượng đó
bằng các hàm biến đổi DrawScope
chuẩn, chẳng hạn như
DrawScope#translate()
.
Hình dạng biến thể
Đối tượng Morph
là một hình dạng mới đại diện cho ảnh động giữa 2 đa giác
hình dạng. Để thay đổi giữa hai hình dạng, hãy tạo hai RoundedPolygons
và Morph
có hai hình dạng này. Để tính toán hình dạng giữa hình dạng bắt đầu và hình dạng kết thúc, hãy cung cấp giá trị progress
từ 0 đến 1 để xác định hình dạng của hình dạng đó giữa hình dạng bắt đầu (0) và hình dạng kết thúc (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() )
Trong ví dụ trên, tiến trình nằm chính xác giữa hai hình dạng (hình tam giác tròn và hình vuông), cho ra kết quả sau:

Trong hầu hết các trường hợp, hiệu ứng biến đổi được thực hiện trong một ảnh động chứ không chỉ là một kết xuất tĩnh. Để tạo ảnh động giữa hai thành phần này, bạn có thể sử dụng API Ảnh động trong Compose chuẩn để thay đổi giá trị tiến trình theo thời gian. Ví dụ: bạn có thể tạo hiệu ứng động biến đổi vô hạn giữa hai hình dạng này như sau:
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() )

Dùng đa giác làm đối tượng cắt
Thông thường, bạn nên sử dụng
clip
đối tượng sửa đổi trong Compose để thay đổi cách hiển thị một thành phần kết hợp và để lấy
tận dụng bóng đổ xung quanh vùng cắt:
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) } }
Sau đó, bạn có thể sử dụng đa giác dưới dạng một đoạn cắt, như trong đoạn mã sau:
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) ) }
Điều này dẫn đến những điều sau:

Giao diện này có thể trông không khác so với kết xuất trước đó, nhưng cho phép để tận dụng các tính năng khác trong Compose. Ví dụ: kỹ thuật này có thể dùng để cắt hình ảnh và áp dụng bóng xung quanh vùng bị cắt:
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) ) }

Nút Morph khi nhấp
Bạn có thể dùng thư viện graphics-shape
để tạo một nút thay đổi giữa
hai hình dạng khi nhấn. Trước tiên, hãy tạo một MorphPolygonShape
mở rộng Shape
, điều chỉnh theo tỷ lệ và dịch để phù hợp. Lưu ý rằng hàm truyền vào
để có thể tạo ảnh động cho hình dạng:
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) } }
Để sử dụng hình dạng biến hình này, hãy tạo 2 đa giác, shapeA
và shapeB
. Tạo và
hãy ghi nhớ Morph
. Sau đó, áp dụng hiệu ứng biến đổi cho nút dưới dạng đường viền clip, sử dụng interactionSource
khi nhấn làm động lực cho ảnh động:
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)) }
Thao tác này sẽ tạo ra ảnh động sau đây khi bạn nhấn vào hộp:

Tạo ảnh động biến đổi hình dạng vô hạn
Để tạo ảnh động liên tục cho một hình dạng biến đổi, hãy sử dụng rememberInfiniteTransition
.
Dưới đây là ví dụ về ảnh hồ sơ thay đổi hình dạng (và xoay)
theo thời gian. Phương pháp này điều chỉnh một chút đối với MorphPolygonShape
ở trên:
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) ) } }
Mã này sẽ cho ra kết quả thú vị sau:

Đa giác tuỳ chỉnh
Nếu hình dạng được tạo từ đa giác thông thường không phù hợp với trường hợp sử dụng của bạn, bạn có thể tạo một hình dạng tuỳ chỉnh hơn với danh sách các đỉnh. Ví dụ: bạn nên tạo một hình trái tim như sau:

Bạn có thể chỉ định các đỉnh riêng lẻ của hình dạng này bằng cách sử dụng phương thức nạp chồng RoundedPolygon
lấy một mảng float gồm các toạ độ x, y.
Để phân tích hình đa giác trái tim, hãy lưu ý rằng hệ toạ độ cực để chỉ định các điểm giúp việc này dễ dàng hơn so với việc sử dụng hệ toạ độ Descartes (x, y), trong đó 0°
bắt đầu ở bên phải và tiến theo chiều kim đồng hồ, với 270°
ở vị trí 12 giờ:

Giờ đây, bạn có thể xác định hình dạng theo cách dễ dàng hơn bằng cách chỉ định góc (𝜭) và bán kính từ tâm tại mỗi điểm:

Giờ đây, bạn có thể tạo và truyền các đỉnh vào hàm 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, ) }
Các đỉnh cần được dịch sang toạ độ Descartes bằng hàm radialToCartesian
này:
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))
Mã trước đó cung cấp cho bạn các đỉnh thô cho hình trái tim, nhưng bạn cần làm tròn các góc cụ thể để có được hình trái tim đã chọn. Các góc tại 90°
và 270°
không được bo tròn, nhưng các góc khác thì có. Để bo tròn tuỳ chỉnh cho từng góc, hãy sử dụng thông số 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) )
Kết quả là hình trái tim màu hồng:

Nếu các hình dạng ở trên không phù hợp với trường hợp sử dụng của bạn, hãy cân nhắc sử dụng Path
để vẽ một lớp tuỳ chỉnh
hình dạng hoặc tải lên một
ImageVector
tệp từ
. Thư viện graphics-shapes
không dùng cho các hình dạng tuỳ ý, mà dành riêng cho việc đơn giản hoá việc tạo các đa giác bo tròn và ảnh động biến đổi giữa các đa giác đó.
Tài nguyên khác
Để biết thêm thông tin và ví dụ, hãy xem các tài nguyên sau:
- Blog: The Shape of Things to Come – Shapes
- Blog: Biến đổi hình dạng trong Android
- Minh hoạ về hình dạng trên GitHub