स्टाइल में स्टेट और ऐनिमेशन

Styles API, यूज़र इंटरफ़ेस (यूआई) में होने वाले बदलावों को मैनेज करने के लिए, डिक्लेरेटिव और स्ट्रीमलाइन किया गया तरीका उपलब्ध कराता है. ये बदलाव, इंटरैक्शन की स्थितियों के दौरान होते हैं. जैसे, hovered, focused, और pressed. इस एपीआई की मदद से, बॉयलरप्लेट कोड को काफ़ी हद तक कम किया जा सकता है. आम तौर पर, मॉडिफ़ायर का इस्तेमाल करते समय इसकी ज़रूरत होती है.

स्टाइलिंग को बेहतर बनाने के लिए, 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 शामिल है. आपको ऐसी LoginButton बनानी है जो GradientButton से इनहेरिट करती है, लेकिन इंटरैक्शन के दौरान उसके रंग बदल जाते हैं. जैसे, दबाए जाने पर.

  • interactionSource स्टाइल के अपडेट चालू करने के लिए, अपने कंपोज़ेबल में interactionSource को पैरामीटर के तौर पर शामिल करें. दिए गए पैरामीटर का इस्तेमाल करें. अगर कोई पैरामीटर नहीं दिया गया है, तो नया MutableInteractionSource शुरू करें.
  • interactionSource देकर, styleState को शुरू करें. पक्का करें कि styleState की चालू स्थिति, दिए गए चालू पैरामीटर की वैल्यू दिखाती हो.
  • interactionSource को focusable और clickable मॉडिफ़ायर असाइन करें. आखिर में, मॉडिफ़ायर के styleable पैरामीटर पर styleState लागू करें.

@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 एपीआई की तरह ही है. यहां दिए गए उदाहरण में, फ़ोकस किए जाने पर 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 एपीआई, ऐनिमेशन कर्व की अवधि या आकार बदलने के लिए 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 कंपोज़ेबल में मौजूद बटन के लिए अलग-अलग स्टाइल चाहिए हों. अपना कस्टम स्टेट बनाने और उसका इस्तेमाल करने के लिए, यह तरीका अपनाएं:

  1. कस्टम कुंजी तय करना
  2. StyleState एक्सटेंशन बनाना
  3. कस्टम स्टेट का लिंक

कस्टम कुंजी तय करना

स्टेट के आधार पर पसंद के मुताबिक स्टाइल बनाने के लिए, सबसे पहले StyleStateKey बनाएं और उसमें डिफ़ॉल्ट स्टेट की वैल्यू पास करें. ऐप्लिकेशन लॉन्च होने पर, मीडिया प्लेयर Stopped स्थिति में होता है. इसलिए, इसे इस तरह से शुरू किया जाता है:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

StyleState एक्सटेंशन फ़ंक्शन बनाना

मौजूदा playState के बारे में क्वेरी करने के लिए, StyleState पर एक एक्सटेंशन फ़ंक्शन तय करें. इसके बाद, 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 })
}

अपने कंपोज़ेबल में styleState को तय करें और styleState.playState को इनकमिंग स्टेट के बराबर सेट करें. मॉडिफ़ायर पर, styleable फ़ंक्शन में styleState पास करें.

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