Compose 提供許多輔助鍵,可用於常見的行為,但您也可以自行建立自訂輔助鍵。
修飾符包含多個部分:
- 修飾符工廠
- 這是
Modifier
的擴充功能函式,可為修飾符提供慣用的 API,並讓修飾符可輕鬆鏈結在一起。修飾符工廠會產生 Compose 用來修改 UI 的修飾符元素。
- 這是
- 修飾符元素
- 您可以在此實作修飾符的行為。
導入自訂修飾符的方式有很多種,視所需功能而定。通常,實作自訂修飾符最簡單的方式,就是實作自訂修飾符工廠,將其他已定義的修飾符工廠結合在一起。如果您需要更多自訂行為,請使用 Modifier.Node
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
建立自訂修飾符時,您可能會遇到以下幾個常見情況。
零參數
如果修飾符沒有參數,就不需要更新,而且也不需要是資料類別。以下是修飾符的實作範例,可將固定邊框間距套用至可組合函式:
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
修飾符相較於使用可組合項工廠所建立的修飾符,優點在於可使用 currentValueOf
從 UI 樹狀結構中使用修飾符的位置,而不是從分配修飾符的位置,讀取組合本機的值。
不過,修飾符節點例項不會自動觀察狀態變更。如要自動回應組合區域變更,您可以在範圍內讀取其目前的值:
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) ) }
選擇停用節點自動無效功能
當對應的 ModifierNodeElement
呼叫更新時,Modifier.Node
節點會自動失效。有時,在更複雜的修飾符中,您可能會選擇停用這項行為,以便更精細地控制修飾符在何時使階段失效。
如果自訂修飾符同時修改版面配置和繪製作業,這個做法就特別實用。停用自動失效功能後,您只需在只有繪圖相關屬性 (例如 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) } } }