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 baca-saja yang stabil dan melacak status aktif elemen (seperti status diaktifkan, ditekan, atau difokuskan). Dalam StyleScope, Anda dapat mengaksesnya melalui properti state untuk menerapkan logika bersyarat langsung dalam definisi Gaya.

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

Gaya dilengkapi dengan dukungan bawaan untuk interaksi umum:

  • Ditekan
  • Diarahkan
  • Dipilih
  • Aktif
  • Diubah

Anda juga dapat mendukung status kustom. Lihat bagian Penataan Gaya 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 di-hover 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 saat kursor diarahkan dan difokuskan.

Anda juga dapat membuat definisi status bertingkat. Misalnya, Anda dapat menentukan gaya tertentu saat tombol ditekan dan diarahkan kursor 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 dihover dan ditekan bersamaan pada tombol.

Composable kustom dengan Modifier.styleable

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

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

  • Untuk mengaktifkan pembaruan gaya interactionSource, sertakan interactionSource sebagai parameter dalam composable Anda. Gunakan parameter yang disediakan atau, jika tidak ada yang diberikan, inisialisasi MutableInteractionSource baru.
  • Lakukan inisialisasi styleState dengan memberikan interactionSource. Pastikan status aktif styleState mencerminkan nilai parameter aktif yang diberikan.
  • 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 = 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,
    )
}

Sekarang Anda dapat menggunakan status interactionSource untuk mendorong modifikasi gaya dengan opsi ditekan, difokuskan, dan diarahkan kursor 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 dengan dukungan animasi bawaan. Anda dapat membungkus properti baru dalam blok perubahan status dengan animate untuk otomatis menambahkan animasi di antara berbagai status. Hal ini mirip dengan API animate*AsState. Contoh berikut menganimasikan borderColor dari hitam menjadi biru saat status berubah menjadi fokus:

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 tombol 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. Mengubah ukuran dan warna saat tombol ditekan.

Gaya visual status kustom dengan StyleState

Bergantung pada kasus penggunaan composable, Anda mungkin memiliki gaya 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. Buat ekstensi StyleState
  3. Link ke status kustom

Menentukan kunci kustom

Untuk membuat gaya berbasis status kustom, pertama-tama buat StyleStateKey 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 mengkueri playState saat ini. Kemudian, buat fungsi ekstensi di StyleScope dengan meneruskan status kustom Anda di 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 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)
}