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)
コンポーザブル モディファイア ファクトリを使用してカスタム モディファイアを作成する
コンポーザブル関数を使用してカスタム修飾子を作成し、既存の修飾子に値を渡すこともできます。これはコンポーザブル モディファイア ファクトリと呼ばれます。
コンポーザブル モディファイア ファクトリーを使用してモディファイアを作成すると、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
を実装して、draw メソッドをオーバーライドできるようにしています。
使用可能なタイプは次のとおりです。
ノード |
使用目的 |
サンプルリンク |
ラップされたコンテンツの測定方法とレイアウト方法を変更する |
||
レイアウトのスペースに描画する |
||
このインターフェースを実装すると、 |
||
テストやユーザー補助機能などのユースケースで使用するために、セマンティクスの Key-Value を追加する |
||
PointerInputChanges を受信する |
||
親レイアウトにデータを提供する |
||
|
||
コンテンツのグローバル位置が変更された可能性がある場合に、レイアウトの最終的な |
||
|
||
他の これは、複数のノード実装を 1 つにコンポーズする場合に便利です。 |
||
|
ノードは、対応する要素で更新が呼び出されると自動的に無効になります。この例は 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
の実装では、次のメソッドをオーバーライドする必要があります。
create
: 修飾子ノードをインスタンス化する関数です。これは、修飾子が最初に適用されたときにノード作成のために呼び出されます。通常、これはノードを作成し、修飾子ファクトリーに渡されたパラメータで構成することです。update
: この関数は、このノードがすでに存在する同じ場所にこの修飾子が指定され、プロパティが変更されたときに呼び出されます。これは、クラスのequals
メソッドによって決まります。前に作成した修飾子ノードは、パラメータとしてupdate
呼び出しに送信されます。この時点で、更新されたパラメータに合わせてノードのプロパティを更新する必要があります。このようにノードを再利用できる機能は、Modifier.Node
がもたらすパフォーマンスの向上の鍵となります。したがって、update
メソッドで新しいノードを作成するのではなく、既存のノードを更新する必要があります。円の例では、ノードの色が更新されます。
また、ModifierNodeElement
の実装では equals
と hashCode
も実装する必要があります。update
は、前の要素との等価比較が 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
修飾子は、CompositionLocal
などの Compose 状態オブジェクトの変更を自動的に監視しません。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) ) }
ノードの自動無効化をオプトアウトする
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) } } }