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

相关推荐
用户060905255225 小时前
Compose 简介和基础使用
android jetpack
用户060905255226 小时前
Compose 重组优化
android jetpack
行墨9 小时前
Jetpack Compose 深入浅出(一)——预览 @Preview
android jetpack
alexhilton2 天前
突破速度障碍:非阻塞启动画面如何将Android 应用启动时间缩短90%
android·kotlin·android jetpack
Pika3 天前
深入浅出 Compose 测量机制
android·android jetpack·composer
fundroid4 天前
掌握 Compose 性能优化三步法
android·android jetpack
ljt27249606618 天前
Compose笔记(五十一)--rememberTextMeasurer
android·笔记·android jetpack
wxson728213 天前
【用androidx.camera拍摄景深合成照片】
kotlin·android jetpack·androidx
天花板之恋13 天前
Compose Navigation总结
android jetpack