Ngoài thành phần kết hợp (composable) Canvas
, Compose còn có một số Modifiers
đồ hoạ hữu ích hỗ trợ vẽ nội dung tuỳ chỉnh. Các đối tượng sửa đổi này hữu ích vì có thể áp dụng cho bất kỳ thành phần kết hợp nào.
Đối tượng sửa đổi bản vẽ
Tất cả các lệnh vẽ đều được thực hiện bằng đối tượng sửa đổi bản vẽ trong Compose. Có 3 đối tượng sửa đổi bản vẽ chính trong Compose:
Đối tượng sửa đổi cơ sở cho bản vẽ là drawWithContent
. Với đối tượng này, bạn có thể quyết định thứ tự vẽ của Thành phần kết hợp và các lệnh vẽ được đưa ra trong đối tượng sửa đổi. drawBehind
là một trình bao bọc tiện lợi xung quanh drawWithContent
, có thứ tự vẽ được đặt phía sau nội dung của thành phần kết hợp. drawWithCache
gọi onDrawBehind
hoặc onDrawWithContent
bên trong nó, đồng thời cung cấp cơ chế để lưu các đối tượng được tạo trong đó vào bộ nhớ đệm.
Modifier.drawWithContent
: Chọn thứ tự vẽ
Với Modifier.drawWithContent
, bạn có thể thực hiện các thao tác DrawScope
trước hoặc sau nội dung của thành phần kết hợp. Hãy nhớ gọi drawContent
để hiển thị nội dung thực tế của thành phần kết hợp. Với đối tượng sửa đổi này, bạn có thể quyết định thứ tự của các thao tác, liệu bạn muốn vẽ nội dung trước hay sau các thao tác vẽ tuỳ chỉnh.
Ví dụ: nếu muốn hiển thị kiểu chuyển màu dạng hình tròn trên nội dung để tạo hiệu ứng đèn pin chiếu qua lỗ khoá trên giao diện người dùng, bạn có thể làm như sau:
var pointerOffset by remember { mutableStateOf(Offset(0f, 0f)) } Column( modifier = Modifier .fillMaxSize() .pointerInput("dragging") { detectDragGestures { change, dragAmount -> pointerOffset += dragAmount } } .onSizeChanged { pointerOffset = Offset(it.width / 2f, it.height / 2f) } .drawWithContent { drawContent() // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI. drawRect( Brush.radialGradient( listOf(Color.Transparent, Color.Black), center = pointerOffset, radius = 100.dp.toPx(), ) ) } ) { // Your composables here }
Modifier.drawBehind
: Vẽ sau một thành phần kết hợp
Modifier.drawBehind
cho phép bạn thực hiện các thao tác DrawScope
phía sau nội dung thành phần kết hợp được vẽ trên màn hình. Nếu xem cách triển khai Canvas
, bạn có thể nhận thấy đây chỉ là một trình bao bọc tiện lợi xung quanh Modifier.drawBehind
.
Cách vẽ một hình chữ nhật góc tròn phía sau Text
:
Text( "Hello Compose!", modifier = Modifier .drawBehind { drawRoundRect( Color(0xFFBBAAEE), cornerRadius = CornerRadius(10.dp.toPx()) ) } .padding(4.dp) )
Điều này dẫn đến kết quả sau:
Modifier.drawWithCache
: Vẽ và lưu đối tượng vẽ vào bộ nhớ đệm
Modifier.drawWithCache
lưu các đối tượng được tạo trong đó vào bộ nhớ đệm. Các đối tượng sẽ được lưu vào bộ nhớ đệm miễn là kích thước của vùng vẽ giống nhau hoặc bất kỳ đối tượng trạng thái nào được đọc không thay đổi. Đối tượng sửa đổi này rất hữu ích trong việc cải thiện hiệu suất của các hàm gọi vẽ, lý do là vì bạn không cần phân bổ lại các đối tượng (chẳng hạn như Brush, Shader, Path
, v.v.) được tạo trên bản vẽ.
Ngoài ra, bạn cũng có thể lưu các đối tượng vào bộ nhớ đệm bằng remember
bên ngoài đối tượng sửa đổi. Tuy nhiên, điều này không phải luôn khả thi vì không phải lúc nào bạn cũng có quyền truy cập vào thành phần kết hợp. Việc sử dụng drawWithCache
có thể hiệu quả hơn nếu bạn chỉ dùng các đối tượng để vẽ.
Ví dụ: nếu bạn tạo một Brush
để vẽ một vùng chuyển màu phía sau Text
, thì việc sử dụng drawWithCache
sẽ lưu đối tượng Brush
vào bộ nhớ đệm cho đến khi kích thước của vùng vẽ thay đổi:
Text( "Hello Compose!", modifier = Modifier .drawWithCache { val brush = Brush.linearGradient( listOf( Color(0xFF9E82F0), Color(0xFF42A5F5) ) ) onDrawBehind { drawRoundRect( brush, cornerRadius = CornerRadius(10.dp.toPx()) ) } } )
Đối tượng sửa đổi đồ hoạ
Modifier.graphicsLayer
: Áp dụng phép biến đổi cho thành phần kết hợp
Modifier.graphicsLayer
là đối tượng sửa đổi giúp nội dung của thành phần kết hợp được vẽ thành một lớp vẽ. Một lớp sẽ cung cấp vài loại chức năng, chẳng hạn như:
- Tách biệt lệnh vẽ (tương tự với
RenderNode
). Lệnh vẽ được ghi dưới dạng một cấu phần của lớp có thể được thực hiện lại một cách hiệu quả bằng quy trình kết xuất mà không cần thực thi lại mã xử lý ứng dụng. - Các phép biến đổi áp dụng cho mọi lệnh vẽ có trong một lớp.
- Tạo điểm ảnh cho các tính năng kết hợp. Khi tạo điểm ảnh cho một lớp, các lệnh vẽ của lớp đó sẽ được thực thi và đầu ra sẽ được ghi vào vùng đệm ngoài màn hình. Việc kết hợp vùng đệm như vậy cho các khung tiếp theo sẽ nhanh hơn so với việc thực thi riêng từng lệnh, nhưng lớp sẽ hoạt động như một bitmap khi áp dụng các phép biến đổi như điều chỉnh theo tỷ lệ hoặc xoay.
Phép biến đổi
Modifier.graphicsLayer
cung cấp tính năng tách biệt lệnh vẽ; ví dụ: bạn có thể áp dụng nhiều phép biến đổi bằng Modifier.graphicsLayer
.
Bạn có thể tạo ảnh động hoặc chỉnh sửa các phép biến đổi này mà không cần thực thi lại hàm lambda vẽ.
Modifier.graphicsLayer
không thay đổi kích thước hoặc vị trí được đo lường của thành phần kết hợp, vì đối tượng chỉnh sửa này chỉ ảnh hưởng đến giai đoạn vẽ. Điều này có nghĩa là thành phần kết hợp của bạn có thể trùng lặp với những thành phần kết hợp khác nếu hình vẽ vượt ra ngoài các giới hạn bố cục.
Bạn có thể áp dụng các phép biến đổi sau bằng đối tượng sửa đổi này:
Điều chỉnh theo tỷ lệ – tăng kích thước
scaleX
và scaleY
lần lượt có chức năng là phóng to hoặc thu nhỏ nội dung theo chiều ngang hoặc chiều dọc. Giá trị 1.0f
cho biết không có thay đổi về tỷ lệ, giá trị 0.5f
có nghĩa là một nửa kích thước.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.scaleX = 1.2f this.scaleY = 0.8f } )
Phép tịnh tiến
Bạn có thể thay đổi translationX
và translationY
bằng graphicsLayer
, translationX
di chuyển thành phần kết hợp sang trái hoặc phải. translationY
di chuyển thành phần kết hợp lên hoặc xuống.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.translationX = 100.dp.toPx() this.translationY = 10.dp.toPx() } )
Phép xoay
Đặt rotationX
để xoay theo chiều ngang, rotationY
để xoay theo chiều dọc và rotationZ
để xoay trên trục Z (phép xoay chuẩn). Giá trị này được chỉ định bằng độ (0-360).
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
Điểm gốc
Bạn có thể chỉ định transformOrigin
. Sau đó, giá trị này sẽ được dùng làm điểm thực hiện các phép biến đổi. Tất cả các ví dụ đã trình bày đều sử dụng TransformOrigin.Center
, tại điểm (0.5f, 0.5f)
. Nếu bạn chỉ định điểm gốc tại (0f, 0f)
, các phép biến đổi sẽ bắt đầu từ góc trên cùng bên trái của thành phần kết hợp.
Nếu thay đổi điểm gốc bằng phép biến đổi rotationZ
, bạn có thể thấy mục xoay quanh phía trên cùng bên trái của thành phần kết hợp:
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.transformOrigin = TransformOrigin(0f, 0f) this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
Phần cắt và hình dạng
Hình dạng chỉ định đường viền mà nội dung được cắt khi clip = true
. Trong ví dụ này, chúng ta đặt 2 hộp có 2 loại phần cắt – một hộp dùng biến cắt graphicsLayer
và hộp còn lại sử dụng trình bao bọc tiện lợi Modifier.clip
.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .size(200.dp) .graphicsLayer { clip = true shape = CircleShape } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(CircleShape) .background(Color(0xFF4DB6AC)) ) }
Nội dung của hộp đầu tiên (văn bản có nội dung "Hello Compose") được cắt theo hình tròn:
Sau đó, nếu áp dụng translationY
vào hình tròn màu hồng trên cùng, bạn sẽ thấy các ranh giới của Thành phần kết hợp vẫn giữ nguyên, nhưng hình tròn này sẽ được vẽ phía dưới hình tròn dưới cùng (và bên ngoài các ranh giới của nó).
Để cắt thành phần kết hợp theo vùng được vẽ, bạn có thể thêm một Modifier.clip(RectangleShape)
khác ở đầu chuỗi đối tượng sửa đổi. Nội dung sẽ vẫn nằm trong các ranh giới ban đầu.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .clip(RectangleShape) .size(200.dp) .border(2.dp, Color.Black) .graphicsLayer { clip = true shape = CircleShape translationY = 50.dp.toPx() } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(RoundedCornerShape(500.dp)) .background(Color(0xFF4DB6AC)) ) }
Alpha
Bạn có thể dùng Modifier.graphicsLayer
để đặt alpha
(độ mờ) cho toàn bộ lớp. 1.0f
là mờ hoàn toàn và 0.0f
là không hiển thị.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
Chiến lược kết hợp
Việc xử lý alpha và độ trong suốt có thể không đơn giản như việc thay đổi một giá trị alpha đơn lẻ. Ngoài việc thay đổi alpha, bạn cũng có thể thiết lập CompositingStrategy
trên graphicsLayer
. CompositingStrategy
xác định cách kết hợp nội dung của thành phần kết hợp với nội dung khác đã được vẽ trên màn hình.
Có các chiến lược sau:
Tự động (mặc định)
Chiến lược kết hợp này do các tham số graphicsLayer
còn lại xác định. Chiến lược này kết xuất lớp trong một vùng đệm ngoài màn hình nếu alpha nhỏ hơn 1.0f hoặc một RenderEffect
được thiết lập. Bất cứ khi nào giá trị alpha nhỏ hơn 1f, một lớp kết hợp sẽ tự động được tạo để hiển thị nội dung, sau đó vẽ vùng đệm ngoài màn hình này vào đích đến bằng giá trị alpha tương ứng. Việc thiết lập RenderEffect
hoặc hiệu ứng cuộn quá mức sẽ luôn kết xuất nội dung tại vùng đệm ngoài màn hình bất kể bạn thiết lập CompositingStrategy
như thế nào.
Ngoài màn hình
Nội dung của thành phần kết hợp luôn được tạo điểm ảnh thành kết cấu hoặc bitmap ngoài màn hình trước khi hiển thị tại đích đến. Điều này hữu ích khi áp dụng các thao tác BlendMode
để che nội dung, cũng như giúp cải thiện hiệu suất khi kết xuất các tập hợp lệnh vẽ phức tạp.
Một ví dụ về cách sử dụng CompositingStrategy.Offscreen
là với BlendModes
. Hãy xem ví dụ dưới đây. Giả sử bạn muốn xoá các phần của thành phần kết hợp Image
bằng cách đưa ra một lệnh vẽ sử dụng BlendMode.Clear
. Nếu bạn không thiết lập compositingStrategy
thành CompositingStrategy.Offscreen
, thì BlendMode
sẽ tương tác với tất cả nội dung bên dưới nó.
Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .size(120.dp) .aspectRatio(1f) .background( Brush.linearGradient( listOf( Color(0xFFC5E1A5), Color(0xFF80DEEA) ) ) ) .padding(8.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } .drawWithCache { val path = Path() path.addOval( Rect( topLeft = Offset.Zero, bottomRight = Offset(size.width, size.height) ) ) onDrawWithContent { clipPath(path) { // this draws the actual image - if you don't call drawContent, it wont // render anything this@onDrawWithContent.drawContent() } val dotSize = size.width / 8f // Clip a white border for the content drawCircle( Color.Black, radius = dotSize, center = Offset( x = size.width - dotSize, y = size.height - dotSize ), blendMode = BlendMode.Clear ) // draw the red circle indication drawCircle( Color(0xFFEF5350), radius = dotSize * 0.8f, center = Offset( x = size.width - dotSize, y = size.height - dotSize ) ) } } )
Bằng cách đặt CompositingStrategy
thành Offscreen
, một kết cấu ngoài màn hình sẽ được tạo để thực thi các lệnh trên đó (chỉ áp dụng BlendMode
cho nội dung của thành phần kết hợp này). Sau đó, hàm này sẽ hiển thị trên nội dung đã hiển thị trên màn hình mà không ảnh hưởng đến nội dung đã vẽ.
Nếu bạn không sử dụng CompositingStrategy.Offscreen
, kết quả áp dụng BlendMode.Clear
sẽ xoá mọi pixel trong đích đến, bất kể bạn đã đặt giá trị nào, theo đó, vùng đệm hiển thị của cửa sổ (màu đen) sẽ xuất hiện. Nhiều BlendModes
liên quan đến alpha sẽ không hoạt động như mong đợi nếu không có vùng đệm ngoài màn hình. Hãy lưu ý vòng tròn màu đen xung quanh chỉ báo vòng tròn màu đỏ:
Để hiểu rõ hơn về điều này: nếu ứng dụng có nền cửa sổ nửa trong suốt và bạn không sử dụng CompositingStrategy.Offscreen
, thì BlendMode
sẽ tương tác với toàn bộ ứng dụng. Chế độ này sẽ xoá mọi pixel để hiển thị ứng dụng hoặc hình nền phía dưới, như trong ví dụ này:
Điểm đáng chú ý là khi sử dụng CompositingStrategy.Offscreen
, một kết cấu ngoài màn hình có kích thước của vùng vẽ sẽ được tạo và hiển thị lại trên màn hình. Theo mặc định, mọi lệnh vẽ được thực hiện bằng chiến lược này đều được cắt theo vùng này. Đoạn mã dưới đây minh hoạ các điểm khác biệt khi chuyển sang dùng kết cấu ngoài màn hình:
@Composable fun CompositingStrategyExamples() { Column( modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center) ) { // Does not clip content even with a graphics layer usage here. By default, graphicsLayer // does not allocate + rasterize content into a separate layer but instead is used // for isolation. That is draw invalidations made outside of this graphicsLayer will not // re-record the drawing instructions in this composable as they have not changed Canvas( modifier = Modifier .graphicsLayer() .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { // ... and drawing a size of 200 dp here outside the bounds drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx())) } Spacer(modifier = Modifier.size(300.dp)) /* Clips content as alpha usage here creates an offscreen buffer to rasterize content into first then draws to the original destination */ Canvas( modifier = Modifier // force to an offscreen buffer .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the content gets clipped */ drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx())) } } }
ModulateAlpha
Chiến lược kết hợp này điều chỉnh alpha cho mỗi lệnh vẽ được ghi trong graphicsLayer
. Chiến lược này sẽ không tạo vùng đệm ngoài màn hình cho alpha dưới 1.0f trừ phi RenderEffect
được đặt giá trị, do đó, việc kết xuất alpha có thể hiệu quả hơn. Tuy nhiên, chiến lược này có thể cung cấp nhiều kết quả cho nội dung chồng chéo. Đối với các trường hợp sử dụng mà biết trước rằng nội dung không chồng chéo, chiến lược này có thể mang đến hiệu suất tốt hơn CompositingStrategy.Auto
với alpha nhỏ hơn 1.
Dưới đây là một ví dụ khác về nhiều chiến lược kết hợp: áp dụng nhiều giá trị alpha cho nhiều phần của các thành phần kết hợp và áp dụng chiến lược Modulate
:
@Preview @Composable fun CompositingStrategy_ModulateAlpha() { Column( modifier = Modifier .fillMaxSize() .padding(32.dp) ) { // Base drawing, no alpha applied Canvas( modifier = Modifier.size(200.dp) ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // Alpha 0.5f applied to whole composable Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { alpha = 0.5f } ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // 0.75f alpha applied to each draw call when using ModulateAlpha Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.ModulateAlpha alpha = 0.75f } ) { drawSquares() } } } private fun DrawScope.drawSquares() { val size = Size(100.dp.toPx(), 100.dp.toPx()) drawRect(color = Red, size = size) drawRect( color = Purple, size = size, topLeft = Offset(size.width / 4f, size.height / 4f) ) drawRect( color = Yellow, size = size, topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f) ) } val Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350)
Ghi nội dung của một thành phần kết hợp vào bitmap
Một trường hợp sử dụng phổ biến là tạo Bitmap
từ một thành phần kết hợp. Để sao chép nội dung của thành phần kết hợp vào Bitmap
, hãy tạo GraphicsLayer
bằng rememberGraphicsLayer()
.
Chuyển hướng các lệnh vẽ đến lớp mới bằng cách sử dụng drawWithContent()
và graphicsLayer.record{}
. Sau đó, vẽ lớp trong canvas hiển thị bằng cách sử dụng drawLayer
:
val coroutineScope = rememberCoroutineScope() val graphicsLayer = rememberGraphicsLayer() Box( modifier = Modifier .drawWithContent { // call record to capture the content in the graphics layer graphicsLayer.record { // draw the contents of the composable into the graphics layer this@drawWithContent.drawContent() } // draw the graphics layer on the visible canvas drawLayer(graphicsLayer) } .clickable { coroutineScope.launch { val bitmap = graphicsLayer.toImageBitmap() // do something with the newly acquired bitmap } } .background(Color.White) ) { Text("Hello Android", fontSize = 26.sp) }
Bạn có thể lưu bitmap vào ổ đĩa và chia sẻ bitmap đó. Để biết thêm thông tin, hãy xem mã nguồn ví dụ đầy đủ. Hãy nhớ kiểm tra quyền trên thiết bị trước khi cố gắng lưu vào ổ đĩa.
Đối tượng sửa đổi bản vẽ tuỳ chỉnh
Để tạo đối tượng sửa đổi tuỳ chỉnh của riêng bạn, hãy triển khai giao diện DrawModifier
. Khi làm theo cách này, bạn có thể truy cập vào ContentDrawScope
mà giống với nội dung sẽ hiển thị khi sử dụng Modifier.drawWithContent()
. Sau đó, bạn có thể trích xuất các thao tác vẽ phổ biến cho đối tượng sửa đổi bản vẽ tuỳ chỉnh để dọn dẹp mã và cung cấp các trình bao bọc tiện lợi. Ví dụ: Modifier.background()
là một DrawModifier
tiện lợi.
Ví dụ: nếu muốn triển khai một Modifier
lật nội dung theo chiều dọc, bạn có thể tạo đối tượng sửa đổi như sau:
class FlippedModifier : DrawModifier { override fun ContentDrawScope.draw() { scale(1f, -1f) { this@draw.drawContent() } } } fun Modifier.flipped() = this.then(FlippedModifier())
Sau đó, hãy dùng đối tượng sửa đổi đã lật này được áp dụng trên Text
:
Text( "Hello Compose!", modifier = Modifier .flipped() )
Tài nguyên khác
Để biết thêm ví dụ về cách sử dụng graphicsLayer
và bản vẽ tuỳ chỉnh, hãy xem các tài nguyên sau:
- Making Jellyfish move in Compose (Tạo ảnh động hình con sứa trong Compose)
- ADS 2022 Layouts and Graphics in Compose (Bố cục và đồ hoạ trong Compose – Hội nghị Nhà phát triển Android 2022)
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Đồ hoạ trong Compose
- Tuỳ chỉnh hình ảnh {:#customize-image}
- Kotlin cho Jetpack Compose