样式中的状态和动画

Styles API 提供了一种声明式且简化的方法,用于在 hoveredfocusedpressed 等互动状态期间管理界面更改。借助此 API,您可以显著减少使用修饰符时通常需要的样板代码。

为了方便实现响应式样式设置,StyleState 可充当稳定、只读的接口,用于跟踪元素(例如其启用、按压或聚焦状态)的活跃状态。在 StyleScope 中,您可以通过 state 属性访问此信息,以便直接在样式定义中实现条件逻辑。

基于状态的互动:悬停、聚焦、按压、选择、启用、切换

样式内置了对常见互动的支持:

  • 已按下
  • 悬停
  • 已选择
  • 已启用
  • 已切换

还可以支持自定义状态。如需了解详情,请参阅使用 StyleState 自定义状态样式部分。

使用样式参数处理互动状态

以下示例演示了如何根据互动状态修改 backgroundborderColor,具体来说,当鼠标悬停时切换为紫色,当聚焦时切换为蓝色:

@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)
            })
        }
    )
}

图 1. 根据悬停和聚焦状态更改背景颜色。

您还可以创建嵌套状态定义。例如,您可以定义一种特定样式,用于在同时按下和悬停按钮时应用:

@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)
            })
        }
    )
}

图 2. 按钮上的悬停和按压状态。

使用 Modifier.styleable 的自定义可组合项

创建自己的 styleable 组件时,您必须将 interactionSource 连接到 styleState。然后,将此状态传递给 Modifier.styleable 以利用它。

假设您的设计系统包含 GradientButton。您可能需要创建一个继承自 GradientButtonLoginButton,但在互动(例如被按下)期间更改其颜色。

  • 如需启用 interactionSource 样式更新,请在可组合项中添加 interactionSource 作为参数。使用提供的参数,或者,如果没有提供参数,则初始化新的 MutableInteractionSource
  • 通过提供 interactionSource 来初始化 styleState。确保 styleState 的启用状态反映了所提供的启用参数的值。
  • interactionSource 分配给 focusableclickable 修饰符。 最后,将 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")
    }
}

图 3.根据 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)) {

    }
}

图 4. 在按下时为颜色变化添加动画效果。

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))
}

图 5. 在按下时动画显示大小和颜色变化。

使用 StyleState 自定义状态样式

根据您的可组合用例,您可能需要使用由自定义状态支持的不同样式。例如,如果您有一个媒体应用,您可能希望根据播放器的播放状态,为 MediaPlayer 可组合项中的按钮设置不同的样式。请按照以下步骤创建和使用您自己的自定义状态:

  1. 定义自定义键
  2. 创建 StyleState 扩展程序
  3. 链接到自定义状态

定义自定义键

如需创建基于状态的自定义样式,请先创建 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 函数。

style lambda 中,您可以使用之前定义的扩展函数,为自定义状态应用基于状态的样式。

以下代码是本示例的完整代码段:

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)
}