#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 머터리얼 디자인 가이드
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
#6-3 본 프로젝트의 가장 최신 Commit
'App 개발 일지 > 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 |