建立自訂修飾符

Compose 提供許多適用於常見行為的修飾符, 但您也可以建立自訂修飾符

修飾符包含多個部分:

  • 修飾符工廠
    • 這是 Modifier 上的擴充功能函式,可以提供慣用的 API ,並讓修飾符可以輕鬆鏈結在一起。 修飾符工廠會產生 Compose 用來修改的修飾符元素 調整使用者介面
  • 修飾符元素
    • 您可以在這裡實作修飾符的行為。

實作自訂修飾符的方法有很多種,具體取決於 功能。實作自訂修飾符最簡單的方式通常是 只需要實作自訂修飾符工廠,並結合其他 修飾符的工廠函式如果您需要更多自訂行為,請導入 修飾符元素使用 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 實作自訂修飾符可分為三個部分:

  • 保留邏輯和 YAML 的 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, 可讓您覆寫繪製方法

可用的類型如下:

節點

用法

範例連結

LayoutModifierNode

Modifier.Node 可變更包裝內容的測量及配置方式。

範例

DrawModifierNode

繪製在版面配置空間中的 Modifier.Node

範例

CompositionLocalConsumerModifierNode

實作這個介面可讓 Modifier.Node 讀取組合本機值。

範例

SemanticsModifierNode

Modifier.Node 可新增語意鍵/值,用於測試、無障礙功能和類似用途。

範例

PointerInputModifierNode

接收 PointerInputChanges.Modifier.Node

範例

ParentDataModifierNode

將資料提供給上層版面配置的 Modifier.Node

範例

LayoutAwareModifierNode

接收 onMeasuredonPlaced 回呼的 Modifier.Node

範例

GlobalPositionAwareModifierNode

Modifier.Node 接收 onGloballyPositioned 回呼,並在內容的全域位置發生變更時,使用版面配置的最終 LayoutCoordinates

範例

ObserverModifierNode

實作 ObserverNodeModifier.Node 可以提供自己的 onObservedReadsChanged 實作;當 observeReads 區塊中讀取的快照物件有所變更時,系統會呼叫該實作項目。

範例

DelegatingNode

Modifier.Node,可將工作委派給其他 Modifier.Node 執行個體。

如要將多個節點實作組合成單一節點,這項功能就能派上用場。

範例

TraversableNode

允許 Modifier.Node 類別在節點樹狀結構中,向上/向下掃遍相同類型類別或特定索引鍵的節點樹狀結構。

範例

在該節點上呼叫更新時,節點會自動失效 元素。由於我們的範例是 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 實作需要覆寫下列方法:

  1. create:這個函式會將修飾符節點執行個體化。如此一來 第一次套用修飾符時建立節點。通常這是一種 可能需處理建構節點和設定節點的參數 是否已傳入修飾符工廠。
  2. update:每次在範本中提供此修飾符時,系統就會呼叫此函式 此節點已存在,但屬性已變更。這是 取決於類別的 equals 方法。先前指定的輔助節點 將會做為參數傳送至 update 呼叫到目前為止 您應更新節點對應於 參數。能否以這種方式重複使用節點,是確保 Modifier.Node 帶來的效能升幅;因此,您必須更新 ,不必在 update 方法中建立新的節點。在我們 圓圈範例時,節點的顏色會更新。

此外,ModifierNodeElement 實作也需要導入 equalshashCodeupdate只有在與 前一個元素會傳回 false。

上述範例使用了資料類別來完成這項作業。這些方法可用來 檢查節點是否需要更新如果元素的屬性 是否影響節點是否需要更新 有關二進位檔相容性原因的類別,您便可手動實作 equalshashCode,例如:邊框間距修飾符元素

修飾符工廠

這是修飾符的公用 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 狀態的變更 物件,例如 CompositionLocalModifier.Node修飾符的優點 透過可組合函式工廠建立的修飾符,可以讀取 在 UI 中使用修飾符的本機組合值 而非修飾符的配置位置 (使用 currentValueOf)。

然而,修飾符節點執行個體不會自動觀察狀態變更。目的地: 自動回應本機樂曲變更,你可以讀取其當前 範圍內的值:

本範例會觀察 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 的修飾符, sizeonClick 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)
        }
    }
}