Material 3 与 Compose 主题系统:打造品牌一致的 UI

2024-04-22 · 20 min · 主题系统

Material 3(Material You)是 Google 最新的设计系统,与 Jetpack Compose 深度集成。本文将带你掌握如何构建一个灵活、可扩展的主题系统。

一、Material 3 基础配置

// build.gradle.kts
dependencies {
    implementation("androidx.compose.material3:material3:1.2.0")
}
@Composable
fun MyApp() {
    MaterialTheme(
        colorScheme = lightColorScheme(),
        typography = Typography,
        shapes = Shapes
    ) {
        // App content
    }
}

二、颜色系统

Material 3 使用基于角色的颜色系统,包含 primary、secondary、tertiary 等色彩角色:

private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF6750A4),
    onPrimary = Color.White,
    primaryContainer = Color(0xFFEADDFF),
    onPrimaryContainer = Color(0xFF21005D),

    secondary = Color(0xFF625B71),
    onSecondary = Color.White,
    secondaryContainer = Color(0xFFE8DEF8),
    onSecondaryContainer = Color(0xFF1D192B),

    tertiary = Color(0xFF7D5260),
    surface = Color(0xFFFFFBFE),
    background = Color(0xFFFFFBFE),
    error = Color(0xFFB3261E)
)

private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFFD0BCFF),
    onPrimary = Color(0xFF381E72),
    primaryContainer = Color(0xFF4F378B),
    onPrimaryContainer = Color(0xFFEADDFF),
    // ... 其他颜色
)

三、动态颜色(Dynamic Color)

Android 12+ 支持从用户壁纸提取颜色:

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    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
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        content = content
    )
}

💡 Material Theme Builder

使用 Material Theme Builder 工具可以快速生成完整的颜色方案代码。

四、Typography 排版系统

val AppTypography = Typography(
    displayLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.W400,
        fontSize = 57.sp,
        lineHeight = 64.sp,
        letterSpacing = (-0.25).sp
    ),
    headlineLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.W400,
        fontSize = 32.sp,
        lineHeight = 40.sp
    ),
    titleLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.W400,
        fontSize = 22.sp,
        lineHeight = 28.sp
    ),
    bodyLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.W400,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp
    ),
    labelSmall = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.W500,
        fontSize = 11.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.5.sp
    )
)

使用自定义字体

val CustomFontFamily = FontFamily(
    Font(R.font.custom_regular, FontWeight.Normal),
    Font(R.font.custom_medium, FontWeight.Medium),
    Font(R.font.custom_bold, FontWeight.Bold)
)

val AppTypography = Typography(
    bodyLarge = TextStyle(
        fontFamily = CustomFontFamily,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    )
)

五、Shape 形状系统

val AppShapes = Shapes(
    extraSmall = RoundedCornerShape(4.dp),
    small = RoundedCornerShape(8.dp),
    medium = RoundedCornerShape(12.dp),
    large = RoundedCornerShape(16.dp),
    extraLarge = RoundedCornerShape(28.dp)
)

六、在组件中使用主题

@Composable
fun ThemedCard() {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surfaceVariant
        ),
        shape = MaterialTheme.shapes.medium
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = "标题",
                style = MaterialTheme.typography.titleMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
            Text(
                text = "正文内容",
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

七、扩展主题:自定义属性

// 定义扩展颜色
data class ExtendedColors(
    val success: Color,
    val onSuccess: Color,
    val warning: Color,
    val onWarning: Color
)

val LocalExtendedColors = staticCompositionLocalOf {
    ExtendedColors(
        success = Color.Unspecified,
        onSuccess = Color.Unspecified,
        warning = Color.Unspecified,
        onWarning = Color.Unspecified
    )
}

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val extendedColors = if (darkTheme) {
        ExtendedColors(
            success = Color(0xFF4CAF50),
            onSuccess = Color.White,
            warning = Color(0xFFFF9800),
            onWarning = Color.Black
        )
    } else {
        ExtendedColors(
            success = Color(0xFF2E7D32),
            onSuccess = Color.White,
            warning = Color(0xFFF57C00),
            onWarning = Color.Black
        )
    }

    CompositionLocalProvider(
        LocalExtendedColors provides extendedColors
    ) {
        MaterialTheme(
            colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme,
            content = content
        )
    }
}

// 使用扩展颜色
val extendedColors: ExtendedColors
    @Composable
    get() = LocalExtendedColors.current

@Composable
fun SuccessBadge() {
    Surface(
        color = extendedColors.success,
        contentColor = extendedColors.onSuccess
    ) {
        Text("成功")
    }
}

八、深色模式最佳实践

// 根据 elevation 调整 surface 颜色
Surface(
    tonalElevation = 3.dp  // Material 3 会自动调整颜色
) {
    // content
}

九、主题预览

@Preview(name = "Light")
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun CardPreview() {
    AppTheme {
        ThemedCard()
    }
}

总结

Material 3 主题系统的核心要点:

构建好主题系统后,整个应用的 UI 一致性将大大提升,品牌识别度也会更强。