在之前的博客中,我已经介绍了Compose 的基础UI和布局组件,现在我们就利用这些基础UI和布局组件去做一个实战项目。Bloom是Google提供的一个假想产品,我们可以作为练手项目使用,这个产品的详细UI设计稿大家可以自行去百度下,个人决定这里主要是熟练去使用Compose UI,不必要纠结设计稿,文末我会贴出项目的github地址,供读者参考。
项目的大致结构就是一个简单的Android项目,如下图所示:
在上图中是这个项目的结构图,我们主要介绍的是ui下theme包类的几个Kotlin配置类,当我们创建一个新项目是,Compose会在项目中生产
ui/theme
目录,包含四个文件,分别是Color.kt、Shape.kt、Theme.kt、Type.kt
;官方建议我没将颜色、字体、形状等配置信息放到这四个配置文件中,便于统一维护管理
配置的颜色信息是一个四字节的Int整形数字,每个字节保存着ARGB
对应的信息。例如Pink-100
对应的值是0xFFF1F1
,这里表示的只是低位RGB
三种颜色对应的信息,如果希望添加透明度,那么就在最高位添加一个字节表示透明度,比如希望透明度是100%
,那么就添加一个0xFF,最终的颜色值是0xFFFFF1F1
。0xFF
对应的十进制是255
,表示100%
的透明度,如果要表示85%
的透明度,则255x85%
对应的16进制是0xD8
,所以0xFFFFF1F1
对应的透明度为85%的颜色值为0xD8FFF1F1
。
在本项目中用到的颜色值定义如下:
val white = Color(0xFFFFFFFF)
val white150 = Color(0x26FFFFFF)
val white850 = Color(0xD9FFFFFF)
val pink100 = Color(0xFFFFF1F1)
val pink900 = Color(0xFF3F2C2C)
val green300 = Color(0xFFB8C9B8)
val green900 = Color(0xFF2D3B2D)
如今的主流APP中我们都可以看到很多的UI控件都使用了圆角,圆角的大小在传统的View开发时通常都是在drawable中新建一个xml定义按钮的圆角样式,这样很不方便,因为如果我们只是想改变圆角的大小时,就需要我们再定义一个xml样式,但是使用Shape.kt后就不存在这个问题,因为圆角大小的配置放到了一个统一的地方。使用的时候直接引用就行了
本项目中的圆角大小定义:
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
主题的配置相对要麻烦一些,由于篇幅原因,这里不多介绍,在后面会有专门的一篇博客对主题进行介绍,这里我们就先按照我提供的主题配置上就行了。本项目的主题配置如下:
private val BloomDarkColorPalette = darkColors(
primary = white,
secondary = green300,
background = green900,
surface = white150,
onPrimary = white850,
onSecondary = green900,
onBackground = pink900,
onSurface = white850,
)
private val BloomLightColorPalette = lightColors(
primary = pink900,
secondary = pink900,
background = pink100,
surface = white850,
onPrimary = pink100,
onSecondary = pink100,
onBackground = pink900,
onSurface = white150
)
open class WelcomePageAssets(var background:Int,var illos:Int,var logo:Int)
//亮色主题资源
object LightWelcomeAssets : WelcomePageAssets(
background = R.drawable.ic_light_welcome_bg,
illos = R.drawable.ic_light_welcome_illos,
logo = R.drawable.ic_light_logo
)
// 暗色主提资源
object DarkWelcomeAssets : WelcomePageAssets(
background = R.drawable.ic_dark_welcome_bg,
illos = R.drawable.ic_dark_welcome_illos,
logo = R.drawable.ic_dark_logo
)
internal var LocalWelcomeAssets = staticCompositionLocalOf { LightWelcomeAssets as WelcomePageAssets }
val welcomeAssets
@Composable
@ReadOnlyComposable
get() = LocalWelcomeAssets.current
enum class BloomTheme{
LIGHT,DARK
}
@Composable
fun GoogleBloomTheme(theme:BloomTheme = BloomTheme.LIGHT,content:@Composable ()->Unit){
val welcomeAssets = if(theme == BloomTheme.DARK) DarkWelcomeAssets else LightWelcomeAssets
CompositionLocalProvider(
LocalWelcomeAssets provides welcomeAssets
) {
MaterialTheme(colors = if (theme == BloomTheme.DARK)
BloomDarkColorPalette else BloomLightColorPalette,
typography = bloomTypoGraphy,
shapes = shapes,
content = content,
)
}
}
我们想用的字体可以在Type.kt中配置,如果要引入新字体,可以将下载下来的字体文件放到res/font目录下,如果没有这个目录可以自己建一个,然后使用如下的方式引入字体
val nunitoSansFamily = FontFamily(
Font(R.font.nunitosans_light, FontWeight.Light),
Font(R.font.nunitosans_semibold, FontWeight.SemiBold),
Font(R.font.nunitosans_bold, FontWeight.Bold)
)
本项目的字体配置
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
)
val nunitoSansFamily = FontFamily(
Font(R.font.nunitosans_light, FontWeight.Light),
Font(R.font.nunitosans_semibold, FontWeight.SemiBold),
Font(R.font.nunitosans_bold, FontWeight.Bold)
)
val bloomTypoGraphy = Typography(
h1 = TextStyle(
fontSize = 18.sp,
fontFamily = nunitoSansFamily,
fontWeight = FontWeight.Bold
),
h2 = TextStyle(
fontSize = 14.sp,
letterSpacing = 0.15.sp,
fontFamily = nunitoSansFamily,
fontWeight = FontWeight.Bold
),
subtitle1 = TextStyle(
fontSize = 16.sp,
fontFamily = nunitoSansFamily,
fontWeight = FontWeight.Light
),
body1 = TextStyle(
fontSize = 14.sp,
fontFamily = nunitoSansFamily,
fontWeight = FontWeight.Light
),
body2 = TextStyle(
fontSize = 12.sp,
fontFamily = nunitoSansFamily,
fontWeight = FontWeight.Light
),
button = TextStyle(
fontSize = 14.sp,
letterSpacing = 1.sp,
fontFamily = nunitoSansFamily,
fontWeight = FontWeight.SemiBold,
color = white
),
caption = TextStyle(
fontSize = 12.sp,
fontFamily = nunitoSansFamily,
fontWeight = FontWeight.SemiBold
)
)
所谓沉浸式主题栏适配就是指手机状态栏的颜色和我们的应用背景色相同,看起来感觉状态栏和我们的页面就像是一体的一样,有的做法是给标题栏设置一个透明的颜色,而本项目中使用的办法是将标题栏的颜色设置成和页面的背景色相同就可以了。设置标题栏的颜色需要用到一个库:
implementation "com.google.accompanist:accompanist-systemuicontroller:0.31.0-alpha"
引入这个库后,使用下面的方法设置状态栏的颜色:
@Composable
fun TransparentSystemBars(color: Color) {
val systemUiController = rememberSystemUiController()
val useDarkIcons = !isSystemInDarkTheme()
SideEffect {
systemUiController.setSystemBarsColor(
color = color,
darkIcons = useDarkIcons,
isNavigationBarContrastEnforced = false,
)
}
}
然后在对应的Activity中设置好主题,代码如下:
class MainActivity : ComponentActivity() {
private lateinit var theme: BloomTheme
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
actionBar?.apply { hide() }
setContent {
theme = if(isSystemInDarkTheme()){
BloomTheme.DARK
}else {
BloomTheme.LIGHT
}
GoogleBloomTheme(theme) {
val color = MaterialTheme.colors.background
TransparentSystemBars(color)
// 展示界面,例如本项目中展示欢迎页:WelcomePage()
}
}
}
}
如上面代码所示,我们根据当前的系统是否是深色主题判断使用的主题样式,然后把应用的当前的背景色传给设置状态栏颜色的方法,其他页面也是相同的做法。
首先我们可以先看下欢迎页:
我们可以将这个页面看成是背景加上内容,然后我们对显示的内容划分下,如下所示:
根据前面所学的知识,我们可以使用一个Column组件搞定这个页面的内容布局。而背景和内容的结合,我们可以使用Box组件,代码如下:
使用Box组件将内容和背景融合到一起,内容我们可以抽成一个组件继续去划分实现
@Composable
fun WelcomePage(){
Box(modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
){
Image(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = welcomeAssets.background)
),
contentDescription = "welcome page",
modifier = Modifier.fillMaxSize()
)
WelcomeContent()
}
}
内容组件又包括了一张叶子样式的图片,标题和两个按钮,我们可以分别抽成组件
@Composable
fun WelcomeContent(){
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(72.dp))
LeafImage()
Spacer(modifier = Modifier.height(48.dp))
WelcomeTitle()
Spacer(modifier = Modifier.height(40.dp))
WelcomeButtons()
}
}
上面的WelcomeContent()函数将要显示的内容拆分成了三个更小的组件,实现如下所示:
@Composable
fun LeafImage(){
Image(painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = welcomeAssets.illos)
),
contentDescription = "welcome leaf image",
modifier = Modifier
.wrapContentSize()
.padding(start = 88.dp))
}
@Composable
fun WelcomeTitle(){
Column(horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()) {
Image(painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = welcomeAssets.logo)),
contentDescription = "welcome logo",
modifier = Modifier
.wrapContentSize()
.height(32.dp))
Box(modifier = Modifier
.fillMaxWidth()
.height(32.dp),
contentAlignment = Alignment.BottomCenter){
Text(text = "Beautiful home garden solutions",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.primary)
}
}
}
@Composable
fun WelcomeButtons(){
Column(horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()) {
Button(onClick = { /*TODO*/ },
modifier = Modifier
.height(48.dp)
.padding(horizontal = 16.dp)
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium),
colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary)
) {
Text(
text = "Create account",
style = MaterialTheme.typography.button,
color = MaterialTheme.colors.onSecondary
)
}
Spacer(modifier = Modifier.height(24.dp))
TextButton(onClick = { /*TODO*/ }) {
Text(
text = "Log in",
style = MaterialTheme.typography.button,
color = MaterialTheme.colors.primary )
}
}
}
至此,一个使用Compose实现的欢迎页就完成了。是不是很简单,代码也很少。
建议读者将源码下载下来,跟着敲一遍,不懂的可以查官网,百度下,主要是为了熟悉如何使用Compose的开发。有问题也可以在评论区一起交流。
源码地址