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:
- 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 tương thích cho đối tượng sửa đổi và cho phép đối tượng sửa đổi dễ dàng liên kết với nhau. Nhà máy đối tượng sửa đổi tạo ra các thành phần đối tượng sửa đổi mà Compose sử 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
- Một 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 dễ nhất để triển khai một đối tượng sửa đổi tuỳ chỉnh là chỉ triển khai một nhà máy đối tượng sửa đổi tuỳ chỉnh. Nhà máy này kết hợp các nhà máy đối tượng sửa đổi khác đã được xác định với nhau. Nếu bạn cần nhiều hành vi tuỳ chỉnh hơn, hãy triển khai phần tử đối tượng sửa đổi bằng cách sử dụng các API Modifier.Node
. Đây là các API có cấp độ thấp hơn nhưng mang lại tính linh hoạt cao hơn.
Liên kết đối tượng sửa đổi hiện có với nhau
Thông thường, bạn có thể tạo các đối tượng sửa đổi tuỳ chỉnh chỉ bằng cách sử dụng các đối tượng sửa đổi hiện có. Ví dụ: Modifier.clip()
được triển khai bằ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 bạn 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 một đối tượng sửa đổi tuỳ chỉnh bằng cách sử dụng nhà máy đối tượng sửa đổi có thể kết hợp
Bạn cũng có thể tạo một đối tượng sửa đổi tuỳ chỉnh bằng cách sử dụng hàm có khả năng kết hợp để truyền các giá trị đến một đối tượng sửa đổi hiện có. Đây được gọi là đối tượng sửa đổi thành phần kết hợp (composable).
Việc sử dụng factory đối tượng sửa đổi có thể kết hợp để tạo đối tượng sửa đổi cũng cho phép 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 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 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 các giá trị mặc định từ CompositionLocal
, thì cách dễ nhất để triển khai là sử dụng nhà máy đối tượng sửa đổi có thể 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ố cảnh báo được nêu chi tiết dưới đây.
Các 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 đối tượng sửa đổi tuỳ chỉnh bằng cách sử dụng nhà máy của đối tượng sửa đổi có thể kết hợp, các thành phần kết hợp cục bộ sẽ lấy giá trị từ cây thành phần mà chúng được tạo ra, chứ khô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 lấy ví dụ về đối tượng sửa đổi cục bộ của thành phần kết hợp ở trên, được triển khai hơi khác bằng cách sử dụng hàm có khả năng kết hợp:
@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 đây không phải là cách bạn mong đợi đối tượng sửa đổi hoạt động, hãy sử dụng Modifier.Node
tuỳ chỉnh, vì thành phần kết hợp cục bộ sẽ được phân giải chính xác tại trang web sử dụng và có thể được di chuyển lên trên một cách an toàn.
Đối tượng sửa đổi hàm có khả năng kết hợp không bao giờ bị bỏ qua
Đối tượng sửa đổi nhà máy của thành phần kết hợp không bao giờ bị bỏ qua vì không thể bỏ qua các hàm có khả năng kết hợp 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. Việc này có thể gây tốn kém nếu kết hợp lại thường xuyên.
Phải gọi các đối tượng sửa đổi hàm có khả năng kết hợp trong một hàm có khả năng kết hợp
Giống như tất cả các hàm có khả năng kết hợp, bạn phải gọi đối tượng sửa đổi nhà máy có thể kết hợp trong thành phần kết hợp. Điều này giới hạn vị trí đối tượng sửa đổi có thể được chuyển lên, vì đối tượng sửa đổi đó không bao giờ có thể được di chuyển ra khỏi cấu trúc. Khi so sánh, các nhà máy của đối tượng sửa đổi không phải thành phần kết hợp có thể được chuyển lên khỏi các hàm có khả năng kết hợp để cho phép sử dụng lại và cải thiện hiệu suất dễ dàng hơn:
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 cũng là API mà Compose triển khai đố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:
- Cách triển khai
Modifier.Node
chứa logic và trạng thái của đối tượng sửa đổi. ModifierNodeElement
tạo và cập nhật các thực thể nút của đối tượng sửa đổi.- Nhà máy đối tượng sửa đổi không bắt buộc như đã trình bày chi tiết ở trên.
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 quá trình 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 trong 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à trình bày ví dụ về cách xây dựng một đối tượng sửa đổi tuỳ chỉnh để vẽ một vòng tròn.
Modifier.Node
Việc triển khai Modifier.Node
(trong ví dụ này là CircleNode
) sẽ 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, nó vẽ vòng tròn có màu được truyền vào hàm đối tượng sửa đổi.
Một nút triển khai Modifier.Node
cũng như không có hoặc nhiều loại nút. Có các loại nút khác nhau tuỳ thuộc vào chức năng của đối tượng sửa đổi yêu cầu. Ví dụ ở trên cần vẽ được, vì vậy ví dụ này sẽ triển khai DrawModifierNode
để cho phép ghi đè phương thức vẽ.
Có các loại sau:
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 |
||
|
||
|
||
|
||
Các |
||
Điều này có thể hữu ích khi kết hợp nhiều phương thức triển khai nút vào một. |
||
Cho phép các lớp |
Các nút sẽ tự động mất hiệu lực khi lệnh cập nhật được gọi trên phần tử tương ứng. Ví dụ của chúng ta là DrawModifierNode
, nên bất kỳ lần cập nhật nào được gọi trên phần tử, nút này 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ư nêu chi tiết bên dưới.
ModifierNodeElement
ModifierNodeElement
là một lớp bất biến, 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. Lệnh này được gọi để tạo nút khi áp dụng đối tượng sửa đổi lần đầu tiên. Thông thường, quá trình này tương đương với việc tạo và định cấu hình nút bằng các tham số đã được truyền đến nhà máy của đố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í đã tồn tại nút này, nhưng một thuộc tính đã thay đổi. Giá trị này được xác định bằng phương thứcequals
của lớp. Nút đối tượng sửa đổi đã tạo trước đó sẽ được gửi dưới dạng tham số đến 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 các nút được sử dụng lại theo cách này là yếu tố then chốt để tăng hiệu suất màModifier.Node
mang lại; do đó, bạn phải cập nhật nút hiện có thay vì tạo nút mới trong phương thứcupdate
. Trong ví dụ hình tròn của chúng ta, màu của nút sẽ được cập nhật.
Ngoài ra, việ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ên sử dụng một lớp dữ liệu để làm việc này. Các phương thức này 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 liệu 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 với tệp 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 quá trình triển khai chỉ cần 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 cùng nhau tạo ra đối tượng sửa đổi tuỳ chỉnh nhằm vẽ một vòng tròn bằng cách sử dụng các 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 thường gặp khi sử dụng Modifier.Node
Khi tạo đối tượng sửa đổi tuỳ chỉnh bằng Modifier.Node
, sau đây là một số trường hợp phổ biến bạn có thể gặp phải.
Tham số bằng 0
Nếu đối tượng sửa đổi không có tham số, thì đối tượng này 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. Dưới đây là cách triển khai mẫu của một đối tượng sửa đổi áp dụng lượng khoảng đệm cố định cho một thành phần kết hợp:
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 các 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
. Đối tượng sửa đổi Modifier.Node
lợi thế hơn đối tượng sửa đổi vừa được tạo bằng nhà máy có thể kết hợp là chúng có thể đọc giá trị của thành phần kết hợp cục bộ từ vị trí đố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 vị trí đố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ể của 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ộ của thành phần kết hợp, bạn có thể đọc giá trị hiện tại của thành phần đó trong một phạm vi:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
vàIntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
Ví dụ này quan sát giá trị của LocalContentColor
để vẽ nền dựa trên màu sắc. Khi ContentDrawScope
quan sát các thay đổi về bản tổng quan nhanh, đối tượng 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
. Dưới đây là một ví dụ đơ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) } }
Đối tượng sửa đổi ảnh động
Quá trình triển khai Modifier.Node
có quyền truy cập vào coroutineScope
. Điều này cho phép bạn sử dụng API Animatable của Compose. Ví dụ: đoạn mã này sửa đổi CircleNode
từ bên trên để làm mờ và nhỏ đi nhiều lần:
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private val alpha = Animatable(1f) override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { 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 giữa các đối tượng sửa đổi.
Ví dụ: cách triển khai cơ bản của nút đối tượng sửa đổi có thể nhấp và 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 sử dụng tính năng tự động vô hiệu hoá nút
Các nút Modifier.Node
tự động vô hiệu hoá khi lệnh gọi ModifierNodeElement
tương ứng cập nhật. Đôi khi, trong một đối tượng sửa đổi phức tạp hơn, bạn có thể muốn chọn không sử dụng hành vi này để có quyền kiểm soát chi tiết hơn đối với thời điểm đối tượng sửa đổi mất hiệu lực theo các giai đoạn.
Điều này có thể đặc biệt hữu ích nếu đối tượng sửa đổi tuỳ chỉnh 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ẽ (như color
) thay đổi, chứ không phải vô hiệu hoá bố cục.
Điều này có thể cải thiện hiệu suất của đối tượng sửa đổi.
Một ví dụ giả định về vấn đề này được hiển thị bên dưới với một đối tượng sửa đổi có các thuộc tính lambda color
, size
và onClick
. Đối tượng sửa đổi này chỉ vô hiệu hoá những nội dung bắt buộc và bỏ qua mọi trường hợp vô hiệu hoá không:
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) } } }