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
を実装しています。これにより、描画メソッドをオーバーライドできます。
使用可能なタイプは次のとおりです。
ノード |
使用目的 |
サンプルリンク |
ラップされたコンテンツの測定方法と配置方法を変更する |
||
レイアウトのスペースに描画する |
||
このインターフェースを実装すると、 |
||
テストやユーザー補助などのユースケースで使用するセマンティクスの Key-Value を追加する |
||
PointerInputChanges を受け取る |
||
親レイアウトにデータを提供する |
||
|
||
コンテンツのグローバル位置が変更された可能性がある場合に、レイアウトの最終的な |
||
|
||
他の これは、複数のノード実装を 1 つにまとめる場合に便利です。 |
||
|
ノードは、対応する要素で更新が呼び出されると自動的に無効化されます。この例は 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
も実装する必要があります。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
を使用して修飾子が割り当てられた場所ではなく、修飾子が使用されている場所からコンポジション ローカルの値を読み取ることができることです。
ただし、修飾子ノードのインスタンスは、状態の変化を自動的に監視しません。コンポジション ローカルの変更に自動的に反応するには、スコープ内の現在の値を読み取ることができます。
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) } } }