오프로드 프로젝트에서 디자인 시스템 구성 다음으로 맡은 부분이 커스텀 바텀바 생성이었다.
가운데에 튀어나온 아이콘을 포함한 바텀바를 직접 커스텀해야 했다. 어떻게 할까 서치를 했는데 방법은 정말 다양했다. 각자 원하는 방식으로 구현을 했었다. 나는 고민을 하다가 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을 자세히 살펴보았다.
@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 내부에는 여러 파라미터가 있다.
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)
)
}
}
}