樣式中的狀態和動畫

Styles API 提供簡化的宣告式方法,可在 hoveredfocusedpressed 等互動狀態期間管理 UI 變更。使用這項 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。您可能想建立從 GradientButton 繼承的 LoginButton,但在互動期間 (例如按下時) 變更顏色。

  • 如要啟用 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 狀態,透過樣式區塊中的已按下、已聚焦和已懸停選項,驅動樣式修改:

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