Motywy ze stylami

Aplikacje możesz tworzyć na kilka sposobów, korzystając ze stylów. Wybór zależy od tego, na jakim etapie wdrażania Material Design jest Twoja aplikacja:

  1. W pełni niestandardowy system projektowania, który nie korzysta z Material Design.
    • Zalecenie: zdefiniuj style komponentów, które korzystają z wartości z motywu, i udostępnij parametry stylu w komponentach systemu projektowania.
  2. Korzystanie z Material Design.
    • Zalecenie: poczekaj na wdrożenie Material, aby zintegrować go ze stylami. W miarę możliwości używaj stylów w swoich komponentach.

Warstwa stylu

W tradycyjnym modelu Compose dostosowywanie często polega na zastępowaniu tokenów globalnych (kolorów i typografii) udostępnianych przez MaterialTheme lub na opakowywaniu i zastępowaniu właściwości komponentu systemu projektowania, jeśli jest to możliwe. Czasami w warstwie Material występują właściwości, które nie są udostępniane przez podsystemy ani parametry, ale są zakodowane na stałe jako wartości domyślne w samym komponencie.

W interfejsie Styles API dostępna jest nowa warstwa abstrakcji, która stanowi pomost między podsystemami a komponentami – style.

Warstwa Odpowiedzialność Przykład
Wartości podsystemu Nazwane wartości val Primary = Color(0xFF34A85E)
Style atomowe Styl, który zmienia dokładnie 1 właściwość val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then largeSize then interactiveShadowAtomic
Style komponentów Konfiguracje specyficzne dla komponentu Przycisk z niebieskim tłem i dopełnieniem 16 dp. val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }
Komponenty Funkcjonalny element interfejsu, który korzysta ze stylu. Button(style = buttonStyle) { ... }
Diagram przedstawiający motywy ze stylami z wprowadzeniem nowej warstwy
Rysunek 1. Przykład komponentu i sposobu, w jaki uzyskuje on dostęp do stylów z motywu.

Style atomowe a style monolityczne

Dzięki interfejsowi Styles API możesz podzielić styl na osobne style atomowe. Zamiast definiować złożone style specyficzne dla komponentu, takie jak baseButtonStyle, możesz też tworzyć małe style narzędziowe o jednym przeznaczeniu. Działają one jak „atomy”.

// Define single-purpose "atomic" styles
val paddingAtomic = Style {
    contentPadding(16.dp)
}
val roundedCornerShapeAtomic = Style {
    shape(RoundedCornerShape(8.dp))
}
val primaryBackgroundAtomic = Style {
    background(Color.Blue)
}
val largeSizeAtomic = Style {
    size(100.dp, 40.dp)
}
val interactiveShadowAtomic = Style {
    hovered {
        animate {
            dropShadow(
                Shadow(
                    offset = DpOffset(
                        0.dp,
                        0.dp
                    ),
                    radius = 2.dp,
                    spread = 0.dp,
                    color = Color.Blue,
                )
            )
        }
    }
}

Kompozycja z użyciem „then”

Jedną z zaawansowanych funkcji nowego interfejsu Styles API jest operator then, który umożliwia łączenie wielu obiektów Style. Dzięki temu możesz tworzyć komponenty za pomocą atomowych klas narzędziowych.

Tradycyjne (nieatomowe):

// One large monolithic style
val buttonStyle = Style {
    contentPadding(16.dp)
    shape(RoundedCornerShape(8.dp))
    background(Color.Blue)
}

Refaktoryzacja atomowa:

// Combine atoms to create the final appearance
val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic

Wdrażanie stylów w systemie projektowania

Podczas wdrażania stylów w systemie projektowania rozważ te opcje, w zależności od tego, na jakim etapie wdrażania jest Twój system projektowania.

Niestandardowy system projektowania ze stylami

Rozważ, gdy: masz obszerny przewodnik po marce, który nie jest oparty na Material Design, i nie planujesz korzystać z Material Design.

Strategia: wdrożenie w pełni niestandardowego systemu projektowania i udostępnienie stylów jako części motywu.

Ta opcja jest ścieżką niestandardową, jeśli nie używasz Material jako głównego języka systemu projektowania. Całkowicie pomijasz MaterialTheme w przypadku definicji wizualnych i masz już utworzony własny motyw niestandardowy. Tworzysz CompanyTheme, który działa jako kontener dla Twoich stylów.

  • Jak to działa: utwórz obiekt CompanyTheme, który zawiera obiekty Style dla każdego komponentu w systemie. Twoje komponenty (opakowania logiki Material lub niestandardowe implementacje Box lub Layout) bezpośrednio korzystają z tych stylów i udostępniają parametr Style dla użytkowników Twojego systemu projektowania.
  • Warstwa stylu: style są podstawową definicją Twojego systemu projektowania. Tokeny to nazwane zmienne, które są przekazywane do tych stylów. Umożliwia to głębokie dostosowywanie, np. definiowanie unikalnych animacji zmian stanu (np. animowanie skali i koloru po naciśnięciu).

Jeśli tworzysz własny motyw niestandardowy bez użycia Material i chcesz wdrożyć style, dodaj listę stylów do motywu. Dzięki temu możesz uzyskać dostęp do stylów podstawowych z dowolnego miejsca w projekcie.

  1. Utwórz klasę Styles, która będzie przechowywać różne style w aplikacji, i utwórz wartości domyślne. Na przykład w aplikacji Jetsnack klasa ma nazwę JetsnackStyles:

    object JetsnackStyles{
        val buttonStyle: Style = Style {
            shape(shapes.medium)
            background(colors.brand)
            contentColor(colors.textPrimary)
            contentPaddingVertical(8.dp)
            contentPaddingHorizontal(24.dp)
            textStyle(typography.labelLarge)
            disabled {
                animate {
                    background(colors.brandSecondary)
                }
            }
        }
        val cardStyle: Style = Style {
            shape(shapes.medium)
            background(colors.uiBackground)
            contentColor(colors.textPrimary)
        }
    }

  2. Udostępnij Styles jako część ogólnego motywu i udostępnij pomocnicze funkcje rozszerzające w StyleScope, aby uzyskać dostęp do podsystemów:

    @Immutable
    class JetsnackTheme(
        val colors: JetsnackColors = LightJetsnackColors,
        val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(),
        val shapes: Shapes = Shapes()
    ) {
        companion object {
            val colors: JetsnackColors
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.colors
    
            val typography: androidx.compose.material3.Typography
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.typography
    
            val shapes: Shapes
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.shapes
    
            val styles: JetsnackStyles = JetsnackStyles
    
            val LocalJetsnackTheme: ProvidableCompositionLocal<JetsnackTheme>
                get() = LocalJetsnackThemeInstance
        }
    }
    
    val StyleScope.colors: JetsnackColors
        get() = LocalJetsnackTheme.currentValue.colors
    
    val StyleScope.typography: androidx.compose.material3.Typography
        get() = LocalJetsnackTheme.currentValue.typography
    
    val StyleScope.shapes: Shapes
        get() = LocalJetsnackTheme.currentValue.shapes
    
    internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() }
    
    @Composable
    fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
        val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors
        val theme = JetsnackTheme(colors = colors)
    
        CompositionLocalProvider(
            LocalJetsnackTheme provides theme,
        ) {
            MaterialTheme(
                typography = LocalJetsnackTheme.current.typography,
                shapes = LocalJetsnackTheme.current.shapes,
                content = content,
            )
        }
    }

  3. Uzyskaj dostęp do JetsnackStyles w komponencie:

    @Composable
    fun CustomButton(modifier: Modifier,
                     style: Style = Style,
                     text: String) {
        val interactionSource = remember { MutableInteractionSource() }
        val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    
        // Apply style to top level container in combination with incoming style from parameter.
        Box(modifier = modifier
            .clickable(
                interactionSource = interactionSource,
                indication = null,
                enabled = true,
                role = Role.Button,
                onClick = {
    
                },
            )
            .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) {
            Text(text)
        }
    }

Oprócz wdrożenia motywu globalnego istnieją alternatywne strategie włączania Styles do aplikacji. Możesz używać Styles w wierszu w przypadku konkretnych miejsc wywołań lub używać definicji statycznych, gdy pełne możliwości motywu są zbędne. Styles nie należy zamieniać warunkowo, chyba że cały styl jest zasadniczo inny. Zamiast przełączać się między różnymi obiektami stylu, lepiej jest uzyskiwać dostęp do tokenów dynamicznych w definicji wizualnej.