原文: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似乎不支援一個水平滾動巢狀垂直滾動(或垂直滾動中巢狀水平滾動),所以相應佈局需要合理設計

此外,提及下,如果想使用像ListViewRecyclerView那樣的列表元件,在Compose中可以使用LazyRowLazyColumn,這部分內容之後會講解到,敬請期待

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
)
Card(modifier = Modifier.fillMaxWidth().padding(20.dp),elevation = 10.dp) {
Text(text = "hello world")
}

效果如下:

參考