Состояние и анимация в стилях

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

Рисунок 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 . Возможно, вы захотите создать LoginButton , который наследует от GradientButton , но меняет свой цвет при взаимодействии, например, при нажатии.

  • Чтобы включить обновление стиля interactionSource , добавьте interactionSource в качестве параметра в ваш составной объект. Используйте предоставленный параметр или, если он не указан, инициализируйте новый MutableInteractionSource .
  • Инициализируйте styleState указав interactionSource . Убедитесь, что статус enabled styleState соответствует значению предоставленного параметра enabled.
  • Присвойте свойство 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 для управления изменениями стиля параметров press, focus и 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 , чтобы автоматически добавлять анимацию между различными состояниями. Это похоже на API animate*AsState . В следующем примере анимируется изменение 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. Анимация изменения цвета при печати.

API animate принимает ` 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 , лямбда-функцию с конкретным состоянием и стиль.

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

Define the styleState in your composable and set the styleState.playState equal to incoming state. Pass styleState into the styleable function on the modifier.

Внутри лямбда- style вы можете применять стилизацию на основе состояния для пользовательских состояний, используя ранее определенные функции расширения.

Ниже приведён полный фрагмент кода для этого примера:

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