[Android] Custom Bottombar

유정현·2024년 7월 19일
0

오프로드 프로젝트에서 디자인 시스템 구성 다음으로 맡은 부분이 커스텀 바텀바 생성이었다.

가운데에 튀어나온 아이콘을 포함한 바텀바를 직접 커스텀해야 했다. 어떻게 할까 서치를 했는데 방법은 정말 다양했다. 각자 원하는 방식으로 구현을 했었다. 나는 고민을 하다가 ConstraintLayout을 사용했다.

ConstraintLayout?

각 레이아웃에 ID를 부여해 xml에서의 ConstraintLayout 작업을 할 수 있도록 한다. createRefs() 또는 createRef()를 사용한다.

ConstraintLayout() {
	val (a, b) = createRefs()
    
    Text("text1", modifier = Modifier.constrainAs(a)) { }
    Text("text2", modifier = Modifier.constrainAs(b)) { }
}

modifier에서 constrainAs를 통해 어떤 참조에 해당하는 composable인지 명시해 표시할 수 있다. 그리고 linkTo()를를 통해 다른 composable과 제약 조건을 지정한다.

적용

오프로드에서는 다음과 같이 적용했다.

@Composable
private fun RowScope.MainBottomBarItem(
    tab: MainNavTab,
    selected: Boolean,
    ordinal: Int,
    colors: NavigationBarItemColors,
    onClick: () -> Unit,
) {
    ConstraintLayout(
        modifier = Modifier
            .weight(1f)
            .fillMaxHeight()
            .selectable(
                selected = selected,
                indication = null,
                role = null,
                interactionSource = remember { MutableInteractionSource() },
                onClick = onClick,
            )
    ) {
        val navBtn = createRef()
        when (ordinal) {
            0 -> {
                Icon(
                    painter = painterResource(tab.iconResId),
                    contentDescription = tab.contentDescription,
                    modifier = Modifier
                        .size(50.dp)
                        .constrainAs(navBtn) {
                            start.linkTo(parent.start, margin = 48.dp)
                            top.linkTo(parent.top)
                            bottom.linkTo(parent.bottom)
                        },
                    tint = if (selected) colors.selectedIconColor else colors.unselectedIconColor,
                )
            }

            1 -> {
                Icon(
                    painter = painterResource(tab.iconResId),
                    contentDescription = tab.contentDescription,
                    modifier = Modifier
                        .size(78.dp)
                        .constrainAs(navBtn) {
                            bottom.linkTo(parent.bottom, margin = 20.dp)
                            start.linkTo(parent.start)
                            end.linkTo(parent.end)
                        },
                    tint = Color.Unspecified,
                )
            }

            2 -> {
                Icon(
                    painter = painterResource(tab.iconResId),
                    contentDescription = tab.contentDescription,
                    modifier = Modifier
                        .size(50.dp)
                        .constrainAs(navBtn) {
                            end.linkTo(parent.end, margin = 48.dp)
                            top.linkTo(parent.top)
                            bottom.linkTo(parent.bottom)
                        },
                    tint = if (selected) colors.selectedIconColor else colors.unselectedIconColor,
                )
            }
        }

    }
}

기존의 바텀바 형태로 먼저 코드를 작성한 후에, 각 탭을 createRefs()로 3개의 요소들간의 관계를 표현했다. 그리고 margin 값을 통해 바텀바를 커스텀했다.

여기서 문제가 발생했다. 바텀바는 잘 완성했는데 탭 간에 이동하면서 가운데 아이콘이 깜빡이는 이슈가 발생했다. 이것저것 해보면서 애니메이션 속성이 들어가서 발생하는 문제라는 것을 알게 되었다.

AnimatedVisibility

그래서 바텀바 커스텀에 사용한 AnimatedVisibility을 자세히 살펴보았다.

@Composable
fun AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
    val transition = updateTransition(visible, label)
    AnimatedVisibilityImpl(transition, { it }, modifier, enter, exit, content = content)
}

AnimatedVisibility 내부에는 여러 파라미터가 있다.

  • visible: visible 상태에 따라 애니메이션이 트리거 된다. true 이면 enter transition이, false일 때는 exit transition이 수행된다.
  • enter과 exit: boolean 값에 따라 트리거 애니매이션 적용 여부를 결정한다.
  • label: 애니메이션에 대한 description을 지정한다.
  • content: animated visibility를 적용하는 content이다.

적용

AnimatedVisibility에 enter와 exit transition이 있다는 것을 알게 되었다. 그래서 결국 enter와 exit 파라미터를 None으로 지정해주면서 애니메이션이 사라졌고 깜빡이는 문제를 해결할 수 있었다.

@Composable
internal fun MainBottomBar(
    modifier: Modifier = Modifier,
    visible: Boolean,
    tabs: PersistentList<MainNavTab>,
    currentTab: MainNavTab?,
    onTabSelected: (MainNavTab) -> Unit,
) {
    AnimatedVisibility(
        visible = visible,
        enter = EnterTransition.None,
        exit = ExitTransition.None,
    ) {
        Column {
            Row(
                modifier = modifier
                    .fillMaxWidth()
                    .height(98.dp)
            ) {
                tabs.forEach { tab ->
                    MainBottomBarItem(
                        tab = tab,
                        selected = tab == currentTab,
                        ordinal = tab.ordinal,
                        colors = NavigationBarItemDefaults.colors(
                            selectedIconColor = Main1,
                            unselectedIconColor = BottomBarInactive,
                        ),
                        onClick = { onTabSelected(tab) },
                    )
                }
            }
            Spacer(
                modifier = Modifier
                    .height(48.dp)
                    .fillMaxWidth()
                    .background(Sub4)
            )
        }
    }
}

0개의 댓글