告别XML,使用Compose Theme为你的app轻松换皮

在这里插入图片描述

1. Compose挑战赛第三周


关注过我前面文章的朋友应该对最近举行的Compose挑战赛有所了解,本周挑战赛进入到第三轮。#AndroidDevChallenge Week 3
在这里插入图片描述
与前两轮规则不同,本轮主要是比拼速度。只有第一个按要求完成并提交的人能胜出,奖品是Pixel 5手机一台。题目要求基于Compose完成以下三个页面,Google会提供完成页面必须的一些资源以及视觉设计稿。
在这里插入图片描述
题目本身难度不高,主要是拼手速。自从结婚后老夫的手速退化严重,top1出线就不指望了,但本着重在参与的精神仍然坚持完成了项目,主要是希望从中找到一些可与大家分享的东西。

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


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都会默认提供${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
}

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 account 按钮,

 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不仅有利于样式的统一配置,在使用时还可以节省不少代码量。


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是否友好,是否可以减少不必要的参数传递,或者为其提供默认值。


5. 总结


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

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

6. 参考


AndroidDevChallenge#Bloom
Theming in Compose

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fundroid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值