Styles API 提供了一种声明式且简化的方法,用于在 hovered、focused 和 pressed 等互动状态期间管理界面更改。借助此 API,您可以显著减少使用修饰符时通常需要的样板代码。
为了方便实现响应式样式设置,StyleState 可充当稳定、只读的接口,用于跟踪元素(例如其启用、按压或聚焦状态)的活跃状态。在 StyleScope 中,您可以通过 state 属性访问此信息,以便直接在样式定义中实现条件逻辑。
基于状态的互动:悬停、聚焦、按压、选择、启用、切换
样式内置了对常见互动的支持:
- 已按下
- 悬停
- 已选择
- 已启用
- 已切换
还可以支持自定义状态。如需了解详情,请参阅使用 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 状态来驱动样式修改,方法是在样式块内使用 pressed、focused 和 hovered 选项:
@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(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 设置为等于传入的状态。将 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(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) }