#1 ๊ฐ์
#1-1 NutrientChatBar()
...
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
NutricapturenewTheme {
...
Scaffold(
...
bottomBar = {
when(currentRoute) {
Destination.NutrientScreen.route -> NutrientChatBar()
else -> MainNavigationBar(navController)
}
},
...
) { ...
...
}
}
}
@Composable
private fun MainNavigationBar(navController: NavHostController) {
...
}
@Composable
fun NutrientChatBar() {
// TODO
}
}
...
์๋ BottomAppBar()๊ฐ ์๋ ์๋ฆฌ์, ์ปค์คํ ์ปดํฌ์ ๋ธ์ธ NutrientChatBar()๋ฅผ ๋ฃ๋๋ค. ๋ง์ ์ํ์ฐฉ์ค ๋์ ๊ธฐ๋ณธ ์ ๊ณต๋๋ BottomBar์ธ BottomAppBar()๋ ์ฌ์ฉํ๊ธฐ ์ด๋ ต๋ค๋ ํ๋จ์ ๋ด๋ ธ๋ค. ํ๋จ์ ๊ฐ์ฅ ์ฃผ์ํ ๊ทผ๊ฑฐ๋ BottomAppBar()์ ๊ฑธ๋ ค ์๋ ๋์ด ์ ํ์ด๋ค. ์ฌ๋ฌ๊ฐ์ง ๋ฐฉ๋ฒ์ ์จ ๋ดค์ง๋ง, 80.dp์ ์ด๊ณผํด ๋์์ง๊ฒ ๋ง๋ค ์ ์์๋ค. ํน์๋ผ๋ ์ด๋ป๊ฒํด์๋ ๋์ด ์ ํ์ ์์จ ๋ฐฉ๋ฒ์ ์ฐพ์๋, ์ด์ ๊ฐ์ ์ธ ๋ฐ ์๋ ๋ ๋ค๋ฅธ ์ ์ฝ์กฐ๊ฑด์ด ๋ช ๊ฐ ์กด์ฌํ ๊ฒ์ด๋ ์๊ฐ์ด ๋ค์๋ค. ์ด๋ด ๋ฐ์ ๊ทธ๋ฅ ์ปค์คํ BottomBar๋ฅผ ์ฐ๊ธฐ๋ก ํ ๊ฒ์ด๋ค. ๋, BottomAppBar() ์์ฒด๊ฐ ์ฑํ ๊ธฐ๋ฅ์ ์ํด ์กด์ฌํ๋ ์ปดํฌ์ ๋ธ๋ ์๋๋ค. ์ฉ๋๊ฐ ๋ค๋ฅด๋ ๊ตณ์ด BottomAppBar()๋ฅผ ๊ณ ์งํ ํ์๋ ์๋ค.
#1-2 ๋จธํฐ๋ฆฌ์ผ ๋์์ธ ๊ฐ์ด๋
Components – Material Design 3
Discover over 30 Material Design UI components and their functions. Understand how to use them to design intuitive and visually appealing user experiences.
m3.material.io
NutrientChatBar()์ ๋ชจ์ ๊ทธ๋ฆฌ๊ณ ์์ผ๋ก ์ ์ฉ๋ ๋ชจ๋ ์ปดํฌ์ ๋ธ ํจ์์ ์์น๋ ์ ๋งํฌ์ ์๋ ๋จธํฐ๋ฆฌ์ผ ๋์์ธ ๊ฐ์ด๋์ ๊ธฐ๋ฐํ ๊ฒ์ด๋ค.
#1-3 object Dimens { ... }
// package com.example.nutri_capture_new.ui.theme
object Dimens {
}
๋จธํฐ๋ฆฌ์ผ ๊ฐ์ด๋์ ๊ธฐ๋ฐํ๋ค๋ ๊ฒ์, ๋์์ธ ๊ฐ์ด๋์ ๋ช ์๋ ์์น๋ฅผ ๊ณ์ ์ฌ์ฌ์ฉํ๋ค๋ ๊ฒ์ด๋ค. ์ด๋ฅผ ์ํด Dimens ์ค๋ธ์ ํธ๋ฅผ ์ ์ธํด์ ๊ทธ ์์ ํ๋กํผํฐ๋ฅผ ์ฌ์ฌ์ฉํ๋ ค๊ณ ํ๋ค. ์ถ๊ฐ๋ก, ์ธ์ ๋ 100% ๋์์ธ ๊ฐ์ด๋๋ฅผ ๋ฐ๋ฅผ ์ ์์ ๊ฒ์ด๋ค. ํ๋ก์ ํธ์ ๋ง๊ฒ ์ฝ๊ฐ์ ์์น ์กฐ์ ์ด ํ์ํ ๋๊ฐ ์๊ธฐ ๋๋ฌธ์ด๋ค. ์ด๋ฐ ๊ฒฝ์ฐ๊น์ง ๊ณ ๋ คํด Dimens ์ ์ฝ๋๋ฅผ ์์ฑํ๋ค. ํ ๋ง๋๋ก ๋จธํฐ๋ฆฌ์ผ ๊ฐ์ด๋๋ฅผ ๋ฐ๋ฅด๋ ๋ฐ๋ฅด์ง ์๋ , ๋ณธ ํ๋ก์ ํธ์ ์ฐ์ผ ๋ชจ๋ ์์น๋ ์ฌ๊ธฐ์์ ๊ด๋ฆฌํ๋ค.
#2 ์ฝ๋ - NutrientChatBar()
#2-1 Dimens
// package com.example.nutri_capture_new.ui.theme
import androidx.compose.ui.unit.dp
object Dimens {
object ChatBar {
val minHeight = 80.dp
val maxHeight = 200.dp
val paddingTop = 12.dp
val paddingBottom = 12.dp
val paddingStart = 12.dp
val paddingEnd = 12.dp
}
}
NutrientChatBar๋ฅผ ์ํ ์์น๋ค. minHeight์ padding๋ค์ ๋จธํฐ๋ฆฌ์ผ ๋์์ธ ๊ฐ์ด๋ - Bottom app bar๋ฅผ ์ฐธ์กฐํ๋ค. maxHeight๋ ์ฐ์ 200.dp๋ก ์์ ์ค์ ํ๋ค. ์ถํ ์ธ๋ถ์กฐ์ ํ๊ฒ ๋ค.
#2-2 MainActivity
...
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
}
...
@Composable
fun NutrientChatBar() {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = Dimens.ChatBar.minHeight, max = Dimens.ChatBar.maxHeight)
.navigationBarsPadding() // ์์ผ๋ฉด ์ด ์ปดํฌ์ ๋ธ์ด ์์คํ
๋ค๋น๊ฒ์ด์
๋ฐ ๊ฐ๋ฆผ
.padding(
start = Dimens.ChatBar.paddingStart,
top = Dimens.ChatBar.paddingTop,
end = Dimens.ChatBar.paddingEnd,
bottom = Dimens.ChatBar.paddingBottom
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom
) {
}
}
}
...
Modifier.navigationBarsPadding()์ ์์คํ ๋ค๋น๊ฒ์ด์ ๋ฐ์ ์์ญ์ ์์คํ ์ผ๋ก๋ถํฐ ๋ฐ๋๋ค. ์ฌ์ฉ์์ ํด๋ํฐ์ด ์ด๋ค ๋์ด์ ๋ค๋น๊ฒ์ด์ ์์ญ์ ๊ฐ์ง๋๋ผ๋ ๋์ํ ์ ์๋ค.
#3 ์ฝ๋ - TextField
#3-1 Dimens
// package com.example.nutri_capture_new.ui.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
object Dimens {
object ChatBar {
...
}
object TextField {
val minHeight = 56.dp
val maxHeight = 200.dp
val roundedCorner = minHeight / 2
@Composable
fun textStyle(): TextStyle { // MaterialTheme.typography๋ @Composable ํจ์์์๋ง ์ ๊ทผ ๊ฐ๋ฅ
return MaterialTheme.typography.bodyLarge
}
}
}
TextField๋ฅผ ์ํ ์์น๋ค. ๋จธํฐ๋ฆฌ์ผ ๋์์ธ ๊ฐ์ด๋ - Text fields๋ฅผ ์ฐธ์กฐํ๋ค. Typography๋ @Composable ์์ญ์์๋ง ์ ๊ทผํ ์ ์๊ธฐ์, textStyle()๋ฅผ getter ์ญํ ์ ์ํํ @Composable ํจ์์ ํํ๋ก ๋์๋ค.
#3-2 MainActivity
...
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
}
...
@Composable
fun NutrientChatBar() {
Row(
...
) {
val inputtedText = remember { mutableStateOf("") }
TextField(
value = inputtedText.value,
onValueChange = { newText -> inputtedText.value = newText },
modifier = Modifier
.weight(1f)
.heightIn(min = Dimens.TextField.minHeight, max = Dimens.TextField.maxHeight)
.padding() // ์ด TextField()๋ฅผ ๊ฐ์ธ๋ Row()์ padding์ด, TextField์ padding ์ญํ ์ ๋์ ์ํ
.clip(shape = RoundedCornerShape(Dimens.TextField.roundedCorner)),
textStyle = Dimens.TextField.textStyle(),
placeholder = {
Text(
text = "๋ฉ์์ง ์
๋ ฅ",
style = Dimens.TextField.textStyle()
)
},
colors = TextFieldDefaults.colors().copy(
unfocusedIndicatorColor = Color.Transparent, // ๋งจ ํ๋จ์ ์๋ ๋ฐ์ค ํฌ๋ช
ํ
focusedIndicatorColor = Color.Transparent // TextField๊ฐ ํฌ์ปค์ค๋ ๋ ๋งจ ํ๋จ ๋ฐ์ค ์ ํฌ๋ช
ํ
)
)
}
}
}
...
colors ์์ฑ์์ TextField์ ์กด์ฌํ๋ ํ๋จ๋ถ ๋ฐ์ค์ ์์ด๋ค. ์ด ๋ฐ์ค์ ๊ทธ ๋ฐ์ค์ด ์ํ TextField๊ฐ ํ์ฌ Focus๋์๋์ง๋ฅผ ํ์ํ๋ ์ญํ ์ธ๋ฐ, TextField๊ฐ ํ ํ๋ฉด์ ๋จ ํ๋๋ง ์กด์ฌํ ์ํฉ์ด๊ธฐ์ ๋ฐ์ค์ ์ญํ ์ ํฐ ์๋ฏธ๋ฅผ ๊ฐ์ง์ง ๋ชปํ๋ค. ๋ฐ๋ผ์ ๊ฑฐ์ฌ๋ฆฌ๊ธฐ๋ง ํ ๋ฟ์ด๋ค.
#4 ์ฝ๋ - Icon ๋ฐ IconButton
#4-1 Dimens
// package com.example.nutri_capture_new.ui.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
object Dimens {
object ChatBar {
...
}
object TextField {
...
}
object IconButton {
val iconSize = 24.dp
val stateLayer = 40.dp
val targetSize = 48.dp
}
}
IconButton ๋ฐ ๊ทธ ์ Icon์ ์ํ ์์น๋ค. ๋จธํฐ๋ฆฌ์ผ ๋์์ธ ๊ฐ์ด๋ - Icon buttons๋ฅผ ์ฐธ์กฐํ๋ค. targetSize๋ ํฐ์น๊ฐ ๋๋ ์์ญ, stateLayer๋ ๋ฒํผ์ ์๊ฐ์ ์์ญ์ ์๋ฏธํ๋ค. stateLayer๋ targetSize์ ์ข ์
#4-2 MainActivity
...
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
}
...
@Composable
fun NutrientChatBar() {
Row(
...
verticalAlignment = Alignment.Bottom
) {
...
Spacer(modifier = Modifier.width(4.dp))
Box(
modifier = Modifier.height(Dimens.TextField.minHeight), // TextField์ minHeight์ ๋ง์ท์
contentAlignment = Alignment.Center
) {
FilledTonalIconButton(
onClick = {
inputtedText.value = ""
},
modifier = Modifier
.size(Dimens.IconButton.targetSize)
.padding((Dimens.IconButton.targetSize - Dimens.IconButton.stateLayer) / 2) // ์ด padding() ์ ๊ฑฐ ์, stateLayer๋ ์ฌ๋ผ์ง๊ฒ ๋จ (= stateLayer๊ฐ targetSize์ ๋๊ฐ์ ํฌ๊ธฐ๊ฐ ๋จ)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Send,
contentDescription = "์ ์ก",
modifier = Modifier.size(Dimens.IconButton.iconSize)
)
}
}
}
}
}
...
IconButton์ ๋์ด๋ 48.dp์ธ๋ฐ, ์ด๋ฌ๋ฉด TextField์ ์ต์ ๋์ด์ธ 56.dp๊ณผ ๊ฐ์ด ์ฐจ์ด๋๊ฒ ๋๋ค. Row()์ verticalAlignment๊ฐ Alignment.Bottom์ด๊ธฐ ๋๋ฌธ์, 'TextField์ ์ ์ค์์ ๊ฐ๋ก์ง๋ฅด๋ ์ํ์ '๊ณผ 'IconButton์ ์ ์ค์์ ๊ฐ๋ก์ง๋ฅด๋ ์ํ์ '์ด ์ผ์นํ์ง ์๊ฒ ๋๋ค. ๋ฐ๋ผ์, IconButton์ Box()๋ก ๊ฐ์ผ๋ค. Box()์ ๋์ด๋ฅผ TextField์ minHeight์ ๊ฐ๊ฒ ๋๊ณ , Box() ์ ์ปจํ ์ธ ์ ์ ๋ ฌ์ ์ค์ ์ ๋ ฌ๋ก ๋๋ค.
padding((Dimens.IconButton.targetSize - Dimens.IconButton.stateLayer) / 2) ๋ถ๋ถ์ targetSize์ stateLayer๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํ ์ฝ๋๋ค. ํ์ง๋ง padding์ ๊ทธ๋ฅ ์์ ๋ฒ๋ฆด๊น ์ถ๋ค. ์ด padding ๋๋ฌธ์ (= stateLayer ๋๋ฌธ์) ๋ฒํผ์ด ๊ณผ๋ํ๊ฒ ์์๋ณด์ด๊ธฐ ๋๋ฌธ์ด๋ค. ํ์ง๋ง padding์ ์์ ๋ฉด stateLayer๋ ์ฌ๋ผ์ง๊ฒ ๋๋ค. ์ฐ์ ์ ๋ ๋๊ณ ๋์ค์ ์ฑ์ ์ ์ฒด์ ์ผ๋ก ๋ค๋ฌ์ ๋ ๋ค์ ํ์ธํด๋ณด๊ฒ ๋ค. ์๋ง ์์จ ํ๋ฅ ์ด ๋๊ฒ ๋ค.
#5 ์์ฝ
์์น ์กฐ์ ์ ์ํ Dimens ์ค๋ธ์ ํธ๋ฅผ ์ ์ธํ๊ณ , ์ด์ ๊ธฐ๋ฐํด ChatBar์ ์ธ๊ด์ ๋ง๋ค์๋ค.
#6 ์์ฑ๋ ์ฑ
#6-1 ์คํฌ๋ฆฐ์ท
#6-2 ์ด ๊ฒ์๊ธ ์์ ์ Commit
GitHub - Kanmanemone/nutri-capture-new
Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.
github.com
#6-3 ๋ณธ ํ๋ก์ ํธ์ ๊ฐ์ฅ ์ต์ Commit
GitHub - Kanmanemone/nutri-capture-new
Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.
github.com
'๊ฐ๋ฐ ์ผ์ง ๐ป > Nutri Capture' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Nutri Capture ๋ฐฑ์๋ - ChatBar์ ViewModel ์ฐ๊ฒฐ (0) | 2025.01.02 |
---|---|
Nutri Capture ๋ฐฉํฅ์ฑ - ๊ฐ๋ฐ ์ผ์ ํ (1์ฐจ) (0) | 2024.12.31 |
Nutri Capture ํ๋ก ํธ์๋ - Typography (2) | 2024.12.28 |
Nutri Capture ํ๋ก ํธ์๋ - bottomBar ๋์ ๋ณ๊ฒฝ (1) | 2024.12.18 |
Nutri Capture ๋ฐฑ์๋ - ์ด์ง ํ์ ์ ์ฉ (1) | 2024.12.17 |