整个开发过程中,除了会用到Layout
、Modifier
等基本技术以外,最大的体会就是Compose的Theme太好用了!,这也是Google想在这个题目中考察和传达的重点。虽然不使用Theme
也可以完成上面三个页面,但无疑开发效率会大大折扣。
<br/>
2. Compose Theme
传统Android开发中也需要配置Theme,即主题。Theme可以为UI控件提供统一的颜色和样式等,保证App视觉的一致性。主要区别在与:传统Theme依赖xml
,而Compose完全基于Kotlin
,类型更安全、性能更优秀、使用更简单!
Kotlin的优势
当我们在AndroidStudio新建一个Compose模板工程时,IDE会自动创建theme文件夹
Color.kt、Shape.kt、Type.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
定义文字样式。h1
、body1
等也是来自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 solutions的Text
为例,文字颜色需要根据主题(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
,
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
设置了backgroundColor
为MaterialTheme.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)
}
}
}
如上,分别对DarkTheme和LightTheme进行预览,@Preview
中设置分辨率,然后就可以实时看到预览效果了。
@Preview
是通过Composabl的实际运行实现真实的预览效果的,因此预览之前需要build,但是相对于安装到设备查看的方式已经快多了。
基于runtime的preview还有好处就是连交互也可以预览,点击右上角“手指”icon可以与preview进行交互;点击“手机”icon可以将预览画面部署到真机查看。
需要注意的是,因为预览需要保证Composable是可运行的,所以Preview只能接受无参的Composable。对于携带参数的Composable可以通过@PreviewParameter
进行mock,但是mock数据本身也有成本,所以我们在设计Composable接口签名时要考虑对Preview是否友好,是否可以减少不必要的参数传递,或者为其提供默认值。
<br/>
5. 最后
最后对Theme的功能以及使用心得做一些总结:
- Compose的Theme相对于xml方式更加高效、方便
- 合理地使用Theme还有助于减少代码量
- 建议在项目开始之前,要求PM或者设计出具详细的Theme定义,提高RD开发效率
- 为Composable创建配套的@Preview,将大大提高UI的开发体验