Styles API 提供了一种声明式精简方法,用于管理互动状态(例如 hovered、focused 和 pressed)期间的界面更改。借助此
API,您可以显著减少使用修饰符时通常需要的样板代码。
为了方便响应式样式设置,StyleState 充当一个稳定的只读接口,用于跟踪元素的活跃状态(例如其启用、按下或聚焦状态)。在
StyleScope 中,您可以通过 state 属性访问此接口,以便直接在样式定义中实现条件逻辑。
基于状态的互动:悬停、聚焦、按下、选择、启用、切换
样式内置支持常见互动:
- 已按下
- 已悬停在“”上
- 已选择
- 已启用
- 已切换
您还可以支持自定义状态。如需了解详情,请参阅使用 StyleState进行自定义状态样式设置部分。
使用 Style 参数处理互动状态
以下示例演示了如何修改 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 = rememberUpdatedStyleState(interactionSource) { it.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、具有特定状态的 lambda 和样式。
// 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(block: () -> Unit) { state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing }) } fun StyleScope.playerPaused(block: () -> Unit) { state(playerStateKey, block, { key, state -> state[key] == PlayerState.Paused }) }
链接到自定义状态
在可组合项中定义 styleState,并将 styleState.playState
设置为等于传入状态。将 styleState 传递到修饰符上的 styleable 函数。
@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 lambda 中,您可以使用之前定义的扩展函数为自定义状态应用基于状态的样式。
@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(block: () -> Unit) { state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing }) } fun StyleScope.playerPaused(block: () -> Unit) { state(playerStateKey, block, { 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) }