使用 Jetpack Compose 進行主題設定

1. 簡介

在本程式碼研究室中,您將瞭解如何透過使用 Jetpack Compose 的主題設定 API,打造應用程式樣式。我們會說明如何自訂顏色、形狀和字型排版,為整個應用程式打造一致的風格,並支援淺色和深色等多個主題。

學習目標

在本程式碼研究室,您將學到:

  • Material Design 的入門介紹,以及如何根據品牌需求進行自訂
  • Compose 實作 Material Design 系統的方式
  • 如何在應用程式中定義及使用顏色、字體排版和形狀
  • 如何設定元件樣式
  • 如何支援淺色和深色主題

建構目標

在本程式碼研究室中,我們會為新聞閱讀應用程式設定樣式,先從還沒有樣式的應用程式著手,然後再運用所學知識來打造應用程式主題,並支援深色主題。

新聞閱讀應用程式 Jetnews 套用樣式前的圖片。

新聞閱讀應用程式 Jetnews 套用樣式後的圖片。

這個圖片將顯示 Jetnews,一個以深色主題設計的新聞閱讀應用程式。

變更前:未設定樣式的應用程式

變更後:樣式化應用程式

變更後:深色主題

必要條件

2. 開始設定

在這個步驟中,您將下載適用的程式碼,其中包含我們要設定樣式的簡易新聞閱讀器應用程式。

軟硬體需求

下載程式碼

如果您已安裝 Git,只要執行下列指令即可。如要檢查 Git 是否已安裝完成,請在終端機或指令列中輸入 git --version,並確認該指令可正確執行。

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

如果您沒有 Git,可以點選下方按鈕,下載這個程式碼研究室的所有程式碼:

在 Android Studio 中開啟專案,依序選取「File」>「Import Project」,然後前往 ThemingCodelabM2 目錄。

這項專案含有三個主要套件:

  • com.codelab.theming.data 包含模型類別和範例資料。在本程式碼研究室中,這個套件不需要編輯。
  • com.codelab.theming.ui.start 這是此程式碼研究室的起點,您應該在這個套件中進行本程式碼研究室中的所有變更。
  • com.codelab.theming.ui.finish 這是程式碼研究室的結束狀態,供您參考。

建立應用程式並加以執行

應用程式有 2 個執行設定,代表程式碼研究室的開始和結束狀態。選取設定並按下執行按鈕,即可將程式碼部署至裝置或模擬器。

a43ae3c4fa75836e.png

這個應用程式也具有 Compose 版面配置預覽。在 startfinish 套件中瀏覽至 Home.kt,並開啟設計檢視畫面,其中會顯示幾個預覽畫面,方便您在 UI 程式碼中快速疊代:

758a285ad8a6cd51.png

3. Material Design 主題設定

Jetpack Compose 提供 Material Design 這項產品,這是用於建立數位介面的全方位設計系統。Material Design 元件 (按鈕、資訊卡、切換鈕等) 是以 Material Design 主題設定為基礎建構而成,能夠以系統化的方式自訂 Material Design,更準確地反映產品品牌。Material Design 主題由顏色字體排版形狀屬性構成。自訂這些元素會自動反映在您建構應用程式的元件中。

對 Material Design 主題設定有所掌握,有助於瞭解如何為 Jetpack Compose 應用程式設定主題,以下我們將簡短說明相關概念。如果您已熟悉 Material Design 主題設定,可以選擇略過。

顏色

Material Design 定義了一些按照語意命名的顏色,可用於整個應用程式:

62ccfe5761fd9eda.png

主要顏色為主要品牌顏色,次要顏色則用於提供強調色。您可以為對比的區域提供顏色更深/更淺的變化版本。背景和表面顏色可用於元件所在的容器,這些元件理論上位於應用程式中的「表面」。Material Design 也定義了「上方」顏色,這些顏色用於任一已命名色彩上方的內容,例如「表面」彩色容器中的文字顏色應為在「表面上方」的顏色。Material Design 元件會設為使用這些主題顏色,例如,懸浮動作按鈕預設為 secondary資訊卡預設為 surface

定義已命名顏色後,應用程式即可提供替代調色盤,例如淺色和深色主題:

1a9b78141ddfa87b.png

我們也鼓勵您定義小型的調色盤,並在整個應用程式中採用一致的調色盤。Material Design 色彩工具可協助您挑選顏色並建立調色盤,甚至確保相應組合可供存取。

字體排版

同樣地,Material Design 會定義許多依語意命名的字體樣式:

1d44de3ff2f7fd1c.png

雖然您可能不會依主題更改字體樣式,但使用字體比例調整功能可以提高應用程式的一致性。如果您提供自己的字型和其他字體自訂功能,這些設定會反映在應用程式所用的 Material Design 元件中。舉例來說,應用程式列預設使用 h6 樣式,按鈕則使用 button。Material Design 的字體比例產生器可協助您建構字體比例。

形狀

Material Design 支援系統性地運用形狀來呈現您的品牌。可定義的類別分為 3 種:小型、中型和大型元件;而每種類別都可以定義要使用的形狀,並自訂邊角樣式 (直角或圓角) 和大小。

886b811cc9cad18e.png

當您自訂形狀主題後,相應設定會反映在許多元件上。舉例來說,根據預設,按鈕文字欄位會使用小型形狀主題,資訊卡對話方塊使用中型形狀主題,試算表則使用大型形狀主題。如要全盤瞭解元件對應的形狀主題,請按這裡。Material Design 的形狀自訂工具可協助您產生形狀主題。

基準

Material Design 預設採用「基準」主題,也就是紫色的色彩配置、Roboto 字體比例,以及上圖所示的略微圓角形狀。如未指定或自訂主題,元件將使用基準主題。

4. 定義主題

MaterialTheme

在 Jetpack Compose 中實作主題設定的核心元素是 MaterialTheme 可組合函式。將這個可組合函式放入可組合階層中,您就能指定自訂元件中所有元件的色彩、類型和形狀。以下是在程式庫中定義這個可組合函式的方式:

@Composable
fun MaterialTheme(
    colors: Colors,
    typography: Typography,
    shapes: Shapes,
    content: @Composable () -> Unit
) { ...

您之後可以使用 MaterialTheme object 擷取傳送至這個可組合函式的參數,以顯示 colorstypographyshapes 屬性。我們稍後會進一步說明相關細節。

開啟 Home.kt 並找出 Home 可組合函式,這是應用程式的主要進入點。請注意,在宣告 MaterialTheme 時,我們不會指定任何參數,因此會接收預設的「基準」樣式:

@Composable
fun Home() {
  ...
  MaterialTheme {
    Scaffold(...

讓我們建立顏色、類型和形狀參數,以便為應用程式實作主題。

新建主題

如要集中管理樣式,建議您自行建立用於納入及設定 MaterialTheme 的可組合函式。如此一來,您就能在單一位置指定主題自訂項目,並在各處 (例如多個螢幕或 @Preview) 輕鬆重複使用。您可以視需要建立多個主題可組合函式,例如針對應用程式的不同部分支援不同樣式。

com.codelab.theming.ui.start.theme 套件中,建立名為 Theme.kt 的新檔案。新增名為 JetnewsTheme 的可組合函式,接受其他可組合函式做為內容,並納入 MaterialTheme

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(content = content)
}

現在切換回 Home.kt 並使用 JetnewsTheme 取代 MaterialTheme (然後匯入):

-  MaterialTheme {
+  JetnewsTheme {
    ...

這個畫面的 @Preview 中暫時不會有任何變更。更新 PostItemPreviewFeaturedPostPreview,將其內容納入新的 JetnewsTheme 可組合函式,讓預覽畫面使用新的主題:

@Preview("Featured Post")
@Composable
private fun FeaturedPostPreview() {
  val post = remember { PostRepo.getFeaturedPost() }
+ JetnewsTheme {
    FeaturedPost(post = post)
+ }
}

顏色

以下是我們要在應用程式中實作的調色盤 (目前只是淺色主題的調色盤,我們很快就會回來設定,讓調色盤支援深色主題):

b2635ed3ec4bfc8f.png

Compose 中的顏色是使用 Color 類別定義。藉由多個不同的建構函式,您可將顏色指定為 ULong,或按個別的顏色管道指定。

theme 套件中建立新檔案 Color.kt。請在這個檔案中新增下列顏色做為頂層公開屬性:

val Red700 = Color(0xffdd0d3c)
val Red800 = Color(0xffd00036)
val Red900 = Color(0xffc20029)

我們定義了應用程式顏色後,現在要把它們合併到 MaterialTheme 所需的 Colors 物件中,進而將特定顏色指派至 Material Design 中的已命名顏色。請切換回 Theme.kt,並新增以下內容:

private val LightColors = lightColors(
    primary = Red700,
    primaryVariant = Red900,
    onPrimary = Color.White,
    secondary = Red700,
    secondaryVariant = Red900,
    onSecondary = Color.White,
    error = Red800
)

此處使用 lightColors 函式建構 Colors,這能提供合理的預設值,因此我們不必指定 Material Design 調色盤中的所有顏色。舉例來說,您可以發現我們並未指定 background 顏色或許多「上方」顏色,而是使用預設值。

現在,我們要在應用程式中使用這些顏色。只要更新 JetnewsTheme 可組合函式,就能使用新的 Colors

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
+   colors = LightColors,
    content = content
  )
}

開啟「Home.kt」並重新整理預覽畫面。請注意,新的色彩配置會反映在 TopAppBar 等元件中。

字體排版

我們想要在應用程式中實作的字體比例如下:

54c420f78529b77d.png

在 Compose 中,我們可以定義 TextStyle 物件,藉此定義設定部分文字樣式所需的資訊。屬性範例:

data class TextStyle(
    val color: Color = Color.Unset,
    val fontSize: TextUnit = TextUnit.Inherit,
    val fontWeight: FontWeight? = null,
    val fontStyle: FontStyle? = null,
    val fontFamily: FontFamily? = null,
    val letterSpacing: TextUnit = TextUnit.Inherit,
    val background: Color = Color.Unset,
    val textAlign: TextAlign? = null,
    val textDirection: TextDirection? = null,
    val lineHeight: TextUnit = TextUnit.Inherit,
    ...
)

我們期望的類型調整功能使用 Montserrat 來處理標題,使用 Domine 來處理內文。相關字型檔案已新增至專案的 res/fonts 資料夾中。

theme 套件中建立新檔案 Typography.kt。首先,定義 FontFamily (結合每個 Font 的不同權重):

private val Montserrat = FontFamily(
    Font(R.font.montserrat_regular),
    Font(R.font.montserrat_medium, FontWeight.W500),
    Font(R.font.montserrat_semibold, FontWeight.W600)
)

private val Domine = FontFamily(
    Font(R.font.domine_regular),
    Font(R.font.domine_bold, FontWeight.Bold)
)

現在,建立 MaterialTheme 可接受的 Typography 物件,為比例中的每個語意樣式指定 TextStyle

val JetnewsTypography = Typography(
    h4 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 30.sp
    ),
    h5 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 24.sp
    ),
    h6 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 20.sp
    ),
    subtitle1 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    ),
    subtitle2 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    body1 = TextStyle(
        fontFamily = Domine,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    ),
    body2 = TextStyle(
        fontFamily = Montserrat,
        fontSize = 14.sp
    ),
    button = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    caption = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp
    ),
    overline = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 12.sp
    )
)

開啟 Theme.kt 並更新 JetnewsTheme 可組合函式,使用新的 Typography

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
+   typography = JetnewsTypography,
    content = content
  )
}

開啟 Home.kt 並重新整理預覽畫面,查看新的字體排版效果。

形狀

我們想運用形狀,在應用程式中呈現品牌特色。具體來說,我們要在多項元素上使用直角形狀,如下所示:

9b60c78a78c61570.png

Compose 提供 RoundedCornerShapeCutCornerShape 類別,可用來定義形狀主題。

theme 套件中建立新檔案 Shape.kt,並新增下列指令:

val JetnewsShapes = Shapes(
    small = CutCornerShape(topStart = 8.dp),
    medium = CutCornerShape(topStart = 24.dp),
    large = RoundedCornerShape(8.dp)
)

開啟 Theme.kt 並更新 JetnewsTheme 可組合函式,使用這些 Shapes

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
    typography = JetnewsTypography,
+   shapes = JetnewsShapes,
    content = content
  )
}

開啟 Home.kt 並重新整理預覽畫面,查看顯示精選貼文的 Card 如何反映新套用的形狀主題。

深色主題

在應用程式中支援深色主題,不僅有助於應用程式與使用者裝置整合 (從 Android 10 以上版本開始提供通用深色主題切換功能),還能降低耗電量及滿足無障礙需求。如要瞭解如何建立深色主題,請參考 Material Design 提供的設計指南。以下是我們想為深色主題實作的替代調色盤:

21768b33f0ccda5f.png

開啟 Color.kt 並新增下列顏色:

val Red200 = Color(0xfff297a2)
val Red300 = Color(0xffea6d7e)

現在,請開啟 Theme.kt 並新增:

private val DarkColors = darkColors(
    primary = Red300,
    primaryVariant = Red700,
    onPrimary = Color.Black,
    secondary = Red300,
    onSecondary = Color.Black,
    error = Red200
)

現在更新 JetnewsTheme

@Composable
fun JetnewsTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  MaterialTheme(
+   colors = if (darkTheme) DarkColors else LightColors,
    typography = JetnewsTypography,
    shapes = JetnewsShapes,
    content = content
  )
}

我們在這裡新增了新的參數以使用深色主題,並預設為查詢裝置以尋找 全域設定。這提供了良好的預設值,但如果想讓特定畫面永遠/永不顯示為深色,或者設定深色主題為 @Preview,還是可以輕鬆覆寫。

開啟 Home.kt,為 FeaturedPost 可組合函式建立新的預覽畫面,並以深色主題顯示:

@Preview("Featured Post • Dark")
@Composable
private fun FeaturedPostDarkPreview() {
    val post = remember { PostRepo.getFeaturedPost() }
    JetnewsTheme(darkTheme = true) {
        FeaturedPost(post = post)
    }
}

重新整理預覽窗格,即可查看深色主題預覽。

84f93b209ce4fd46.png

5. 使用色彩

在上一個步驟中,我們學到了如何自行建立主題,為應用程式設定顏色、字體樣式和形狀。所有 Material Design 元件都能直接使用這些自訂項目。舉例來說,FloatingActionButton 可組合函式預設會使用主題中的 secondary 顏色,但您可以為這個參數指定不同的值,設定替代顏色:

@Composable
fun FloatingActionButton(
  backgroundColor: Color = MaterialTheme.colors.secondary,
  ...
) {

您不一定會使用預設設定。本節將說明如何在應用程式中使用色彩。

原始色彩

如先前所述,Compose 提供 Color 類別。您可以在本機建立這些類別,並將其存放於 object 等元素中:

Surface(color = Color.LightGray) {
  Text(
    text = "Hard coded colors don't respond to theme changes :(",
    textColor = Color(0xffff00ff)
  )
}

Color 提供多種實用的方法,例如 copy 可讓您以不同 Alpha/紅色/綠色/藍色值建立新的顏色。

主題顏色

更靈活的做法是從主題中擷取顏色:

Surface(color = MaterialTheme.colors.primary)

這裡,我們使用 MaterialTheme object,其中 colors 屬性會傳回 MaterialTheme 可組合函式中設定的 Colors。也就是說,只要為主題提供不同的顏色組合,我們無需變更應用程式程式碼,就能支援不同的外觀和風格樣式。例如 AppBar 使用 primary 顏色,螢幕背景為 surface,當我們變更主題顏色後,相關設定就會反映在這些可組合函式中:

b0b0ca02b52453a7.png

253ab041d7ea904e.png

由於主題中的每種顏色都是 Color 例項,我們也可以使用 copy 方法輕鬆「衍生」顏色:

val derivedColor = MaterialTheme.colors.onSurface.copy(alpha = 0.1f)

這裡,我們要建立 onSurface 顏色的副本,但不透明度為 10%。這種做法可確保顏色在不同主題下正常顯示,而無需以硬式編碼寫入靜態顏色。

表面和內容顏色

許多元件都接受一組顏色和「內容顏色」:

Surface(
  color: Color = MaterialTheme.colors.surface,
  contentColor: Color = contentColorFor(color),
  ...

TopAppBar(
  backgroundColor: Color = MaterialTheme.colors.primarySurface,
  contentColor: Color = contentColorFor(backgroundColor),
  ...

這不僅可讓您設定可組合函式的色彩,還能為「內容」(例如其中的可組合函式) 提供預設顏色。許多可組合函式會預設使用這個內容顏色,例如 Text 顏色或 Icon 色調。contentColorFor 方法會針對任一主題顏色擷取適當的「上方」顏色。例如,如果設定了 primary 背景,此方法會傳回 onPrimary 做為內容顏色。如果您設定了非主題背景顏色,請務必自行提供合理的內容顏色。

Surface(color = MaterialTheme.colors.primary) {
  Text(...) // default text color is 'onPrimary'
}
Surface(color = MaterialTheme.colors.error) {
  Icon(...) // default tint is 'onError'
}

您可以使用 LocalContentColor CompositionLocal 擷取與目前背景形成對比的顏色:

BottomNavigationItem(
  unselectedContentColor = LocalContentColor.current ...

設定任何元素的顏色時,建議您使用 Surface,這樣做可設定合適的內容顏色 CompositionLocal 值。請注意直接呼叫 Modifier.background 並不會設定適當的內容顏色。

-Row(Modifier.background(MaterialTheme.colors.primary)) {
+Surface(color = MaterialTheme.colors.primary) {
+  Row(
...

目前,Header 元件一律具有 Color.LightGray 背景。在淺色主題中看起來很正常,但與深色主題中的背景對比度高。他們也未指定特定的文字顏色,因此沿用目前可能不會與背景形成對比的顏色作為內容顏色:

7329ac6ead5097eb.png

接著就來解決這個問題。在 Home.ktHeader 可組合函式中,移除指定硬式編碼顏色的 background 修飾符。請改為將 Text 納入包含主題衍生顏色的 Surface 中,並指定內容應設為 primary 顏色:

+ Surface(
+   color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
+   contentColor = MaterialTheme.colors.primary,
+   modifier = modifier
+ ) {
  Text(
    text = text,
    modifier = Modifier
      .fillMaxWidth()
-     .background(Color.LightGray)
      .padding(horizontal = 16.dp, vertical = 8.dp)
  )
+ }

內容 Alpha 值

我們往往會想強調或淡化內容,藉此傳達重要資訊,並在視覺上提供不同的層次感。Material Design 建議採用不同程度的不透明度,以便傳達不同的重要性等級。

Jetpack Compose 會透過 LocalContentAlpha 執行這項工作。您可以為這個 CompositionLocal 提供值,從而為階層指定內容 Alpha 值。子項可組合函式可以使用這個值,例如:TextIcon 預設會使用調整為使用 LocalContentAlpha 後的 LocalContentColor 組合。Material Design 會指定某些標準 Alpha 值 (highmediumdisabled),這些值則由 ContentAlpha 物件建模。請注意,MaterialTheme 預設 LocalContentAlphaContentAlpha.high

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

這樣一來,就能輕鬆採取一致的方式來傳達元件的重要性。

我們會使用內容 Alpha 值來釐清精選貼文的資訊階層。在 Home.kt 中的 PostMetadata 可組合函式,讓中繼資料 medium 強調:

+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
  Text(
    text = text,
    modifier = modifier
  )
+ }

103ff62c71744935.png

深色主題

如先前所述,如要在 Compose 中實作深色主題,只需透過該主題提供不同的顏色組合並查詢顏色即可。幾個例外狀況如下:

您可以檢查目前是否為在淺色主題中執行:

val isLightTheme = MaterialTheme.colors.isLight

這個值是由 lightColors (淺色) / darkColors (深色) 建構工具函式設定。

在 Material Design 中,深色主題中高度較高的表面將取得高度重疊 (背景會變亮)。使用深色調色盤時,系統會自動實作此效果:

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

我們在應用程式中使用的 TopAppBarCard 元件內都能看到這個自動行為;這些元件預設的高度為 4dp 和 1dp,因此使用深色主題時,背景會自動調亮,更有效地呈現此高度:

cb8c617b8c151820.png

Material Design 建議避免在深色主題中使用大面積的明亮色彩。常見模式為在淺色主題中將容器設為 primary 顏色,而在深色主題中設為 surface 顏色。根據預設,許多元件都會採用這項策略,例如應用程式列底部導覽列。為了方便實作,Colors 提供 primarySurface 顏色,提供此行為及這些元件的預設使用選項。

應用程式目前是將應用程式列設為 primary 顏色,我們可將其改為 primarySurface,或直接移除這項參數 (因為此為預設值),這樣就能遵循這項原則。在 AppBar 可組合函式中,變更 TopAppBarbackgroundColor 參數:

@Composable
private fun AppBar() {
  TopAppBar(
    ...
-   backgroundColor = MaterialTheme.colors.primary
+   backgroundColor = MaterialTheme.colors.primarySurface
  )
}

6. 處理文字

處理文字時,我們會使用 Text 可組合函式來顯示文字、使用 TextFieldOutlinedTextField 來輸入文字,並透過 TextStyle 為文字套用單一樣式。我們可以使用 AnnotatedString 將多個樣式套用到文字。

我們發現有了顏色,顯示文字的 Material Design 元件會採取自訂主題字體排版:

Button(...) {
  Text("This text will use MaterialTheme.typography.button style by default")
}

實現此做法會比使用有顏色的預設參數略為複雜一些。這是因為元件通常不會自行顯示文字,而是會提供版位 API,讓您傳入 Text 可組合函式。那麼,元件該如何設定主題字型樣式?基本上,它們會使用 ProvideTextStyle 可組合函式 (其本身使用 CompositionLocal) 來設定「目前」的 TextStyle。如未提供具體的 textStyle 參數,Text 可組合函式會預設查詢這個「目前」樣式。

例如,從 Compose 的 ButtonText 類別查詢:

@Composable
fun Button(
    // many other parameters
    content: @Composable RowScope.() -> Unit
) {
  ...
  ProvideTextStyle(MaterialTheme.typography.button) { //set the "current" text style
    ...
    content()
  }
}

@Composable
fun Text(
    // many, many parameters
    style: TextStyle = LocalTextStyle.current // get the value set by ProvideTextStyle
) { ...

主題文字樣式

和使用顏色一樣,建議您從目前主題擷取 TextStyle,鼓勵使用一致的小型樣式組合,讓樣式更易於維護。MaterialTheme.typography 會擷取 MaterialTheme 可組合函式中的 Typography 例項組合,方便您使用您定義的樣式:

Text(
  style = MaterialTheme.typography.subtitle2
)

如要自訂 TextStyle,您可以將其 copy 並覆寫屬性 (僅為 data class) 或 Text 可組合函式接受多個重疊於任一 TextStyle 頂端的樣式參數:

Text(
  text = "Hello World",
  style = MaterialTheme.typography.body1.copy(
    background = MaterialTheme.colors.secondary
  )
)
Text(
  text = "Hello World",
  style = MaterialTheme.typography.subtitle2,
  fontSize = 22.sp // explicit size overrides the size in the style
)

應用程式中的許多地方都會自動套用主題 TextStyle,例如 TopAppBar 會將 title 的樣式設為 h6ListItem 則會將主要和次要文字的樣式分別設為 subtitle1body2

接下來,我們要將主題字體樣式套用到應用程式的其餘部分。請將 Header 設為使用 subtitle2FeaturedPost 中的文字則設為使用 h6 做為標題、body2 用做作者和中繼資料:

@Composable
fun Header(...) {
  ...
  Text(
    text = text,
+   style = MaterialTheme.typography.subtitle2

45dbf11d6c1013a0.png

多種樣式

如果您需要對某些文字套用多種樣式,可以使用 AnnotatedString 類別來套用標記,並在文字範圍中加上 SpanStyle。您可以動態新增這些變數,或使用 DSL 語法建立內容:

val text = buildAnnotatedString {
  append("This is some unstyled text\n")
  withStyle(SpanStyle(color = Color.Red)) {
    append("Red text\n")
  }
  withStyle(SpanStyle(fontSize = 24.sp)) {
    append("Large text")
  }
}

現在,我們要替標記設定樣式,這些標記用於描述應用程式中的每則貼文,目前採用與其餘中繼資料相同的文字樣式;我們將使用 overline 文字樣式和背景顏色來區分兩者。在 PostMetadata 可組合函式中:

+ val tagStyle = MaterialTheme.typography.overline.toSpanStyle().copy(
+   background = MaterialTheme.colors.primary.copy(alpha = 0.1f)
+ )
post.tags.forEachIndexed { index, tag ->
  ...
+ withStyle(tagStyle) {
    append(" ${tag.toUpperCase()} ")
+ }
}

3f504aaa0a94599a.png

7. 使用形狀

就像顏色和字體一樣,設定形狀主題也會反映在 Material Design 元件中,例如 Button 會選用針對小型元件所設的形狀:

@Composable
fun Button( ...
  shape: Shape = MaterialTheme.shapes.small
) {

和顏色一樣,Material Design 元件會使用預設參數,因此您可以直接檢查元件會使用的形狀類別,或是提供替代選項。如要查看元件與形狀類別的完整對應關係,請參閱說明文件

請注意,部分元件會配合情境使用修改過的主題形狀。舉例來說,TextField 預設使用小型形狀主題,但會在底部角落套用零邊角:

@Composable
fun FilledTextField(
  // other parameters
  shape: Shape = MaterialTheme.shapes.small.copy(
    bottomStart = ZeroCornerSize, // overrides small theme style
    bottomEnd = ZeroCornerSize // overrides small theme style
  )
) {

1f5fa6cf1355e7a6.png

主題形狀

當然,在建立自己的元件時,您可以自行採用各種形狀,方法是使用可組合函式或接受各類形狀的 Modifier,例如 SurfaceModifier.clipModifier.backgroundModifier.border 等。

@Composable
fun UserProfile(
  ...
  shape: Shape = MaterialTheme.shapes.medium
) {
  Surface(shape = shape) {
    ...
  }
}

我們現在要在 PostItem 顯示的圖片中新增形狀主題設定;具體會套用主題的 small 形狀,並以 clip Modifier 剪下左上角:

@Composable
fun PostItem(...) {
  ...
  Image(
    painter = painterResource(post.imageThumbId),
+   modifier = Modifier.clip(shape = MaterialTheme.shapes.small)
  )

2f989c7c1b8d9e63.png

8. 元件「樣式」

Compose 並未提供明確的方法來讓我們擷取元件的樣式 (例如 Android View 樣式或 css 樣式)。由於所有 Compose 元件都是以 Kotlin 編寫而成,因此還有其他方法可以達成相同的目標。您可以改為自行建立自訂元件程式庫,並在應用程式中全面使用這些元件。

我們已經在應用程式中執行過這項措施:

@Composable
fun Header(
  text: String,
  modifier: Modifier = Modifier
) {
  Surface(
    color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
    contentColor = MaterialTheme.colors.primary,
    modifier = modifier.semantics { heading() }
  ) {
    Text(
      text = text,
      style = MaterialTheme.typography.subtitle2,
      modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 16.dp, vertical = 8.dp)
    )
  }
}

Header 可組合函式基本上是樣式化的 Text,可用於整個應用程式中。

我們發現所有元件都是使用較低層級的建構元素所建構,因此您可以使用相同的建構模塊來自訂Material Design 的元件。舉例來說,我們發現 Button 會使用 ProvideTextStyle 可組合函式作為傳遞的內容設定預設文字樣式。您可以按照相同的機制自行設定文字樣式:

@Composable
fun LoginButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonConstants.defaultButtonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier
    ) {
        ProvideTextStyle(...) { // set our own text style
            content()
        }
    }
}

在本例中,我們會納入標準 Button 類別,並指定不同的 backgroundColor 和文字樣式等特定屬性,建立自己的 LoginButton「樣式」。

沒有預設樣式的概念,例如自訂元件類型預設外觀的方法。同樣,為了達成此目的,您可以建立自己的元件,用來納入及自訂程式庫元件。舉例來說,假設您想自訂應用程式中所有 Button 的形狀,但不想變更小型形狀主題,進而影響其他 (非 Button) 元件。為此,請建立自己的可組合函式,並在應用程式中全面使用:

@Composable
fun AcmeButton(
  // expose Button params consumers should be able to change
) {
  val acmeButtonShape: Shape = ...
  Button(
    shape = acmeButtonShape,
    // other params
  )
}

9. 恭喜

恭喜,您已成功完成本程式碼研究室,以及設定 Jetpack Compose 應用程式的風格!

您已實作 Material Design 主題,並自訂整個應用程式中使用的色彩、字體和形狀,以傳達品牌形象、提高一致性。現在可以開始支援淺色和深色主題。

後續步驟

請參閱 Compose 課程中的其他程式碼研究室:

其他資訊

範例應用程式

  • 展示多個主題的 Owl
  • 展示動態主題設定功能的 Jetcaster
  • 展示導入自訂設計系統的 Jetsnack

參考文件