ユーザー インターフェース コンポーネントは、ユーザー操作に応答する方法でデバイス ユーザーにフィードバックを返します。コンポーネントはそれぞれの方法で操作に応答するため、操作の効果がユーザーにわかりやすくなっています。たとえば、ユーザーがデバイスのタッチスクリーン上でボタンをタップすると、(ハイライト色が加わるなど)なんらかの方法でボタンが変化します。このように変化することで、ユーザーはボタンをタップしたことを把握できます。ユーザーが ボタンから指を離してから そうしないと、ボタンがアクティブになります。
Compose のジェスチャーのドキュメントでは、ポインタの移動やクリックなど、Compose コンポーネントによる下位レベルのポインタ イベントの処理について説明しています。Compose では、そうした下位レベルのイベントが上位レベルの操作に抽象化されるようになっています。たとえば、一連のポインタ イベントがボタンのタップとリリースとして処理される場合があります。このような上位レベルの抽象化を理解することで、ユーザーに対する UI の応答をカスタマイズできるようになります。たとえば、ユーザーが操作したときにコンポーネントの外観がどのように変化するかをカスタマイズしたり、それらのユーザー アクションのログを保持したりすることができます。このドキュメントには、標準の UI 要素の変更や、独自の UI の設計に必要な情報が記載されています。
<ph type="x-smartling-placeholder">操作
通常、Compose コンポーネントがユーザー操作をどのように解釈しているかを知る必要はありません。たとえば、Button
は Modifier.clickable
によって、ユーザーがボタンをクリックしたかどうかを判別します。アプリに一般的なボタンを追加する場合、ボタンの onClick
コードを定義すると、Modifier.clickable
が必要に応じてそのコードを実行します。つまり、ユーザーが画面をタップしたか、キーボードでボタンを選択したかを知る必要はありません。ユーザーがクリックしたことを Modifier.clickable
が判別し、onClick
コードを実行して応答します。
ただし、ユーザーの操作に対する UI コンポーネントの応答をカスタマイズするには、内部で何が起きているのかを把握しなければなりません。このセクションでは、その方法について説明します。
ユーザーが UI コンポーネントを操作すると、システムはいくつかの Interaction
イベントを生成することで、ユーザーの操作を表します。たとえば、ユーザーがボタンをタップすると PressInteraction.Press
が生成されます。ユーザーがボタン内で指を離すと PressInteraction.Release
が生成され、クリックが完了したことをボタンが認識します。一方、ユーザーが指をボタンの外にドラッグしてから指を離すと、PressInteraction.Cancel
が生成され、ボタンのタップが完了せずにキャンセルされたことを示します。
こうした操作は固定的なものではありません。つまり、このような下位レベルの操作イベントは、ユーザー アクションの意味や順序を解釈するものではありません。また、特定のユーザー アクションが他のアクションに優先するかどうかも解釈しません。
これらの操作は通常、開始と終了がペアになっています。2 番目の操作には、最初の操作に対する参照が含まれています。たとえば、ユーザーがボタンをタップして指を離した場合、タップによって PressInteraction.Press
操作が生成され、指を離すことで PressInteraction.Release
が生成されます。Release
には、最初の PressInteraction.Press
を識別する press
プロパティがあります。
特定のコンポーネントに対する操作は InteractionSource
によって確認できます。InteractionSource
は Kotlin 上に構築されている
ため、そこから同じ方法でインタラクションを収集できます
他のフローを使用できますこの設計上の決定について詳しくは
詳しくは、ブログ投稿「Iluminating Interactions」をご覧ください。
操作の状態
操作を自分で追跡することで、コンポーネントの組み込み機能を拡張することもできます。たとえば、ボタンがタップされたときに色を変えたいとします。操作を追跡する最も簡単な方法は、適切な操作状態を監視することです。InteractionSource
には、操作のさまざまなステータスを状態として示す、各種のメソッドが用意されています。たとえば、特定のボタンがタップされたかどうかを確認する場合は InteractionSource.collectIsPressedAsState()
メソッドを呼び出します。
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Compose には、collectIsPressedAsState()
以外にも、collectIsFocusedAsState()
、collectIsDraggedAsState()
、collectIsHoveredAsState()
が用意されています。これらのメソッドは、実際には下位レベルの InteractionSource
API 上にビルドされるコンビニエンス メソッドです。必要に応じて、それら下位レベルの関数を直接使用することもあります。
たとえば、あるボタンがタップされ、さらにドラッグされているかを判別する必要があるとします。collectIsPressedAsState()
と collectIsDraggedAsState()
の両方を使用すると、Compose で多数の処理が重複することになります。その場合、すべての操作が正しい順序で実行される保証はありません。そのような状況では、InteractionSource
を直接使用することをおすすめします。インタラクションのトラッキングについて詳しくは、
InteractionSource
を自分で使用する場合は、InteractionSource
を使用するをご覧ください。
次のセクションでは、Google Cloud Storage の
InteractionSource
と MutableInteractionSource
です。
Interaction
の消費と出力
InteractionSource
は、Interactions
の読み取り専用ストリームを表します。
Interaction
を InteractionSource
に出力できます。出力
Interaction
を使用するには、次を拡張する MutableInteractionSource
を使用する必要があります。
InteractionSource
。
修飾子とコンポーネントは、Interactions
を使用、出力、または使用および出力できます。
以降のセクションでは、両方のサービスからのインタラクションの使用と出力の方法について説明します。
修飾子とコンポーネントが含まれています。
修飾子の使用例
フォーカス状態の枠線を描画する修飾子の場合、必要なのは、
Interactions
なので、InteractionSource
を受け入れることができます。
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
関数のシグネチャから、この修飾子がコンシューマであることは明らかです。
Interaction
を使用できますが、出力できません。
修飾子の生成の例
Modifier.hoverable
などのホバーイベントを処理する修飾子の場合、
Interactions
を出力し、MutableInteractionSource
を引数として受け取る必要があります。
パラメータを使用します。
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
この修飾子はプロデューサーであり、提供された
MutableInteractionSource
: カーソルを合わせたとき、またはカーソルを合わせたときに HoverInteractions
を出力
あります。
消費と生成を行うコンポーネントを構築する
マテリアル Button
などの上位コンポーネントは、プロデューサーと
提供しています入力イベントとフォーカス イベントを処理し、外観も変更します。
さまざまなイベントに対応して、波紋の表示やイベントの
表します。その結果、MutableInteractionSource
を
使用して独自の記憶されたインスタンスを指定できます。
@Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, // exposes MutableInteractionSource as a parameter interactionSource: MutableInteractionSource? = null, elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { /* content() */ }
これにより、
コンポーネントから MutableInteractionSource
を取得し、
コンポーネントによって生成された Interaction
。これを使用して
UI 内の他のコンポーネントの外観を
変更することはできません
インタラクティブな大まかなコンポーネントを独自に構築する場合は、
(このように MutableInteractionSource
をパラメータとして公開します)その他
状態ホイスティングのベスト プラクティスに従うため、
コンポーネントの視覚的な状態を、他の種類の
状態(有効状態など)を読み取って制御できます。
Compose は多層アーキテクチャ アプローチを採用しており、
基礎的な構築の上に構築される
ハイレベルのマテリアルコンポーネントです
リップルなどの制御に必要な Interaction
を生成するブロック
適用できます。基盤ライブラリには高レベルのインタラクション修飾子が用意されている
Modifier.hoverable
、Modifier.focusable
、
Modifier.draggable
。
ホバーイベントに応答するコンポーネントを作成するには、
Modifier.hoverable
を使用し、パラメータとして MutableInteractionSource
を渡します。
コンポーネントにカーソルを合わせると、必ず HoverInteraction
が出力されます。
コンポーネントの表示方法を変更できます。
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
このコンポーネントをフォーカス可能にするには、Modifier.focusable
を追加して
パラメータとして同じ MutableInteractionSource
を指定します。では、
HoverInteraction.Enter/Exit
と FocusInteraction.Focus/Unfocus
が出力されます
同じ MutableInteractionSource
を使用できます。また、
両方のタイプのインタラクションを同じ場所で表示:
// This InteractionSource will emit hover and focus interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource) .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Modifier.clickable
はさらに高い
hoverable
や focusable
よりも高いレベルの抽象化です。
暗黙的にカーソルを合わせることが可能で、クリック可能なコンポーネントは、
焦点を絞ることができますModifier.clickable
を使用すると、Pod を実行できるコンポーネントを
カーソルを合わせる、フォーカスする、押すなどの操作を可能にし、下部の
サポートしています。コンポーネントもクリック可能にする場合は、
hoverable
と focusable
を clickable
に置き換えます。
// This InteractionSource will emit hover, focus, and press interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .clickable( onClick = {}, interactionSource = interactionSource, // Also show a ripple effect indication = ripple() ), contentAlignment = Alignment.Center ) { Text("Hello!") }
InteractionSource
と連携する
コンポーネントの操作に関する下位レベルの情報が必要な場合は、そのコンポーネントの InteractionSource
に対して標準のフロー API を使用できます。たとえば、InteractionSource
に対するタップとドラッグ操作のリストを保持するとします。このコードでは、新たなタップがリストに追加されるまでの処理が行われます。
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is DragInteraction.Start -> { interactions.add(interaction) } } } }
その場合は、新しい操作を追加するだけでなく、終了した操作(たとえばユーザーがコンポーネントから指を離すなど)を削除する必要があります。ただし、終了操作は関連する開始操作に対する参照を常に伴うため、終了した操作の削除は簡単です。終了した操作を削除するコードを次に示します。
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } is DragInteraction.Start -> { interactions.add(interaction) } is DragInteraction.Stop -> { interactions.remove(interaction.start) } is DragInteraction.Cancel -> { interactions.remove(interaction.start) } } } }
コンポーネントがタップされているかドラッグされているかは、interactions
が空かどうかを見るだけで確認できます。
val isPressedOrDragged = interactions.isNotEmpty()
直近のインタラクションは、直近のインタラクションを リストの項目を選択します。たとえば、Compose のリップル実装は次のようになります。 次のようにして、直近のインタラクションに使用する適切な状態オーバーレイを見つけます。
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
すべての Interaction
は同じ構造に従うため、
異なる種類のユーザー操作を扱う場合のコードの違いがあります。
全体的なパターンは同じです。
このセクションの前の例は、次の Flow
を表します。
State
を使用したインタラクション
これにより、更新された値をモニタリングしたり、
状態値を読み取ると自動的に再コンポーズが行われるためです。ただし、
プリフレームでバッチ処理される。つまり、状態が変更されると、
同じフレーム内で元に戻る場合、状態を監視しているコンポーネントは
確認します。
インタラクションは定期的に開始および終了する可能性があるため、インタラクションにおいては重要です。
同じフレーム内に収めますたとえば、前の例で Button
を使用すると、次のようになります。
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
押下が同じフレーム内で開始し終了した場合、テキストは
「押しました!」ほとんどの場合、これは問題ではありません。
ちらつきが発生し、
ユーザーの目に触れる場合もあります波紋効果や
少なくとも 1 回は効果を表示できるようにすることをおすすめします。
ボタンが押されなくなるとすぐに停止しません。宛先
そうすると、collect 関数の内部から直接アニメーションを
ラムダを使用します。このパターンの例を
アニメーション枠線付きの高度な Indication
を作成するセクションを参照してください。
例: カスタム操作処理を含むビルド コンポーネント
コンポーネントを作成し、入力に対するカスタム応答を指定する方法については、以下の変更されたボタンの例をご覧ください。この例に示すボタンは、タップに対して、外観が変わることで応答しています。
ここでは、Button
に基づくカスタム コンポーザブルを作成し、icon
パラメータを追加してアイコン(この場合はショッピング カート)を描画します。collectIsPressedAsState()
を呼び出して、ユーザーがボタンにカーソルを合わせているかどうかを追跡します。そしてカーソルが合わさった場合にアイコンが表示されるようにします。コードは次のようになります。
@Composable fun PressIconButton( onClick: () -> Unit, icon: @Composable () -> Unit, text: @Composable () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource? = null ) { val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false Button( onClick = onClick, modifier = modifier, interactionSource = interactionSource ) { AnimatedVisibility(visible = isPressed) { if (isPressed) { Row { icon() Spacer(Modifier.size(ButtonDefaults.IconSpacing)) } } } text() } }
新しいコンポーザブルは次のように使用します。
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
この新しい PressIconButton
は既存の Material Button
上にビルドされているため、ユーザーの操作に通常の方法で反応します。ユーザーが
ボタンを押すと、通常の画面と同じように、不透明度がわずかに変化します。
素材 Button
。
Indication
で再利用可能なカスタム エフェクトを作成して適用する
前のセクションでは、レスポンスに応じてコンポーネントの一部を変更する方法を
押されたときにアイコンを表示するなど、さまざまな Interaction
に割り当てることができます。同じ
モデルに指定するパラメータの値を変更するには、
コンポーネント内に表示されるコンテンツの変更が
コンポーネント単位でのみ適用されます。多くの場合、アプリケーション / 設計システムは
ステートフルな視覚効果のための汎用システムが用意されます
すべてのコンポーネントに一貫した方法で適用できます。
このようなデザイン システムを構築する場合は、コンポーネントを 1 つカスタマイズして このカスタマイズを他のコンポーネントに再利用するのは、 理由は次のとおりです。
- デザイン システムのすべてのコンポーネントに同じボイラープレートが必要
- 新しく作成したコンポーネントやカスタム アプリケーションにこのエフェクトを適用するのは、忘れがちです。 クリック可能なコンポーネント
- カスタム効果を他の効果と組み合わせるのは難しい場合があります
これらの問題を回避し、システム全体でカスタム コンポーネントを簡単にスケールするには、
Indication
を使用できます。
Indication
は、以下全体に適用できる再利用可能な視覚効果を表します。
設計するコンポーネントです。Indication
は 2 つに分割されています
part:
IndicationNodeFactory
: 次の条件を満たすModifier.Node
インスタンスを作成するファクトリ 視覚効果をレンダリングできますシンプルな実装では、 コンポーネント間で変更する場合はシングルトン(オブジェクト)にして、 管理できます。これらのインスタンスは、ステートフルまたはステートレスになります。コンテナイメージは
CompositionLocal
から値を取得して、 他のコンポーネントと同様に、特定のコンポーネント内で表示や動作を行います。Modifier.Node
。Modifier.indication
: 特定の値に対してIndication
を描画する修飾子 説明します。Modifier.clickable
とその他の高レベルのインタラクション修飾子 指示パラメータを直接受け取るため、Interaction
だけでなく、ユーザーの視覚効果をInteraction
描画することもできます 出力します。そのため、単純なケースでは、何も指定せずにModifier.clickable
を使用できます。Modifier.indication
が必要。
エフェクトを Indication
に置き換える
このセクションでは、画像に適用された手動スケール効果を置き換える方法について説明します。 複数のイベント間で再利用できる、表示と同等の内容を持つ特定のボタン 説明します。
次のコードは、押すと下に縮小するボタンを作成します。
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") Button( modifier = Modifier.scale(scale), onClick = { }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
上記のスニペットのスケール効果を Indication
に変換するには、次のようにします。
手順は次のとおりです。
スケール効果を適用する
Modifier.Node
を作成します。 接続されると、ノードは前述のようにインタラクション ソースを監視します。 説明します。唯一の違いは、アニメーションが直接起動されることです。 状態に変換する必要はありません。ノードで
DrawModifierNode
を実装して、オーバーライドできるようにする必要があるContentDrawScope#draw()
を実行し、同じ描画を使用してスケール効果をレンダリングします。 使用できます。ContentDrawScope
レシーバーから利用可能なdrawContent()
を呼び出すと、描画が行われますIndication
を適用する実際のコンポーネントなので、 スケール変換内でこの関数を呼び出す必要があります。必ずIndication
の実装は、常にdrawContent()
を呼び出します。 そうしないと、Indication
を適用するコンポーネントは描画されません。private class ScaleNode(private val interactionSource: InteractionSource) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedScalePercent = Animatable(1f) private suspend fun animateToPressed(pressPosition: Offset) { currentPressPosition = pressPosition animatedScalePercent.animateTo(0.9f, spring()) } private suspend fun animateToResting() { animatedScalePercent.animateTo(1f, spring()) } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collectLatest { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { scale( scale = animatedScalePercent.value, pivot = currentPressPosition ) { this@draw.drawContent() } } }
IndicationNodeFactory
を作成します。その唯一の役割は、 新しいノード インスタンスを返します。専用の インジケータを構成する場合、Factory にはオブジェクトを指定できます。object ScaleIndication : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ScaleNode(interactionSource) } override fun equals(other: Any?): Boolean = other === ScaleIndication override fun hashCode() = 100 }
Modifier.clickable
は内部でModifier.indication
を使用するため、 クリック可能なコンポーネントをScaleIndication
で指定したい場合は、clickable
へのパラメータとしてIndication
:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
また、カスタム テンプレートを使用して、高レベルの再利用可能なコンポーネントを簡単に構築できます。
Indication
- ボタンは次のようになります。@Composable fun ScaleButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, shape: Shape = CircleShape, content: @Composable RowScope.() -> Unit ) { Row( modifier = modifier .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) .clickable( enabled = enabled, indication = ScaleIndication, interactionSource = interactionSource, onClick = onClick ) .border(width = 2.dp, color = Color.Blue, shape = shape) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) }
その後、次のようにボタンを使用できます。
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }で確認できます。
枠線がアニメーション化された高度な Indication
を作成する
Indication
は、スケーリングなどの変換効果だけでなく、
説明します。IndicationNodeFactory
は Modifier.Node
を返すため、次のように描画できます。
その他の描画 API と同様に、コンテンツの上下にあらゆる効果を適用できるわけではありません。対象
たとえば、コンポーネントの周りにアニメーションの枠線を描画し、オーバーレイを
最上部に配置されます。
ここでの Indication
の実装は、前の例と非常によく似ています。
いくつかのパラメータを持つノードが作成されるだけです。アニメーション化された枠線は、
Indication
が使用されるコンポーネントのシェイプと境界、
Indication
の実装では、シェイプと枠線の幅も指定する必要があります。
指定することもできます。
data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return NeonNode( shape, // Double the border size for a stronger press effect borderWidth * 2, interactionSource ) } }
Modifier.Node
の実装も概念的には同じですが、
コードの描画がより複雑になります。前と同様に、InteractionSource
を監視します。
アニメーションを起動し、DrawModifierNode
を実装して描画を行う
効果が次のようになっています。
private class NeonNode( private val shape: Shape, private val borderWidth: Dp, private val interactionSource: InteractionSource ) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedProgress = Animatable(0f) val animatedPressAlpha = Animatable(1f) var pressedAnimation: Job? = null var restingAnimation: Job? = null private suspend fun animateToPressed(pressPosition: Offset) { // Finish any existing animations, in case of a new press while we are still showing // an animation for a previous one restingAnimation?.cancel() pressedAnimation?.cancel() pressedAnimation = coroutineScope.launch { currentPressPosition = pressPosition animatedPressAlpha.snapTo(1f) animatedProgress.snapTo(0f) animatedProgress.animateTo(1f, tween(450)) } } private fun animateToResting() { restingAnimation = coroutineScope.launch { // Wait for the existing press animation to finish if it is still ongoing pressedAnimation?.join() animatedPressAlpha.animateTo(0f, tween(250)) animatedProgress.snapTo(0f) } } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( currentPressPosition, size ) val brush = animateBrush( startPosition = startPosition, endPosition = endPosition, progress = animatedProgress.value ) val alpha = animatedPressAlpha.value drawContent() val outline = shape.createOutline(size, layoutDirection, this) // Draw overlay on top of content drawOutline( outline = outline, brush = brush, alpha = alpha * 0.1f ) // Draw border on top of overlay drawOutline( outline = outline, brush = brush, alpha = alpha, style = Stroke(width = borderWidth.toPx()) ) } /** * Calculates a gradient start / end where start is the point on the bounding rectangle of * size [size] that intercepts with the line drawn from the center to [pressPosition], * and end is the intercept on the opposite end of that line. */ private fun calculateGradientStartAndEndFromPressPosition( pressPosition: Offset, size: Size ): Pair<Offset, Offset> { // Convert to offset from the center val offset = pressPosition - size.center // y = mx + c, c is 0, so just test for x and y to see where the intercept is val gradient = offset.y / offset.x // We are starting from the center, so halve the width and height - convert the sign // to match the offset val width = (size.width / 2f) * sign(offset.x) val height = (size.height / 2f) * sign(offset.y) val x = height / gradient val y = gradient * width // Figure out which intercept lies within bounds val intercept = if (abs(y) <= abs(height)) { Offset(width, y) } else { Offset(x, height) } // Convert back to offsets from 0,0 val start = intercept + size.center val end = Offset(size.width - start.x, size.height - start.y) return start to end } private fun animateBrush( startPosition: Offset, endPosition: Offset, progress: Float ): Brush { if (progress == 0f) return TransparentBrush // This is *expensive* - we are doing a lot of allocations on each animation frame. To // recreate a similar effect in a performant way, it would be better to create one large // gradient and translate it on each frame, instead of creating a whole new gradient // and shader. The current approach will be janky! val colorStops = buildList { when { progress < 1 / 6f -> { val adjustedProgress = progress * 6f add(0f to Blue) add(adjustedProgress to Color.Transparent) } progress < 2 / 6f -> { val adjustedProgress = (progress - 1 / 6f) * 6f add(0f to Purple) add(adjustedProgress * MaxBlueStop to Blue) add(adjustedProgress to Blue) add(1f to Color.Transparent) } progress < 3 / 6f -> { val adjustedProgress = (progress - 2 / 6f) * 6f add(0f to Pink) add(adjustedProgress * MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 4 / 6f -> { val adjustedProgress = (progress - 3 / 6f) * 6f add(0f to Orange) add(adjustedProgress * MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 5 / 6f -> { val adjustedProgress = (progress - 4 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } else -> { val adjustedProgress = (progress - 5 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxYellowStop to Yellow) add(MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } } } return linearGradient( colorStops = colorStops.toTypedArray(), start = startPosition, end = endPosition ) } companion object { val TransparentBrush = SolidColor(Color.Transparent) val Blue = Color(0xFF30C0D8) val Purple = Color(0xFF7848A8) val Pink = Color(0xFFF03078) val Orange = Color(0xFFF07800) val Yellow = Color(0xFFF0D800) const val MaxYellowStop = 0.16f const val MaxOrangeStop = 0.33f const val MaxPinkStop = 0.5f const val MaxPurpleStop = 0.67f const val MaxBlueStop = 0.83f } }
主な違いは、新しい ReplicaSet の
animateToResting()
関数を使用してアニメーションを作成します。
押すアニメーションは継続します。また
animateToPressed
の先頭で複数回クイック押す(1 回押した場合)
既存の押下または休止アニメーションの最中に発生した場合、前のアニメーションは
キャンセルされ、押下アニメーションが最初から再生されます。複数の
同時効果(新しいリップル アニメーションが描画されるリップルなど)
表示)、リストでアニメーションを追跡できます。
既存のアニメーションをキャンセルして新しいアニメーションを開始します。
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- ジェスチャーについて
- Jetpack Compose で Kotlin を使用する
- マテリアル コンポーネントとレイアウト