[Compose] BottomNavigation 구현하기 (2/2) - 토스처럼 커스텀하기

코코아의 개발일지·2024년 3월 30일
0
post-thumbnail

✍🏻 요구사항 분석

지난 편에서는 BottomNavigation 기능 자체를 구현하는 데 집중했다.
‼️ 하지만 ‼️ 클론 코딩은 어디까지나 실제 앱마냥 착각할 수 있을 만큼 만들어야하는 것.
지난 편에서 구현한 것과, 실제 토스를 비교해보면서 디테일을 조금 더 살려보자.

지난 편에 구현한 모습 실제 토스

[커스텀 목록]
1. 상단 모서리 둥글게하기
2. 아이템 클릭 효과 커스텀하기
3. 시스템 모드(라이트/다크) 적용하기

사실 다른 BottomNavigationItem을 클릭했을 때 잠깐 아이콘이 작아졌다가 손을 떼면 원래대로 돌아오도록 만들고 싶었는데.. 방법을 찾지 못했다. 시도해보고 성공하게 된다면 다음번에 글을 하나 더 써보겠다.
그럼 우선 위의 커스텀 목록을 하나씩 살펴보겠다!

🤓 원래 코드

@Composable
fun BottomBar(navController: NavHostController) {
    val screens = listOf<BottomNavItem>(
        //...
    )
    //...

    BottomNavigation(
        backgroundColor = MaterialTheme.colorScheme.primaryContainer
    ) {
        screens.forEach { screens ->
            AddItem(item = screens, currentDestination = currentDestination, navController =navController )
        }
    }
}


@Composable
fun RowScope.AddItem(
    item: BottomNavItem,
    currentDestination: NavDestination?,
    navController: NavHostController
) {
    BottomNavigationItem(
        label = { Text(text = stringResource(item.title), fontSize = 10.sp) },
        icon = {
            Icon(
                painter = painterResource(item.icon),
                contentDescription = item.route,
            )
        },
        selected = currentDestination?.hierarchy?.any {
            it.route == item.route
        } == true,
        selectedContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
        unselectedContentColor = MaterialTheme.colorScheme.onSurface,
        onClick = {
            navController.navigate(item.route){
                popUpTo(navController.graph.findStartDestination().id)
                launchSingleTop =true
            }
        }
    )
}

사용되는 부분만 들고왔다. MainScreen.kt 전체 코드는 이 글 마지막에 다시 정리하겠다.

💻 코드 작성

1️⃣ 상단 모서리 둥글게하기

BottomBar() 함수 안에서 BottomNavigation를 Box로 감싸주면 된다.

// 상단 모서리 둥글게하기
    Box(Modifier
        .fillMaxWidth()
        .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp))
        .background(Color.Transparent)
        .border(
            0.3.dp,
            MaterialTheme.colorScheme.tertiaryContainer,
            RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
        )
    ) {
        BottomNavigation(
            // ..
        ){}
    }

상단 쪽만 radius를 줄 거기 때문에 topStart, topEnd를 20.dp로 RoundedCornerShape를 사용해 둥근 상단을 만들어준다.
border도 살짝 만들어준다.

그럼 이렇게 만들어진다!
하단에 네비게이션바 색은 우선 무시하도록 하자. (실제 토스는 투명색이다)

2️⃣ 아이템 클릭 효과 커스텀하기

@Composable
fun RowScope.AddItem(
    item: BottomNavItem,
    currentDestination: NavDestination?,
    navController: NavHostController
) {
    BottomNavigationItem(
        // label, icon, selectedContentColor 등 지정
        
        // 아이템 클릭 효과
        modifier = Modifier
            .width(24.dp)
            .padding(4.dp)
            .background(Color.Transparent, RoundedCornerShape(16.dp))
            .clip(RoundedCornerShape(16.dp))
    )
}

클릭 시 모습

이전 이후 실제 토스

실제 토스와 완벽하게 똑같지는 않지만.. (실제 토스는 클릭 효과가 거의 정사각형이고, 클릭하면 아이콘이 조금 작아진다.) Modifier에 width와 height를 둘 다 주거나, width 대신 size로 지정하게 되면 아이콘과 라벨이 나오는 영역이 조금 이상해져서ㅜㅜ 우선 저기에서 타협을 보기로 했다.
시작할 때 실제 앱마냥 착각할 수 있을 만큼 만들어야한다고 말했던 사람 나야나....
이것도 우선 구현할 다른 기능이 많으니 다음번을 기약하도록 하자....

3️⃣ 시스템 모드(라이트/다크) 적용하기

이건 간단하다!
라이트모드일 때와 다크모드일 때의 색상을 Color.kt에 미리 지정해두고, Theme에서 지정해주면 된다.

Color.kt

// 라이트모드 색상
val md_theme_light_primaryContainer = Color(0xFFFFFFFF) // backgroundColor
val md_theme_light_tertiaryContainer = Color(0xFFE7EBEE) // borderColor
val md_theme_light_onPrimaryContainer = Color(0xFF191F28) // selectedColor
val val md_theme_light_onSurface = Color(0xFFB0B8C1) // unselectedColor

// 다크모드 색상
val md_theme_dark_primaryContainer = Color(0xFF17171C) // backgroundColor
val md_theme_dark_tertiaryContainer = Color(0xFF18171C) // borderColor
val md_theme_dark_onPrimaryContainer = Color(0xFFFFFFFF) // selectedColor
val md_theme_dark_onSurface = Color(0xFF62626D) // unselectedColor

라이트-다크 색상이 1:1로 매칭되면 된다.
색상 코드의 경우 토스 화면을 캡쳐해서 추출한 색을 바탕으로 추가했다.

Theme.kt

private val LightColors = lightColorScheme(
    primaryContainer = md_theme_light_primaryContainer,
    onPrimaryContainer = md_theme_light_onPrimaryContainer,
    tertiaryContainer = md_theme_light_tertiaryContainer,
    onSurface = md_theme_light_onSurface,
    //...
)


private val DarkColors = darkColorScheme(
    primaryContainer = md_theme_dark_primaryContainer,
    onPrimaryContainer = md_theme_dark_onPrimaryContainer,
    tertiaryContainer = md_theme_dark_tertiaryContainer,
    onSurface = md_theme_dark_onSurface,
    // ...
)

@Composable
fun TossTheme(
    useDarkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable() () -> Unit
) {
    val colors = if (!useDarkTheme) {
        LightColors
    } else {
        DarkColors
    }

    MaterialTheme(
        colorScheme = colors,
        shapes = Shapes,
        content = content
    )
}

다른 색상도 추가했지만, BottomNavigation 구현에 쓰인 색상을 저들 뿐이라 나머지는 지웠다.

MainScreen.kt

@Composable
fun BottomBar(navController: NavHostController) {
    // ...

    // 상단 모서리 둥글게
    Box(
        Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp))
            .background(Color.Transparent)
            .border(
                0.5.dp,
                MaterialTheme.colorScheme.tertiaryContainer,
                RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
            )
    ) {
        BottomNavigation(
            backgroundColor = MaterialTheme.colorScheme.primaryContainer,
        ) {
            screens.forEach { screens ->
                AddItem(item = screens, currentDestination = currentDestination, navController =navController )
            }
        }
    }
}


@Composable
fun RowScope.AddItem(
    item: BottomNavItem,
    currentDestination: NavDestination?,
    navController: NavHostController
) {
    BottomNavigationItem(
        // label, icon, selected..
        selectedContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
        unselectedContentColor = MaterialTheme.colorScheme.onSurface,
    )

사용은 MaterialTheme.colorScheme.onPrimaryContainer이런 식으로 Theme에서 적어준 이름을 사용하면 된다.

그럼 라이트 <-> 다크로 변할 때 색상이 알아서 잘 바뀐다.

라이트모드다크모드

📱 완성 화면

light 라이트모드dark 다크모드

화면이 바뀔 때 조금 껌뻑이는 문제, 네이게이션바를 투명하게 하는 법,아이콘에 애니메이션 넣기 등 자잘한 이슈들은 다음번에 추가로 다뤄보겠다!

🔗 Github: https://github.com/nahy-512/Compose_Toss

자세한 코드는 깃허브에서 확인할 수 있다.

profile
우당탕탕 성장하는 개발자

0개의 댓글