Jetpack Compose學習(5)——從登錄頁美化開始學習布局組件使用


原文: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")
}

效果如下:

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM