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

Compose には、一般的な動作のための多くの修飾子が最初から用意されています。 独自の修飾子を作成することもできます

修飾子は複数の要素で構成されます。

  • 修飾子ファクトリ <ph type="x-smartling-placeholder">
      </ph>
    • これは、慣用的な API を提供する Modifier の拡張関数です。 複数の修飾子を簡単につなぐことができます。「 修飾子ファクトリは、Compose が変更に使用する修飾子要素を生成します 作成します。
  • 修飾子要素 <ph type="x-smartling-placeholder">
      </ph>
    • ここで、修飾子の動作を実装できます。

カスタム修飾子を実装する方法は、 必要があります。多くの場合、カスタムの修飾子を実装する最も簡単な方法は これは、すでに定義されている他の修飾子を組み合わせ、 組み合わせることもできます。さらにカスタム動作が必要な場合は、 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 について説明します。たとえば、次のスニペットは、 有効化/無効化時にアルファの変化をアニメーション化する修飾子です。

@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 と同じ 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 であるため、更新が呼び出されるたびに 要素を更新すると、ノードは再描画をトリガーし、色が正しく更新されます。内容 自動無効化を無効にすることができます。詳しくは後述をご覧ください。

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 メソッドで既存のノードを使用します。Google の 場合、ノードの色が更新されます。

また、ModifierNodeElement の実装には equals も実装する必要があります。 および hashCodeupdate は、次と等しい比較が行われた場合にのみ呼び出されます。 前の要素は false を返します。

上記の例では、データクラスを使用してこれを実現しています。これらのメソッドは、 ノードの更新が必要かどうかを確認します要素に ノードの更新の必要性に寄与しない、または 使用する必要がある場合は、equals を手動で実装できます。 と hashCode。たとえば、パディング修飾子要素

修飾子ファクトリ

これは修飾子の公開 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 修飾子が 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 修飾子は他のノードに委任できます。さまざまなユースケースで たとえば、異なる修飾子にまたがって共通の実装を抽出する、 修飾子間で共通の状態を共有するためにも使用できます。

たとえば、この 2 つのノードを共有するクリック可能な修飾子ノードの基本的な実装例は、 インタラクション データ:

class ClickableNode : DelegatingNode() {
    val interactionData = InteractionData()
    val focusableNode = delegate(
        FocusableNode(interactionData)
    )
    val indicationNode = delegate(
        IndicationNode(interactionData)
    )
}

ノードの自動無効化のオプトアウト

Modifier.Node 個のノードは、対応する ModifierNodeElement の呼び出しが更新されました。場合によっては、より複雑な修飾子を使用して、 このような動作をオプトアウトして フェーズを無効化します。

これは、カスタムの修飾子によってレイアウトと 描画します。自動無効化を無効にすると、描画を無効化するだけで 描画関連のプロパティ(color など)のみを使用して変更し、レイアウトを無効にしない。 これにより、修飾子のパフォーマンスが向上します。

以下は、color を持つ修飾子を使用した架空の例です。 プロパティとして size および onClick ラムダを追加します。この修飾子は、定義されている対象のみを 必須になり、次の条件に該当しない無効化はスキップされます。

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