Status dan animasi dalam Gaya

Styles API menawarkan pendekatan deklaratif dan efisien untuk mengelola perubahan UI selama status interaksi seperti hovered, focused, dan pressed. Dengan API ini, Anda dapat mengurangi kode boilerplate secara signifikan yang biasanya diperlukan saat menggunakan pengubah.

Untuk memfasilitasi gaya reaktif, StyleState bertindak sebagai antarmuka stabil dan hanya baca yang melacak status aktif elemen (seperti status diaktifkan, ditekan, atau difokuskan). Dalam StyleScope, Anda dapat mengaksesnya melalui properti state untuk menerapkan logika kondisional langsung dalam definisi Gaya.

Interaksi berbasis status: Diarahkan, difokuskan, ditekan, dipilih, diaktifkan, diubah

Gaya dilengkapi dukungan bawaan untuk interaksi umum:

  • Ditekan
  • Diarahkan
  • Dipilih
  • Diaktifkan
  • Diubah

Anda juga dapat mendukung status kustom. Lihat bagian Gaya Visual Status Kustom dengan StyleState untuk mengetahui informasi selengkapnya.

Menangani status interaksi dengan parameter Gaya

Contoh berikut menunjukkan cara mengubah background dan borderColor sebagai respons terhadap status interaksi, khususnya beralih ke ungu saat diarahkan dan biru saat difokuskan:

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

Gambar 1. Mengubah warna latar belakang berdasarkan status diarahkan dan difokuskan.

Anda juga dapat membuat definisi status bertingkat. Misalnya, Anda dapat menentukan gaya tertentu saat tombol ditekan dan diarahkan secara bersamaan:

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

Gambar 2. Status diarahkan dan ditekan bersama-sama pada tombol.

Composable kustom dengan Modifier.styleable

Saat membuat komponen styleable Anda sendiri, Anda harus menghubungkan interactionSource ke styleState. Kemudian, teruskan status ini ke Modifier.styleable untuk menggunakannya.

Pertimbangkan skenario saat sistem desain Anda menyertakan GradientButton. Anda mungkin ingin membuat LoginButton yang mewarisi dari GradientButton, tetapi mengubah warnanya selama interaksi, seperti saat ditekan.

  • Untuk mengaktifkan update gaya interactionSource, sertakan interactionSource sebagai parameter dalam composable Anda. Gunakan parameter yang disediakan atau, jika tidak ada, inisialisasi MutableInteractionSource baru.
  • Inisialisasi styleState dengan memberikan interactionSource. Pastikan status diaktifkan styleState mencerminkan nilai parameter diaktifkan yang disediakan.
  • Tetapkan interactionSource ke pengubah focusable dan clickable. Terakhir, terapkan styleState ke parameter styleable pengubah.

@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 = rememberUpdatedStyleState(interactionSource) {
        it.isEnabled = enabled
    }
    Row(
        modifier =
            modifier
                .clickable(
                    onClick = onClick,
                    enabled = enabled,
                    interactionSource = interactionSource,
                    indication = null,
                )
                .styleable(styleState, baseGradientButtonStyle then style),
        content = content,
    )
}

Sekarang Anda dapat menggunakan status interactionSource untuk mendorong perubahan gaya dengan opsi ditekan, difokuskan, dan diarahkan di dalam blok gaya:

@Preview
@Composable
fun LoginButton() {
    val loginButtonStyle = Style {
        pressed {
            background(
                Brush.linearGradient(
                    listOf(Color.Magenta, Color.Red)
                )
            )
        }
    }
    GradientButton(onClick = {
        // Login logic
    }, style = loginButtonStyle) {
        BaseText("Login")
    }
}

Gambar 3. Mengubah status composable kustom berdasarkan interactionSource.

Menganimasikan perubahan gaya

Perubahan status gaya dilengkapi dukungan animasi bawaan. Anda dapat menggabungkan properti baru dalam blok perubahan status apa pun dengan animate untuk otomatis menambahkan animasi antar-status yang berbeda. Hal ini mirip dengan animate*AsState API. Contoh berikut menganimasikan borderColor dari hitam menjadi biru saat status berubah menjadi difokuskan:

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

    }
}

Gambar 4. Menganimasikan perubahan warna saat ditekan.

animate API menerima animationSpec untuk mengubah durasi atau bentuk kurva animasi. Contoh berikut menganimasikan ukuran kotak dengan spesifikasi 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))
}

Gambar 5. Menganimasikan perubahan ukuran dan warna saat ditekan.

Gaya visual status kustom dengan StyleState

Bergantung pada kasus penggunaan composable, Anda mungkin memiliki gaya yang berbeda yang didukung oleh status kustom. Misalnya, jika Anda memiliki aplikasi media, Anda mungkin ingin memiliki gaya yang berbeda untuk tombol di composable MediaPlayer bergantung pada status pemutaran pemutar. Ikuti langkah-langkah berikut untuk membuat dan menggunakan status kustom Anda sendiri:

  1. Menentukan kunci kustom
  2. Membuat ekstensi StyleState
  3. Menautkan ke status kustom

Menentukan kunci kustom

Untuk membuat gaya berbasis status kustom, buat StyleStateKey terlebih dahulu dan teruskan nilai status default. Saat aplikasi diluncurkan, pemutar media berada dalam status Stopped, sehingga diinisialisasi dengan cara ini:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

Membuat fungsi ekstensi StyleState

Tentukan fungsi ekstensi di StyleState untuk membuat kueri playState saat ini. Kemudian, buat fungsi ekstensi di StyleScope dengan status kustom Anda yang meneruskan playStateKey, lambda dengan status tertentu, dan gaya.

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

Tentukan styleState di composable Anda dan tetapkan styleState.playState sama dengan status masuk. Teruskan styleState ke fungsi styleable pada pengubah.

Dalam lambda style, Anda dapat menerapkan gaya visual berbasis status untuk status kustom, menggunakan fungsi ekstensi yang ditentukan sebelumnya.

Kode berikut adalah cuplikan lengkap untuk contoh ini:

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