Compose cung cấp nhiều đối tượng sửa đổi cho các hành vi phổ biến ngay từ đầu, nhưng bạn cũng có thể tạo đối tượng sửa đổi tuỳ chỉnh của riêng mình.
Đối tượng sửa đổi có nhiều phần:
- Một nhà máy đối tượng sửa đổi
- Đây là một hàm mở rộng trên
Modifier, cung cấp một API đặc trưng cho đối tượng sửa đổi và cho phép các đối tượng sửa đổi được liên kết với nhau. Nhà máy đối tượng sửa đổi tạo ra các phần tử đối tượng sửa đổi mà Compose dùng để sửa đổi giao diện người dùng của bạn.
- Đây là một hàm mở rộng trên
- Phần tử đối tượng sửa đổi
- Đây là nơi bạn có thể triển khai hành vi của đối tượng sửa đổi.
Có nhiều cách để triển khai đối tượng sửa đổi tuỳ chỉnh, tuỳ thuộc vào chức năng cần thiết. Thông thường, cách đơn giản nhất để triển khai đối tượng sửa đổi tuỳ chỉnh là triển khai một nhà máy đối tượng sửa đổi tuỳ chỉnh kết hợp các nhà máy đối tượng sửa đổi khác đã được xác định. Nếu bạn cần thêm hành vi tuỳ chỉnh, hãy triển khai phần tử đối tượng sửa đổi bằng API Modifier.Node. API này có cấp thấp hơn nhưng linh hoạt hơn.
Nối các đối tượng sửa đổi hiện có với nhau
Bạn thường có thể tạo đối tượng sửa đổi tuỳ chỉnh bằng cách sử dụng đối tượng sửa đổi hiện có. Ví dụ: Modifier.clip() được triển khai bằng cách sử dụng đối tượng sửa đổi graphicsLayer. Chiến lược này sử dụng các phần tử đối tượng sửa đổi hiện có và bạn cung cấp
nhà máy đối tượng sửa đổi tuỳ chỉnh của riêng mình.
Trước khi triển khai đối tượng sửa đổi tuỳ chỉnh của riêng bạn, hãy xem liệu bạn có thể sử dụng cùng một chiến lược hay không.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
Hoặc nếu thấy mình thường xuyên lặp lại cùng một nhóm đối tượng sửa đổi, bạn có thể gói các đối tượng đó vào đối tượng sửa đổi của riêng mình:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Tạo modifier tuỳ chỉnh bằng factory modifier composable
Bạn cũng có thể tạo một modifier tuỳ chỉnh bằng cách sử dụng hàm composable để truyền giá trị đến một modifier hiện có. Đây được gọi là nhà máy đối tượng sửa đổi có khả năng kết hợp.
Việc sử dụng một nhà máy đối tượng sửa đổi có khả năng kết hợp để tạo đối tượng sửa đổi cũng cho phép bạn sử dụng các API Compose cấp cao hơn, chẳng hạn như animate*AsState và các API ảnh động được hỗ trợ trạng thái Compose khác. Ví dụ: đoạn mã sau đây cho thấy một đối tượng sửa đổi tạo ảnh động cho thay đổi alpha khi được bật/tắt:
@Composable fun Modifier.fade(enable: Boolean): Modifier { val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f) return this then Modifier.graphicsLayer { this.alpha = alpha } }
Nếu đối tượng sửa đổi tuỳ chỉnh là một phương thức thuận tiện để cung cấp giá trị mặc định từ một CompositionLocal, thì cách dễ nhất để triển khai là sử dụng một nhà máy đối tượng sửa đổi có khả năng kết hợp:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
Phương pháp này có một số lưu ý được trình bày chi tiết trong các phần sau.
Giá trị CompositionLocal được phân giải tại vị trí gọi của nhà máy đối tượng sửa đổi
Khi tạo một modifier tuỳ chỉnh bằng cách sử dụng factory modifier composable, các thành phần cục bộ của composition sẽ lấy giá trị từ cây composition nơi chúng được tạo, chứ không phải nơi chúng được sử dụng. Điều này có thể dẫn đến kết quả không mong muốn. Ví dụ: hãy xem xét ví dụ về đối tượng sửa đổi cục bộ composition đã đề cập trước đó, được triển khai hơi khác một chút bằng cách sử dụng hàm composable:
@Composable fun Modifier.myBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) } @Composable fun MyScreen() { CompositionLocalProvider(LocalContentColor provides Color.Green) { // Background modifier created with green background val backgroundModifier = Modifier.myBackground() // LocalContentColor updated to red CompositionLocalProvider(LocalContentColor provides Color.Red) { // Box will have green background, not red as expected. Box(modifier = backgroundModifier) } } }
Nếu bạn không muốn đối tượng sửa đổi hoạt động theo cách này, hãy sử dụng Modifier.Node tuỳ chỉnh, vì các thành phần cục bộ sẽ được phân giải chính xác tại vị trí sử dụng và có thể được nâng lên một cách an toàn.
Modifier của hàm composable không bao giờ bị bỏ qua
Bạn không bao giờ có thể bỏ qua các modifier factory composable vì không thể bỏ qua các hàm composable có giá trị trả về. Điều này có nghĩa là hàm đối tượng sửa đổi của bạn sẽ được gọi trong mọi quá trình kết hợp lại. Điều này có thể gây tốn kém nếu quá trình kết hợp lại diễn ra thường xuyên.
Bạn phải gọi các modifier hàm composable trong một hàm composable
Giống như tất cả các hàm composable, đối tượng sửa đổi factory composable phải được gọi từ trong composition. Điều này giới hạn vị trí có thể kéo đối tượng sửa đổi lên, vì đối tượng sửa đổi không bao giờ có thể được kéo ra khỏi thành phần. Ngược lại, các factory modifier không có khả năng kết hợp có thể được kéo ra khỏi các hàm composable để dễ dàng sử dụng lại và cải thiện hiệu suất:
val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations @Composable fun Modifier.composableModifier(): Modifier { val color = LocalContentColor.current.copy(alpha = 0.5f) return this then Modifier.background(color) } @Composable fun MyComposable() { val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher }
Triển khai hành vi của đối tượng sửa đổi tuỳ chỉnh bằng Modifier.Node
Modifier.Node là một API cấp thấp hơn để tạo đối tượng sửa đổi trong Compose. Đây là cùng một API mà Compose triển khai các đối tượng sửa đổi riêng và là cách hiệu quả nhất để tạo đối tượng sửa đổi tuỳ chỉnh.
Triển khai đối tượng sửa đổi tuỳ chỉnh bằng Modifier.Node
Có 3 phần để triển khai đối tượng sửa đổi tuỳ chỉnh bằng Modifier.Node:
- Một phương thức triển khai
Modifier.Nodechứa logic và trạng thái của đối tượng sửa đổi. - Một
ModifierNodeElementtạo và cập nhật các thực thể nút đối tượng sửa đổi. - Một nhà máy đối tượng sửa đổi không bắt buộc, như đã trình bày chi tiết trước đó.
Các lớp ModifierNodeElement không có trạng thái và các thực thể mới được phân bổ cho mỗi lần kết hợp lại, trong khi các lớp Modifier.Node có thể có trạng thái và sẽ tồn tại qua nhiều lần kết hợp lại, thậm chí có thể được sử dụng lại.
Phần sau đây mô tả từng phần và cho thấy ví dụ về cách tạo một đối tượng sửa đổi tuỳ chỉnh để vẽ hình tròn.
Modifier.Node
Cách triển khai Modifier.Node (trong ví dụ này là CircleNode) triển khai chức năng của đối tượng sửa đổi tuỳ chỉnh.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
Trong ví dụ này, hàm vẽ hình tròn có màu được truyền vào hàm bổ trợ.
Một nút triển khai Modifier.Node cũng như không hoặc nhiều loại nút. Có nhiều loại nút dựa trên chức năng mà đối tượng sửa đổi của bạn yêu cầu. Ví dụ trước cần có khả năng vẽ, vì vậy, ví dụ này triển khai DrawModifierNode, cho phép ghi đè phương thức vẽ.
Sau đây là các loại hiện có:
Nút |
Tác dụng |
Đường liên kết mẫu |
|
||
|
||
Việc triển khai giao diện này cho phép |
||
|
||
|
||
Một |
||
|
||
|
||
|
||
Điều này có thể hữu ích khi kết hợp nhiều cách triển khai nút thành một. |
||
Cho phép các lớp |
Các nút sẽ tự động bị vô hiệu hoá khi lệnh cập nhật được gọi trên phần tử tương ứng. Vì ví dụ của chúng ta là DrawModifierNode, nên mỗi khi lệnh cập nhật được gọi trên phần tử, nút sẽ kích hoạt thao tác vẽ lại và màu của nút sẽ cập nhật chính xác. Bạn có thể chọn không tự động vô hiệu hoá, như được trình bày chi tiết trong phần Chọn không tự động vô hiệu hoá nút.
ModifierNodeElement
ModifierNodeElement là một lớp không thể thay đổi, chứa dữ liệu để tạo hoặc cập nhật đối tượng sửa đổi tuỳ chỉnh:
// ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } }
Quá trình triển khai ModifierNodeElement cần ghi đè các phương thức sau:
create: Đây là hàm tạo thực thể cho nút đối tượng sửa đổi. Hàm này được gọi để tạo nút khi đối tượng sửa đổi của bạn được áp dụng lần đầu tiên. Thông thường, việc này tương đương với việc tạo nút và định cấu hình nút bằng các tham số được truyền vào nhà máy đối tượng sửa đổi.update: Hàm này được gọi bất cứ khi nào đối tượng sửa đổi này được cung cấp ở cùng một vị trí mà nút này đã tồn tại, nhưng thuộc tính đã thay đổi. Điều này được xác định bằng phương thứcequalscủa lớp. Nút đối tượng sửa đổi đã tạo trước đó được gửi dưới dạng tham số cho lệnh gọiupdate. Tại thời điểm này, bạn nên cập nhật các thuộc tính của nút để tương ứng với các tham số đã cập nhật. Khả năng sử dụng lại các nút theo cách này là yếu tố then chốt giúp tăng hiệu suất màModifier.Nodemang lại; do đó, bạn phải cập nhật nút hiện có thay vì tạo một nút mới trong phương thứcupdate. Trong ví dụ về hình tròn, màu của nút được cập nhật.
Ngoài ra, các phương thức triển khai ModifierNodeElement cũng cần triển khai equals và hashCode. update sẽ chỉ được gọi nếu phép so sánh bằng với phần tử trước đó trả về giá trị false.
Ví dụ trước sử dụng một lớp dữ liệu để đạt được điều này. Các phương thức này được dùng để kiểm tra xem một nút có cần cập nhật hay không. Nếu phần tử của bạn có các thuộc tính không góp phần vào việc xác định xem một nút có cần được cập nhật hay không, hoặc bạn muốn tránh các lớp dữ liệu vì lý do tương thích nhị phân, thì bạn có thể triển khai equals và hashCode theo cách thủ công, ví dụ: phần tử đối tượng sửa đổi khoảng đệm.
Nhà máy đối tượng sửa đổi
Đây là nền tảng API công khai của đối tượng sửa đổi. Hầu hết các phương thức triển khai đều tạo phần tử đối tượng sửa đổi và thêm phần tử đó vào chuỗi đối tượng sửa đổi:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Ví dụ đầy đủ
Ba phần này kết hợp với nhau để tạo đối tượng sửa đổi tuỳ chỉnh nhằm vẽ một vòng tròn bằng API Modifier.Node:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color) // ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } } // Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
Các trường hợp phổ biến khi sử dụng Modifier.Node
Khi tạo đối tượng sửa đổi tuỳ chỉnh bằng Modifier.Node, bạn có thể gặp một số tình huống phổ biến sau đây.
Không có tham số
Nếu đối tượng sửa đổi không có tham số, thì đối tượng đó không bao giờ cần cập nhật và hơn nữa, không cần phải là một lớp dữ liệu. Sau đây là ví dụ về cách triển khai một modifier áp dụng một khoảng đệm cố định cho một composable:
fun Modifier.fixedPadding() = this then FixedPaddingElement data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() { override fun create() = FixedPaddingNode() override fun update(node: FixedPaddingNode) {} } class FixedPaddingNode : LayoutModifierNode, Modifier.Node() { private val PADDING = 16.dp override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val paddingPx = PADDING.roundToPx() val horizontal = paddingPx * 2 val vertical = paddingPx * 2 val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) val width = constraints.constrainWidth(placeable.width + horizontal) val height = constraints.constrainHeight(placeable.height + vertical) return layout(width, height) { placeable.place(paddingPx, paddingPx) } } }
Tham chiếu thành phần cục bộ
Đối tượng sửa đổi Modifier.Node không tự động quan sát các thay đổi đối với đối tượng trạng thái Compose, chẳng hạn như CompositionLocal. Ưu điểm của đối tượng sửa đổi Modifier.Node so với đối tượng sửa đổi chỉ được tạo bằng một factory composable là chúng có thể đọc giá trị của composition local từ nơi đối tượng sửa đổi được sử dụng trong cây giao diện người dùng, chứ không phải nơi đối tượng sửa đổi được phân bổ, bằng cách sử dụng currentValueOf.
Tuy nhiên, các thực thể nút đối tượng sửa đổi không tự động quan sát các thay đổi về trạng thái. Để tự động phản ứng với một thay đổi cục bộ về thành phần, bạn có thể đọc giá trị hiện tại của thành phần đó trong một phạm vi:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScopevàIntrinsicMeasureScopeSemanticsModifierNode:SemanticsPropertyReceiver
Ví dụ này quan sát giá trị của LocalContentColor để vẽ nền dựa trên màu của giá trị đó. Vì ContentDrawScope quan sát các thay đổi về ảnh chụp nhanh, nên thao tác này sẽ tự động vẽ lại khi giá trị của LocalContentColor thay đổi:
class BackgroundColorConsumerNode : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode { override fun ContentDrawScope.draw() { val currentColor = currentValueOf(LocalContentColor) drawRect(color = currentColor) drawContent() } }
Để phản ứng với các thay đổi về trạng thái bên ngoài phạm vi và tự động cập nhật đối tượng sửa đổi, hãy sử dụng ObserverModifierNode.
Ví dụ: Modifier.scrollable sử dụng kỹ thuật này để quan sát các thay đổi trong LocalDensity. Ví dụ sau đây minh hoạ một trường hợp đơn giản:
class ScrollableNode : Modifier.Node(), ObserverModifierNode, CompositionLocalConsumerModifierNode { // Place holder fling behavior, we'll initialize it when the density is available. val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity)) override fun onAttach() { updateDefaultFlingBehavior() observeReads { currentValueOf(LocalDensity) } // monitor change in Density } override fun onObservedReadsChanged() { // if density changes, update the default fling behavior. updateDefaultFlingBehavior() } private fun updateDefaultFlingBehavior() { val density = currentValueOf(LocalDensity) defaultFlingBehavior.flingDecay = splineBasedDecay(density) } }
Tạo ảnh động cho đối tượng sửa đổi
Các phương thức triển khai Modifier.Node có quyền truy cập vào coroutineScope. Điều này cho phép
sử dụng API Ảnh động trong Compose. Ví dụ: đoạn mã này sửa đổi CircleNode đã hiển thị trước đó để lặp lại hiệu ứng mờ dần và rõ dần:
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private lateinit var alpha: Animatable<Float, AnimationVector1D> override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { alpha = Animatable(1f) coroutineScope.launch { alpha.animateTo( 0f, infiniteRepeatable(tween(1000), RepeatMode.Reverse) ) { } } } }
Chia sẻ trạng thái giữa các đối tượng sửa đổi bằng cách sử dụng tính năng uỷ quyền
Đối tượng sửa đổi Modifier.Node có thể uỷ quyền cho các nút khác. Có nhiều trường hợp sử dụng cho việc này, chẳng hạn như trích xuất các phương thức triển khai phổ biến trên nhiều đối tượng sửa đổi, nhưng cũng có thể dùng để chia sẻ trạng thái chung trên các đối tượng sửa đổi.
Ví dụ: cách triển khai cơ bản của một nút đối tượng sửa đổi có thể nhấp để chia sẻ dữ liệu tương tác:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Chọn không tự động vô hiệu hoá nút
Các nút Modifier.Node sẽ tự động mất hiệu lực khi các lệnh gọi ModifierNodeElement tương ứng cập nhật. Đối với các đối tượng sửa đổi phức tạp, bạn có thể chọn không sử dụng hành vi này để kiểm soát chi tiết hơn thời điểm đối tượng sửa đổi vô hiệu hoá các giai đoạn.
Điều này đặc biệt hữu ích nếu đối tượng sửa đổi tuỳ chỉnh của bạn sửa đổi cả bố cục và bản vẽ. Việc chọn không tự động vô hiệu hoá cho phép bạn chỉ vô hiệu hoá bản vẽ khi chỉ các thuộc tính liên quan đến bản vẽ, chẳng hạn như color, thay đổi. Điều này giúp tránh làm mất hiệu lực bố cục và có thể cải thiện hiệu suất của đối tượng sửa đổi.
Ví dụ giả định về điều này được trình bày trong ví dụ sau đây với một đối tượng sửa đổi
có hàm lambda color, size và onClick làm thuộc tính. Đối tượng sửa đổi này chỉ vô hiệu hoá những gì cần thiết, bỏ qua mọi hoạt động vô hiệu hoá không cần thiết:
class SampleInvalidatingNode( var color: Color, var size: IntSize, var onClick: () -> Unit ) : DelegatingNode(), LayoutModifierNode, DrawModifierNode { override val shouldAutoInvalidate: Boolean get() = false private val clickableNode = delegate( ClickablePointerInputNode(onClick) ) fun update(color: Color, size: IntSize, onClick: () -> Unit) { if (this.color != color) { this.color = color // Only invalidate draw when color changes invalidateDraw() } if (this.size != size) { this.size = size // Only invalidate layout when size changes invalidateMeasurement() } // If only onClick changes, we don't need to invalidate anything clickableNode.update(onClick) } override fun ContentDrawScope.draw() { drawRect(color) } override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val size = constraints.constrain(size) val placeable = measurable.measure(constraints) return layout(size.width, size.height) { placeable.place(0, 0) } } }