カスタム修飾子を作成する

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)

コンポーザブルの修飾子ファクトリを使用してカスタム修飾子を作成する

コンポーズ可能な関数を使用してカスタム修飾子を作成し、既存の修飾子に値を渡すこともできます。これは、コンポーザブル修飾子ファクトリと呼ばれます。

コンポーザブルの修飾子ファクトリを使用して修飾子を作成すると、animate*AsState やその他の Compose の状態に基づくアニメーション API など、上位レベルの Compose API を使用することもできます。たとえば、次のスニペットは、有効または無効になっているときにアルファの変化をアニメーション化する修飾子を示しています。

@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 は、Compose で修飾子を作成するための下位レベルの API です。これは、Compose が独自の修飾子を実装する API と同じであり、カスタム修飾子を作成する最も効率的な方法です。

Modifier.Node を使用してカスタム修飾子を実装する

Modifier.Node を使用してカスタム修飾子を実装する手順は、次の 3 つの部分に分かれます。

  • 修飾子のロジックと状態を保持する 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 と 0 個以上のノードタイプを実装します。修飾子が必要とする機能に応じて、さまざまなノードタイプがあります。上記の例では描画が可能である必要があるため、DrawModifierNode を実装して描画メソッドをオーバーライドしています。

使用可能なタイプは次のとおりです。

ノード

使用目的

リンクの例

LayoutModifierNode

ラップされたコンテンツの測定方法と配置方法を変更する Modifier.Node

サンプル

DrawModifierNode

レイアウトのスペースに描画される Modifier.Node

サンプル

CompositionLocalConsumerModifierNode

このインターフェースを実装すると、Modifier.Node がコンポジション ローカルを読み取ることができます。

サンプル

SemanticsModifierNode

テストやユーザー補助などのユースケースで使用するセマンティクスの Key-Value を追加する Modifier.Node

サンプル

PointerInputModifierNode

PointerInputChanges を受け取る Modifier.Node

サンプル

ParentDataModifierNode

親レイアウトにデータを提供する Modifier.Node

サンプル

LayoutAwareModifierNode

onMeasured コールバックと onPlaced コールバックを受け取る Modifier.Node

サンプル

GlobalPositionAwareModifierNode

コンテンツのグローバル位置が変更された可能性があるときに、レイアウトの最終 LayoutCoordinates を含む onGloballyPositioned コールバックを受け取る Modifier.Node

サンプル

ObserverModifierNode

ObserverNode を実装する Modifier.Node は、observeReads ブロック内で読み取られたスナップショット オブジェクトの変更に応じて呼び出される独自の onObservedReadsChanged の実装を提供できます。

サンプル

DelegatingNode

他の Modifier.Node インスタンスに作業を委任できる Modifier.Node

これは、複数のノード実装を 1 つにまとめる場合に便利です。

サンプル

TraversableNode

Modifier.Node クラスが、同じ型または特定のキーのクラスのノードツリーを上下に移動できるようにします。

サンプル

対応する要素で update が呼び出されると、ノードは自動的に無効になります。この例は DrawModifierNode であるため、要素で update が呼び出されるたびに、ノードが再描画をトリガーし、色が正しく更新されます。後述の手順に沿って、自動無効化をオプトアウトできます。

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 の実装では equalshashCode も実装する必要があります。update は、前の要素との等しい比較で false が返された場合にのみ呼び出されます。

上記の例では、データクラスを使用してこれを実現しています。これらのメソッドを使用して、ノードの更新が必要かどうかを確認します。要素に、ノードの更新が必要かどうかに寄与しないプロパティがある場合や、バイナリ互換性の理由からデータクラスを避けたい場合は、equalshashCodeパディング修飾子要素など)を手動で実装できます。

修飾子ファクトリ

これは修飾子の公開 API サーフェスです。ほとんどの実装では、修飾子要素を作成して修飾子チェーンに追加するだけです。

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

完全な例

次の 3 つの部分を組み合わせて、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 修飾子は、CompositionLocal などの Compose 状態オブジェクトの変更を自動的に監視しません。コンポーザブル ファクトリで作成されたばかりの修飾子よりも Modifier.Node 修飾子の利点は、currentValueOf を使用して、修飾子が割り当てられた場所ではなく、UI ツリー内で修飾子が使用される場所からコンポジション ローカルの値を読み取れる点です。

ただし、修飾子ノード インスタンスは状態変化を自動的に監視しません。コンポジションのローカルの変更に自動的に対応するために、スコープ内の現在の値を読み取ることができます。

この例では、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 のアニメーション化可能な 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 など)のみを変更した場合にのみ描画を無効にできます。レイアウトは無効化されません。これにより、修飾子のパフォーマンスが向上します。

colorsizeonClick ラムダをプロパティとして持つ修飾子を使用した架空の例を以下に示します。この修飾子は、必要なもののみを無効にし、必要のない無効化はスキップします。

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)
        }
    }
}