Styles API は、hovered、focused、pressed などのインタラクション状態での UI の変更を管理するための、宣言的で効率的なアプローチを提供します。この API を使用すると、通常は修飾子を使用する際に必要となるボイラープレート コードを大幅に削減できます。
リアクティブ スタイリングを容易にするため、StyleState は要素のアクティブ状態(有効、押下、フォーカスなどのステータス)をトラッキングする安定した読み取り専用インターフェースとして機能します。StyleScope 内では、state プロパティを介してアクセスし、Style 定義に条件付きロジックを直接実装できます。
状態ベースのインタラクション: ホバー、フォーカス、押下、選択、有効、切り替え
スタイルには、一般的なインタラクションの組み込みサポートが付属しています。
- 押下
- カーソルを合わせた回数
- 選択済み
- 有効
- 切り替え
カスタム状態をサポートすることもできます。詳細については、StyleState を使用したカスタム状態のスタイル設定をご覧ください。
スタイル パラメータでインタラクション状態を処理する
次の例は、インタラクションの状態に応じて background と borderColor を変更する方法を示しています。具体的には、ホバー時に紫、フォーカス時に青に切り替わります。
@Preview @Composable private fun OpenButton() { BaseButton( style = outlinedButtonStyle then { background(Color.White) hovered { background(lightPurple) border(2.dp, lightPurple) } focused { background(lightBlue) } }, onClick = { }, content = { BaseText("Open in Studio", style = { contentColor(Color.Black) fontSize(26.sp) textAlign(TextAlign.Center) }) } ) }
ネストされた状態定義を作成することもできます。たとえば、ボタンが同時に押されてホバーされている場合の特定のスタイルを定義できます。
@Composable private fun OpenButton_CombinedStates() { BaseButton( style = outlinedButtonStyle then { background(Color.White) hovered { // light purple background(lightPurple) pressed { // When running on a device that can hover, whilst hovering and then pressing the button this would be invoked background(lightOrange) } } pressed { // when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only background(lightRed) } focused { background(lightBlue) } }, onClick = { }, content = { BaseText("Open in Studio", style = { contentColor(Color.Black) fontSize(26.sp) textAlign(TextAlign.Center) }) } ) }
Modifier.styleable を使用したカスタム コンポーザブル
独自の styleable コンポーネントを作成する場合は、interactionSource を styleState に接続する必要があります。次に、この状態を Modifier.styleable に渡して利用します。
デザイン システムに GradientButton が含まれているシナリオについて考えてみましょう。GradientButton から継承し、押されたときなどの操作中に色を変更する LoginButton を作成することもできます。
interactionSourceスタイルの更新を有効にするには、コンポーザブル内のパラメータとしてinteractionSourceを含めます。指定されたパラメータを使用するか、パラメータが指定されていない場合は、新しいMutableInteractionSourceを初期化します。interactionSourceを指定してstyleStateを初期化します。styleStateの有効ステータスが、指定された有効パラメータの値を反映していることを確認します。interactionSourceをfocusable修飾子とclickable修飾子に割り当てます。最後に、styleStateを修飾子のstyleableパラメータに適用します。
@Composable private fun GradientButton( onClick: () -> Unit, modifier: Modifier = Modifier, style: Style = Style, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) { val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } styleState.isEnabled = enabled Row( modifier = modifier .clickable( onClick = onClick, enabled = enabled, interactionSource = interactionSource, indication = null, ) .styleable(styleState, baseGradientButtonStyle then style), content = content, ) }
interactionSource 状態を使用して、スタイル ブロック内の押下、フォーカス、ホバーのオプションでスタイルの変更を制御できるようになりました。
@Preview @Composable fun LoginButton() { val loginButtonStyle = Style { pressed { background( Brush.linearGradient( listOf(Color.Magenta, Color.Red) ) ) } } GradientButton(onClick = { // Login logic }, style = loginButtonStyle) { BaseText("Login") } }
interactionSource に基づいてカスタム コンポーザブルの状態を変更する。スタイルの変更をアニメーション化する
スタイルの状態変化には、アニメーションのサポートが組み込まれています。新しいプロパティを animate を使用して状態変更ブロック内にラップすると、異なる状態間のアニメーションが自動的に追加されます。これは animate*AsState API に似ています。次の例では、状態がフォーカスに変わると、borderColor が黒から青にアニメーションします。
val animatingStyle = Style { externalPadding(48.dp) border(3.dp, Color.Black) background(Color.White) size(100.dp) pressed { animate { borderColor(Color.Magenta) background(Color(0xFFB39DDB)) } } } @Preview @Composable private fun AnimatingStyleChanges() { val interactionSource = remember { MutableInteractionSource() } val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } Box(modifier = Modifier .clickable( interactionSource, enabled = true, indication = null, onClick = { } ) .styleable(styleState, animatingStyle)) { } }
animate API は、アニメーション曲線の期間や形状を変更する animationSpec を受け入れます。次の例では、spring 仕様を使用してボックスのサイズをアニメーション化しています。
val animatingStyleSpec = Style { externalPadding(48.dp) border(3.dp, Color.Black) background(Color.White) size(100.dp) transformOrigin(TransformOrigin.Center) pressed { animate { borderColor(Color.Magenta) background(Color(0xFFB39DDB)) } animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) { scale(1.2f) } } } @Preview(showBackground = true) @Composable fun AnimatingStyleChangesSpec() { val interactionSource = remember { MutableInteractionSource() } val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } Box(modifier = Modifier .clickable( interactionSource, enabled = true, indication = null, onClick = { } ) .styleable(styleState, animatingStyleSpec)) }
StyleState を使用したカスタム状態のスタイル設定
コンポーザブルのユースケースによっては、カスタム状態によってサポートされるさまざまなスタイルが存在する場合があります。たとえば、メディアアプリの場合、プレーヤーの再生状態に応じて MediaPlayer コンポーザブルのボタンのスタイルを変更したいことがあります。独自のカスタム状態を作成して使用する手順は次のとおりです。
- カスタムキーを定義する
StyleState拡張機能を作成する- カスタム ステータスへのリンク
カスタムキーを定義する
カスタム状態ベースのスタイルを作成するには、まず StyleStateKey を作成し、デフォルトの状態値を渡します。アプリが起動すると、メディア プレーヤーは Stopped 状態になるため、次のように初期化されます。
enum class PlayerState { Stopped, Playing, Paused } val playerStateKey = StyleStateKey(PlayerState.Stopped)
StyleState 拡張関数を作成する
StyleState で拡張関数を定義して、現在の playState をクエリします。次に、StyleScope に拡張関数を作成し、カスタム状態を渡します。このとき、playStateKey、特定の状態のラムダ、スタイルを渡します。
// Extension Function on MutableStyleState to query and set the current playState var MutableStyleState.playerState get() = this[playerStateKey] set(value) { this[playerStateKey] = value } fun StyleScope.playerPlaying(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) } fun StyleScope.playerPaused(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused }) }
カスタム ステータスへのリンク
コンポーザブルで styleState を定義し、styleState.playState を受信状態に設定します。修飾子の styleable 関数に styleState を渡します。
@Composable fun MediaPlayer( url: String, modifier: Modifier = Modifier, style: Style = Style, state: PlayerState = remember { PlayerState.Paused } ) { // Hoist style state, set playstate as a parameter, val styleState = remember { MutableStyleState(null) } // Set equal to incoming state to link the two together styleState.playerState = state Box( modifier = modifier.styleable(styleState, style)) { ///.. } }
style ラムダ内で、以前に定義した拡張関数を使用して、カスタム状態の状態ベースのスタイルを適用できます。
@Composable fun StyleStateKeySample() { // Using the extension function to change the border color to green while playing val style = Style { borderColor(Color.Gray) playerPlaying { animate { borderColor(Color.Green) } } playerPaused { animate { borderColor(Color.Blue) } } } val styleState = remember { MutableStyleState(null) } styleState[playerStateKey] = PlayerState.Playing // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. MediaPlayer(url = "https://example.com/media/video", style = style, state = PlayerState.Stopped) }
次のコードは、この例の完全なスニペットです。
enum class PlayerState { Stopped, Playing, Paused } val playerStateKey = StyleStateKey<PlayerState>(PlayerState.Stopped) var MutableStyleState.playerState get() = this[playerStateKey] set(value) { this[playerStateKey] = value } fun StyleScope.playerPlaying(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) } fun StyleScope.playerPaused(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused }) } @Composable fun MediaPlayer( url: String, modifier: Modifier = Modifier, style: Style = Style, state: PlayerState = remember { PlayerState.Paused } ) { // Hoist style state, set playstate as a parameter, val styleState = remember { MutableStyleState(null) } // Set equal to incoming state to link the two together styleState.playerState = state Box( modifier = modifier.styleable(styleState, Style { size(100.dp) border(2.dp, Color.Red) }, style, )) { ///.. } } @Composable fun StyleStateKeySample() { // Using the extension function to change the border color to green while playing val style = Style { borderColor(Color.Gray) playerPlaying { animate { borderColor(Color.Green) } } playerPaused { animate { borderColor(Color.Blue) } } } val styleState = remember { MutableStyleState(null) } styleState[playerStateKey] = PlayerState.Playing // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. MediaPlayer(url = "https://example.com/media/video", style = style, state = PlayerState.Stopped) }