Styles API 提供簡化的宣告式方法,可在 hovered、focused 和 pressed 等互動狀態期間管理 UI 變更。使用這項 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 狀態,透過樣式區塊中的已按下、已聚焦和已懸停選項,驅動樣式修改:
@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) }