誤入Android開發-Day4:UI元件們

影山小麥機
19 min readJul 31, 2024

來聊聊怎麼建構UI

Android Studio and Emulator

前言

昨天大致上的介紹了一下Jetpack Compose的用法,基本上可以稍微辨識建構整個UI邏輯的方式,接下來這個章節要談的會是UI元件具體有哪些,然後可以怎麼用。

那參考的reference基本上就是官網

然後這邊也稍微會用AI整理所有的元件可以怎麼使用,那我們就開始囉~

正文

基本元件

Text

用於顯示文本。

Text(text = "Hello, Jetpack Compose!")

Button

可點擊的按鈕。

Button(onClick = { /* Do something */ }) {
Text("Click Me")
}

Image

用於顯示圖片。

Image(painter = painterResource(id = R.drawable.my_image), contentDescription = "My Image")

Icon

用於顯示圖標。

Icon(imageVector = Icons.Default.Home, contentDescription = "Home Icon")

佈局元件

Column
垂直排列子元件。

Column {
Text("First item")
Text("Second item")
}

Row

水平排列子元件。

Row {
Text("First item")
Text("Second item")
}

Box

可以堆疊子元件。
(就是Flutter的Stack)

Box {
Text("Background text")
Text("Foreground text")
}

LazyColumn
用於顯示長列表,類似於 RecyclerView。
(iOS的TableView)

LazyColumn {
items(100) { index ->
Text("Item #$index")
}
}

LazyRow
水平滾動列表。
(iOS的CollectionView)

LazyRow {
items(100) { index ->
Text("Item #$index")
}
}

輸入元件

TextField

用於輸入文本。

var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it })

Checkbox
用於選擇或取消選擇。

var checked by remember { mutableStateOf(false) }
Checkbox(checked = checked, onCheckedChange = { checked = it })

Switch
用於開關選擇。

var switched by remember { mutableStateOf(false) }
Switch(checked = switched, onCheckedChange = { switched = it })

高級元件

這邊就會用到比較多除了佈局或基礎之外的元件,很多時候在佈局的時候,可能外面需要包一層這些物件,你會比較輕鬆的可以做出精緻的UI:

Card

Card {
Text("This is a card")
}

Scaffold
用於構建應用程序的基本結構,例如包含 AppBar、Drawer、FloatingActionButton 等。

(Flutter的Scaffold)

Scaffold(
topBar = { TopAppBar(title = { Text("Scaffold Example") }) },
content = { Text("Content goes here") },
floatingActionButton = {
FloatingActionButton(onClick = { /* Do something */ }) {
Icon(Icons.Filled.Add, contentDescription = "Add")
}
}
)

Snackbar
顯示短暫的消息。

Scaffold(
snackbarHost = { SnackbarHost(it) },
content = { paddingValues ->
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()

Button(onClick = {
coroutineScope.launch {
snackbarHostState.showSnackbar("This is a snackbar")
}
}) {
Text("Show Snackbar")
}
}
)

AlertDialog
用於顯示對話框。

var openDialog by remember { mutableStateOf(true) }

if (openDialog) {
AlertDialog(
onDismissRequest = { openDialog = false },
title = { Text(text = "Title") },
text = { Text("This is an alert dialog") },
confirmButton = {
TextButton(onClick = { openDialog = false }) {
Text("OK")
}
},
dismissButton = {
TextButton(onClick = { openDialog = false }) {
Text("Cancel")
}
}
)
}

Navigation

用於在不同的組件之間導航。

val navController = rememberNavController()

NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("details") { DetailScreen(navController) }
}

Drawer
用於顯示側邊導航抽屜。

val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()

Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
Column {
Text("Drawer Item 1")
Text("Drawer Item 2")
}
},
topBar = {
TopAppBar(
title = { Text("Drawer Example") },
navigationIcon = {
IconButton(onClick = {
coroutineScope.launch { scaffoldState.drawerState.open() }
}) {
Icon(Icons.Filled.Menu, contentDescription = "Menu")
}
}
)
},
content = { Text("Content goes here") }
)

BottomNavigation
用於顯示底部導航欄。

var selectedItem by remember { mutableStateOf(0) }
val items = listOf("Home", "Profile", "Settings")

Scaffold(
bottomBar = {
BottomNavigation {
items.forEachIndexed { index, item ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Home, contentDescription = item) },
label = { Text(item) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
}
) {
Text("Selected item: ${items[selectedItem]}")
}

TabRow

用於顯示標籤頁。

var selectedTabIndex by remember { mutableStateOf(0) }
val tabs = listOf("Tab 1", "Tab 2", "Tab 3")

Column {
TabRow(selectedTabIndex = selectedTabIndex) {
tabs.forEachIndexed { index, title ->
Tab(
selected = selectedTabIndex == index,
onClick = { selectedTabIndex = index },
text = { Text(title) }
)
}
}
when (selectedTabIndex) {
0 -> Text("Content for Tab 1")
1 -> Text("Content for Tab 2")
2 -> Text("Content for Tab 3")
}
}

Surface

用於包裹其他組件並應用背景色、形狀和陰影等效果。就有點像是Flutter的Container。

Surface(
color = MaterialTheme.colors.background,
shape = RoundedCornerShape(8.dp),
elevation = 4.dp
) {
Text("This is inside a Surface")
}

Canvas

用於繪製自定義圖形。

Canvas(modifier = Modifier.size(100.dp)) {
drawCircle(color = Color.Blue, radius = size.minDimension / 2)
}

簡單做個簡單的登入UI吧?

用以下的UI來示範稍稍Demo一下:

以下是程式碼的部分:

import android.annotation.SuppressLint
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Image
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Dp
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.draw.clip
import androidx.navigation.NavController
import com.example.huilaicommumity.android.R

@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun LoginScreen(navController: NavController) {
var account by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var isObscureText by remember { mutableStateOf(false) }

Scaffold(
topBar = {
TopAppBar(
backgroundColor = Color.White.copy(alpha = 0.1f),
title = { /* AppBar Title */ }
)
}
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.community_logo),
contentDescription = null,
modifier = Modifier.size(201.dp, 130.dp)
)

Spacer(modifier = Modifier.height(16.dp))
Column {
Text("手機號碼", style = MaterialTheme.typography.body1)
Spacer(modifier = Modifier.height(5.dp))
TextField(
value = account,
onValueChange = {
account = it
},
label = { Text("請輸入手機號碼") },
placeholder = { Text("請輸入手機號碼") },
modifier = Modifier
.fillMaxWidth()
.border(
width = 1.dp,
color = Color.Gray,
shape = RoundedCornerShape(8.dp)
)
.clip(RoundedCornerShape(8.dp)),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.White
)
)
}

Spacer(modifier = Modifier.height(16.dp))

Column {
Text("密碼", style = MaterialTheme.typography.body1)
Spacer(modifier = Modifier.height(5.dp))
TextField(
value = password,
onValueChange = {
password = it
},
visualTransformation = if (isObscureText) PasswordVisualTransformation() else VisualTransformation.None,
placeholder = { Text("請輸入密碼") },
modifier = Modifier
.fillMaxWidth()
.border(
width = 1.dp,
color = Color.Gray,
shape = RoundedCornerShape(8.dp)
)
.clip(RoundedCornerShape(8.dp)),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.White
),
trailingIcon = {
IconButton(onClick = { isObscureText = !isObscureText }) {
Icon(
imageVector = if (isObscureText) Icons.Filled.Image else Icons.Filled.Image,
contentDescription = null
)
}
},
)
}

Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = {
println("Login Action!")
// Implement login logic
},
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Teal),
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
) {
Text("登入", color = Color.White)
}

Spacer(modifier = Modifier.height(16.dp))

DividerRow()

Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = {
println("Facebook Login!")
// Implement Facebook login logic
},
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp)),
colors = ButtonDefaults.buttonColors(backgroundColor = Color.White),
) {
Image(
painter = painterResource(id = R.drawable.facebook_icon),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("使用 Facebook 帳號登入", color = Color.Black)
}

Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = {
println("Google Login!")
// Implement Google login logic
},
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp)),
colors = ButtonDefaults.buttonColors(backgroundColor = Color.White),
) {
Image(
painter = painterResource(id = R.drawable.google_icon),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("使用 Google 帳號登入", color = Color.Black)
}

Spacer(modifier = Modifier.height(16.dp))

Row(
verticalAlignment = Alignment.CenterVertically
) {
TextButton( onClick = {
println("忘記密碼!")
navController.navigate("ForgetPasswordPage")
}) {
Text("忘記密碼",color = Color.Black, style = MaterialTheme.typography.body1)
}
Spacer(modifier = Modifier.width(5.dp))
Text("還不是會員嗎?", style = MaterialTheme.typography.body1)
Spacer(modifier = Modifier.width(5.dp))
TextButton(onClick = {
println("註冊會員!")
navController.navigate("RegisterPage")
}) {
Text("註冊會員",color = Color.Black, style = MaterialTheme.typography.body1)
}
}
}
}
}

@Composable
fun DividerRow() {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Divider(
color = Color.Black.copy(alpha = 0.1f),
modifier = Modifier.weight(1f)
)
Text("或其他方式")
Divider(
color = Color.Black.copy(alpha = 0.1f),
modifier = Modifier.weight(1f)
)
}
}

好了,以上就是稍微做一點UI,嘗試一下整個Android Studio+Jetpack Compose在建構的時候的感覺。

小結

其實真的跟Flutter沒什麼差別,就是可能元件的名稱不太一樣,但在使用上基本是大同小異,不過Android Studio也是run起來需要很多環境的毛需要順一順,接下來可能會講一些跟ViewModel有關的東西,不過應該是不會太OOP,可能聊聊事件流,那麼就明天見囉!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

影山小麥機
影山小麥機

Written by 影山小麥機

本職為Mobile工程師,熱愛分享視野,也樂意站在ChatGPT的肩膀上。訂閱小麥機,收割技術、職涯、人生的難題。

No responses yet

Write a response