Jeptpack Compose 官網(wǎng)教程學(xué)習(xí)筆記(四)主題

主題

主要學(xué)習(xí)內(nèi)容

  • Material Design 入門指南以及如何針對您的品牌對其進(jìn)行自定義
  • Compose 如何實(shí)現(xiàn) Material Design 系統(tǒng)
  • 如何在應(yīng)用中定義和使用顏色、排版和形狀
  • 如何設(shè)置組件的樣式
  • 如何支持淺色主題和深色主題

在本次學(xué)習(xí)中我們將設(shè)置新聞閱讀應(yīng)用的樣式,從未設(shè)置樣式的應(yīng)用入手,應(yīng)用所學(xué)的內(nèi)容來設(shè)置應(yīng)用的主題,并為設(shè)置深色主題提供支持


構(gòu)建前:未設(shè)置樣式的應(yīng)用
構(gòu)建后:已設(shè)置樣式的應(yīng)用
構(gòu)建后:深色主題

準(zhǔn)備工作

官網(wǎng)示例下載

因?yàn)橹蟮拇a都是基于其中的項(xiàng)目進(jìn)行的,所以還是推薦下載。同時也可以看一下Google人員對于的Compose的代碼編寫風(fēng)格

因?yàn)榇a過多且需要添加drawable資源文件,此處就不將代碼寫出來了

在解壓文件中的ThemingCodelab 目錄中存放本次學(xué)習(xí)的案例代碼

此項(xiàng)目包含 3 個主要軟件包:

  • com.codelab.theming.data - 該軟件包包含模型類和示例數(shù)據(jù),無需修改該軟件包
  • com.codelab.theming.ui.start - 該 示例 的起點(diǎn),您應(yīng)該在該軟件包中完成此 示例 中要求的所有更改
  • com.codelab.theming.ui.finish - 該軟件包是此 示例 的最終狀態(tài),供參考

我們可以選擇Import Project方式進(jìn)行學(xué)習(xí),也可以通過拷貝代碼到自己項(xiàng)目中的方式

我使用的是拷貝代碼的方式,可能之后跟Import Project方式有些區(qū)別請諒解

如果是選擇拷貝代碼的方式請注意:

Empty Activity默認(rèn)Activity繼承androidx.appcompat.app.AppCompatActivity,而要使用 Compose 則需要Activity繼承androidx.activity.ComponentActivity,否則會產(chǎn)生如下異常

Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.

Material主題設(shè)置

Jetpack Compose 提供了 Material Design 的實(shí)現(xiàn),Material Design 是一個用于創(chuàng)建數(shù)字化界面的綜合設(shè)計體系。Material Design 組件(按鈕、卡片、開關(guān)等)在Material Theme 設(shè)置的基礎(chǔ)上構(gòu)建而成,一個 Material Theme由顏色、排版和形狀屬性組成

顏色

Material Theme 定義了一些從語義上命名的顏色,我們可以在應(yīng)用中使用:

Material Theme 調(diào)色板

其中 primary 是應(yīng)用的主要顏色,secondary 用于提供強(qiáng)調(diào)色,我們可以通過這兩種顏色的設(shè)置凸顯出需要對比區(qū)域

backgroundsurface兩種顏色用于在概念上駐留在"應(yīng)用表面"的組件的容器,也就是背景顏色

Material Design 中還定義了on顏色,是與具名顏色產(chǎn)生明顯對比的顏色

例如:采用surface作為背景顏色的容器中文本應(yīng)該采用onSurface顏色

Material 組件已配置并使用這些主題顏色。例如,FloatingActionButton的默認(rèn)顏色為 secondary,Card的默認(rèn)顏色為 surface,諸如此類。

排版

同樣,Material Theme還定義了一些從語義上命名的字體樣式:

Material Theme 排版

雖然我們可能不會按主題來更改字體樣式,但使用 Material Design 中的字體樣式可提升應(yīng)用內(nèi)部的一致性

Material 組件已配置并使字體樣式。例如,TopAppBar默認(rèn)使用 h6 樣式,Button默認(rèn)使用 button樣式,諸如此類。

形狀

Material Theme 中定義了 3 個類別:小型、中型和大型組件;每種組件都可以定義要使用的形狀,從而自定義角的樣式(切角和圓角)和大小

Material Theme 形狀

默認(rèn)情況下,ButtonTextField使用小型形狀主題,CardDialog使用中型形狀主題,Sheet使用大型形狀主題

如需查看組件和形狀主題的完整對應(yīng)關(guān)系,請點(diǎn)擊此處

基準(zhǔn)

Material 默認(rèn)采用“基準(zhǔn)”主題,即紫色的配色方案、Roboto 字體比例,以及以上圖片所示的略呈圓形的形狀。如果您未指定或自定義主題,組件就會使用基準(zhǔn)主題

定義主題

MaterialTheme

在 Jetpack Compose 中實(shí)現(xiàn)主題設(shè)置的核心元素是 MaterialTheme 可組合項(xiàng)。如果將此可組合項(xiàng)放在 Compose 層次結(jié)構(gòu)中,您就可以為其中的所有組件指定顏色、字體和形狀的自定義設(shè)置

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

我們可以使用 MaterialTheme object 檢索傳遞到此可組合項(xiàng)的參數(shù),以公開 colors、typographyshapes 屬性。在之后,我們將逐一進(jìn)行深入介紹

找到 Home 可組合函數(shù) - 這是應(yīng)用的主入口點(diǎn)。請注意,雖然我們聲明了 MaterialTheme,但并未指定任何參數(shù),因此會獲得默認(rèn)的“基準(zhǔn)”樣式:

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

創(chuàng)建主題

如果需集中設(shè)置樣式,官方建議創(chuàng)建自己的可組合項(xiàng),用于封裝和配置 MaterialTheme

這樣做,我們就可以在指定自己的主題自定義設(shè)置,并在多個位置(例如跨多個屏幕或 @Preview)輕松地重復(fù)使用這些自定義設(shè)置

可以根據(jù)需要創(chuàng)建多個主題可組合項(xiàng)。例如,如果您想針對應(yīng)用的不同部分支持不同的樣式

我們可以在Theme.kt中添加一個名為StudyTheme新可組合函數(shù)

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

如果使用Import Project,在start中沒有Theme.kt文件,需要新建Theme.kt文件

我們回到 Home 可組合函數(shù),并將 MaterialTheme 替換為 StudyTheme

@Composable
fun Home() {
  ...
  StudyTheme {
    Scaffold(...){...}
  }
}

同樣在PostItemPreviewFeaturedPostPreview 以使用新的 StudyTheme 可組合項(xiàng)來封裝其內(nèi)容,以便預(yù)覽使用新的主題

@Preview("Post Item")
@Composable
private fun PostItemPreview() {
    val post = remember { PostRepo.getFeaturedPost() }
    StudyTheme{
        Surface {
            PostItem(post = post)
        }
    }
}

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

顏色

我們要在應(yīng)用中實(shí)現(xiàn)的調(diào)色板如下所示:

調(diào)色板

Compose 中的顏色是使用 Color 類定義的。借助多個構(gòu)造函數(shù),您可以將顏色指定為 ULong,也可以按單獨(dú)的顏色通道來指定顏色

若要從用于指定顏色的常用“#dd0d3c”格式進(jìn)行轉(zhuǎn)換,請將“#”替換為“0xff”,即 Color(0xffdd0d3c),其中“ff”表示完整的 Alpha 值

Color.kt中添加以下顏色:

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

在定義顏色時,我們要根據(jù)顏色值“字面意義”命名顏色,而不要“從語義上”命名顏色

例如,命名為 Red500 而不是 primary。這樣一來,我們就可以定義多個主題。例如,在深色主題中或樣式設(shè)置不同的屏幕上,系統(tǒng)可能會將另一種顏色視為 primary

注意導(dǎo)入 Compose 的 Color 類型是 androidx.compose.ui.graphics.Color,而不是 android.graphics.Color

如果使用Import Project,在start中沒有Color.kt文件,需要新建Color.kt文件

現(xiàn)在,我們已經(jīng)定義了應(yīng)用的顏色。接下來,我們將其合并到 MaterialTheme 所需的 Colors 對象中,從而將特定顏色分配到 Material 的具名顏色。切換回 Theme.kt,然后添加以下代碼:

//為外部屬性,不是StudyTheme中的臨時變量
private val LightColors = lightColors(
    primary = Red700,
    primaryVariant = Red900,
    onPrimary = Color.White,
    secondary = Red700,
    secondaryVariant = Red900,
    onSecondary = Color.White,
    error = Red800
)

下面,我們要使用 lightColors 函數(shù)來構(gòu)建 Colors,這樣即可提供合理的默認(rèn)值,讓我們不必將構(gòu)成 Material 調(diào)色板的所有顏色全都指定出來

例如,我們尚未指定 background 顏色或許多“on”顏色,我們將會使用lightColors中的默認(rèn)值

我們在StudyTheme中使用這些顏色:

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

此時刷新預(yù)覽,就會發(fā)現(xiàn)新的配色方案會反映在 TopAppBar 等組件中

排版

我們要在應(yīng)用中實(shí)現(xiàn)的字體樣式如下所示:

字體樣式表

在 Compose 中,我們可以定義 TextStyle 對象,以定義設(shè)置一些文本的樣式所需的信息。下面是其屬性的示例:

@Immutable
class TextStyle(
    val color: Color = Color.Unspecified,
    val fontSize: TextUnit = TextUnit.Unspecified,
    val fontWeight: FontWeight? = null,
    val fontStyle: FontStyle? = null,
    val fontSynthesis: FontSynthesis? = null,
    val fontFamily: FontFamily? = null,
    val fontFeatureSettings: String? = null,
    val letterSpacing: TextUnit = TextUnit.Unspecified,
    val baselineShift: BaselineShift? = null,
    val textGeometricTransform: TextGeometricTransform? = null,
    val localeList: LocaleList? = null,
    val background: Color = Color.Unspecified,
    val textDecoration: TextDecoration? = null,
    val shadow: Shadow? = null,
    val textAlign: TextAlign? = null,
    val textDirection: TextDirection? = null,
    val lineHeight: TextUnit = TextUnit.Unspecified,
    val textIndent: TextIndent? = null
){ ... }

我們所需的字體比例要針對標(biāo)題使用 Montserrat,并針對正文文本使用 Domine

相關(guān)字體文件在 示例 的 res/font 文件夾中

Type.kt文件中定義 FontFamily(結(jié)合了每個 Font 的不同粗細(xì)):

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

然后創(chuàng)建一個 MaterialTheme 接受的 Typography 對象,為比例中的每個語義樣式指定 TextStyle

val StudyTypography = 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
    )
)

我們在StudyTheme中使用新的 Typography

@Composable
fun StudyTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        content = content,
        colors = LightColors,
/*+*/   typography = StudyTypography
    )
}

形狀

Compose 提供了 RoundedCornerShape 類和 CutCornerShape 類,可用于定義形狀主題

Shape.kt,并添加以下代碼:

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

我們在StudyTheme中使用新的 Shapes

@Composable
fun StudyTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        content = content,
        colors = LightColors,
        typography = StudyTypography,
/*+*/   shapes = StudyShapes
    )
}

刷新預(yù)覽,可以看見精選博文的 Card 變?yōu)樽笊锨薪切螤?/p>

image-20220517113932065.png

深色主題

在應(yīng)用中支持深色主題不僅有助于您的應(yīng)用在用戶設(shè)備上更好地集成(從 Android 10 開始,設(shè)備上已提供全局深色主題切換開關(guān)),還有助于降低能耗以及為滿足無障礙功能需求提供支持。Material 提供了關(guān)于如何創(chuàng)建深色主題的接口

以下是我們想為深色主題實(shí)現(xiàn)的調(diào)色板:

深色主題調(diào)色板

打開 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
)

darkColorslightColors一樣,當(dāng)我們沒有提供調(diào)色板顏色時提供默認(rèn)值

然后,更新 StudyTheme

@Composable
fun StudyTheme(
/*+*/darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        content = content,
/*+*/   colors = if (darkTheme) DarkColors else LightColors,
        typography = StudyTypography,
        shapes = StudyShapes
    )
}

此時,我們添加了用于判斷是否使用深色主題的新參數(shù),并將其默認(rèn)設(shè)為查詢設(shè)備的全局設(shè)置

FeaturedPost 可組合項(xiàng)創(chuàng)建新的預(yù)覽,此預(yù)覽能夠以深色主題顯示該可組合項(xiàng):

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

預(yù)覽效果對比

預(yù)覽效果對比

處理顏色

我們現(xiàn)在可以創(chuàng)建自己的Theme,設(shè)置應(yīng)用的顏色、字體樣式、形狀。而所有的Material 組件默認(rèn)支持這些自定義屬性

例如,FloatingActionButton 可組合項(xiàng)默認(rèn)使用主題中的 secondary 顏色,當(dāng)然我們可以通過為此參數(shù)指定不同的值來設(shè)置顏色:

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

原色

Compose 提供了一個 Color 類。您可以在本地創(chuàng)建這些類,并將其保留在 object 等元素中:

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

注意:在靜態(tài)聲明顏色定義時,請務(wù)必小心,因?yàn)檫@些定義會導(dǎo)致更難/無法支持不同的主題(例如,淺色/深色主題)

Color 中有許多有用的方法,例如 copy,您可以通過此方法使用不同的 alpha/red/green/blue 值來創(chuàng)建新的顏色

主題顏色

我們可以從主題中檢索顏色

Surface(color = MaterialTheme.colors.primary)

通過使用 MaterialTheme object,其colors屬性會返回MaterialTheme可組合項(xiàng)中設(shè)置的Colors。也就是說,我們只需為主題提供不同的顏色集,即可支持不同的外觀和風(fēng)格,而無需處理應(yīng)用代碼

由于主題中的每種顏色都是Color實(shí)例,因此我們可以通過copy派生出不同的顏色

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

這種方法可以確保顏色可以在不同主題下正常顯示,無需編寫靜態(tài)顏色代碼

背景色和內(nèi)容顏色

許多組件都接受一對顏色和“內(nèi)容顏色”:

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

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

這樣我們不僅可以設(shè)置可組合項(xiàng)的背景顏色,還可以為"內(nèi)容"(即包含在內(nèi)的可組合項(xiàng))提供默認(rèn)顏色。默認(rèn)情況下,許多可組合項(xiàng)都會使用這種內(nèi)容顏色,如:Text、Icon

contentColorFor 方法可以為任何主題顏色檢索適當(dāng)?shù)摹皁n”顏色,例如,如果您設(shè)置 primary 背景,它就會返回 onPrimary 作為內(nèi)容顏色。如果您設(shè)置非主題背景顏色,則應(yīng)自行提供合理的內(nèi)容顏色

跟蹤contentColorFor方法最終會進(jìn)入該方法,所以如果backgroundColor使用非主題顏色,需要提供contentColor

fun Colors.contentColorFor(backgroundColor: Color): Color {
    return when (backgroundColor) {
        primary -> onPrimary
        primaryVariant -> onPrimary
        secondary -> onSecondary
        secondaryVariant -> onSecondary
        background -> onBackground
        surface -> onSurface
        error -> onError
        else -> Color.Unspecified
    }
}

我們還可以使用 LocalContentColor CompositionLocal 來檢索與當(dāng)前背景形成對比的顏色:

如果對于CompositionLocal很陌生,可以看看我的另一篇簡書 簡書-CompositionLocal

BottomNavigationItem(
    unselectedContentColor = LocalContentColor.current 
    ...
  ){ ... }

當(dāng)設(shè)置任何元素的顏色時,最好使用 Surface 來實(shí)現(xiàn)此目的,因?yàn)樗鼤O(shè)置適當(dāng)?shù)膬?nèi)容顏色 CompositionLocal

請慎用直接 Modifier.background 調(diào)用,這種調(diào)用不會設(shè)置適當(dāng)?shù)膬?nèi)容顏色

目前,我們的 Header 組件background始終使用 Color.LightGray 背景。這在淺色主題中看起來沒有問題,但在深色主題中,就會與背景形成高度對比。而且也不會指定與背景顏色形成對比的文本顏色

//使用uiMode,設(shè)置預(yù)覽時使用深色主題
@Preview("Home",group="Home",uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun HomePreview() {
    Home()
}
image-20220517132639268.png

接下來,我們通過Surface去解決這個問題

Header 可組合項(xiàng)中,移除用于指定硬編碼顏色的 background 修飾符。改為將 Text 封裝在包含主題派生顏色的 Surface 中,并指定相應(yīng)內(nèi)容應(yīng)采用 primary 顏色:

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

內(nèi)容 Alpha 值

通常情況下,我們通過強(qiáng)調(diào)或弱化內(nèi)容來突出重點(diǎn)并體現(xiàn)出視覺上的層次感。Material Design 建議采用不同的不透明度來傳達(dá)這些不同的重要程度

Jetpack Compose 通過 LocalContentAlpha 實(shí)現(xiàn)此功能。您可以通過為此 CompositionLocal 提供一個值來為層次結(jié)構(gòu)指定內(nèi)容 Alpha 值

子可組合項(xiàng)可以使用此值,例如 Text 和 Icon 默認(rèn)使用LocalContentColor的顏色值 ,其中的Alpha會調(diào)整為 LocalContentAlpha的值

@Composable
fun Text(...){
    val textColor = color.takeOrElse {
        style.color.takeOrElse {
            LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
        }
    }
    ...
}

@Composable
fun Icon(
    ...
    tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
){ ... }

Material 指定了一些標(biāo)準(zhǔn) Alpha 值(high、medium、disabled),這些值由 ContentAlpha 對象提供

object ContentAlpha {
    val high: Float
        @Composable
        get()=...
    
    val medium: Float
        @Composable
        get()=...
    
    val disabled: Float
        @Composable
        get()=...
}

請注意,MaterialTheme 默認(rèn)將 LocalContentAlpha 設(shè)置為 ContentAlpha.high

我們將使用內(nèi)容 Alpha 值來闡明精選博文的信息層次結(jié)構(gòu)。在 PostMetadata 可組合項(xiàng)中,重點(diǎn)突出元數(shù)據(jù) medium

@Composable
private fun PostMetadata(
    post: Post,
    modifier: Modifier = Modifier
) {
    ...
/*+*/CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
        Text(
            text = text,
            modifier = modifier
        )
/*+*/}
}

深色主題

若要在 Compose 中實(shí)現(xiàn)深色主題,我們只需提供不同的顏色集并通過主題查詢顏色即可

我們可以通過下面代碼檢測是否在淺色主題中運(yùn)行:

val isLightTheme = MaterialTheme.colors.isLight

此值由 lightColors/[darkColors 構(gòu)建器函數(shù)設(shè)置

Material Design 建議避免在深色主題中使用大面積的明亮顏色

一種常見模式是在淺色主題中將容器背景顏色設(shè)為 primary ,并在深色主題中將其設(shè)為 surface ;許多組件都默認(rèn)使用此策略,例如TopAppBarBottomNavigation

為了便于實(shí)現(xiàn),Colors 提供了 primarySurface 顏色,以準(zhǔn)確完成上述行為

@Composable
fun BottomNavigation(
    ...
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    ...
){ ... }

@Composable
fun TopAppBar(
    ...
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    ...
){ ... }
val Colors.primarySurface: Color get() = if (isLight) primary else surface

遵循此指南,我們需要將AppBar中的TopAppBarbackgroundColor 切換為primarySurface或移除此參數(shù)(因?yàn)榇藚?shù)為默認(rèn)設(shè)置)即可

@Composable
private fun AppBar() {
    TopAppBar(
        navigationIcon = {
            Icon(
                imageVector = Icons.Rounded.Palette,
                contentDescription = null,
                modifier = Modifier.padding(horizontal = 12.dp)
            )
        },
        title = {
            Text(text = stringResource(R.string.app_title))
        },
        //更換為primarySurface或直接刪除即可
        backgroundColor = MaterialTheme.colors.primarySurface
    )
}

在 Material 中,如果采用的是深色主題,則高度較高 (elevation) 的 Surface 會獲得高度疊加層(其背景顏色會變淺)。在使用深色主題時,系統(tǒng)會自動實(shí)現(xiàn)此效果:

高度疊加層效果

處理文本

在處理文本時,我們使用 Text 可組合項(xiàng)來顯示文本,使用 TextFieldOutlinedTextField 進(jìn)行文本輸入,并使用 TextStyle 對文本應(yīng)用單一樣式。我們可以使用 AnnotatedString 對文本應(yīng)用多種樣式

和顏色一樣,用于顯示文本的 Material 組件可以獲取到主題排版自定義設(shè)置:

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

不過實(shí)現(xiàn)此目的要比使用默認(rèn)參數(shù)(如在設(shè)置顏色時所看到的那樣)略微復(fù)雜一些

因?yàn)榻M件本身往往不會顯示文本,而是提供槽 API,讓您能夠傳入 Text 可組合項(xiàng)。那么,組件是如何設(shè)置主題排版樣式的呢?

在后臺,它們使用 ProvideTextStyle 可組合項(xiàng)(本身就使用 CompositionLocal)來設(shè)置"current"TextStyle。如果您未提供具體的 textStyle 參數(shù),Text 可組合項(xiàng)會默認(rèn)查詢此"current"樣式

@Composable
fun Button(
    ...
) {
    val contentColor by colors.contentColor(enabled)
    Surface(
        ...
    ) {
        CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
            ProvideTextStyle(
                value = MaterialTheme.typography.button
            ) {
                ...
            }
        }
    }
}

@Composable
fun Text(
    ...
    style: TextStyle = LocalTextStyle.current
) { ... }

主題文本樣式

就像顏色一樣,最好從當(dāng)前主題中檢索 TextStyle,使用一組數(shù)量少且一致的樣式,并使其更易于維護(hù)

MaterialTheme.typography 會檢索在 MaterialTheme 可組合項(xiàng)中設(shè)置的 Typography 實(shí)例,讓我們能夠使用自己定義的樣式:

Text(
  style = MaterialTheme.typography.subtitle2
)

如果您需要自定義 TextStyle,可以對其執(zhí)行 copy 操作并替換相關(guān)屬性,或者讓 Text 可組合項(xiàng)接受大量樣式參數(shù),這些參數(shù)會疊加到任何 TextStyle 的上層:

//使用copy操作替換屬性
Text(
  text = "Hello World",
  style = MaterialTheme.typography.body1.copy(
    background = MaterialTheme.colors.secondary
  )
)
// 使用樣式參數(shù)覆蓋樣式中的值
Text(
  text = "Hello World",
  style = MaterialTheme.typography.subtitle2,
  fontSize = 22.sp 
)

在我們的應(yīng)用中,許多地方都會自動應(yīng)用主題 TextStyle,例如,TopAppBar 將其 title 的樣式設(shè)為 h6,而 ListItem將其主要文本和輔助文本的樣式分別設(shè)為 subtitle1body2

接下來,我們要將主題排版樣式應(yīng)用于應(yīng)用的其余部分。將 Header 設(shè)為使用 subtitle2;對于 FeaturedPost 中的文本,將標(biāo)題設(shè)為 h6,并將作者信息和PostMetadata設(shè)為 body2

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

多種樣式

如果您需要對某些文本應(yīng)用多種樣式,可以使用 AnnotatedString 類來應(yīng)用標(biāo)記,從而為一系列文本添加 SpanStyle。您可以動態(tài)添加這些元素,也可以使用 DSL 語法來創(chuàng)建內(nèi)容:

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

接下來,我們要為描述應(yīng)用中的各個博文的標(biāo)簽設(shè)置樣式。目前,它們使用與元數(shù)據(jù)其余部分相同的文本樣式;我們將使用 overline 文本樣式和背景顏色來區(qū)分它們。在 PostMetadata 可組合項(xiàng)中:

@Composable
private fun PostMetadata(
    post: Post,
    modifier: Modifier = Modifier
) {
    val divider = "  ?  "
    val tagDivider = "  "
    val text = buildAnnotatedString {
        append(post.metadata.date)
        append(divider)
        append(stringResource(R.string.read_time, post.metadata.readTimeMinutes))
        append(divider)

/*+*/   val tagStyle=MaterialTheme.typography.overline.toSpanStyle().copy(
/*+*/       background = MaterialTheme.colors.primary.copy(alpha = 0.1f)
/*+*/   )
        post.tags.forEachIndexed { index, tag ->
            if (index != 0) {
                append(tagDivider)
            }
/*+*/       withStyle(tagStyle){
                append(" ${tag.uppercase(Locale.getDefault())} ")
/*+*/       }
        }
    }
    ...
}
image-20220517151906032.png

處理形狀

與顏色和排版一樣,如果設(shè)置形狀主題,相應(yīng)設(shè)置會反映在 Material 組件中。例如,Button 會獲取為小型組件設(shè)置的形狀:

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

與顏色一樣,Material 組件使用默認(rèn)參數(shù),我們可以直接查看組件將要使用的形狀類別,或提供替代方案。如需查看組件和形狀類別的完整對應(yīng)關(guān)系,請參閱此文檔

請注意,有些組件會使用經(jīng)過修改的主題形狀,以適應(yīng)其上下文的要求。例如,默認(rèn)情況下,TextField 使用小型形狀主題,但它會對底角應(yīng)用零邊角大?。?/p>

@Composable
fun TextField(
    ...
    shape: Shape =
        MaterialTheme.shapes.small.copy(
            bottomEnd = ZeroCornerSize, 
            bottomStart = ZeroCornerSize
        ),
    ...
) { ... }

主題形狀

在創(chuàng)建自己的組件時,我們可以自行使用各種形狀;為此,需要使用接受形狀的可組合項(xiàng)或 Modifier(例如,SurfaceModifier.clip、Modifier.background、Modifier.border 等)

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

接下來,我們要將形狀主題添加到 PostItem 中顯示的圖片;我們要對其應(yīng)用主題的 small 形狀,并使用 Modifier.clip 應(yīng)用該形狀:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PostItem(
    post: Post,
    modifier: Modifier = Modifier
) {
    ListItem(
        modifier = modifier
            .clickable { /* todo */ }
            .padding(vertical = 8.dp),
        icon = {
            Image(
                painter = painterResource(post.imageThumbId),
                contentDescription = null,
/*+*/           modifier = Modifier.clip(shape = MaterialTheme.shapes.small)
            )
        },
        text = {
            Text(text = post.title)
        },
        secondaryText = {
            PostMetadata(post)
        }
    )
}
image-20220517153124898.png

組件樣式

Compose 沒有提供用于提取組件樣式(例如,Android View 樣式或 CSS 樣式)的明確方法。由于所有 Compose 組件都是用 Kotlin 編寫的,因此還可通過其他方法來實(shí)現(xiàn)相同的目的

我們可以改為創(chuàng)建自己的自定義組件庫,并在整個應(yīng)用中使用這些組件

比如 示例 中:

@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 可組合項(xiàng)本質(zhì)上是樣式化的 Text,可供我們在整個應(yīng)用中使用

所有組件都是由較低級別的構(gòu)建塊構(gòu)造而成的,我們可以使用同樣的構(gòu)建塊來自定義 Material 組件

例如, Button 使用 ProvideTextStyle 可組合項(xiàng)為傳遞給它的內(nèi)容設(shè)置默認(rèn)文本樣式。您可以使用完全相同的機(jī)制來設(shè)置自己的文本樣式

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容