Motywy w Compose with Material 3

1. Wprowadzenie

Z tego ćwiczenia dowiesz się, jak stosować motywy w aplikacjach napisanych w 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 dostosować motyw aplikacji w spersonalizowany i dostępny sposób.

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

Czego się nauczysz

Z tego ćwiczenia dowiesz się:

  • Najważniejsze aspekty motywów Material 3
  • Schematy kolorów Material 3 i generowanie motywów 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 nadamy motyw aplikacji klienta poczty e-mail o nazwie Odpowiedz. 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 szczegółów aplikacji. Dodasz też do aplikacji obsługę dynamicznych motywów. Po ukończeniu tego ćwiczenia będziesz mieć w aplikacji obsługę zarówno kolorów, jak i dynamicznych motywów.

1357cdbfaaa67721.png

Punkt końcowy ćwiczenia w Codelabs dotyczącego motywów z jasnym motywem kolorystycznym i jasnym motywem dynamicznym.

1357cdbfaaa67721.png

Punkt końcowy ćwiczenia w Codelabs dotyczącego motywów z motywami w ciemnych kolorach i ciemnymi motywami dynamicznymi.

Co będzie potrzebne

2. Przygotowania

W tym kroku pobierzesz pełny kod aplikacji Reply, którą 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 zakresu 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 ćwiczenia w Codelabs 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 Design.
  • 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”, w 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 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 3 obejmuje te podsystemy, które umożliwiają dodanie motywu do aplikacji: schemat kolorów, typografiakształ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 uzupełniają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 uzupełniających.

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 kolorów neutralnych.

Cztery tonalne kolory 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 z Twojej 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 głównego 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 oferuje szeroką gamę ról kolorów, które pozwalają elastycznie określać 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” do tworzenia 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.

Element kompozycyjny 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 nowy schemat kolorów z nowymi kolorami motywu, nadal widzisz podstawowy motyw, ponieważ nie został on zastosowany w aplikacji Compose.

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

Aplikacja z podstawowym motywem, 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 w 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 jasnym i ciemnym motywie z zaimportowanymi motywami kolorystycznymi.

Material 3 obsługuje zarówno jasne, jak i ciemne schematy kolorów. Aplikacja została tylko opakowana zaimportowanym motywem, a 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ędowych.

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.

Wzniesienia tonalne i cieniowe

Material 3 przedstawia wysokość głównie za pomocą nakładek z kolorami tonalnymi. Jest to nowy sposób na odróżnianie od siebie kontenerów i platform – 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 wysokości w ciemnych motywach również zostały zmienione na nakładki w kolorach tonalnych w Material Design 3. Kolor nakładki pochodzi z miejsca na kolor podstawowy.

Powierzchnia M3 – kompozycja bazowa 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 znamy już 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 kompozycyjnego aplikacji w 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 wartości 5 dp, aby nadać powierzchni kolor tonalny podstawowego miejsca, co pomoże zapewnić kontrast z elementem listy i paskiem wyszukiwania znajdującym się nad nim. Domyślnie podniesienie tonalne i cieni dla 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 stronę listy, jak i stronę szczegółów, zobaczysz, że tonalna powierzchnia jest zastosowana do całej aplikacji.

be7a661b4553167b.png e70d762495173610.png

Tło aplikacji bez powierzchni i koloru tonalnego (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 ona zastępowana domyślną powierzchnią bazową. 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 komponentu RowModifier.

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

Powinno być teraz widać wyraźne oddzielenie powierzchni tonalnej od paska 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 przycisk typu FAB z motywem w kolorze 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 przycisku typu FAB na kolor tertiaryContainer, a kolor treści na onTertiaryContainer, aby zachować ułatwienia dostępu i kontrast kolorów.

ReplyListContent.kt

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

Uruchom aplikację, aby zobaczyć, jak wygląda przycisk typu FAB 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 oddzielenie powierzchni od koloru karty. Compose udostępnia też implementacje ElevatedCardOutlinedCard.

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ą koloru dodatkowego kontenera na powierzchni tonalnej.

Kolor elementu listy szczegółów

Ekran główny ma teraz motyw. Aby wyświetlić stronę z informacjami, kliknij dowolny element listy e-mailowej.

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 do niego dopełnienie, aby utworzyć odstęp 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
    }
}

Widać, że samo podanie 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 . Zobacz, 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 spersonalizowane kolory z tapety użytkownika, aby zastosować je w aplikacjach i interfejsie systemu.

Dynamiczne motywy sprawiają, że aplikacje są bardziej spersonalizowane. Zapewnia też użytkownikom spójne i bezproblemowe korzystanie z motywu systemowego.

Dynamiczne kolory są dostępne na Androidzie 12 i nowszych. Jeśli dynamiczny kolor jest dostępny, możesz skonfigurować schemat dynamicznych kolorów za pomocą 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 zastosowany dynamiczny motyw z użyciem 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 zastosowano w aplikacji 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ć do niej typografię.

6. Typografia

W Material Design 3 zdefiniowano skalę typów. Nazewnictwo i grupowanie zostały uproszczone do: reklama displayowa, nagłówek, tytuł, tekst i etykieta, z rozmiarami dużym, średnim i małym dla każdego z nich.

999a161dcd9b0ec4.png

Wielkość czcionek Material 3.

Określanie typografii

Biblioteka Compose udostępnia klasę M3 Typography – wraz z dotychczasowymi klasami TextStylefont-related – do modelowania skali 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 artykule o stylach typograficznych i ich wartościach domyślnych.

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

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

Ekran prezentują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() wewnątrz 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 czcionek 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 ćwiczeniu 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 głównej

Następnie zastosuj typografię do funkcji ReplyEmailListItemui/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 funkcje kompozycyjne tekstu 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 Compose 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 SemiBoldlineHeight na 32.sp w przypadku titleLarge, który jest używany jako temat elementu listy. Dzięki temu temat będzie bardziej wyeksponowany i wyraźnie oddzielony od innych treści.

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

Biblioteka Compose udostępnia klasę Shapes z rozszerzonymi parametrami, które umożliwiają implementowanie 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ą różne rozmiary kształtów:

  • 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 M3 MaterialTheme, 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, kształty możesz stosować 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 podać i zastosować 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 – RectangleShapeCircleShape – które są częścią funkcji Compose. 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 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 uzyskiwany za pomocą różnych odcieni koloru i jego kombinacji z kolorem tekstu. 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 uzyskać 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 określić 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 funkcji kompozycyjnych 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ślnie cały tekst ma 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