@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainScreenView()
}
}
}
✨ MainScreenView 함수는 하단 바를 포함하여 메인 화면을 구성하며, 화면 탐색을 위한 네비게이션 그래프를 호스팅하는 역할을 한다.
Scaffold는 앱의 레이아웃을 설정하는 데 사용된다.
bottomBar는 Scaffold의 하단에 배치되는 요소로 BottomNavigationUI 함수를 호출하여 바텀 네비게이션 뷰를 구성한다.
NavigationGraph는 Scaffold의 메인 Content로 NavigationGraph 함수가 호출되어 네비게이션 그래프를 설정한다.
@SuppressLint("UnusedMaterialScaffoldPaddingParameter", "UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun MainScreenView() {
val navController = rememberNavController()
Scaffold(
bottomBar = { BottomNavigationUI(navController = navController) } // 바텀 네비게이션 뷰
) {
NavigationGraph(navController = navController) // 네비게이션 그래프
}
}
✨ BottomNavigationUI 함수는 사용자가 여러 화면을 이동할 때 사용되는 바텀 네비게이션을 UI로 정의하는 역할을 한다.
@Composable
fun BottomNavigationUI(navController: NavHostController) {
// 바텀 네비게이션에 표시될 아이템 목록
val items = listOf(
BottomNavItem.Search,
BottomNavItem.Storage,
)
// 현재 선택된 라우트를 추적하기 위한 MutableState를 선언
val selectedRoute = remember { mutableStateOf(BottomNavItem.Search.screenRoute) }
// 네비게이션을 감싸 그림자와 모양을 설정하기 위한 Surface
Surface(
// Modifier.shadow 함수 내부에서 그림자를 그리기 위해 shadowElevation을 0으로 설정
shadowElevation = 0.dp,
modifier = Modifier
.padding(12.dp)
.height(60.dp)
.shadow(
color = colorResource(id = R.color.empty),
borderRadius = 32.dp,
blurRadius = 32.dp,
offsetY = 12.dp,
offsetX = 12.dp,
spread = 1f.dp
),
color = Color.Transparent
) {
BottomNavigation(
backgroundColor = Color.White,
contentColor = Color(0xFF3F414E),
modifier = Modifier
.clip(RoundedCornerShape(32.dp))
) {
// 현재의 BackStackEntry와 루트를 찾아 현재 경로를 결정
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
// 아이템 목록을 순회하여 BottomNavigationItem을 구성
items.forEach { item ->
BottomNavigationItem(
icon = {
Icon(
painter = painterResource(id = item.icon), // 아이콘 설정
contentDescription = stringResource(id = item.title), // 아이콘 설명
modifier = Modifier
.width(26.dp)
.height(26.dp), // 아이콘 크기
tint = if (currentRoute == item.screenRoute) {
colorResource(id = R.color.color_1) // 선택된 아이콘 색상
} else {
colorResource(id = R.color.empty) // 선택되지 않은 아이콘 색상
}
)
},
label = {
Text(
stringResource(id = item.title), // 라벨 텍스트
fontSize = 9.sp,
color = if (currentRoute == item.screenRoute) {
colorResource(id = R.color.color_1)
} else {
colorResource(id = R.color.empty)
}
)
},
selected = currentRoute == item.screenRoute,
alwaysShowLabel = true,
onClick = {
// 클릭된 아이템의 화면 라우트를 선택된 라우트로 설정
selectedRoute.value = item.screenRoute
// 네비게이션 그래프에서 시작 목적지의 경로를 찾음
val startDestination = navController.graph.findStartDestination().route
// 클릭된 아이템의 화면으로 네비게이션을 시작합니다.
navController.navigate(item.screenRoute) {
// 시작 경로로 백 스택을 pop하고 상태를 저장
if (startDestination != null) {
popUpTo(startDestination) {
saveState = true
}
}
// 목적지가 백 스택의 맨 위에 있으면 해당 목적지를 다시 시작하지 않도록 지정
launchSingleTop = true
// 목적지로 이동할 때 이전 상태를 복원하도록 지정
restoreState = true
}
}
)
}
}
}
}
onClick = {
// 클릭된 아이템의 화면 라우트를 선택된 라우트로 설정
selectedRoute.value = item.screenRoute
// 네비게이션 그래프에서 시작 목적지의 경로를 찾음
val startDestination = navController.graph.findStartDestination().route
// 클릭된 아이템의 화면으로 네비게이션을 시작
navController.navigate(item.screenRoute) {
// 시작 경로로 백 스택을 pop하고 상태를 저장
if (startDestination != null) {
popUpTo(startDestination) {
saveState = true
}
}
// 목적지가 백 스택의 맨 위에 있으면 해당 목적지를 다시 시작하지 않도록 지정
launchSingleTop = true
// 목적지로 이동할 때 이전 상태를 복원하도록 지정
restoreState = true
}
}
- selectedRoute.value = item.screenRoute : 클릭된 아이템의 화면 라우트를
selectedRoute
에 설정하며 현재 선택된 아이템을 추적하는 데 사용된다.- val startDestination = navController.graph.findStartDestination().route : 네비게이션 그래프에서 시작 목적지의
route
를 찾으며 현재 경로를 비교하여back stack
을 설정하는 데 사용된다.- navController.navigate(item.screenRoute) { ... } : 네비게이션을 시작한다.
item.screenRoute
는 클릭된 바텀 네비게이션 아이템의 화면 라우트이다.- if (startDestination != null) { popUpTo(startDestination) { saveState = true } } : 시작 경로로 백 스택을
pop
하고, 상태를 저장한다.- launchSingleTop = true : 목적지가 이미 스택에 있으면 새 인스턴스를 만들지 않습니다.
- restoreState = true : 목적지로 이동할 때 이전 상태를 복원하도록 지정한다.
✨ NavigationGraph 함수는 네비게이션을 설정하는 역할을 한다.
NavHost
를 사용하여 네비게이션을 호스팅하며 화면 간의 이동을 관리한다.
NavHost
는navController
를 사용하여 화면 간의 이동을 처리한다.startDestination
를 사용하여 네비게이션의 시작점을 정의한다.navHostController
는 네비게이션을 관리하는 데 사용되는 객체이다.composable
함수를 사용하여 각각의 바텀 네비게이션 아이템에 대한 화면을 정의한다.
sealed class BottomNavItem(
val title: Int, // 아이템의 제목 리소스
val icon: Int, // 아이템의 아이콘 리소스
val screenRoute: String // 아이템에 연결된 화면의 경로
) {
data object Search : BottomNavItem(R.string.fragment_search, R.drawable.ic_tab_search, SEARCH)
data object Storage :
BottomNavItem(R.string.fragment_favorite, R.drawable.ic_tab_storage, STORAGE)
}
@Composable
fun NavigationGraph(navHostController: NavHostController) {
// 네비게이션의 호스트를 설정
NavHost(navController = navHostController, startDestination = BottomNavItem.Search.screenRoute) {
// 바텀 네비게이션 아이템에 따른 화면을 정의
composable(route = BottomNavItem.Search.screenRoute) {
SearchListScreen() // 검색 화면을 표시하는 컴포저블을 호출
}
composable(route = BottomNavItem.Storage.screenRoute) {
StorageScreen() // 보관함 화면을 표시하는 컴포저블을 호출
}
}
}
fun Modifier.shadow(
color: Color = Color.Black,
borderRadius: Dp = 0.dp,
blurRadius: Dp = 0.dp,
offsetY: Dp = 0.dp,
offsetX: Dp = 0.dp,
spread: Dp = 0f.dp,
modifier: Modifier = Modifier
) = this.then(
modifier.drawBehind {
this.drawIntoCanvas {
val paint = Paint()
val frameworkPaint = paint.asFrameworkPaint()
val spreadPixel = spread.toPx()
val leftPixel = (0f - spreadPixel) + offsetX.toPx()
val topPixel = (0f - spreadPixel) + offsetY.toPx()
val rightPixel = (this.size.width + spreadPixel)
val bottomPixel = (this.size.height + spreadPixel)
if (blurRadius != 0.dp) {
frameworkPaint.maskFilter =
(BlurMaskFilter(blurRadius.toPx(), BlurMaskFilter.Blur.NORMAL))
}
frameworkPaint.color = color.toArgb()
it.drawRoundRect(
left = leftPixel,
top = topPixel,
right = rightPixel,
bottom = bottomPixel,
radiusX = borderRadius.toPx(),
radiusY = borderRadius.toPx(),
paint
)
}
}
)