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ần phụ thuộc
graphics-shapes vào
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 dạng đa giác chỉ có các cạnh thẳng và góc nhọn, nhưng các hình dạng này cho phép có các góc bo tròn tuỳ chọn. Đ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ỳ ý rất khó và thường là vấn đề về thời gian thiết kế. Tuy nhiên, thư viện này giúp bạn dễ dàng biến đổi giữa các hình dạng có cấu trúc đa giác tương tự.
Tạo đa giác
Đoạn mã sau đây tạo một hình dạng đa giác cơ bản có 6 điểm ở giữ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 sẽ 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 một ứng dụng Compose, bạn phải lấy đối tượng Path từ đó để đưa hình dạng vào một 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 sử dụng tham số CornerRounding. Tham số 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 bậc ba, tâm của đường cong có hình dạng cung tròn, trong khi hai đường cong bên ("bao quanh") chuyển đổi từ cạnh của hình dạng sang đường cong ở tâm.
Bán kính
radius là bán kính của hình tròn dùng để bo tròn một đỉnh.
Ví dụ: tam giác góc bo tròn 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.Làm mượt
Làm mượt là một yếu tố xác định thời gian cần thiết để chuyển từ phần bo tròn hình tròn của góc sang cạnh. Hệ số làm mượt là 0 (không làm mượt, giá trị mặc định cho CornerRounding) dẫn đến việc bo tròn góc hoàn toàn hình tròn. Hệ số làm mượt khác 0 (tối đa là 1.0) dẫn đến việc góc được bo tròn bằng 3 đường cong riêng biệt.
Ví dụ: đoạn mã dưới đây minh hoạ sự khác biệt tinh tế trong việc đặt giá trị làm mượt thành 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 là 1 quanh tâm (0, 0). Bán kính này biểu thị 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 đó. Xin lưu ý rằng việc bo tròn các góc sẽ tạo ra một hình dạng nhỏ hơn vì các góc bo tròn sẽ gần tâm hơn so với các đỉnh được bo 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à chế độ xoay của đối tượng
bằng các hàm biến đổi DrawScope tiêu chuẩn như
DrawScope#translate().
Biến đổi hình dạng
Đối tượng Morph là một hình dạng mới đại diện cho ảnh động giữa hai hình dạng đa giác. Để biến đổi giữa hai hình dạng, hãy tạo hai RoundedPolygons và một đối tượng Morph lấy 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ày nằm chính xác ở giữa hai hình dạng (tam giác bo tròn và hình vuông), tạo ra kết quả sau:
Trong hầu hết các trường hợp, việc biến đổi được thực hiện như một phần của ảnh động, chứ không chỉ là kết xuất tĩnh. Để tạo ảnh động giữa hai hình dạng này, bạn có thể sử dụng các API Ảnh động tiêu chuẩn trong Compose để thay đổi giá trị tiến trình theo thời gian. Ví dụ: bạn có thể tạo ảnh động vô hạn cho quá trình biến đổi 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() )
Sử dụng đa giác làm vùng cắt
Bạn thường dùng đối tượng sửa đổi
clip
trong Compose để thay đổi cách kết xuất thành phần kết hợp và tận dụng
bóng đổ vẽ 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 làm vùng cắt, như minh hoạ 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:
Hình này có thể không khác nhiều so với hình đang 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ụ: bạn có thể sử dụng kỹ thuật này để cắt một hình ảnh và áp dụng bóng đổ xung quanh vùng đã 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) ) }
Biến đổi nút khi nhấp
Bạn có thể sử dụng thư viện graphics-shape để tạo một nút biến đổ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 tỷ lệ và dịch chuyển để phù hợp. Lưu ý việc truyền tiến trình để 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 đổi này, hãy tạo hai đa giác, shapeA và shapeB. Tạo và ghi nhớ Morph. Sau đó, áp dụng quá trình biến đổi cho nút dưới dạng đường viền vùng cắt, sử dụng interactionSource khi nhấn làm động lực thúc đẩy ả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)) }
Điều này dẫn đến ảnh động sau khi bạn nhấn vào hộp:
Tạo ảnh động cho quá trình biến đổi hình dạng vô hạn
Để tạo ảnh động vô tận cho 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) vô hạn theo thời gian. Phương pháp này sử dụng một điều chỉnh nhỏ cho MorphPolygonShape như minh hoạ ở 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 mang lại kết quả thú vị sau:
Đa giác tuỳ chỉnh
Nếu các hình dạng được tạo từ đa giác thông thường không bao gồm trường hợp sử dụng của bạn, thì bạn có thể tạo một hình dạng tuỳ chỉnh hơn bằng danh sách các đỉnh. Ví dụ: bạn có thể muốn tạo 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 dấu phẩy động gồm toạ độ x, y.
Để chia nhỏ đa giác hình 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 ở phía bên phải và tiến hành 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 các đỉnh 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, ) }
Bạn cần dịch các đỉnh thành 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 bo tròn các góc cụ thể để có được hình trái tim đã chọn. Các góc ở 90° và 270° không có độ bo tròn, nhưng các góc khác thì có. Để đạt được độ bo tròn tuỳ chỉnh cho từng góc, hãy sử dụng tham 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) )
Điều này dẫn đến hình trái tim màu hồng:
Nếu các hình dạng trước đó không bao gồm trường hợp sử dụng của bạn, hãy cân nhắc sử dụng Path
lớp để vẽ hình dạng tuỳ chỉnh, hoặc tải tệp
ImageVector từ
đĩa. Thư viện graphics-shapes không dùng cho các hình dạng tuỳ ý, mà được thiết kế đặc biệt để đơn giản hoá việc tạo đ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: Shape morphing in Android
- Shapes Github demonstration