Motywy w Compose with Material 3

1. Wprowadzenie

Z tego ćwiczenia dowiesz się, jak stosować motywy w aplikacjach Jetpack Compose przy użyciu Material Design 3. Dowiesz się też o kluczowych elementach składowych schematów kolorów, typografii i kształtów Material Design 3, które pomogą Ci spersonalizować aplikację i uczynić ją bardziej dostępną.

Dowiesz się też, jak działa dynamiczne motywy i jakie są różne poziomy wyróżnienia.

Czego się nauczysz

Z tego przewodnika dowiesz się:

  • Najważniejsze aspekty motywów Material 3
  • Schematy kolorów Material 3 i generowanie motywów dla aplikacji
  • Obsługa dynamicznych i jasnych/ciemnych motywów w aplikacji
  • Typografia i kształty do personalizacji aplikacji
  • Komponenty Material 3 i dostosowywanie stylu aplikacji

Co utworzysz

W tym ćwiczeniu nadasz motyw aplikacji klienta poczty e-mail o nazwie Reply. Zaczniesz od aplikacji bez stylu, korzystając z motywu podstawowego, a następnie zastosujesz zdobytą wiedzę, aby nadać aplikacji motyw i obsługiwać ciemne motywy.

d15db3dc75a9d00f.png

Domyślny punkt początkowy naszej aplikacji z motywem podstawowym.

Utworzysz motyw z schematem kolorów, typografią i kształtami, a następnie zastosujesz go do listy e-mailowej i strony z informacjami aplikacji. Dodasz też do aplikacji obsługę dynamicznych motywów. Po ukończeniu ćwiczenia Twoja aplikacja będzie obsługiwać zarówno kolorowe, jak i dynamiczne motywy.

1357cdbfaaa67721.png

Punkt końcowy kursu programowania dotyczącego motywów z motywami w jasnych kolorach i jasnymi motywami dynamicznymi.

1357cdbfaaa67721.png

Punkt końcowy kursu programowania dotyczącego motywów z motywami w ciemnych kolorach i ciemnymi motywami dynamicznymi.

Czego potrzebujesz

2. Przygotowania

W tym kroku pobierzesz pełny kod aplikacji Reply, który będziesz stylować w tym ćwiczeniu.

Pobierz kod

Kod tego ćwiczenia znajdziesz w repozytorium GitHub android-compose-codelabs. Aby go sklonować, uruchom:

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

Możesz też pobrać 2 pliki ZIP:

Sprawdź przykładową aplikację

Pobrany kod zawiera kod wszystkich dostępnych ćwiczeń z programowania w Compose. Aby ukończyć to ćwiczenie, otwórz projekt ThemingCodelab w Androidzie Studio.

Zalecamy rozpoczęcie od kodu w głównej gałęzi i wykonywanie kolejnych kroków w tempie, które Ci odpowiada. W dowolnym momencie możesz uruchomić dowolną wersję w Androidzie Studio, zmieniając gałąź Git projektu.

Poznaj kod startowy

Główny kod zawiera pakiet interfejsu, który ma te główne pakiety i pliki, z którymi będziesz wchodzić w interakcję:

  • MainActivity.kt – punkt wejścia, w którym uruchamiasz aplikację Odpowiedz.
  • com.example.reply.ui.theme – ten pakiet zawiera motywy, typografię i schematy kolorów. W tym pakiecie dodasz motyw Material.
  • com.example.reply.ui.components – zawiera niestandardowe komponenty aplikacji, takie jak elementy listy, paski aplikacji itp. Do tych komponentów zastosujesz motywy.
  • ReplyApp.kt – to nasza główna funkcja typu „composable”, od której zaczyna się drzewo interfejsu. W tym pliku zastosujesz motywowanie najwyższego poziomu.

To ćwiczenie skupia się na plikach pakietu ui.

3. Motywy Material 3

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 do marki produktu.

Motyw Material 3 obejmuje te podsystemy, które umożliwiają dodanie motywu do aplikacji: schemat kolorów, typografia i kształty. Gdy dostosujesz te wartości, zmiany zostaną automatycznie odzwierciedlone w komponentach M3, których używasz do tworzenia aplikacji. Przyjrzyjmy się każdemu podsystemowi i wdrożmy go w przykładowej aplikacji.

Podsystemy Material Design: kolor, typografia i kształty.

Podsystem Material 3 obejmujący kolory, typografię i kształty.

4. Schematy kolorów

Podstawą schematu kolorów jest zestaw 5 kluczowych kolorów, z których każdy jest powiązany z paletą tonalną składającą się z 13 odcieni używanych przez komponenty Material 3.

5 podstawowych kolorów kluczowych do tworzenia motywu M3.

5 podstawowych kolorów kluczowych do tworzenia motywu M3.

Każdy kolor akcentujący (podstawowy, dodatkowy i trzeciorzędowy) jest następnie dostępny w 4 kompatybilnych kolorach o różnych odcieniach, które można łączyć, aby podkreślać elementy i wyrażać się wizualnie.

4 odcienie kolorów podstawowych, dodatkowych i trzeciorzędowych.

Cztery odcienie kolorów podstawowych, dodatkowych i trzeciorzędowych.

Podobnie kolory neutralne są podzielone na 4 kompatybilne odcienie używane na powierzchniach i w tle. Są one również ważne, aby podkreślić ikony tekstowe umieszczone na dowolnej powierzchni.

4 kolory tonalne z podstawowej palety neutralnych kolorów.

4 kolory tonalne podstawowych kolorów neutralnych.

Dowiedz się więcej o schemacie kolorów i rolach kolorów.

Generowanie schematów kolorów

Możesz utworzyć niestandardową ColorScheme ręcznie, ale często łatwiej jest wygenerować ją za pomocą kolorów źródłowych marki. Możesz to zrobić za pomocą narzędzia Material Theme Builder, a opcjonalnie wyeksportować kod motywu Compose.

Możesz wybrać dowolny kolor, ale w naszym przypadku użyjemy domyślnego podstawowego koloru odpowiedzi #825500. W sekcji Kolory podstawowe po lewej stronie kliknij Kolor podstawowy i dodaj kod w selektorze kolorów.

294f73fc9d2a570e.png

Dodawanie kodu koloru podstawowego w Kreatorze motywów Material.

Po dodaniu koloru podstawowego w narzędziu Material Theme Builder w prawym górnym rogu powinny pojawić się motyw i opcja eksportowania. W tym ćwiczeniu wyeksportujesz motyw w Jetpack Compose.

Kreator motywów Material z opcją eksportowania w prawym górnym rogu.

Kreator motywów Material z opcją eksportowania w prawym górnym rogu.

Kolor podstawowy #825500 generuje ten motyw, który dodasz do aplikacji. Material 3 zapewnia szeroką gamę ról kolorów, które pozwalają elastycznie wyrażać stan, widoczność i znaczenie komponentu.

Wyeksportowano jasny i ciemny schemat kolorów z koloru podstawowego.

Wyeksportowano jasny i ciemny schemat kolorów z koloru podstawowego.

The Color.kt wygenerowany plik zawiera kolory motywu ze wszystkimi rolami zdefiniowanymi zarówno dla jasnego, jak i ciemnego motywu.

Color.kt

package com.example.reply.ui.theme
import androidx.compose.ui.graphics.Color

val md_theme_light_primary = Color(0xFF825500)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
val md_theme_light_onPrimaryContainer = Color(0xFF291800)
val md_theme_light_secondary = Color(0xFF6F5B40)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFFBDEBC)
val md_theme_light_onSecondaryContainer = Color(0xFF271904)
val md_theme_light_tertiary = Color(0xFF51643F)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFD4EABB)
val md_theme_light_onTertiaryContainer = Color(0xFF102004)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFFFBFF)
val md_theme_light_onBackground = Color(0xFF1F1B16)
val md_theme_light_surface = Color(0xFFFFFBFF)
val md_theme_light_onSurface = Color(0xFF1F1B16)
val md_theme_light_surfaceVariant = Color(0xFFF0E0CF)
val md_theme_light_onSurfaceVariant = Color(0xFF4F4539)
val md_theme_light_outline = Color(0xFF817567)
val md_theme_light_inverseOnSurface = Color(0xFFF9EFE7)
val md_theme_light_inverseSurface = Color(0xFF34302A)
val md_theme_light_inversePrimary = Color(0xFFFFB951)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF825500)
val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
val md_theme_light_scrim = Color(0xFF000000)

val md_theme_dark_primary = Color(0xFFFFB951)
val md_theme_dark_onPrimary = Color(0xFF452B00)
val md_theme_dark_primaryContainer = Color(0xFF633F00)
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)
val md_theme_dark_secondary = Color(0xFFDDC2A1)
val md_theme_dark_onSecondary = Color(0xFF3E2D16)
val md_theme_dark_secondaryContainer = Color(0xFF56442A)
val md_theme_dark_onSecondaryContainer = Color(0xFFFBDEBC)
val md_theme_dark_tertiary = Color(0xFFB8CEA1)
val md_theme_dark_onTertiary = Color(0xFF243515)
val md_theme_dark_tertiaryContainer = Color(0xFF3A4C2A)
val md_theme_dark_onTertiaryContainer = Color(0xFFD4EABB)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF1F1B16)
val md_theme_dark_onBackground = Color(0xFFEAE1D9)
val md_theme_dark_surface = Color(0xFF1F1B16)
val md_theme_dark_onSurface = Color(0xFFEAE1D9)
val md_theme_dark_surfaceVariant = Color(0xFF4F4539)
val md_theme_dark_onSurfaceVariant = Color(0xFFD3C4B4)
val md_theme_dark_outline = Color(0xFF9C8F80)
val md_theme_dark_inverseOnSurface = Color(0xFF1F1B16)
val md_theme_dark_inverseSurface = Color(0xFFEAE1D9)
val md_theme_dark_inversePrimary = Color(0xFF825500)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFFFFB951)
val md_theme_dark_outlineVariant = Color(0xFF4F4539)
val md_theme_dark_scrim = Color(0xFF000000)


val seed = Color(0xFF825500)

The Theme.kt wygenerowany plik zawiera konfigurację jasnych i ciemnych schematów kolorów oraz motywu aplikacji. Zawiera też główną funkcję typu „composable” dotyczącą motywów, czyli AppTheme().

Theme.kt

package com.example.reply.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable


private val LightColors = lightColorScheme(
   primary = md_theme_light_primary,
   onPrimary = md_theme_light_onPrimary,
   primaryContainer = md_theme_light_primaryContainer,
   onPrimaryContainer = md_theme_light_onPrimaryContainer,
   secondary = md_theme_light_secondary,
   onSecondary = md_theme_light_onSecondary,
   secondaryContainer = md_theme_light_secondaryContainer,
   onSecondaryContainer = md_theme_light_onSecondaryContainer,
   tertiary = md_theme_light_tertiary,
   onTertiary = md_theme_light_onTertiary,
   tertiaryContainer = md_theme_light_tertiaryContainer,
   onTertiaryContainer = md_theme_light_onTertiaryContainer,
   error = md_theme_light_error,
   errorContainer = md_theme_light_errorContainer,
   onError = md_theme_light_onError,
   onErrorContainer = md_theme_light_onErrorContainer,
   background = md_theme_light_background,
   onBackground = md_theme_light_onBackground,
   surface = md_theme_light_surface,
   onSurface = md_theme_light_onSurface,
   surfaceVariant = md_theme_light_surfaceVariant,
   onSurfaceVariant = md_theme_light_onSurfaceVariant,
   outline = md_theme_light_outline,
   inverseOnSurface = md_theme_light_inverseOnSurface,
   inverseSurface = md_theme_light_inverseSurface,
   inversePrimary = md_theme_light_inversePrimary,
   surfaceTint = md_theme_light_surfaceTint,
   outlineVariant = md_theme_light_outlineVariant,
   scrim = md_theme_light_scrim,
)


private val DarkColors = darkColorScheme(
   primary = md_theme_dark_primary,
   onPrimary = md_theme_dark_onPrimary,
   primaryContainer = md_theme_dark_primaryContainer,
   onPrimaryContainer = md_theme_dark_onPrimaryContainer,
   secondary = md_theme_dark_secondary,
   onSecondary = md_theme_dark_onSecondary,
   secondaryContainer = md_theme_dark_secondaryContainer,
   onSecondaryContainer = md_theme_dark_onSecondaryContainer,
   tertiary = md_theme_dark_tertiary,
   onTertiary = md_theme_dark_onTertiary,
   tertiaryContainer = md_theme_dark_tertiaryContainer,
   onTertiaryContainer = md_theme_dark_onTertiaryContainer,
   error = md_theme_dark_error,
   errorContainer = md_theme_dark_errorContainer,
   onError = md_theme_dark_onError,
   onErrorContainer = md_theme_dark_onErrorContainer,
   background = md_theme_dark_background,
   onBackground = md_theme_dark_onBackground,
   surface = md_theme_dark_surface,
   onSurface = md_theme_dark_onSurface,
   surfaceVariant = md_theme_dark_surfaceVariant,
   onSurfaceVariant = md_theme_dark_onSurfaceVariant,
   outline = md_theme_dark_outline,
   inverseOnSurface = md_theme_dark_inverseOnSurface,
   inverseSurface = md_theme_dark_inverseSurface,
   inversePrimary = md_theme_dark_inversePrimary,
   surfaceTint = md_theme_dark_surfaceTint,
   outlineVariant = md_theme_dark_outlineVariant,
   scrim = md_theme_dark_scrim,
)

@Composable
fun AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
   val colors = if (!useDarkTheme) {
       LightColors
   } else {
       DarkColors
   }

   MaterialTheme(
       colorScheme = colors,
       content = content
   )
}

Podstawowym elementem implementacji motywów w Jetpack Compose jest funkcja kompozycyjna MaterialTheme.

Funkcję kompozycyjną MaterialTheme() umieszczasz w funkcji AppTheme(), która przyjmuje 2 parametry:

  • useDarkTheme – ten parametr jest powiązany z funkcją isSystemInDarkTheme(), która umożliwia obserwowanie ustawień motywu systemu i stosowanie jasnego lub ciemnego motywu. Jeśli chcesz ręcznie ustawić jasny lub ciemny motyw aplikacji, możesz przekazać wartość logiczną do useDarkTheme.
  • content – treść, do której zostanie zastosowany motyw.

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
   val colors = if (!useDarkTheme) {
       LightColors
   } else {
       DarkColors
   }

   MaterialTheme(
       colorScheme = colors,
       content = content
   )
}

Jeśli teraz spróbujesz uruchomić aplikację, powinna wyglądać tak samo. Mimo że zaimportowano nasz nowy schemat kolorów z nowymi kolorami motywu, nadal widzisz podstawowy motyw, ponieważ nie został on zastosowany w aplikacji Compose.

Aplikacja z motywem podstawowym, gdy nie jest zastosowany żaden motyw.

Aplikacja z motywem podstawowym, gdy nie jest zastosowany żaden motyw.

Aby zastosować nowy motyw, w pliku MainActivity.kt umieść główną funkcję typu „composable” ReplyApp w głównej funkcji motywu AppTheme().

MainActivity.kt

setContent {
   val uiState by viewModel.uiState.collectAsStateWithLifecycle()

   AppTheme {
       ReplyApp(/*..*/)
   }
}

Zaktualizujesz też funkcje podglądu, aby zobaczyć motyw zastosowany w podglądach aplikacji. Umieść funkcję kompozycyjną ReplyApp wewnątrz funkcji ReplyAppPreview() z funkcją AppTheme, aby zastosować motyw do podglądów.

W parametrach podglądu masz zdefiniowane zarówno jasny, jak i ciemny motyw systemowy, więc zobaczysz oba podglądy.

MainActivity.kt

@Preview(
   uiMode = Configuration.UI_MODE_NIGHT_YES,
   name = "DefaultPreviewDark"
)
@Preview(
   uiMode = Configuration.UI_MODE_NIGHT_NO,
   name = "DefaultPreviewLight"
)
@Composable
fun ReplyAppPreview() {
   AppTheme {
       ReplyApp(
           replyHomeUIState = ReplyHomeUIState(
               emails = LocalEmailsDataProvider.allEmails
           )
       )
   }
}

Jeśli teraz uruchomisz aplikację, powinny się wyświetlić podglądy aplikacji z zaimportowanymi kolorami motywu zamiast motywu podstawowego.

fddf7b9cc99b1fe3.png be7a661b4553167b.png

Aplikacja z motywem podstawowym (po lewej).

Aplikacja z zaimportowanym motywem kolorystycznym (po prawej)

674cec6cc12db6a0.png

Podglądy aplikacji w trybie jasnym i ciemnym z zaimportowanymi motywami kolorystycznymi.

Material 3 obsługuje zarówno jasne, jak i ciemne schematy kolorów. Aplikacja została tylko opakowana zaimportowanym motywem. Komponenty Material 3 używają domyślnych ról kolorów.

Zanim zaczniesz dodawać kolory do aplikacji, dowiedz się więcej o rolach kolorów i ich zastosowaniu.

Role kolorów i ułatwienia dostępu

Każda rola koloru może być używana w różnych miejscach w zależności od stanu, widoczności i znaczenia komponentu.

1f184a05ea57aa84.png

Role kolorów podstawowych, dodatkowych i trzeciorzędnych.

Podstawowy to kolor bazowy, który jest używany w przypadku głównych komponentów, takich jak wyróżnione przyciski i stany aktywne.

Drugorzędny kolor kluczowy jest używany w mniej widocznych komponentach interfejsu, takich jak elementy filtra.

Kolor kluczowy trzeciorzędny służy do tworzenia kontrastujących akcentów, a kolory neutralne są używane w przypadku tła i powierzchni w aplikacji.

System kolorów Material Design udostępnia standardowe wartości odcieni i pomiary, które można wykorzystać do uzyskania dostępnych współczynników kontrastu. Używaj koloru on-primary na kolorze primary, on-primary-container na kolorze primary-container i tak dalej w przypadku innych kolorów akcentujących i neutralnych, aby zapewnić użytkownikowi kontrast ułatwiający dostępność.

Więcej informacji znajdziesz w artykule Role kolorów i ułatwienia dostępu.

Wysokości tonalne i cieni

Material 3 reprezentuje wysokość głównie za pomocą nakładek kolorów tonalnych. Jest to nowy sposób na odróżnianie od siebie kontenerów i powierzchni – zwiększenie poziomu tonalnego powoduje użycie bardziej wyrazistego odcienia – oprócz cieni.

Wysokość tonalna z wysokością cienia Podniesienie tonalne na poziomie 2, które pobiera kolor z miejsca na kolor podstawowy.

Nakładki z efektem podniesienia w ciemnych motywach zostały też zmienione na nakładki z kolorami tonalnymi w Material Design 3. Kolor nakładki pochodzi z miejsca na kolor podstawowy.

M3 Surface, czyli kompozycja stanowiąca podstawę większości komponentów M3, obsługuje zarówno tonalne, jak i cieniowe podniesienie:

Surface(
   modifier = modifier,
   tonalElevation = {..}
   shadowElevation = {..}
) {
   Column(content = content)
}

Dodawanie kolorów do aplikacji

Jeśli uruchomisz aplikację, zobaczysz wyeksportowane kolory w miejscach, w których komponenty mają domyślne kolory. Teraz, gdy już znamy role kolorów i ich zastosowanie, możemy zastosować w aplikacji odpowiednie role kolorów.

be7a661b4553167b.png

Aplikacja z motywem kolorystycznym i komponentami, które przyjmują domyślne role kolorów.

Kolory powierzchni

Na ekranie głównym zaczniesz od umieszczenia głównego komponentu aplikacji w komponencie Surface(), aby zapewnić bazę, na której będzie można umieścić treść aplikacji. Otwórz MainActivity.kt i otocz funkcję kompozycyjną ReplyApp() elementem Surface.

Podasz też podniesienie tonalne o 5 dp, aby nadać powierzchni kolor tonalny głównego miejsca, co pomoże zapewnić kontrast z elementem listy i paskiem wyszukiwania nad nim. Domyślnie tonalne i cieniowe podniesienie powierzchni wynosi 0 dp.

MainActivity.kt

AppTheme {
   Surface(tonalElevation = 5.dp) {
       ReplyApp(
           replyHomeUIState = uiState,
          // other parameters
         )
   }
}

Jeśli teraz uruchomisz aplikację i zobaczysz zarówno listę, jak i stronę szczegółów, powinna być ona w całości w odcieniach koloru podstawowego.

be7a661b4553167b.png e70d762495173610.png

Tło aplikacji bez koloru powierzchni i odcienia (po lewej).

Tło aplikacji z zastosowanym kolorem powierzchni i kolorem tonalnym (po prawej).

Kolory paska aplikacji

Nasz niestandardowy pasek wyszukiwania u góry nie ma przezroczystego tła, jak wymaga tego projekt. Domyślnie jest to domyślna powierzchnia bazowa. Możesz dodać tło, aby wyraźnie oddzielić od siebie poszczególne elementy.

5779fc399d8a8187.png

Niestandardowy pasek wyszukiwania bez tła (po lewej).

Niestandardowy pasek wyszukiwania z tłem (po prawej).

Teraz możesz edytować element ui/components/ReplyAppBars.kt, który zawiera pasek aplikacji. Dodasz MaterialTheme.colorScheme.background do elementu Modifier elementu kompozycyjnego Row.

ReplyAppBars.kt

@Composable
fun ReplySearchBar(modifier: Modifier = Modifier) {
   Row(
       modifier = modifier
           .fillMaxWidth()
           .padding(16.dp)
           .background(MaterialTheme.colorScheme.background),
       verticalAlignment = Alignment.CenterVertically
   ) {
       // Search bar content
   }
}

Powinna być teraz widoczna wyraźna granica między powierzchnią tonalną a paskiem aplikacji z kolorem tła.

b1b374b801dadc06.png

Pasek wyszukiwania z kolorem tła na powierzchni w odcieniach szarości.

Kolory pływającego przycisku polecenia

70ceac87233fe466.png

Duży przycisk FAB bez zastosowanego motywu (po lewej).

Duży pływający przycisk działania z motywem i kolorem dodatkowym (po prawej).

Na ekranie głównym możesz poprawić wygląd pływającego przycisku polecenia (FAB), aby wyróżniał się jako przycisk wezwania do działania. Aby to zrobić, zastosuj do niego trzeciorzędowy kolor uzupełniający.

W pliku ReplyListContent.kt zaktualizuj containerColor dla przycisku FAB na kolor tertiaryContainer, a kolor treści na onTertiaryContainer, aby zachować dostępność i kontrast kolorów.

ReplyListContent.kt

ReplyInboxScreen(/*..*/) {
// Email list content
  LargeFloatingActionButton(
    containerColor = MaterialTheme.colorScheme.tertiaryContainer,
    contentColor = MaterialTheme.colorScheme.onTertiaryContainer
  ){
   /*..*/   
  }
}

Uruchom aplikację, aby zobaczyć pływający przycisk działania z motywem. W tym ćwiczeniu używasz LargeFloatingActionButton.

Kolory kart

Lista e-maili na ekranie głównym korzysta z komponentu karty. Domyślnie jest to wypełniona karta, która używa koloru wariantu powierzchni jako koloru kontenera, aby zapewnić wyraźne rozróżnienie między kolorem powierzchni a kolorem karty. Biblioteka Compose udostępnia też implementacje funkcji ElevatedCard i OutlinedCard.

Możesz dodatkowo wyróżnić niektóre ważne elementy, podając drugorzędne odcienie kolorów. Zmodyfikujesz ui/components/ReplyEmailListItem.kt, aktualizując kolor kontenera karty za pomocą CardDefaults.cardColors() w przypadku ważnych e-maili:

ReplyEmailListItem.kt

Card(
   modifier =  modifier
       .padding(horizontal = 16.dp, vertical = 4.dp)
       .semantics { selected = isSelected }
       .clickable { navigateToDetail(email.id) },
   colors = CardDefaults.cardColors(
       containerColor = if (email.isImportant)
           MaterialTheme.colorScheme.secondaryContainer
       else MaterialTheme.colorScheme.surfaceVariant
   )
){
  /*..*/   
}

5818200be0b01583.png 9367d40023db371d.png

Wyróżnij element listy za pomocą dodatkowego koloru kontenera na powierzchni tonalnej.

Kolor elementu listy szczegółów

Ekran główny ma teraz motyw. Kliknij dowolny element listy adresów e-mail, aby wyświetlić stronę szczegółów.

7a9ea7cf3e91e9c7.png 79b3874aeca4cd1.png

Domyślna strona szczegółów bez elementu listy z motywem (po lewej).

Element listy szczegółów z zastosowanym motywem tła (po prawej).

Element listy nie ma zastosowanego koloru, dlatego używany jest domyślny tonalny kolor powierzchni. Zastosuj kolor tła do elementu listy, aby go wyróżnić, i dodaj dopełnienie, aby utworzyć odstępy wokół tła.

ReplyEmailThreadItem.kt

@Composable
fun ReplyEmailThreadItem(
   email: Email,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
           .fillMaxWidth()
           .padding(16.dp)
           .background(MaterialTheme.colorScheme.background)
           .padding(20.dp)
    ) {
      // List item content
    }
}

Widzisz, że samo dodanie tła powoduje wyraźne oddzielenie powierzchni tonalnej od elementu listy.

Masz teraz strony główne i szczegółowe z prawidłowymi rolami kolorów i ich zastosowaniem . Zobaczmy, jak Twoja aplikacja może wykorzystywać dynamiczne kolory, aby zapewnić jeszcze bardziej spersonalizowane i spójne środowisko.

5. Dodawanie dynamicznych kolorów w aplikacji

Dynamiczne kolory to kluczowy element Material 3. Algorytm wyodrębnia z tapety użytkownika spersonalizowane kolory, które są stosowane w aplikacjach i interfejsie systemu.

Dynamiczne motywy sprawiają, że aplikacje są bardziej spersonalizowane. Zapewnia też użytkownikom spójność i bezproblemową obsługę dzięki motywowi systemowemu.

Dynamiczne kolory są dostępne na urządzeniach z Androidem 12 lub nowszym. Jeśli dynamiczny kolor jest dostępny, możesz skonfigurować schemat dynamicznych kolorów za pomocą opcji dynamicDarkColorScheme() lub dynamicLightColorScheme(). W przeciwnym razie użyj domyślnego jasnego lub ciemnego ColorScheme.

Zastąp kod funkcji AppTheme w pliku Theme.kt kodem podanym poniżej:

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean =  isSystemInDarkTheme(),
   content: @Composable () -> Unit
) {
   val context = LocalContext.current
   val colors = when {
       (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
           if (useDarkTheme) dynamicDarkColorScheme(context)
           else dynamicLightColorScheme(context)
       }
       useDarkTheme -> DarkColors
       else -> LightColors
   }
   
      MaterialTheme(
       colorScheme = colors,
       content = content
     )
}

fecc63b4c6034236.png

Dynamiczny motyw zaczerpnięty z tapety Androida 13.

Gdy teraz uruchomisz aplikację, zobaczysz dynamiczne motywy zastosowane przy użyciu domyślnej tapety Androida 13.

Możesz też chcieć, aby pasek stanu był dynamicznie stylizowany w zależności od schematu kolorów użytego do motywu aplikacji.

1095e2b2c1ffdc14.png

Aplikacja bez zastosowanego koloru paska stanu (po lewej).

Aplikacja z zastosowanym kolorem paska stanu (po prawej).

Aby zaktualizować kolor paska stanu w zależności od głównego koloru motywu, dodaj kolor paska stanu po wyborze schematu kolorów w funkcji kompozycyjnej AppTheme:

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean =  isSystemInDarkTheme(),
   content: @Composable () -> Unit
) {
 
 // color scheme selection code

 // Add primary status bar color from chosen color scheme.
 val view = LocalView.current
 if (!view.isInEditMode) {
    SideEffect {
        val window = (view.context as Activity).window
        window.statusBarColor = colors.primary.toArgb()
        WindowCompat
            .getInsetsController(window, view)
            .isAppearanceLightStatusBars = useDarkTheme
    }
 }
   
  MaterialTheme(
    colorScheme = colors,
     content = content
   )
}

Po uruchomieniu aplikacji pasek stanu powinien mieć kolor podstawowy. Możesz też wypróbować dynamiczne motywy jasne i ciemne, zmieniając ciemny motyw systemu.

69093b5bce31fd43.png

Dynamiczny jasny (po lewej) i ciemny (po prawej) motyw zastosowany z domyślną tapetą Androida 13.

Do tej pory w aplikacji zostały zastosowane kolory, które poprawiły jej wygląd. Możesz jednak zauważyć, że cały tekst w aplikacji ma ten sam rozmiar, więc możesz teraz dodać typografię do aplikacji.

6. Typografia

W Material Design 3 zdefiniowano skalę typograficzną. Nazewnictwo i grupowanie zostały uproszczone do: wyświetlanie, nagłówek, tytuł, treść i etykieta, z rozmiarami dużym, średnim i małym dla każdego z nich.

999a161dcd9b0ec4.png

Wielkość czcionek Material 3.

Definiowanie typografii

Biblioteka Compose udostępnia klasę M3 Typography, a także istniejące klasy TextStyle i font-related, aby modelować skalę typów Material 3.

Konstruktor Typografia oferuje wartości domyślne dla każdego stylu, więc możesz pominąć wszystkie parametry, których nie chcesz dostosowywać. Więcej informacji znajdziesz w stylach typografii i ich wartościach domyślnych.

W aplikacji użyjesz 5 stylów typograficznych: headlineSmall, titleLarge, bodyLarge, bodyMedium i labelMedium. Style te będą obejmować zarówno ekran główny, jak i ekran szczegółów.

Ekran pokazujący użycie typografii w tytule, etykiecie i stylu tekstu.

Ekran pokazujący użycie typografii w przypadku tytułu, etykiety i stylu tekstu.

Następnie przejdź do pakietu ui/theme i otwórz Type.kt. Dodaj ten kod, aby zamiast wartości domyślnych podać własną implementację niektórych stylów tekstu:

Type.kt

val typography = Typography(
   headlineSmall = TextStyle(
       fontWeight = FontWeight.SemiBold,
       fontSize = 24.sp,
       lineHeight = 32.sp,
       letterSpacing = 0.sp
   ),
   titleLarge = TextStyle(
       fontWeight = FontWeight.Normal,
       fontSize = 18.sp,
       lineHeight = 28.sp,
       letterSpacing = 0.sp
   ),
   bodyLarge = TextStyle(
       fontWeight = FontWeight.Normal,
       fontSize = 16.sp,
       lineHeight = 24.sp,
       letterSpacing = 0.15.sp
   ),
   bodyMedium = TextStyle(
       fontWeight = FontWeight.Medium,
       fontSize = 14.sp,
       lineHeight = 20.sp,
       letterSpacing = 0.25.sp
   ),
   labelMedium = TextStyle(
       fontWeight = FontWeight.SemiBold,
       fontSize = 12.sp,
       lineHeight = 16.sp,
       letterSpacing = 0.5.sp
   )
)

Typografia została zdefiniowana. Aby dodać go do motywu, przekaż go do funkcji kompozycyjnej MaterialTheme() w funkcji AppTheme:

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
  // dynamic theming content

   MaterialTheme(
       colorScheme = colors,
       typography = typography,
       content = content
   )
}

Praca z typografią

Podobnie jak w przypadku kolorów, do stylu typografii bieżącego motywu uzyskasz dostęp za pomocą MaterialTheme.typography. Dzięki temu uzyskasz instancję typografii, która umożliwia używanie wszystkich zdefiniowanych typów pisma w Type.kt.

Text(
   text = "Hello M3 theming",
   style = MaterialTheme.typography.titleLarge
)

Text(
   text = "you are learning typography",
   style = MaterialTheme.typography.bodyMedium
)

Twój produkt prawdopodobnie nie będzie potrzebować wszystkich 15 domyślnych stylów z skali typograficznej Material Design. W tym laboratorium wybrano 5 rozmiarów, a pozostałe pominięto.

Ponieważ nie zastosowano typografii do funkcji kompozycyjnych Text(, domyślnie cały tekst jest wyświetlany w stylu Typography.bodyLarge.

Typografia listy domowej

Następnie zastosuj typografię do funkcji ReplyEmailListItem w ui/components/ReplyEmailListItem.kt, aby odróżnić tytuły od etykiet:

ReplyEmailListItem.kt

Text(
   text = email.sender.firstName,
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = email.createdAt,
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = email.subject,
   style = MaterialTheme.typography.titleLarge,
   modifier = Modifier.padding(top = 12.dp, bottom = 8.dp),
)

Text(
   text = email.body,
   maxLines = 2,
   style = MaterialTheme.typography.bodyLarge,
   overflow = TextOverflow.Ellipsis
)

90645c0765167bb7.png 6c4af2f412c18bfb.png

Ekran główny bez zastosowanej typografii (po lewej).

Ekran główny z zastosowaną typografią (po prawej).

Typografia listy szczegółów

Podobnie dodasz typografię na ekranie szczegółów, aktualizując wszystkie komponenty tekstowe ReplyEmailThreadItem w ui/components/ReplyEmailThreadItem.kt:

ReplyEmailThreadItem.kt

Text(
   text = email.sender.firstName,
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = stringResource(id = R.string.twenty_mins_ago),
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = email.subject,
   style = MaterialTheme.typography.bodyMedium,
   modifier = Modifier.padding(top = 12.dp, bottom = 8.dp),
)

Text(
   text = email.body,
   style = MaterialTheme.typography.bodyLarge,
   color = MaterialTheme.colorScheme.onSurfaceVariant
)

543ac09e43d8761.png 3412771e95a45f36.png

Ekran szczegółów bez zastosowanej typografii (po lewej).

Ekran szczegółów z zastosowaną typografią (po prawej).

Dostosowywanie typografii

W przypadku funkcji Tworzenie wiadomości bardzo łatwo jest dostosować styl tekstu lub podać własną czcionkę. Możesz zmodyfikować TextStyle, aby dostosować rodzaj czcionki, rodzinę czcionek, odstępy między literami itp.

Styl tekstu zmienisz w pliku theme/Type.kt, co będzie widoczne we wszystkich komponentach, które go używają.

Zaktualizuj fontWeight na SemiBold i lineHeight na 32.sp w przypadku titleLarge, który jest używany jako temat elementu listy. Dzięki temu temat będzie bardziej wyeksponowany, a poszczególne sekcje będą wyraźnie oddzielone.

Type.kt

...
titleLarge = TextStyle(
   fontWeight = FontWeight.SemiBold,
   fontSize = 18.sp,
   lineHeight = 32.sp,
   letterSpacing = 0.0.sp
),
...

f8d2212819eb0b61.png

Stosowanie niestandardowej typografii do tekstu tematu.

7. Kształty

Powierzchnie materiałów mogą być wyświetlane w różnych kształtach. Kształty przyciągają uwagę, identyfikują komponenty, informują o stanie i wyrażają markę.

Definiowanie kształtów

Klasa Compose udostępnia rozszerzone parametry Shapes, które umożliwiają wdrażanie nowych kształtów M3. Skala kształtów M3, podobnie jak skala typograficzna, umożliwia stosowanie w interfejsie szerokiej gamy kształtów.

Na skali kształtów są dostępne różne rozmiary:

  • Bardzo mały
  • mały,
  • średni,
  • Duży
  • Bardzo duży

Domyślnie każdy kształt ma wartość domyślną, którą można zastąpić. W przypadku aplikacji użyjesz średniego kształtu, aby zmodyfikować element listy, ale możesz też zadeklarować inne kształty. Utwórz nowy plik o nazwie Shape.kt w pakiecie ui/theme i dodaj kod kształtów:

Shape.kt

package com.example.reply.ui.theme

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp

val shapes = Shapes(
   extraSmall = RoundedCornerShape(4.dp),
   small = RoundedCornerShape(8.dp),
   medium = RoundedCornerShape(16.dp),
   large = RoundedCornerShape(24.dp),
   extraLarge = RoundedCornerShape(32.dp)
)

Po zdefiniowaniu shapes przekaż go do MaterialTheme M3, tak jak w przypadku kolorów i typografii:

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
  // dynamic theming content

   MaterialTheme(
       colorScheme = colors,
       typography = typography,
       shapes = shapes
       content = content
   )
}

Praca z kształtami

Podobnie jak w przypadku kolorów i typografii, możesz stosować kształty do komponentów Material za pomocą MaterialTheme.shape, co daje Ci instancję Shape umożliwiającą dostęp do kształtów Material.

Wiele komponentów Material ma już zastosowane domyślne kształty, ale możesz udostępniać i stosować własne kształty do komponentów za pomocą dostępnych slotów.

Card(shape = MaterialTheme.shapes.medium) { /* card content */ }
FloatingActionButton(shape = MaterialTheme.shapes.large) { /* fab content */}

Domyślne wartości kształtów dla wszystkich komponentów Material 3. *Mapowanie komponentów Material Design przy użyciu różnych typów kształtów.*

Mapowanie kształtów wszystkich komponentów znajdziesz w dokumentacji dotyczącej kształtów.

Dostępne są jeszcze 2 kształty – RectangleShape i CircleShape – które są częścią funkcji Twórz. Prostokąt nie ma zaokrąglonych krawędzi, a okrąg ma w pełni zaokrąglone krawędzie.

Możesz też zastosować kształt do komponentów za pomocą Modifiers, które przyjmują kształty, np. Modifier.clip, Modifier.background i Modifier.border.

Kształt paska aplikacji

Chcemy, aby pasek aplikacji miał zaokrąglone rogi:

f873392abe535494.png

Element TopAppBar używa elementu Row z kolorem tła. Aby uzyskać tło z zaokrąglonymi rogami, zdefiniuj kształt tła, przekazując wartość CircleShape do modyfikatora tła:

ReplyAppBars.kt

@Composable
fun ReplySearchBar(modifier: Modifier = Modifier) {
   Row(
       modifier = modifier
           .fillMaxWidth()
           .padding(16.dp)
           .background(
               MaterialTheme.colorScheme.background,
               CircleShape
           ),
       verticalAlignment = Alignment.CenterVertically
   ) {
       // Search bar content
   }
}

f873392abe535494.png

Kształt elementu listy szczegółów

Na ekranie głównym używasz karty, która domyślnie korzysta z Shape.Medium. Na stronie szczegółów użyto jednak kolumny z kolorem tła. Aby lista wyglądała jednolicie, zastosuj do niej średni kształt.

3412771e95a45f36.png 80ee881c41a98c2a.png

Kolumna elementu listy szczegółowej bez kształtu na elemencie listy (po lewej) i ze średnim kształtem na liście (po prawej).

ReplyEmailThreadItem.kt

@Composable
fun ReplyEmailThreadItem(
   email: Email,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
           .fillMaxWidth()
           .padding(8.dp)
           .background(
               MaterialTheme.colorScheme.background,
               MaterialTheme.shapes.medium
           )
           .padding(16.dp)

   ) {
      // List item content
      
   }
}

Po uruchomieniu aplikacji zobaczysz szczegółową listę elementów ekranu w kształcie medium.

8. Wyróżnienie

Wyróżnienie w interfejsie pomaga podkreślić niektóre treści, np. gdy chcesz odróżnić tytuł od napisów. W M3 nacisk jest kładziony na warianty kolorów i ich kombinacje z kolorem „on-color”. Możesz to zrobić na 2 sposoby:

  1. Używanie kolorów powierzchni, wariantu powierzchni i tła wraz z kolorami na powierzchni i na wariantach powierzchni z rozszerzonego systemu kolorów M3.

Na przykład koloru powierzchni można używać z kolorem wariantu powierzchni, a koloru wariantu powierzchni z kolorem powierzchni, aby zapewnić różne poziomy wyróżnienia.

Warianty powierzchni można też stosować z kolorami uzupełniającymi, aby uzyskać mniejsze wyróżnienie niż w przypadku kolorów akcentujących, ale nadal zachować dostępność i współczynnik kontrastu.

Kolory powierzchni, tła i wariantów powierzchni.

Kolory powierzchni, tła i wariantów powierzchni.

  1. Używanie różnych grubości czcionki w tekście. Jak widać w sekcji dotyczącej typografii, możesz podać niestandardowe grubości w skali typograficznej, aby uzyskać różne efekty.

Następnie zaktualizuj ReplyEmailListItem.kt, aby zapewnić różnicę w wyróżnieniu za pomocą wariantu powierzchni. Domyślnie treść karty przyjmuje domyślny kolor treści w zależności od tła.

Zaktualizujesz kolor tekstu godziny i tekstu głównego do onSurfaceVariant. Zmniejsza to jego znaczenie w porównaniu z onContainerColors, które jest domyślnie stosowane do elementów kompozycyjnych tekstu tematu i tytułu.

2c9b7f2bd016edb8.png 6850ff391f21e4ba.png

Czas i tekst o takim samym znaczeniu jak temat i tytuł (po lewej).

Czas i treść z mniejszym naciskiem w porównaniu z tematem i tytułem (po prawej).

ReplyEmailListItem.kt

Text(
   text = email.createdAt,
   style = MaterialTheme.typography.labelMedium,
   color = MaterialTheme.colorScheme.onSurfaceVariant
)

Text(
   text = email.body,
   maxLines = 2,
   style = MaterialTheme.typography.bodyLarge,
   color = MaterialTheme.colorScheme.onSurfaceVariant
   overflow = TextOverflow.Ellipsis
)

W przypadku karty ważnego e-maila z tłem secondaryContainer domyślnym kolorem całego tekstu jest kolor onSecondaryContainer. W przypadku pozostałych e-maili tło jest surfaceVariant,, więc cały tekst ma domyślnie kolor onSurfaceVariant.

9. Gratulacje

Gratulacje! Udało Ci się ukończyć to ćwiczenie w Codelabs. Wdrożono motywy Material w Compose, używając kolorów, typografii i kształtów wraz z dynamicznymi kolorami, aby dostosować motyw aplikacji i zapewnić spersonalizowane wrażenia.

2d8fcabf15ac5202.png 5a4d31db0185dca6.png ce009e4ce560834d.png

Koniec wyników motywu z zastosowanymi dynamicznymi kolorami i motywem kolorystycznym.

Co dalej?

Zapoznaj się z innymi ćwiczeniami z programowania na ścieżce Compose:

Więcej informacji

Przykładowe aplikacje

Dokumentacja