原文:Jetpack Compose學習(5)——從登錄頁美化開始學習布局組件使用 | Stars-One的雜貨小窩
本篇主要講解常用的布局,會與原生Android的布局控件進行對比說明,請確保了解Android原生基本布局的知識,否則閱讀文章會存在有難度
之前我也是在第一篇中的入門實現了一個簡單的登錄頁面,也是有讀者評論說我界面太丑了💢
當時入門便是想整的簡單些,今天我便是實現美化來學習下布局的相關使用,這位同學看好了哦!😏
本系列以往文章請查看此分類鏈接Jetpack compose學習
登錄頁的美化工作
首先,我是先到網上找到了一份比較好看的登錄頁,地址為登錄頁|UI|APP界面|喵喵wbh - 原創作品 - 站酷 (ZCOOL),如下圖所示
我們照着實現,最終效果是這樣的(可能稍微有點不太像,不過應該還湊合看得過去吧!!)
背景圖設置和注冊按鈕
按照UI設計圖,我們需要設置背景圖,這里compose並不想之前Android原生組件,可以直接設置圖片,我是采取的Box布局來實現
Box布局與Frameayout相似,組件會按照順序從下向上排(z軸方向)
圖片由於設計圖沒給出來,於是我自己隨便找了張圖片代替
Box(Modifier.fillMaxSize()) {
Image(painter = painterResource(id = R.drawable.bg_login), contentDescription = null)
}
Modifier.fillMaxSize()
作用是讓布局填充滿寬度(與原生中的match_parent
同作用)
效果如下圖所示
這個時候我們考慮右上角加上有個注冊按鈕,同時,還需要個白色背景(放輸入框和登錄按鈕等),於是我們可以這樣寫
Box(Modifier.fillMaxSize()) {
Image(painter = painterResource(id = R.drawable.bg_login), contentDescription = null)
Text(
text = "注冊",
color = Color.White,
fontSize = 20.sp,
textAlign = TextAlign.End,
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
)
Column() {
Spacer(modifier = Modifier.weight(1f))
Column(
modifier = Modifier
.weight(3f)
.background(Color.White)
.padding(40.dp)
.fillMaxWidth()
) {
//后面輸入框等組件在這里加,由於代碼過長,為了方便閱讀,后續貼出的代碼都是在這里的代碼
}
}
}
textAlign
是文字對齊方式,但是需要Text自身寬度有空余才能看見效果(即設置個超過文本字數的寬度或直接填充父布局),Text組件的默認寬度是自適應的
Spacer
是空格布局,其背景色是透明的,Android原生的margin屬性的替代組件(因為設計問題,compose組件只提供padding設置)
Modifier.weight(1f)
表示權重,接收Float類型的數值,如果在Row使用,就是寬度權重占1,在Column使用,則是高度權重占1
上述代碼,我們將注冊的文字設置在右上方,且又加上加上了個Column,這個時候我們是將Column又分成了兩個組件,一個是Spacer(占1/4),一個是Column(占3/4)
由於上方是Spacer,其背景色是透明的,所以不會影響展示注冊文字按鈕(當然這里,我是用的Text組件,其實也可以使用TextButton組件)
效果如下所示
輸入框樣式調整
接下來我們調整下輸入框的樣式
val pwdVisualTransformation = PasswordVisualTransformation()
var showPwd by remember {
mutableStateOf(true)
}
val transformation = if (showPwd) pwdVisualTransformation else VisualTransformation.None
Column() {
TextField(
modifier = Modifier.fillMaxWidth(),
value = name,
placeholder = {
Text("請輸入用戶名")
},
onValueChange = { str -> name = str },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),
leadingIcon = {
Icon(
imageVector = Icons.Default.AccountBox,
contentDescription = null
)
})
TextField(
value = pwd, onValueChange = { str -> pwd = str },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text("請輸入密碼")
},
visualTransformation = transformation,
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),
leadingIcon = {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null
)
},trailingIcon = {
if (showPwd) {
IconButton(onClick = { showPwd = !showPwd}) {
Icon(painter = painterResource(id = R.drawable.eye_hide), contentDescription =null,Modifier.size(30.dp))
}
} else {
IconButton(onClick = { showPwd = !showPwd}) {
Icon(painter = painterResource(id = R.drawable.eye_show), contentDescription =null,Modifier.size(30.dp))
}
}
}
)
}}
這里設置了輸入框的背景色,改為了Color.Transparent
,且給前面設置了一個圖標
密碼則是有個顯示和隱藏密碼的開關,具體解釋可以看之前文章Jetpack Compose學習(3)——圖標(Icon) 按鈕(Button) 輸入框(TextField) 的使用 | Stars-One的雜貨小窩
效果如下圖所示
快捷登錄與忘記密碼
Row(horizontalArrangement = Arrangement.SpaceBetween,modifier = Modifier.fillMaxWidth()) {
Text(text = "快捷登錄", fontSize = 16.sp, color = Color.Gray)
Text(text = "忘記密碼", fontSize = 16.sp, color = Color.Gray)
}
horizontalArrangement
設置Row水平排列方式,取值感覺和前端的Flex布局很相似
SpaceBetween
的效果是布局里的組件元素左右兩邊對齊
效果如下
登錄按鈕
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
if (name == "test" && pwd == "123") {
Toast.makeText(context, "登錄成功", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "登錄失敗", Toast.LENGTH_SHORT).show()
}
},
shape = RoundedCornerShape(50),
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xff5c59fe)),
contentPadding = PaddingValues(12.dp, 16.dp)
) {
Text("登錄", color = Color.White, fontSize = 18.sp)
}
登錄按鈕設置為圓角的按鈕,且改變了下顏色
注意: 顏色的設置好像不支持這種類型:
#5c59fe
,使用的使用應該這樣使用:Color(0xff5c59fe)
,需要把#
替換為0xff
第三方登錄
Row(horizontalArrangement = Arrangement.SpaceBetween,verticalAlignment = Alignment.CenterVertically) {
Row(
Modifier
.height(1.dp)
.weight(1f)
.background(Color(0xFFCFC5C5))
.padding(end = 10.dp)){}
Text(text = "第三方登錄", fontSize = 16.sp, color = Color.Gray)
Row(
Modifier
.height(1.dp)
.weight(1f)
.background(Color(0xFFCFC5C5))
.padding(start = 10.dp)){}
}
Spacer(modifier = Modifier.height(20.dp))
Row(Modifier.fillMaxWidth(),horizontalArrangement = Arrangement.Center) {
repeat(3){
Column(Modifier.weight(1f),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
Image(modifier = Modifier.size(50.dp),painter = painterResource(id = R.drawable.qq), contentDescription = null)
Text("QQ", color = Color(0xffcdcdcd), fontSize = 16.sp,fontWeight = FontWeight.Bold)
}
}
}
下面的第三方登錄左右兩邊各有一個橫線,我是使用了Row作為線條(compose里也沒有組件,這樣做應該沒啥大問題)
至於底部的布局,每個Item是個Column,並使用居中堆積,且使用了權重平分了外面一個Row布局
這里簡單起見,就直接用了個循環(不會告訴你我懶得下圖標了)😑
至此,美化的工作就到這里了,下面針對上述出現的布局進行使用的講解
源碼
@Preview(showBackground = true)
@Composable
fun LoginPageDemo() {
var name by remember { mutableStateOf("") }
var pwd by remember { mutableStateOf("") }
val pwdVisualTransformation = PasswordVisualTransformation()
var showPwd by remember {
mutableStateOf(true)
}
val transformation = if (showPwd) pwdVisualTransformation else VisualTransformation.None
ComposeDemoTheme {
Box(Modifier.fillMaxSize()) {
Image(painter = painterResource(id = R.drawable.bg_login), contentDescription = null)
Text(
text = "注冊",
color = Color.White,
fontSize = 20.sp,
textAlign = TextAlign.End,
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
)
Column() {
Spacer(modifier = Modifier.weight(1f))
Column(
modifier = Modifier
.weight(3f)
.background(Color.White)
.padding(40.dp)
.fillMaxWidth()
) {
Column() {
TextField(
modifier = Modifier.fillMaxWidth(),
value = name,
placeholder = {
Text("請輸入用戶名")
},
onValueChange = { str -> name = str },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),
leadingIcon = {
Icon(
imageVector = Icons.Default.AccountBox,
contentDescription = null
)
})
TextField(
value = pwd, onValueChange = { str -> pwd = str },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text("請輸入密碼")
},
visualTransformation = transformation,
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),
leadingIcon = {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null
)
}, trailingIcon = {
if (showPwd) {
IconButton(onClick = { showPwd = !showPwd }) {
Icon(
painter = painterResource(id = R.drawable.eye_hide),
contentDescription = null,
Modifier.size(30.dp)
)
}
} else {
IconButton(onClick = { showPwd = !showPwd }) {
Icon(
painter = painterResource(id = R.drawable.eye_show),
contentDescription = null,
Modifier.size(30.dp)
)
}
}
}
)
}
Spacer(modifier = Modifier.height(20.dp))
Row(horizontalArrangement = Arrangement.SpaceBetween,modifier = Modifier.fillMaxWidth()) {
Text(text = "快捷登錄", fontSize = 16.sp, color = Color.Gray)
Text(text = "忘記密碼", fontSize = 16.sp, color = Color.Gray)
}
Spacer(modifier = Modifier.height(20.dp))
Button(
modifier = Modifier
.fillMaxWidth(),
onClick = {
if (name == "test" && pwd == "123") {
Toast.makeText(context, "登錄成功", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "登錄失敗", Toast.LENGTH_SHORT).show()
}
},
shape = RoundedCornerShape(50),
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xff5c59fe)),
contentPadding = PaddingValues(12.dp, 16.dp)
) {
Text("登錄", color = Color.White, fontSize = 18.sp)
}
Spacer(modifier = Modifier.height(100.dp))
Row(horizontalArrangement = Arrangement.SpaceBetween,verticalAlignment = Alignment.CenterVertically) {
Row(
Modifier
.height(1.dp)
.weight(1f)
.background(Color(0xFFCFC5C5))
.padding(end = 10.dp)){}
Text(text = "第三方登錄", fontSize = 16.sp, color = Color.Gray)
Row(
Modifier
.height(1.dp)
.weight(1f)
.background(Color(0xFFCFC5C5))
.padding(start = 10.dp)){}
}
Spacer(modifier = Modifier.height(20.dp))
Row(Modifier.fillMaxWidth(),horizontalArrangement = Arrangement.Center) {
repeat(3){
Column(Modifier.weight(1f),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
Image(modifier = Modifier.size(50.dp),painter = painterResource(id = R.drawable.qq), contentDescription = null)
Text("QQ", color = Color(0xffcdcdcd), fontSize = 16.sp,fontWeight = FontWeight.Bold)
}
}
}
}
}
}
}
}
布局容器
Box
首先介紹一下Box布局,和FrameLayout的特性一樣,是按順序排的
fun Box(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content: @Composable BoxScope.() -> Unit
)
- modifier 修飾符(下一篇講)
- contentAlignment 內容對齊方式(之前在Image圖片使用的時候提過了,詳見上一篇)
- propagateMinConstraints 是否應將傳入的最小約束傳遞給內容,不太懂具體是什么效果 😂
Row
Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
)
-
horizontalArrangement 子元素的水平方向排列效果
-
verticalAlignmentment 子元素的垂直方向對齊效果
horizontalArrangement
由上述代碼提示圖片,取值有五種,分別為:
Arrangement.Start
左排列Arrangement.Center
居中排列Arrangement.End
右排列Arrangement.SpaceBetween
左右對齊排列,最左和最右組件元素靠邊Arrangement.SpaceArround
左右對齊排列,最左和左右組件元素有間隔,且間隔相同,中間則是平分Arrangement.SpaceEvenly
左右對齊排列,且各組件元素間距相同
注意:使用此布局也是需要Row布局的寬度並不是自適應的
Column() {
Row(horizontalArrangement = Arrangement.Start,modifier = Modifier.fillMaxWidth()) {
Box(
Modifier
.background(Color.Green)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Blue)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Red)
.size(100.dp)) {
}
}
Row(horizontalArrangement = Arrangement.Center,modifier = Modifier.fillMaxWidth()) {
Box(
Modifier
.background(Color.Green)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Blue)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Red)
.size(100.dp)) {
}
}
Row(horizontalArrangement = Arrangement.End,modifier = Modifier.fillMaxWidth()) {
Box(
Modifier
.background(Color.Green)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Blue)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Red)
.size(100.dp)) {
}
}
Row(horizontalArrangement = Arrangement.SpaceBetween,modifier = Modifier.fillMaxWidth()) {
Box(
Modifier
.background(Color.Green)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Blue)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Red)
.size(100.dp)) {
}
}
Row(horizontalArrangement = Arrangement.SpaceAround,modifier = Modifier.fillMaxWidth()) {
Box(
Modifier
.background(Color.Green)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Blue)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Red)
.size(100.dp)) {
}
}
Row(horizontalArrangement = Arrangement.SpaceEvenly,modifier = Modifier.fillMaxWidth()) {
Box(
Modifier
.background(Color.Green)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Blue)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Red)
.size(100.dp)) {
}
}
}
PS: 感覺和前端的Flex布局很像,這里用文字描述可能不太清楚,可以參考下我的文章CSS Flex 彈性布局使用 | Stars-One的雜貨小窩或者參考下Flex布局的學習資料
補充下,Row本身是不支持滾動的(Column同理),但是想要滾動的話,可以使用Modifier.horizontalScroll()
來實現,代碼如下
Row(Modifier.horizontalScroll(rememberScrollState())) {
}
Modifier.horizontalScroll()
水平滾動Modifier.verticalScroll()
垂直滾動
注意:compose似乎不支持一個水平滾動嵌套垂直滾動(或垂直滾動中嵌套水平滾動),所以相應布局需要合理設計
此外,提及下,如果想使用像ListView
或RecyclerView
那樣的列表組件,在Compose中可以使用LazyRow
或LazyColumn
,這部分內容之后會講解到,敬請期待
verticalAlignmentment
取值有三個值:
- Alignment.CenterVertically 居中
- Alignment.Top 靠頂部
- Alignment.Bottom 靠底部
與上面一樣,布局高度如果是自適應的,則不會有效果
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
Modifier
.background(Color.Green)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Blue)
.size(100.dp)) {
}
Box(
Modifier
.background(Color.Red)
.size(100.dp)) {
}
}
Column
此布局和Row布局的參數一樣,只是名字有所區別,使用方法和上面都一樣
- verticalArrangement 垂直方向排列
- horizontalAlignmentment 水平方向對齊
Spacer
Spacer,直接翻譯的話,應該是空格,其主要就是充當margin的作用,一般使用modifier
修飾符來設置寬高占位來達到margin效果
Card
官方封裝好的Material Design的卡片布局
fun Card(
modifier: Modifier = Modifier,
shape: Shape = MaterialTheme.shapes.medium,
backgroundColor: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(backgroundColor),
border: BorderStroke? = null,
elevation: Dp = 1.dp,
content: @Composable () -> Unit
)
- shape 形狀,使用詳見Jetpack Compose學習(3)——圖標(Icon) 按鈕(Button) 輸入框(TextField) 的使用 | Stars-One的雜貨小窩
- backgroundColor 背景色
- contentColor 內容的背景色
- border 邊框,使用詳見Jetpack Compose學習(3)——圖標(Icon) 按鈕(Button) 輸入框(TextField) 的使用 | Stars-One的雜貨小窩
- elevation 陰影高度
Card(modifier = Modifier.fillMaxWidth().padding(20.dp),elevation = 10.dp) {
Text(text = "hello world")
}
效果如下: