使用Jetpack Compose Theme 为 App 轻松换肤

简介: Jetpack Compose Theme

#AndroidDevChallenge Week 3

在这里插入图片描述

整个开发过程中,除了会用到LayoutModifier等基本技术以外,最大的体会就是Compose的Theme太好用了!,这也是Google想在这个题目中考察和传达的重点。虽然不使用Theme也可以完成上面三个页面,但无疑开发效率会大大折扣。

<br/>

2. Compose Theme

传统Android开发中也需要配置Theme,即主题。Theme可以为UI控件提供统一的颜色和样式等,保证App视觉的一致性。主要区别在与:传统Theme依赖xml,而Compose完全基于Kotlin类型更安全、性能更优秀、使用更简单!

Kotlin的优势

当我们在AndroidStudio新建一个Compose模板工程时,IDE会自动创建theme文件夹

在这里插入图片描述

Color.ktShape.ktType.kt中通过Kotlin的常量分别定义各种样式,
Theme.kt中将这些样式应用到全局主题:

//Thmem.kt
private val DarkColorPalette = darkColors(
        primary = purple200,
        primaryVariant = purple700,
        secondary = teal200
)

private val LightColorPalette = lightColors(
        primary = purple500,
        primaryVariant = purple700,
        secondary = teal200
)

@Composable
fun MyAppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
    //根据theme的不同设置不同颜色
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
            colors = colors,
            typography = typography,
            shapes = shapes,
            content = content
    )
}

如上,使用Kotlin定义和切换theme都是如此简单,在Composable中基于if语句选择配置,然后静等下次composition生效就好了。

Theme工作原理

每个工程都提供${app name}Theme,用于自定义主题。例如MyAppTheme,最终会调用MaterialTheme,通过一些列Provider将配置映射为环境变量:

@Composable
fun MaterialTheme(
    colors: Colors = MaterialTheme.colors,
    typography: Typography = MaterialTheme.typography,
    shapes: Shapes = MaterialTheme.shapes,
    content: @Composable () -> Unit
) {
    val rememberedColors = remember { colors }.apply { updateColorsFrom(colors) }
    val rippleIndication = rememberRipple()
    val selectionColors = rememberTextSelectionColors(rememberedColors)
    CompositionLocalProvider(
        LocalColors provides rememberedColors,
        LocalContentAlpha provides ContentAlpha.high,
        LocalIndication provides rippleIndication,
        LocalRippleTheme provides MaterialRippleTheme,
        LocalShapes provides shapes,
        LocalTextSelectionColors provides selectionColors,
        LocalTypography provides typography
    ) {
        ProvideTextStyle(value = typography.body1, content = content)
    }
}

后续的UI都创建在MyAppTheme的content中,共享Provider提供的配置

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                // A surface container using the 'background' color from the theme
                ...
            }
        }
    }
}

当需要使用主题配置时,通过MaterialTheme静态对象访问,如下:

@Composable
fun Scaffold(
    ...
    drawerShape: Shape = MaterialTheme.shapes.large,
    ...
    drawerBackgroundColor: Color = MaterialTheme.colors.surface,
    ...
    backgroundColor: Color = MaterialTheme.colors.background,
    ...
    content: @Composable (PaddingValues) -> Unit
)

MaterialTheme从Provider中获取当前配置。

object MaterialTheme {

    val colors: Colors
        @Composable
        @ReadOnlyComposable
        get() = LocalColors.current

    val typography: Typography
        @Composable
        @ReadOnlyComposable
        get() = LocalTypography.current

    val shapes: Shapes
        @Composable
        @ReadOnlyComposable
        get() = LocalShapes.current
}

<br/>

3. 实战Theme

Bloom是这次挑战赛项目的名字,借助于Compose的Theme,我基本还原了设计稿的要求。

以下是完成效果,代码地址:Bloom

在这里插入图片描述

定义Theme

根据设计稿中的要求,我们在代码中定义Theme:

Color

在这里插入图片描述

首先在Color.kt中定义相关常量

//Color.kt
val pink100 = Color(0xFFFFF1F1)
val pink900 = Color(0xFF3f2c2c)
val gray = Color(0xFF232323)
val white = Color.White
val whit850 = Color.White.copy(alpha = .85f)
val whit150 = Color.White.copy(alpha = .15f)
val green900 = Color(0xFF2d3b2d)
val green300 = Color(0xFFb8c9b8)

然后通过lightColors定义白天的颜色

private val LightColorPalette = lightColors(
    primary = pink100,
    primaryVariant = purple700,
    secondary = pink900,
    background = white,
    surface = whit850,
    onPrimary = gray,
    onSecondary = white,
    onBackground = gray,
    onSurface = gray,
)

其中,primary等的定义来自MaterialDesign设计规范,根据颜色的使用场景频次等进行区分。有兴趣的可以参考MD的设计规范。

onPrimary等表示对应的背景色下的默认前景色,例如text,icon的颜色等:
在这里插入图片描述

相应的,夜间主题定义如下:

在这里插入图片描述

private val DarkColorPalette = darkColors(
    primary = green900,
    primaryVariant = purple700,
    secondary = green300,
    background = gray,
    surface = whit150,
    onPrimary = white,
    onSecondary = gray,
    onBackground = white,
    onSurface = whit850,
)

Type

在这里插入图片描述

//type.kt
val typography = Typography(
    h1 = TextStyle(
        fontFamily = FontFamily.SansSerif,
        fontWeight = FontWeight.Bold,
        fontSize = 18.sp,
    ),

    h2 = TextStyle(
        fontFamily = FontFamily.SansSerif,
        fontWeight = FontWeight.Bold,
        fontSize = 14.sp,
        letterSpacing = 0.15.sp
    ),

    subtitle1 = TextStyle(
        fontFamily = FontFamily.SansSerif,
        fontWeight = FontWeight.Light,
        fontSize = 16.sp
    ),

    body1 = TextStyle(
        fontFamily = FontFamily.SansSerif,
        fontWeight = FontWeight.Light,
        fontSize = 11.sp
    ),

    body2 = TextStyle(
        fontFamily = FontFamily.SansSerif,
        fontWeight = FontWeight.Light,
        fontSize = 12.sp
    ),

    button = TextStyle(
        fontFamily = FontFamily.SansSerif,
        fontWeight = FontWeight.SemiBold,
        fontSize = 14.sp,
        letterSpacing = 1.sp
    ),

    caption = TextStyle(
        fontFamily = FontFamily.SansSerif,
        fontWeight = FontWeight.SemiBold,
        fontSize = 12.sp
    )

)

Typography定义文字样式。h1body1等也是来自MaterialDesign中对于文字用途的定义。

Shape

在这里插入图片描述

//Shape.kt
val shapes = Shapes(
    small = RoundedCornerShape(4.dp),
    medium = RoundedCornerShape(26.dp),
    large = RoundedCornerShape(0.dp)
)

使用Theme

接下来,在代码中通过MaterialTheme获取当前配置就OK了,无需关心当前究竟是何主题。

在这里插入图片描述

以欢迎页的Beautiful home garden solutionsText为例,文字颜色需要根据主题(Light or Dart)变化。

如下,通过MaterialTheme设置Color可以避免if语句的出现

Text(
    "Beautiful home graden solutions",
    style = MaterialTheme.typography.subtitle1,
    // color = MaterialTheme.colors.onPrimary, //可省略
    modifier = Modifier.align(Alignment.CenterHorizontally),
)

前文介绍过,当背景色为primary时,前景默认会使用onPrimary,所以此处即使不设置Color,也会自动选择最合适的颜色。

再看下面Create accountButton

 Button(
    onClick = {},
    modifier = Modifier
        .height(48.dp)
        .fillMaxWidth()
        .padding(start = 16.dp, end = 16.dp)
        .clip(MaterialTheme.shapes.medium),
        //.background(MaterialTheme.colors.secondary),//Modifier设置背景色
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        )
) {
        Text(
             "Create account",
            // style = MaterialTheme.typography.button // 可省略
        )
}

文字需要以typography.button的样式显示,Button内部的text默认套用button样式,所以此处也可以省略。

Note:需要注意Button有专用的颜色设置字段,使用Modifier设置background无效

由于Button设置了backgroundColorMaterialTheme.colors.secondary,所以,内部的Text的颜色自动应用onSecondary,无需额外指定。

可见,Theme不仅有利于样式的统一配置,还可以节省不少代码量。

<br/>

4. 活用@Preview


视觉的调教有时需要反复确认,如果每次都要安装到设备查看效果将非常耗时。相对于传统xml布局鸡肋的预览效果,Compose提供的@Preview可以达到与真机无异的预览效果,而且还可以同屏预览多个主题、多种分辨率,便于对比。

@Preview(widthDp = 360, heightDp = 640)
@Composable
fun PreviewWelcomeLight() {
    MyTheme(darkTheme = false) {
        Surface(color = MaterialTheme.colors.background) {
            WelcomeScreen(darkTheme = false)
        }
    }
}

@Preview(widthDp = 360, heightDp = 640)
@Composable
fun PreviewWelcomeDark() {
    MyTheme(darkTheme = true) {
        Surface(color = MaterialTheme.colors.background) {
            WelcomeScreen(darkTheme = true)
        }
    }
}

如上,分别对DarkThemeLightTheme进行预览,@Preview中设置分辨率,然后就可以实时看到预览效果了。

在这里插入图片描述

@Preview是通过Composabl的实际运行实现真实的预览效果的,因此预览之前需要build,但是相对于安装到设备查看的方式已经快多了。

基于runtime的preview还有好处就是连交互也可以预览,点击右上角“手指”icon可以与preview进行交互;点击“手机”icon可以将预览画面部署到真机查看。

需要注意的是,因为预览需要保证Composable是可运行的,所以Preview只能接受无参的Composable。对于携带参数的Composable可以通过@PreviewParameter进行mock,但是mock数据本身也有成本,所以我们在设计Composable接口签名时要考虑对Preview是否友好,是否可以减少不必要的参数传递,或者为其提供默认值。

<br/>

5. 最后


最后对Theme的功能以及使用心得做一些总结:

  1. Compose的Theme相对于xml方式更加高效、方便
  2. 合理地使用Theme还有助于减少代码量
  3. 建议在项目开始之前,要求PM或者设计出具详细的Theme定义,提高RD开发效率
  4. 为Composable创建配套的@Preview,将大大提高UI的开发体验

参考

AndroidDevChallenge #Bloom

Theming in Compose

目录
相关文章
|
4月前
|
存储 缓存 编译器
探索 Jetpack Compose 内核:深入 SlotTable 系统
探索 Jetpack Compose 内核:深入 SlotTable 系统
73 1
|
4月前
|
IDE API 开发工具
Google I/O :Android Jetpack 最新变化(四)Compose
Google I/O :Android Jetpack 最新变化(四)Compose
111 0
|
4月前
|
前端开发 API Android开发
Jetpack Compose 实现波浪加载效果
Jetpack Compose 实现波浪加载效果
75 0
|
4月前
|
前端开发 算法 PHP
Jetpack Compose Runtime : 声明式 UI 的基础
Jetpack Compose Runtime : 声明式 UI 的基础
50 0
|
4月前
|
XML 前端开发 IDE
在 Compose 中使用 Jetpack 组件库
在 Compose 中使用 Jetpack 组件库
105 0
|
7月前
|
存储 算法 Android开发
Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(下)
Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(下)
37 0
|
8月前
|
前端开发 Android开发
Jetpack Compose 学习指南(二)
Jetpack Compose 学习指南(二)
88 0
|
7月前
|
缓存 API Android开发
Jetpack-Compose 学习笔记(一)—— Compose 初探(下)
Jetpack-Compose 学习笔记(一)—— Compose 初探(下)
48 0
|
1月前
|
XML 移动开发 Android开发
构建高效安卓应用:采用Jetpack Compose实现动态UI
【4月更文挑战第10天】 在现代移动开发中,用户界面的流畅性和响应性对于应用的成功至关重要。随着技术的不断进步,安卓开发者寻求更加高效和简洁的方式来构建动态且吸引人的UI。本文将深入探讨Jetpack Compose这一革新性技术,它通过声明式编程模型简化了UI构建过程,并提升了性能与跨平台开发的可行性。我们将从基本概念出发,逐步解析如何利用Jetpack Compose来创建具有数据动态绑定能力的安卓应用,同时确保应用的高性能和良好用户体验。
19 0
|
1月前
|
XML 开发工具 Android开发
构建高效的安卓应用:使用Jetpack Compose优化UI开发
【4月更文挑战第7天】 随着Android开发不断进化,开发者面临着提高应用性能与简化UI构建流程的双重挑战。本文将探讨如何使用Jetpack Compose这一现代UI工具包来优化安卓应用的开发流程,并提升用户界面的流畅性与一致性。通过介绍Jetpack Compose的核心概念、与传统方法的区别以及实际集成步骤,我们旨在提供一种高效且可靠的解决方案,以帮助开发者构建响应迅速且用户体验优良的安卓应用。