Compose 預設提供許多修飾符,讓您立即就能處理常見行為,不過您也可以建立自訂修飾詞。
修飾符包含多個部分:
- 修飾符工廠
- 這是
Modifier
上的擴充功能函式,可為修飾符提供慣用的 API,並可讓修飾符輕鬆鏈結在一起。修飾符工廠會產生 Compose 用來修改 UI 的修飾符元素。
- 這是
- 修飾符元素
- 您可以在這裡實作修飾符的行為。
實作自訂修飾符的方式有很多種,視所需的功能而定。一般而言,如要實作自訂修飾符,最簡單的方法是實作自訂修飾符工廠,後者結合其他已定義的修飾符工廠。如果您需要更多自訂行為,請使用較低層級的 Modifier.Node
API 來實作修飾元元素,這種 API 級別較低,但提供更多彈性。
將現有修飾符鏈結在一起
通常只要使用現有的修飾詞即可建立自訂修飾詞。例如,Modifier.clip()
是使用 graphicsLayer
修飾符實作。此策略會使用現有的修飾符元素,而您可以自行提供自訂修飾符工廠。
在導入自己的自訂修飾符前,請先確認是否使用相同的策略。
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
或者,如果您經常重複相同的一組修飾符,則可將其納入自己的修飾符中:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
使用可組合修飾符工廠建立自訂修飾符
您也可以使用可組合函式建立自訂修飾符,將值傳遞給現有的修飾符。這就是所謂的可組合修飾符工廠。
使用可組合修飾符工廠來建立修飾符,也允許使用較高階的 Compose API,例如 animate*AsState
和其他 Compose 狀態支援的動畫 API。例如,下列程式碼片段顯示了修飾符,在啟用/停用時為 Alpha 變更加上動畫效果:
@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 } }
如果您的自訂修飾符是用於提供 CompositionLocal
預設值的簡便方法,最簡單的實作方式就是使用可組合項修飾詞工廠:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
這種方法有一些注意事項如下。
已在修飾符工廠的呼叫站解析 CompositionLocal
值
使用可組合修飾符工廠建立自訂修飾符時,組合本機值會從建立時的組合樹狀結構中採用值,而非使用。這可能會導致非預期的結果。例如,以上述的組合本機修飾符範例為例,使用可組合函式的實作方式略有不同:
@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) } } }
如果這不符合您預期修飾符的運作方式,請改用自訂的 Modifier.Node
,因為本機組合會在相關使用網站上正確解析,而且可以安全地提升。
一律不會略過可組合函式修飾符
系統絕不會「略過」可組合的工廠修飾符,因為您無法略過具有回傳值的可組合函式。這表示每次重組時都會呼叫修飾符函式,如果經常重組,可能會耗用較多資源。
您必須在可組合函式中呼叫可組合函式修飾符
如同所有可組合函式,您必須在組合中呼叫可組合工廠修飾符。此做法會限制修飾符可提升的位置,因為該修飾符可能無法從組合中提升。相較之下,非可組合項的修飾符工廠可以從可組合函式中提升,以便重複使用及提升效能:
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 }
使用 Modifier.Node
實作自訂修飾符行為
Modifier.Node
是較低的 API,用於在 Compose 中建立修飾符。這與 Compose 實作自身修飾符的 API 相同,也是建立自訂修飾符的效率最佳方式。
使用 Modifier.Node
實作自訂修飾符
使用 Modifier.Node 實作自訂修飾符分為三個部分:
- 保留修飾符邏輯和狀態的
Modifier.Node
實作。 - 用於建立及更新修飾符節點執行個體的
ModifierNodeElement
。 - 按照上述說明選取選用的修飾符工廠。
ModifierNodeElement
類別是無狀態類別,系統會為每個重組分配新的執行個體;而 Modifier.Node
類別可以是有狀態,並且會在多次重組期間繼續有效,甚至可以重複使用。
下節將說明每個部分,並提供範例,說明如何建立自訂修飾符來繪製圓形。
Modifier.Node
Modifier.Node
實作 (在此範例中為 CircleNode
) 會實作自訂修飾符的功能。
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
在這個範例中,系統會用傳遞至修飾符函式的顏色來繪製圓形。
節點會實作 Modifier.Node
以及零或多個節點類型。視修飾符需要的功能而定,您可以使用不同的節點類型。上述範例需要能夠繪圖,因此實作 DrawModifierNode
,允許其覆寫繪圖方法。
可用的類型如下:
節點 |
用法 |
範例連結 |
變更已包裝內容的測量和配置方式的 |
||
可繪製在版面配置空間的 |
||
實作此介面可讓 |
||
|
||
接收 PointerInputChanges 的 |
||
提供資料給上層版面配置的 |
||
接收 |
||
當內容的全域位置可能變更時,此 |
||
實作 |
||
可將工作委派給其他 如要將多個節點實作項目組合成單一節點,這項功能會很實用。 |
||
允許 |
在節點對應的元素上呼叫更新時,節點會自動失效。由於我們的範例是 DrawModifierNode
,因此每當元素呼叫更新時,節點就會觸發重新繪製動作,並正確更新其顏色。您可以選擇停用自動撤銷功能,詳情請參閱下文。
ModifierNodeElement
ModifierNodeElement
是不可變動的類別,其內含資料以建立或更新自訂修飾符:
// ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } }
ModifierNodeElement
實作需要覆寫下列方法:
create
:這個函式會將修飾符節點執行個體化。首次套用修飾符時,系統會呼叫此方法來建立節點。通常要建構節點並使用已傳入修飾符工廠的參數來設定節點數量。update
:每當在已有這個節點的相同位置提供此修飾符,但屬性有所變更時,就會呼叫此函式。這是由類別的equals
方法決定。先前建立的修飾符節點會以參數的形式傳送至update
呼叫。此時,您應更新節點的屬性,使其與更新後的參數對應。以這種方式重複使用節點的能力是Modifier.Node
提升效能的關鍵;因此,您必須更新現有節點,而不是在update
方法中建立新節點。在這個圓形範例中,節點的顏色已更新。
此外,ModifierNodeElement
實作也必須實作 equals
和 hashCode
。只有在與上一個元素的等值比較傳回 false 時,系統才會呼叫 update
。
上述範例使用資料類別達成此目的。這些方法可用於檢查節點是否需要更新。如果元素中的屬性無法影響節點是否需要更新,或是您基於二進位檔相容性因素而不想使用資料類別,您可以手動實作 equals
和 hashCode
(例如邊框間距修飾符元素)。
修飾符工廠
這是修飾符的公用 API 介面。大多數實作項目只會建立修飾符元素並新增至修飾符鏈結:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
完整範例
以下三個部分會結合在一起,用來建立自訂修飾詞,以便使用 Modifier.Node
API 繪製圓形:
// 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) } }
使用 Modifier.Node
的常見情況
使用 Modifier.Node
建立自訂修飾符時,您可能會遇到以下幾個常見情況。
0 個參數
如果您的修飾符沒有參數,則其也不需要更新,而且也不需要是資料類別。以下為修飾符實作範例,將固定數量的邊框間距套用至可組合項:
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) } } }
參照組合本機
Modifier.Node
修飾符不會自動觀察 Compose 狀態物件的變更 (例如 CompositionLocal
)。Modifier.Node
修飾符的優勢在於使用可組合項工廠建立的輔助鍵,其優點在於可透過在 UI 樹狀結構中使用修飾符的位置 (而非配置修飾符的位置) 讀取本機組合的值,而非使用 currentValueOf
進行配置。
不過,修飾符節點執行個體不會自動觀察狀態變更。如要自動回應本機組合的變更,可以讀取範圍內目前的值:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
和IntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
這個範例觀察 LocalContentColor
的值,以便根據顏色繪製背景。ContentDrawScope
會觀察快照變更,因此當 LocalContentColor
的值變更時,系統會自動重新繪製:
class BackgroundColorConsumerNode : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode { override fun ContentDrawScope.draw() { val currentColor = currentValueOf(LocalContentColor) drawRect(color = currentColor) drawContent() } }
如要回應範圍以外的狀態變更,並自動更新修飾符,請使用 ObserverModifierNode
。
舉例來說,Modifier.scrollable
會使用這項技術觀察 LocalDensity
的變化。以下提供一個簡化範例:
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) } }
動畫修飾符
Modifier.Node
實作項目可以存取 coroutineScope
。這樣即可使用 Compose Animatable API。例如,這個程式碼片段會將上述的 CircleNode
修改為反覆淡入和淡出:
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) ) { } } } }
使用委派功能在修飾符之間共用狀態
Modifier.Node
修飾符可以委派給其他節點。這個做法有許多用途,例如擷取不同修飾符的通用實作項目,但也可以在不同修飾符之間共用通用狀態。
舉例來說,如果是共用互動資料,可點擊修飾符節點的基本實作方式:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
停用節點自動撤銷功能
Modifier.Node
節點對應的 ModifierNodeElement
呼叫更新時,會自動撤銷。有時,在較複雜的修飾符中,您可能會想要選擇不採用這項行為,以便更精細地控管修飾符何時失效階段。
如果您的自訂修飾符會修改版面配置和繪圖,這項功能就特別實用。停用自動撤銷功能,可讓您在只使用繪圖相關屬性 (例如 color
、變更) 且不會使版面配置失效時,撤銷繪圖作業。這有助於提升修飾符的效能。
以下範例的假設是,修飾符含有以 color
、size
和 onClick
lambda 做為屬性的修飾符。此修飾符只會將必要項目失效,且會略過任何無效的撤銷:
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) } } }