Material Design 2 w sekcji Utwórz

Jetpack Compose oferuje implementację Material Design, kompleksowego systemu projektowania do tworzenia interfejsów cyfrowych. Komponenty Material Design (przyciski, karty, przełączniki itp.) są oparte na Material Theming, czyli systematycznym sposobie dostosowywania Material Design, aby lepiej odzwierciedlać markę produktu. Motyw Material zawiera atrybuty koloru, typografii i kształtu. Gdy dostosujesz te atrybuty, zmiany zostaną automatycznie odzwierciedlone w komponentach, których używasz do tworzenia aplikacji.

Jetpack Compose implementuje te koncepcje za pomocą funkcji kompozycyjnej MaterialTheme:

MaterialTheme(
    colors = // ...
    typography = // ...
    shapes = // ...
) {
    // app content
}

Skonfiguruj parametry przekazywane do MaterialTheme, aby dostosować motyw aplikacji.

Dwa kontrastujące ze sobą zrzuty ekranu. Pierwszy zrzut ekranu przedstawia domyślny styl MaterialTheme, a drugi – zmodyfikowany styl.
Rysunek 1. Na pierwszym zrzucie ekranu widać aplikację, która nie konfiguruje elementu `MaterialTheme`, więc używa domyślnego stylu. Drugi zrzut ekranu pokazuje aplikację, która przekazuje parametry do `MaterialTheme` w celu dostosowania stylu.

Kolor

Kolory są modelowane w Compose za pomocą klasy Color, która przechowuje dane.

val Red = Color(0xffff0000)
val Blue = Color(red = 0f, green = 0f, blue = 1f)

Możesz je uporządkować w dowolny sposób (jako stałe najwyższego poziomu, w ramach singletonu lub zdefiniowane w tekście), ale zdecydowanie zalecamy określenie kolorów w motywie i pobieranie ich z niego. To podejście umożliwia obsługę ciemnego motywu i motywów zagnieżdżonych.

Przykład palety kolorów motywu
Rysunek 2. System kolorów Material.

Compose udostępnia klasę Colors do modelowania systemu kolorów Material. Colors udostępnia funkcje kreatora do tworzenia zestawów jasnych lub ciemnych kolorów:

private val Yellow200 = Color(0xffffeb46)
private val Blue200 = Color(0xff91a4fc)
// ...

private val DarkColors = darkColors(
    primary = Yellow200,
    secondary = Blue200,
    // ...
)
private val LightColors = lightColors(
    primary = Yellow500,
    primaryVariant = Yellow400,
    secondary = Blue700,
    // ...
)

Po zdefiniowaniu Colors możesz przekazać je do MaterialTheme:

MaterialTheme(
    colors = if (darkTheme) DarkColors else LightColors
) {
    // app content
}

Używanie kolorów motywu

Możesz pobrać Colors przekazany do elementu kompozycyjnego MaterialTheme za pomocą MaterialTheme.colors.

Text(
    text = "Hello theming",
    color = MaterialTheme.colors.primary
)

Kolor powierzchni i treści

Wiele komponentów akceptuje parę kolorów i kolor treści:

Surface(
    color = MaterialTheme.colors.surface,
    contentColor = contentColorFor(color),
    // ...
) { /* ... */ }

TopAppBar(
    backgroundColor = MaterialTheme.colors.primarySurface,
    contentColor = contentColorFor(backgroundColor),
    // ...
) { /* ... */ }

Dzięki temu możesz nie tylko ustawić kolor elementu kompozycyjnego, ale też podać domyślny kolor treści, czyli elementów kompozycyjnych, które się w nim znajdują. Wiele funkcji kompozycyjnych domyślnie używa tego koloru treści. Na przykład Text opiera swój kolor na kolorze treści elementu nadrzędnego, a Icon używa tego koloru do ustawienia odcienia.

Dwa przykłady tego samego banera w różnych kolorach
Rysunek 3. Ustawienie różnych kolorów tła powoduje, że kolory tekstu i ikon są różne.

Metoda contentColorFor() pobiera odpowiedni kolor „on” dla dowolnych kolorów motywu. Jeśli na przykład ustawisz kolor tła primary na Surface, ta funkcja ustawi kolor treści na onPrimary. Jeśli ustawisz kolor tła spoza motywu, musisz też określić odpowiedni kolor treści. Użyj LocalContentColor, aby pobrać preferowany kolor treści dla bieżącego tła na danej pozycji w hierarchii.

Przezroczystość treści

Często chcesz zmieniać stopień wyróżnienia treści, aby przekazać ich znaczenie i zapewnić hierarchię wizualną. Zalecenia dotyczące czytelności tekstu w Material Design sugerują stosowanie różnych poziomów przezroczystości do przekazywania różnych poziomów ważności.

Jetpack Compose implementuje to za pomocą LocalContentAlpha. Możesz określić wartość alfa treści dla hierarchii, podając wartość dla tego CompositionLocal. Zagnieżdżone elementy kompozycyjne mogą używać tej wartości, aby zastosować przezroczystość do swoich treści. Na przykład Text i Icon domyślnie używają kombinacji LocalContentColor dostosowanej do używania LocalContentAlpha. Material określa pewne standardowe wartości alfa (high, medium, disabled), które są modelowane przez obiekt ContentAlpha.

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(
        // ...
    )
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(
        // ...
    )
    Text(
        // ...
    )
}

Więcej informacji o CompositionLocal znajdziesz w artykule Dane o zasięgu lokalnym z CompositionLocal.

Zrzut ekranu przedstawiający tytuł artykułu z różnymi poziomami wyróżnienia tekstu
Rysunek 4. Zastosuj różne poziomy wyróżnienia tekstu, aby wizualnie przedstawić hierarchię informacji. Pierwszy wiersz tekstu to tytuł, który zawiera najważniejsze informacje, dlatego używa elementu ContentAlpha.high. Drugi wiersz zawiera mniej ważne metadane, dlatego używa stylu ContentAlpha.medium.

Ciemny motyw

W Compose jasny i ciemny motyw implementuje się, przekazując różne zestawy Colors do funkcji kompozycyjnej MaterialTheme:

@Composable
fun MyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
        /*...*/
        content = content
    )
}

W tym przykładzie MaterialTheme jest opakowany we własną funkcję typu „composable”, która akceptuje parametr określający, czy używać ciemnego motywu. W takim przypadku funkcja pobiera domyślną wartość darkTheme, sprawdzając ustawienie motywu urządzenia.

Możesz użyć takiego kodu, aby sprawdzić, czy bieżące Colors są jasne czy ciemne:

val isLightTheme = MaterialTheme.colors.isLight
Icon(
    painterResource(
        id = if (isLightTheme) {
            R.drawable.ic_sun_24
        } else {
            R.drawable.ic_moon_24
        }
    ),
    contentDescription = "Theme"
)

Nakładki wysokości

W Material Design powierzchnie w ciemnych motywach o wyższych wysokościach otrzymują nakładki wysokości, które rozjaśniają ich tło. Im wyższa jest wysokość powierzchni (im bliżej jest ona domniemanego źródła światła), tym jaśniejsza się staje.

Funkcja kompozycyjna Surface automatycznie stosuje te nakładki, gdy używane są ciemne kolory. Podobnie działają inne funkcje kompozycyjne Material, które używają powierzchni:

Surface(
    elevation = 2.dp,
    color = MaterialTheme.colors.surface, // color will be adjusted for elevation
    /*...*/
) { /*...*/ }

Zrzut ekranu aplikacji przedstawiający subtelnie różne kolory używane w przypadku elementów na różnych poziomach wysokości
Rysunek 5. Karty i dolny pasek nawigacyjny mają tło w kolorze surface. Karty i dolny pasek nawigacyjny znajdują się na różnych poziomach nad tłem, dlatego mają nieco inne kolory – karty są jaśniejsze niż tło, a dolny pasek nawigacyjny jest jaśniejszy niż karty.

W przypadku niestandardowych scenariuszy, które nie obejmują elementu Surface, użyj LocalElevationOverlay, czyli elementu CompositionLocal zawierającego element ElevationOverlay używany przez komponenty Surface:

// Elevation overlays
// Implemented in Surface (and any components that use it)
val color = MaterialTheme.colors.surface
val elevation = 4.dp
val overlaidColor = LocalElevationOverlay.current?.apply(
    color, elevation
)

Aby wyłączyć nakładki wysokości, podaj null w wybranym punkcie hierarchii funkcji kompozycyjnych:

MyTheme {
    CompositionLocalProvider(LocalElevationOverlay provides null) {
        // Content without elevation overlays
    }
}

Ograniczone akcenty kolorystyczne

W przypadku ciemnych motywów Material zaleca stosowanie ograniczonych akcentów kolorystycznych, preferując w większości przypadków kolor surface zamiast koloru primary. Komponenty Material, takie jak TopAppBar i BottomNavigation, domyślnie implementują to zachowanie.

Zrzut ekranu przedstawiający ciemny motyw Material, na którym górny pasek aplikacji używa koloru powierzchni zamiast koloru podstawowego, aby ograniczyć akcenty kolorystyczne
Rysunek 6. Ciemny motyw Material z ograniczonymi akcentami kolorystycznymi. Górny pasek aplikacji używa koloru podstawowego w jasnym motywie i koloru powierzchni w ciemnym motywie.

W przypadku scenariuszy niestandardowych użyj właściwości rozszerzenia primarySurface:

Surface(
    // Switches between primary in light theme and surface in dark theme
    color = MaterialTheme.colors.primarySurface,
    /*...*/
) { /*...*/ }

Typografia

Material definiuje system typów, zachęcając do używania niewielkiej liczby stylów o nazwach semantycznych.

Przykład kilku różnych krojów pisma w różnych stylach
Rysunek 7. system Material.

Compose implementuje system typów za pomocą klas Typography, TextStyle i związanych z czcionkami. Konstruktor Typography oferuje domyślne wartości dla każdego stylu, więc możesz pominąć te, których nie chcesz dostosowywać:

val raleway = FontFamily(
    Font(R.font.raleway_regular),
    Font(R.font.raleway_medium, FontWeight.W500),
    Font(R.font.raleway_semibold, FontWeight.SemiBold)
)

val myTypography = Typography(
    h1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    )
    /*...*/
)
MaterialTheme(typography = myTypography, /*...*/) {
    /*...*/
}

Jeśli chcesz używać tego samego kroju pisma w całym dokumencie, określ parametr defaultFontFamily i pomiń parametr fontFamily we wszystkich elementach TextStyle:

val typography = Typography(defaultFontFamily = raleway)
MaterialTheme(typography = typography, /*...*/) {
    /*...*/
}

Używanie stylów tekstu

Dostęp do elementów TextStyle można uzyskać za pomocą MaterialTheme.typography. Pobierz elementy TextStyle w ten sposób:

Text(
    text = "Subtitle2 styled",
    style = MaterialTheme.typography.subtitle2
)

Zrzut ekranu przedstawiający różne kroje pisma do różnych celów
Rysunek 8. Używaj różnych krojów pisma i stylów, aby wyrazić swoją markę.

Kształt

Material definiuje system kształtów, który pozwala określać kształty dużych, średnich i małych komponentów.

Wyświetla różne kształty w stylu Material Design
Rysunek 9. System kształtów Material.

Compose implementuje system kształtów za pomocą klasy Shapes, która umożliwia określenie CornerBasedShape dla każdej kategorii rozmiaru:

val shapes = Shapes(
    small = RoundedCornerShape(percent = 50),
    medium = RoundedCornerShape(0f),
    large = CutCornerShape(
        topStart = 16.dp,
        topEnd = 0.dp,
        bottomEnd = 0.dp,
        bottomStart = 16.dp
    )
)

MaterialTheme(shapes = shapes, /*...*/) {
    /*...*/
}

Wiele komponentów domyślnie używa tych kształtów. Na przykład Button, TextField i FloatingActionButton domyślnie mają rozmiar mały, AlertDialog – średni, a ModalDrawer – duży. Pełne mapowanie znajdziesz w dokumentacji schematu kształtów.

Używanie kształtów

Dostęp do elementów Shape można uzyskać za pomocą MaterialTheme.shapes. Pobierz elementy Shape za pomocą takiego kodu:

Surface(
    shape = MaterialTheme.shapes.medium, /*...*/
) {
    /*...*/
}

Zrzut ekranu aplikacji, która używa kształtów Material Design do przekazywania informacji o stanie elementu
Rysunek 10. Używaj kształtów, aby wyrazić markę lub stan.

Style domyślne

W Compose nie ma odpowiednika stylów domyślnych z widoków Androida. Podobną funkcjonalność możesz zapewnić, tworząc własne funkcje typu „composable” overload, które opakowują komponenty Material Design. Aby na przykład utworzyć styl przycisku, umieść przycisk we własnej funkcji typu „composable”, bezpośrednio ustawiając parametry, które chcesz lub musisz zmienić, i udostępniając inne jako parametry funkcji typu „composable”.

@Composable
fun MyButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

Nakładki motywów

Odpowiednik nakładek motywu z widoków Androida możesz uzyskać w Compose, zagnieżdżając komponenty MaterialTheme. Ponieważ MaterialTheme domyślnie ustawia kolory, typografię i kształty na bieżącą wartość motywu, wszystkie inne parametry zachowują swoje wartości domyślne, gdy motyw ustawia tylko jeden z tych parametrów.

Ponadto podczas migracji ekranów opartych na widokach do Compose zwróć uwagę na użycie atrybutu android:theme. Prawdopodobnie potrzebujesz nowego elementu MaterialTheme w tej części drzewa interfejsu Compose.

W tym przykładzie ekran szczegółów używa elementu PinkTheme na większości ekranu, a elementu BlueTheme w sekcji powiązanej. Tę koncepcję ilustrują poniższy zrzut ekranu i kod:

Zrzut ekranu aplikacji przedstawiający zagnieżdżone motywy, z różowym motywem na ekranie głównym i niebieskim motywem w powiązanej sekcji
Rysunek 11. Zagnieżdżone motywy.

@Composable
fun DetailsScreen(/* ... */) {
    PinkTheme {
        // other content
        RelatedSection()
    }
}

@Composable
fun RelatedSection(/* ... */) {
    BlueTheme {
        // content
    }
}

Stany komponentów

Komponenty Material, z którymi można wchodzić w interakcje (klikać, przełączać itp.), mogą mieć różne stany wizualne. Możliwe stany to m.in. włączony, wyłączony i naciśnięty.

Funkcje kompozycyjne często mają parametr enabled. Ustawienie wartości false uniemożliwia interakcję i zmienia właściwości, takie jak kolor i wysokość, aby wizualnie przekazać stan komponentu.

Zrzut ekranu przedstawiający 2 przyciski: jeden włączony, drugi wyłączony, pokazujący ich różne stany wizualne
Rysunek 12. Przycisk z enabled = true (po lewej) i enabled = false (po prawej).

W większości przypadków możesz polegać na wartościach domyślnych, takich jak kolor i wysokość. Jeśli chcesz skonfigurować wartości używane w różnych stanach, możesz skorzystać z klas i funkcji ułatwiających pracę. Przyjrzyj się temu przykładowi przycisku:

Button(
    onClick = { /* ... */ },
    enabled = true,
    // Custom colors for different states
    colors = ButtonDefaults.buttonColors(
        backgroundColor = MaterialTheme.colors.secondary,
        disabledBackgroundColor = MaterialTheme.colors.onBackground
            .copy(alpha = 0.2f)
            .compositeOver(MaterialTheme.colors.background)
        // Also contentColor and disabledContentColor
    ),
    // Custom elevation for different states
    elevation = ButtonDefaults.elevation(
        defaultElevation = 8.dp,
        disabledElevation = 2.dp,
        // Also pressedElevation
    )
) { /* ... */ }

Zrzut ekranu przedstawiający 2 przyciski z dostosowanym kolorem i wysokością w stanach włączenia i wyłączenia
Rysunek 13. Przycisk z enabled = true (po lewej) i enabled = false (po prawej) z dostosowanymi wartościami koloru i wysokości.

Kręgi na wodzie

Komponenty Material Design używają efektu fali, aby wskazać, że użytkownik wchodzi z nimi w interakcję. Jeśli w hierarchii używasz MaterialTheme, Ripple jest używany jako domyślny Indication w modyfikatorach takich jak clickable i indication.

W większości przypadków możesz użyć domyślnego ustawienia Ripple. Jeśli chcesz skonfigurować ich wygląd, możesz użyć RippleTheme, aby zmienić właściwości takie jak kolor i alfa.

Możesz rozszerzyć klasę RippleTheme i użyć funkcji użytkowych defaultRippleColor i defaultRippleAlpha. Następnie możesz podać niestandardowy motyw efektu fali w hierarchii, używając LocalRippleTheme:

@Composable
fun MyApp() {
    MaterialTheme {
        CompositionLocalProvider(
            LocalRippleTheme provides SecondaryRippleTheme
        ) {
            // App content
        }
    }
}

@Immutable
private object SecondaryRippleTheme : RippleTheme {
    @Composable
    override fun defaultColor() = RippleTheme.defaultRippleColor(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )

    @Composable
    override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )
}

Animowany GIF pokazujący przyciski z różnymi efektami fali po kliknięciu
Rysunek 14. Przyciski z różnymi wartościami efektu fali podanymi za pomocą RippleConfiguration.

Więcej informacji

Więcej informacji o Material Theming w Compose znajdziesz w tych materiałach.

Codelabs

Filmy