Stiller'deki durum ve animasyonlar

Styles API, hovered, focused ve pressed gibi etkileşim durumları sırasında kullanıcı arayüzü değişikliklerini yönetmek için bildirim temelli ve kolaylaştırılmış bir yaklaşım sunar. Bu API ile, değiştiriciler kullanılırken genellikle gerekli olan standart kodu önemli ölçüde azaltabilirsiniz.

Reaktif stil oluşturmayı kolaylaştırmak için StyleState, bir öğenin etkin durumunu (ör. etkin, basılı veya odaklanılmış durumu) izleyen kararlı ve salt okunur bir arayüz görevi görür. Bir StyleScope içinde, koşullu mantığı doğrudan stil tanımlarınızda uygulamak için state özelliği üzerinden bu özelliğe erişebilirsiniz.

Duruma dayalı etkileşim: Fareyle üzerine gelme, odaklanma, basma, seçme, etkinleştirme, değiştirme

Stiller, yaygın etkileşimler için yerleşik destekle birlikte gelir:

  • Basıldı
  • Fareyle üzerine gelindiğinde
  • Seçili
  • Etkin
  • Açma/kapatma

Özel durumları da desteklemek mümkündür. Daha fazla bilgi için StyleState ile Özel Durum Stili Oluşturma bölümüne bakın.

Stil parametreleriyle etkileşim durumlarını işleme

Aşağıdaki örnekte, etkileşim durumlarına yanıt olarak background ve borderColor öğelerinin nasıl değiştirileceği gösterilmektedir. Özellikle fareyle üzerine gelindiğinde mor, odaklanıldığında ise mavi renge geçiş yapılmaktadır:

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

Şekil 1. Arka plan rengini, fareyle üzerine gelme ve odaklanma durumlarına göre değiştirme.

İç içe yerleştirilmiş durum tanımları da oluşturabilirsiniz. Örneğin, bir düğmeye hem basıldığında hem de fareyle üzerine gelindiğinde kullanılacak belirli bir stil tanımlayabilirsiniz:

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

Şekil 2. Bir düğmede hem fareyle üzerine gelme hem de basma durumu birlikte kullanıldığında.

Modifier.styleable ile özel composable'lar

Kendi styleable bileşenlerinizi oluştururken bir interactionSource öğesini styleState öğesine bağlamanız gerekir. Ardından, bu durumu kullanmak için Modifier.styleable'a aktarın.

Tasarım sisteminizin GradientButton içerdiği bir senaryoyu ele alalım. GradientButton öğesinden devralan ancak etkileşimler sırasında (ör. basıldığında) renklerini değiştiren bir LoginButton oluşturmak isteyebilirsiniz.

  • interactionSource stil güncellemelerini etkinleştirmek için composable'ınıza parametre olarak interactionSource ekleyin. Sağlanan parametreyi kullanın veya parametre sağlanmamışsa yeni bir MutableInteractionSource başlatın.
  • interactionSource değerini sağlayarak styleState değerini başlatın. styleState'nın etkinleştirilmiş durumunun, sağlanan etkinleştirilmiş parametrenin değerini yansıttığından emin olun.
  • interactionSource öğesini focusable ve clickable değiştiricilerine atayın. Son olarak, styleState değerini değiştiricinin styleable parametresine uygulayın.

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

Artık stil bloğundaki basılı, odaklanılmış ve fareyle üzerine gelinen seçenekleriyle stil değişikliklerini yönlendirmek için interactionSource durumunu kullanabilirsiniz:

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

Şekil 3. interactionSource değerine göre özel bir composable durumunu değiştirme.

Stil değişikliklerine animasyon uygulama

Stil durumu değişiklikleri, yerleşik animasyon desteğiyle birlikte gelir. Yeni özelliği, farklı durumlar arasında otomatik olarak animasyon eklemek için animate ile herhangi bir durum değişikliği bloğuna sarabilirsiniz. Bu, animate*AsState API'lerine benzer. Aşağıdaki örnekte, durum odaklanmış olarak değiştiğinde borderColor siyah renkten mavi renge animasyonla dönüştürülür:

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

    }
}

Şekil 4. Basıldığında renk değişikliklerini animasyonla gösterme

animate API, animasyon eğrisinin süresini veya şeklini değiştirmek için animationSpec kabul eder. Aşağıdaki örnekte, spring spesifikasyonuyla kutunun boyutu animasyonlu hale getiriliyor:

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

Şekil 5. Basıldığında boyut ve renk değişikliklerini animasyonla gösterme.

StyleState ile özel durum stili

Birleştirilebilir kullanım alanınıza bağlı olarak, özel durumlarla desteklenen farklı stilleriniz olabilir. Örneğin, bir medya uygulamanız varsa oynatıcının oynatma durumuna bağlı olarak MediaPlayer composable'ınızdaki düğmeler için farklı stiller kullanmak isteyebilirsiniz. Kendi özel durumunuzu oluşturmak ve kullanmak için aşağıdaki adımları uygulayın:

  1. Özel anahtar tanımlama
  2. StyleState uzantısı oluşturma
  3. Özel duruma bağlantı

Özel anahtar tanımlama

Özel duruma dayalı stil oluşturmak için önce bir StyleStateKey oluşturun ve varsayılan durum değerini iletin. Uygulama başlatıldığında medya oynatıcı Stopped durumunda olur. Bu nedenle, aşağıdaki şekilde başlatılır:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

StyleState uzantısı işlevleri oluşturma

Mevcut playState sorgulamak için StyleState üzerinde bir uzantı işlevi tanımlayın. Ardından, StyleScope üzerinde uzantı işlevleri oluşturun. Bu işlevlerde, playStateKey içinde özel durumlarınız, belirli bir duruma sahip lambda ve stil iletilir.

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

Composable'ınızda styleState öğesini tanımlayın ve styleState.playState öğesini gelen duruma eşit olacak şekilde ayarlayın. styleState öğesini değiştiricideki styleable işlevine iletin.

style lambda'sında, daha önce tanımlanmış uzantı işlevlerini kullanarak özel durumlar için duruma dayalı stil uygulayabilirsiniz.

Aşağıdaki kod, bu örneğin tam snippet'idir:

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