Compose 主题 MaterialTheme

1 简介

MeterialTheme 是Compose为实现Material Design 设计规范提供的核心组件,用于集中管理应用的视觉样式(颜色、字体、形状),确保应用的全局UI的一致性并支持动态主题切换。

  • 关键词:

    • 视觉样式,不只是颜色,还支持字体、形状
    • 全局UI的一致性
    • 支持动态配置

2 基础使用

已经在AndroidManifest中配置uiMode,意味着在切换深浅模式时,MainActivity不会自动重建且未重写onConfigurationChanged()

ini 复制代码
android:configChanges="uiMode"

2.1 效果展示 --- 省略

2.2 代码实现

  • 创建Compose项目时自动生成代码 Theme
ini 复制代码
// 定义应用的主题函数
@Composable
fun TestTheme(
    // 是否使用深色主题,默认根据系统设置决定
    darkTheme: Boolean = isSystemInDarkTheme(),
    // 是否使用动态颜色,Android 12+ 可用,默认为 false
    dynamicColor: Boolean = false,
    // 内容组件,使用 @Composable 函数类型
    content: @Composable () -> Unit
) {
    // 根据条件选择颜色方案
    val colorScheme = when {
        // 如果启用动态颜色且系统版本支持,则使用系统动态颜色方案
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        // 如果是深色主题,则使用深色颜色方案
        darkTheme -> DarkColorScheme
// 否则使用浅色颜色方案
        else -> LightColorScheme
}

    // 应用 Material Design 3 主题
    MaterialTheme(
        // 设置颜色方案
        colorScheme = colorScheme,
        // 设置排版样式
        typography = Typography,
        // 设置内容组件
        content = content
    )
}

// 定义深色主题的颜色方案
private val DarkColorScheme = darkColorScheme(
    // 主要颜色设置为蓝色
    primary = Color(0xFF0000FF),
    // 次要颜色使用预定义的紫色
    secondary = PurpleGrey80,
    // 第三颜色使用预定义的粉色
    tertiary = Pink80,
    // 表面颜色设置为白色
    surface = Color(0xFFFFFFFF)
)

// 定义浅色主题的颜色方案
private val LightColorScheme = lightColorScheme(
    // 主要颜色设置为深红色(猩红色)
    primary = Color(0xFFDC143C),
    // 次要颜色使用预定义的紫色
    secondary = PurpleGrey40,
    // 第三颜色使用预定义的粉色
    tertiary = Pink40,
    // 表面颜色设置为黑色
    surface = Color(0xFF000000)
    /* 其他可覆盖的默认颜色
    background = Color(0xFFFFFBFE),
    surface = Color(0xFFFFFBFE),
    onPrimary = Color.White,
    onSecondary = Color.White,
    onTertiary = Color.White,
    onBackground = Color(0xFF1C1B1F),
    onSurface = Color(0xFF1C1B1F),
    */
)
  • 界面中使用
kotlin 复制代码
//Activity中使用
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
TestTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting1(
                        modifier = Modifier.padding(innerPadding)
                    )
                }
}
}
}
}

@Composable
fun Greeting1(modifier: Modifier = Modifier) {
    Box(
        modifier = Modifier
            .padding(start = 100.dp, top = 100.dp)
            .size(100.dp, 100.dp)
            .background(MaterialTheme.colorScheme.surface)
    )
    MyText()
    MyText2()
}
@Composable
fun MyText() {
    Text(
        text = "Hello Android!",
        modifier = Modifier
            .padding(start = 100.dp, top = 250.dp)
            .background(MaterialTheme.colorScheme.surface),
        color = MaterialTheme.colorScheme.primary
    )
}

@Composable
fun MyText2() {
    Text(
        text = "Hello Chery!",
        modifier = Modifier
            .padding(start = 300.dp, top = 250.dp)
            .background(Color.Blue),
        color = Color.White
    )
}

2.3 代码分析

2.3.1 参数解析

  • darkTheme 主题模式

    默认就深/浅两种模式,那么可以直接使用系统默认isSystemInDarkTheme()值,如果项目存在其它类型的主题模式就需要自定义了(之前参与的项目中--金色模式)。

    isSystemInDarkTheme()是一个有返回值的可组合函数。

    a、前面在说可组合函数特性时,其中一个特性是"可组合函数无返回值",其实更准确的说应该是"用于直接描述 UI 的可组合函数无返回值(返回 Unit),但用于提供数据或计算结果的可组合函数可以有返回值"。

    b、isSystemInDarkTheme() 是连接 "系统主题状态" 与 "应用 UI 主题" 的桥梁,它虽不是可观察状态,但依赖于 Compose 内部可观察的 LocalConfiguration。当系统主题模式切换时,LocalConfiguration 发生变化,导致 isSystemInDarkTheme() 返回值更新,进而驱动依赖它的 TestTheme() 重组,实现应用 UI 主题的更新。

kotlin 复制代码
//系统源码
@Composable
@ReadOnlyComposable
internal actual fun _isSystemInDarkTheme(): Boolean {
    val uiMode = LocalConfiguration.current.uiMode
    return (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
}
  • dynamicColor 系统色

Android 12 + 后可使用,从代码上可以清楚的看到,当false时根据系统模式使用DarkColorScheme/LightColorScheme,当true时根据系统模式使用dynamicDarkColorScheme/dynamicLightColorScheme。

(DarkColorScheme、LightColorScheme、dynamicDarkColorScheme、dynamicLightColorScheme都Compose提供的ColorScheme模板,都可以更加我们项目自定义定制)

java 复制代码
// 根据条件选择颜色方案
val colorScheme = when {
    // 如果启用动态颜色且系统版本支持,则使用系统动态颜色方案
    dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
        val context = LocalContext.current
        if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
    }

    // 如果是深色主题,则使用深色颜色方案
    darkTheme -> DarkColorScheme
// 否则使用浅色颜色方案
    else -> LightColorScheme
}
  • content 可组合函数

描述UI的可组合函数(即 布局)

2.3.2 保证正确性,无依赖可组合函数连带重组

添加日志打印,可以看出MyText2()不依赖MaterialTheme颜色,在之前跳过重组的时候也说过"可组合函数参数不发生变化时会跳过重组",但在切换系统模式时为了保证正确性,Compose对无依赖可组合函数连带重组。这是Compose框架在全局状态变化时优先保证UI正确性的设计选中

scss 复制代码
//初始化
D  Greeting1,-----start----
D  MyText,---start---
D  MyText,---end---
D  MyText2,---start---
D  MyText2,---end---
D  Greeting1,-----end----

//切换系统模式
D  Greeting1,-----start----
D  MyText,---start---
D  MyText,---end---
D  MyText2,---start---
D  MyText2,---end---
D  Greeting1,-----end----

2.3.3 字体与形状

这里主要对颜色进行了分析,对于另外字体、形状也是一样,Compose也提供对应的入参和模板,不过实际开发中很少使用到,就简单介绍一下。(如果HMI侧对所有项目的标题、内容严格遵守一套标准,那么我们也可以实现字体、形状的平台化)

less 复制代码
//系统源码
@Composable
fun MaterialTheme(
    // 颜色
    colorScheme: ColorScheme = MaterialTheme.colorScheme,
    // 形状
    shapes: Shapes = MaterialTheme.shapes,
    //字体
    typography: Typography = MaterialTheme.typography,
    //可组合函数(即布局)
    content: @Composable () -> Unit
) {}

形状:

kotlin 复制代码
@Immutable
class Shapes(
    // 超小尺寸控件的圆角形状,适用于紧凑的小型元素(如小标签、 Chips、小型图标按钮等)
    val extraSmall: CornerBasedShape = ShapeDefaults.ExtraSmall,
    // 小尺寸控件的圆角形状,适用于常规小型交互元素(如按钮、小型卡片、输入框等)
    val small: CornerBasedShape = ShapeDefaults.Small,
    // 中等尺寸控件的圆角形状,适用于中型容器元素(如标准卡片、弹窗、列表项等)
    val medium: CornerBasedShape = ShapeDefaults.Medium,
    // 大尺寸控件的圆角形状,适用于大型容器元素(如页面级卡片、对话框、底部弹窗等)
    val large: CornerBasedShape = ShapeDefaults.Large,
    // 超大尺寸控件的圆角形状,适用于全屏级容器元素(如全屏弹窗、侧边栏、页面容器等)
    val extraLarge: CornerBasedShape = ShapeDefaults.ExtraLarge,
) {}

字体:

less 复制代码
@Immutable
class Typography(
    // 超大标题样式,用于页面级核心标题(如应用首页主标题),视觉层级最高,通常字数极少
    val displayLarge: TextStyle = TypographyTokens.DisplayLarge,
    // 大标题样式,用于重要区块的主标题(如长页面中的章节标题),层级次于 displayLarge
    val displayMedium: TextStyle = TypographyTokens.DisplayMedium,
    // 中标题样式,用于次要区块的主标题(如大型模块的标题),层级次于 displayMedium
    val displaySmall: TextStyle = TypographyTokens.DisplaySmall,

    // 大标题样式,用于突出显示的内容标题(如卡片组的总标题),视觉重量略低于 display 系列
    val headlineLarge: TextStyle = TypographyTokens.HeadlineLarge,
    // 中标题样式,用于中等重要性的内容标题(如列表组标题),层级次于 headlineLarge
    val headlineMedium: TextStyle = TypographyTokens.HeadlineMedium,
    // 小标题样式,用于次要内容的标题(如小模块标题),层级次于 headlineMedium
    val headlineSmall: TextStyle = TypographyTokens.HeadlineSmall,

    // 大标题样式,用于核心交互元素的标题(如卡片标题、弹窗标题),强调内容的可交互性
    val titleLarge: TextStyle = TypographyTokens.TitleLarge,
    // 中标题样式,用于中等交互元素的标题(如列表项标题、按钮组标题)
    val titleMedium: TextStyle = TypographyTokens.TitleMedium,
    // 小标题样式,用于次要交互元素的标题(如标签标题、小型控件标题)
    val titleSmall: TextStyle = TypographyTokens.TitleSmall,

    // 大正文样式,用于主要内容的长文本(如文章正文、详情描述),可读性优先
    val bodyLarge: TextStyle = TypographyTokens.BodyLarge,
    // 中正文样式,用于常规内容文本(如列表项描述、说明文字),最常用的正文样式
    val bodyMedium: TextStyle = TypographyTokens.BodyMedium,
    // 小正文样式,用于辅助性内容文本(如补充说明、注释),层级低于主要正文
    val bodySmall: TextStyle = TypographyTokens.BodySmall,

    // 大标签样式,用于重要标签或按钮文本(如主要按钮文字、状态标签)
    val labelLarge: TextStyle = TypographyTokens.LabelLarge,
    // 中标签样式,用于常规标签文本(如次要按钮文字、分类标签)
    val labelMedium: TextStyle = TypographyTokens.LabelMedium,
    // 小标签样式,用于辅助性标签文本(如小按钮文字、提示标签)
    val labelSmall: TextStyle = TypographyTokens.LabelSmall,
) {}

3 核心亮点

3.1 高效性、实时性

MaterialTheme 基于Compose"状态驱动机制",支持系统模式和系统色(Android 12+)动态切换,且无需重建界面或遍历View树,以最小成本实时自动切换效果。

3.2 集中性

MaterialTheme 通过 colorScheme(配色)、typography(字体)、shapes(形状) 三个核心维度,将应用的视觉样式集中管理,避免了传统 XML 中样式分散在多个资源文件(colors.xml、styles.xml 等)的碎片化问题。

3.3 灵活性、扩展性

MaterialTheme 并非固定样式模板,而是可高度定制的框架,满足不同场景下的各种需求:- 自定义主题扩展 除了默认colorScheme(配色)、typography(字体)、shapes(形状),还可通过CompositionLocal 扩展自定义主题属性。(下面会举例)- 多主题共存

假设在同一页面中存在两个Text,A Text跟随系统主题,B Text跟随自定义主题 。那么通过嵌套的方式局部的覆盖。(建议使用CompositionLocal 扩展实现,代码集中性和可读性更好。)

scss 复制代码
MaterialTheme(colorScheme = GlobalColors) {
    // 全局主题
    Column {
        MaterialTheme(colorScheme = SpecialColors) {
            Text("局部特殊主题文本") // 使用 SpecialColors
        }
        Text("全局主题文本") // 使用 GlobalColors
    }
}

4 MaterialTheme 扩展使用

上面我们已经介绍了MaterialTheme 提供的颜色、形状、字体模板,模板的目的满足全局绝大部分需求,但在实际开发中我们还存在切换系统模式/系统色时图片资源的变化,以及要求某些组件要求始终如一。

那么我们就需要通过compositionLocalOf/staticCompositionLocalOf 和 扩展自定义主题属性了。

4.1 效果展示

  • Image 随系统模式变化使用不同图片资源
  • Text 背景和文字不跟随系统模式变化

4.2 定义 CompositionLocal实例

  • compositionLocalOf,创建一个可变的CompositionLocal实例,值发生变化时触发依赖组件重组。
  • staticCompositionLocalOf,创建一个不可变的 CompositionLocal实例,值发生变化时触发整个子树重组。
  • 值变化,是指对象引用(单纯的btnBackgroundColor/btnTitleColor 变化不会导致重组)
  • 整个子树重组,在使用staticCompositionLocalOf的CompositionLocalProvider内部的Content都会重组,且不会跳过重组。(如下示例是直接在Activity中使用,那么整个界面上的组件都会发生重组)
scss 复制代码
// 定义扩展主题
@Stable
class ExtendScheme(
    btnBackgroundColor: Color,
    btnTitleColor: Color
) {
    /** 按钮背景颜色 */
var btnBackgroundColor by mutableStateOf(btnBackgroundColor)
        internal set

    /** 按钮标题颜色 */
var btnTitleColor by mutableStateOf(btnTitleColor)
        internal set
}

// 扩展主题 --浅色
private val LightExtendScheme = ExtendScheme(
    btnBackgroundColor = Color(0xFFF00FFF),
    btnTitleColor = Color(0xFFFFFFFF),
)

// 扩展主题 --深色
private val DarkExtendScheme = ExtendScheme(
    btnBackgroundColor = Color(0xFFF00FFF),
    btnTitleColor = Color(0xFFFFFFFF),
)

// 定义一个存储 ExtendScheme 类型的CompositionLocal,默认值是浅色主题
val LocalExtendScheme = compositionLocalOf {
 LightExtendScheme
}

// 定义主题资源
@Stable
class ResScheme(
    imageRes: Int,
) {
    var imageRes by mutableIntStateOf(imageRes)
}

// 图片资源--浅色
private val LightResScheme = ResScheme(
    imageRes =  R.drawable.ic_navi_home_light,
)

// 图片资源--深色
private val DarkResScheme = ResScheme(
    imageRes = R.drawable.ic_navi_home_drak,
)

// 定义一个存储 ResScheme 类型的CompositionLocal,默认值是浅色资源
val LocalResScheme = compositionLocalOf {
 LightResScheme
} 

4.3 CompositionLocalProvider 提供数据

CompositionLocalProvider是Compose中用于在Compoasable(可组合函数)树中传递数据的核心组件,允许你在某个层级定义"局部全局变量",让其所有子组件(无论嵌套多深)都可以便捷访问,解决了:

  • 传统父组件 -> 子组件 ->孙组件这种层层传递的方式。
  • 有点类似于静态变量,但相对于静态变量的全局性和唯一性,CompositionLocalProvider作用范围仅限于其内部的所有子组件,所以可以理解为"局部全局变量"
kotlin 复制代码
// 定义应用的主题函数
@Composable
fun TestTheme(
    // 是否使用深色主题,默认根据系统设置决定
    darkTheme: Boolean = isSystemInDarkTheme(),
    // 是否使用动态颜色,Android 12+ 可用,默认为 false
    dynamicColor: Boolean = false,
    // 内容组件,使用 @Composable 函数类型
    content: @Composable () -> Unit
) {
    // 。。。。。 省略前面的
    
    // 定义扩展主题
    val extendScheme = if (darkTheme) {
        DarkExtendScheme
} else {
        LightExtendScheme
}
    // 定义图片资源
    val resScheme = if (darkTheme) {
        DarkResScheme
} else {
        LightResScheme
}

    // 应用 Material Design 3 主题
    MaterialTheme(
        // 设置颜色方案
        colorScheme = colorScheme,
        // 设置排版样式
        typography = Typography,
        // 设置内容组件
        content = {
             // 提供LocalExtendScheme 和 LocalResScheme 数据,内部所有组件都可以访问
CompositionLocalProvider(
                LocalExtendScheme provides extendScheme,
                LocalResScheme provides resScheme
            ) {
content()
            }
}
)
}

4.4 使用

在Theme中根据需求配置完成后,无需再关心后续的系统模式/系统色变化了。

ini 复制代码
@Composable
fun Greeting1(modifier: Modifier = Modifier) {
    Image(
        modifier = Modifier
            .padding(start = 300.dp, top = 100.dp)
            .size(200.dp, 200.dp)
            .background(Color.Gray),
            // 使用图片资源
        painter = painterResource(LocalResScheme.current.imageRes),
        contentDescription = null,
    )
    Text(
        text = "Hello Android!",
        modifier = Modifier
            .padding(start = 200.dp, top = 500.dp)
            .size(300.dp, 200.dp)
            //使用扩展颜色
            .background(LocalExtendScheme.current.btnBackgroundColor),
        color = LocalExtendScheme.current.btnTitleColor
    )
}

5 参考资料

  • 基础组件、布局组件使用

写在开头 | 你好 Compose

相关推荐
alexhilton1 天前
学会在Jetpack Compose中加载Lottie动画资源
android·kotlin·android jetpack
ljt27249606614 天前
Compose笔记(六十一)--SelectionContainer
android·笔记·android jetpack
QING6185 天前
Jetpack Compose 中的 ViewModel 作用域管理 —— 新手指南
android·kotlin·android jetpack
惟恋惜5 天前
Jetpack Compose 的状态使用之“界面状态”
android·android jetpack
喜熊的Btm5 天前
探索 Kotlin 的不可变集合库
kotlin·android jetpack
惟恋惜5 天前
Jetpack Compose 界面元素状态(UI Element State)详解
android·ui·android jetpack
惟恋惜5 天前
Jetpack Compose 多页面架构实战:从 Splash 到底部导航,每个 Tab 拥有独立 ViewModel
android·ui·架构·android jetpack
alexhilton7 天前
Jetpack Compose 2025年12月版本新增功能
android·kotlin·android jetpack
モンキー・D・小菜鸡儿8 天前
Android Jetpack Compose 基础控件介绍
android·kotlin·android jetpack·compose
darryrzhong10 天前
FluxImageLoader : 基于Coil3封装的 Android 图片加载库,旨在提供简单、高效且功能丰富的图片加载解决方案
android·github·android jetpack