La API de Styles ofrece un enfoque declarativo y optimizado para administrar los cambios en la IU durante los estados de interacción, como hovered, focused y pressed. Con esta API, puedes reducir significativamente el código estándar que suele ser necesario cuando se usan modificadores.
Para facilitar el diseño reactivo, StyleState actúa como una interfaz estable de solo lectura que hace un seguimiento del estado activo de un elemento (como su estado habilitado, presionado o enfocado). Dentro de un StyleScope, puedes acceder a esto a través de la propiedad state para implementar lógica condicional directamente en tus definiciones de diseño.
Interacción basada en el estado: Enfoque, presión, selección, habilitación, activación o desactivación
Los estilos incluyen compatibilidad integrada para interacciones comunes:
- Presionados
- Flotaba
- Seleccionado
- Habilitado
- Activado
También es posible admitir estados personalizados. Consulta la sección Diseño de estados personalizados con StyleState para obtener más información.
Cómo controlar estados de interacción con parámetros de diseño
En el siguiente ejemplo, se muestra cómo modificar background y borderColor en respuesta a los estados de interacción, específicamente, cambiar a morado cuando se coloca el cursor sobre el elemento y a azul cuando se enfoca:
@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) }) } ) }
También puedes crear definiciones de estado anidadas. Por ejemplo, puedes definir un estilo específico para cuando se presiona y se coloca el cursor sobre un botón de forma simultánea:
@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) }) } ) }
Elementos componibles personalizados con Modifier.styleable
Cuando crees tus propios componentes styleable, debes conectar un interactionSource a un styleState. Luego, pasa este estado a Modifier.styleable para utilizarlo.
Considera una situación en la que tu sistema de diseño incluye un GradientButton. Es posible que desees crear un LoginButton que herede de GradientButton, pero que altere sus colores durante las interacciones, como cuando se presiona.
- Para habilitar las actualizaciones de estilo de
interactionSource, incluye uninteractionSourcecomo parámetro dentro de tu elemento componible. Usa el parámetro proporcionado o, si no se proporciona uno, inicializa un nuevoMutableInteractionSource. - Inicializa el objeto
styleStateproporcionando el objetointeractionSource. Asegúrate de que el estado habilitado destyleStaterefleje el valor del parámetro habilitado proporcionado. - Asigna el
interactionSourcea los modificadoresfocusableyclickable. Por último, aplica elstyleStateal parámetrostyleabledel modificador.
@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, ) }
Ahora puedes usar el estado interactionSource para controlar las modificaciones de estilo con las opciones pressed, focused y hovered dentro del bloque de estilo:
@Preview @Composable fun LoginButton() { val loginButtonStyle = Style { pressed { background( Brush.linearGradient( listOf(Color.Magenta, Color.Red) ) ) } } GradientButton(onClick = { // Login logic }, style = loginButtonStyle) { BaseText("Login") } }
interactionSourceCómo animar los cambios de estilo
Los cambios de estado de los diseños incluyen compatibilidad con animaciones integradas. Puedes incluir la nueva propiedad dentro de cualquier bloque de cambio de estado con animate para agregar automáticamente animaciones entre diferentes estados. Es similar a las APIs de animate*AsState. En el siguiente ejemplo, se anima el borderColor de negro a azul cuando el estado cambia a enfocado:
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)) { } }
La API de animate acepta un animationSpec para cambiar la duración o la forma de la curva de animación. En el siguiente ejemplo, se anima el tamaño de la caja con una especificación de 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)) }
Diseño de estados personalizados con StyleState
Según tu caso de uso componible, es posible que tengas diferentes estilos respaldados por estados personalizados. Por ejemplo, si tienes una app de contenido multimedia, es posible que desees tener un diseño diferente para los botones en tu elemento MediaPlayer componible según el estado de reproducción del reproductor. Sigue estos pasos para crear y usar tu propio estado personalizado:
- Define una clave personalizada
- Crea la extensión
StyleState - Vínculo a un estado personalizado
Define una clave personalizada
Para crear un diseño personalizado basado en el estado, primero crea un objeto StyleStateKey y pasa el valor de estado predeterminado. Cuando se inicia la app, el reproductor multimedia se encuentra en el estado Stopped, por lo que se inicializa de la siguiente manera:
enum class PlayerState { Stopped, Playing, Paused } val playerStateKey = StyleStateKey(PlayerState.Stopped)
Crea funciones de extensión de StyleState
Define una función de extensión en StyleState para consultar el playState actual.
Luego, crea funciones de extensión en StyleScope con tus estados personalizados pasando playStateKey, una lambda con el estado específico y el estilo.
// 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 }) }
Vínculo a un estado personalizado
Define styleState en tu elemento componible y establece styleState.playState como igual al estado entrante. Pasa styleState a la función styleable en el modificador.
@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)) { ///.. } }
Dentro de la expresión lambda style, puedes aplicar un diseño basado en el estado para estados personalizados con las funciones de extensión definidas anteriormente.
@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) }
El siguiente código es el fragmento completo de este ejemplo:
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) }