지난 포스팅에 있었던 BottomSheet에 이어, SnackBar도 디자인시스템을 구현하면서 공부했던 것을 기록하고자 한다.
스낵바도 바텀시트와 동일하게 컴포즈로 작성이 되어 있으며 Material3 라이브러리를 사용하지 않는다.
사실 스낵바 관련 포스팅은 여러 번 올렸었다.
📍스와이프 이벤트 처리: https://comyou.tistory.com/130
📍LineBreak 속성: https://comyou.tistory.com/131
📍단어 단위 개행: https://comyou.tistory.com/136
그래서 이번 글에서는 Material 속성에 기반한 구현방식에 집중해서 작성할 것이다.
SnackBarHostState
SnackBarHostState는 스낵바의 상태를 관리하는 클래스이다.
Material과 흐름은 같으며, 필요한 부분만 가져왔다.
- mutex: 동시성 문제를 해결하기 위해 사용하였다. 이를 통해 한 번에 하나의 스낵바만 표시할 수 있게 되었다.
- currentSnackBarData: 현재 나타나는 스낵바의 데이터를 담기 위한 변수이다.
interface SnackBarData {
val message: String
val type: SnackBarType
fun dismiss()
}
- showSnackBar: 이 함수를 통해 스낵바를 표시한다. SnackBarDataImpl을 설정하여 화면에 나타낸다. (이는 바로 아래에서 다룬다.)
SnackBarDataImpl
SnackBarDataImpl는 스낵바에 나타낼 데이터를 표시해주는 클래스이다.
- message: 스낵바에 나타낼 텍스트이다.
- type: 스낵바의 타입을 나타낸다.
enum class SnackBarType {
Info,
Error,
}
- continuation: 스낵바가 사라질 때 코루틴을 해제하는 역할을 한다.
- dismiss()가 호출되면, showSnackBar()의 suspendCancellableCoroutine이 종료되면서 currentSnackBarData는 null이 된다.
SnackBarHost
SnackBarHost는 스낵바를 화면에 표시하는 컴포저블 함수이다.
우선 Material에 있는 SnackBarHost를 먼저 살펴보면,
LaunchedEffect를 활용하여 currentSnackBarData가 null이 아닐 경우, 일정 시간 딜레이 후 dismiss() 함수를 호출해 주고 있다.
그리고 FadeInFadeOutWithScale 함수를 통해 애니메이션을 적용하여 사라지고 나타내는 동작을 자연스럽게 해주고 있다.
내가 만든 SnackBarHost의 경우 Material과 크게 달라진 점은 snackBarData.type에 따라 알맞은 컴포저블 UI 함수를 띄운다는 것이다.
이때 snackBarHostState에 저장되어 있는 message가 있다면 띄우고, 스낵바를 닫는 콜백 함수가 불려지면 dismiss() 함수를 호출한다.
FadeInFadeOut
FadeInFadeOut은 머테리얼의 FadeInFadeOutWithScale와 같은 기능을 하는 함수이다.
각 변수에 대해 먼저 설명하자면,
- scheduledSnackBarData: 현재 표시 예정인 스낵바 데이터이다.
- snackBarTransitions: 화면에 표시할 스낵바 애니메이션 목록을 저장하는 리스트이다.
data class SnackBarTransitionItem(
val snackBarData: SnackBarData?,
val opacityTransition: OpacityTransition
)
typealias OpacityTransition = @Composable (snackBar: @Composable () -> Unit) -> Unit
- scope: 리컴포지션을 관리하기 위한 RecomposeScope이다.
그리고 이어지는 코드이다.
새로운 newSnackBarData가 들어오면, 기존 scheduledSnackBarData와 비교하여 scheduledSnackBarData 업데이트 한다.
그러면 현재 활성화된 snackBarTransitions에서 newSnackBarData를 추가해 준다.
이전에 있던 스낵바 애니메이션 데이터는 clear()를 통해 초기화해준다.
그리고 새로운 리스트를 만들고, 애니메이션을 적용해주는 것이다.
- 이때, 투명도 애니메이션을 위해서 animatedOpacity 함수를 만들어 주었다.
@Composable
private fun animatedOpacity(
visible: Boolean,
animateInSpec: AnimationSpec<Float>,
animateOutSpec: AnimationSpec<Float>,
): State<Float> {
val alpha = remember { Animatable(0f) }
LaunchedEffect(visible) {
alpha.animateTo(
if (visible) 1f else 0f,
animationSpec = if (visible) animateInSpec else animateOutSpec
)
}
return alpha.asState()
}
- 그리고 y축 offset 애니메이션을 위해서 animatedOffset 함수를 만들어 주었다.
@Composable
private fun animatedOffset(
visible: Boolean,
animateInSpec: AnimationSpec<Float>,
animateOutSpec: AnimationSpec<Float>,
): State<Float> {
val offsetY = remember { Animatable(0f) }
LaunchedEffect(visible) {
if (visible) {
offsetY.animateTo(
targetValue = TARGET_VALUE,
animationSpec = animateInSpec
)
} else {
offsetY.animateTo(
targetValue = 0f,
animationSpec = animateOutSpec
)
}
}
return offsetY.asState()
}
마지막으로 위의 애니메이션들을 적용한 스낵바 리스트를 UI에 표시해주면 된다.
동작 방식은 아래와 같다.
- 현재 존재하는 snackBarTransitions 리스트를 반복하며 각 스낵바를 표시한다.
- 각 스낵바에 고유한 key를 부여하여 리컴포지션 최적화한다.
- opacity를 통해 각 스낵바에 애니메이션 적용 후 표시한다.
디자인시스템, Handy
확실히 디자인시스템을 직접 구현하면서 그간 무심코 사용했던 컴포넌트들의 동작 방식을 파악할 수 있었던 것 같다.
이번에도 Handy 레포를 첨부하며 마무리 하도록 하겠다.
궁금한 점이 있다면 아래 레포를 통해 확인해주길 바란다.
https://github.com/yourssu/Handy-Android
GitHub - yourssu/Handy-Android: 내 주머니속에 쏙 들어오는 유어슈 디자인 시스템 for Android
내 주머니속에 쏙 들어오는 유어슈 디자인 시스템 for Android. Contribute to yourssu/Handy-Android development by creating an account on GitHub.
github.com
'Develop > Android' 카테고리의 다른 글
[Android] Baseline Profile로 성능 개선하기 (0) | 2025.02.18 |
---|---|
[Android] ProcessPhoenix 오픈소스 뜯어보기 (0) | 2025.02.02 |
[Android] BottomSheet 디자인시스템 구현기 (Material 뜯어보기) (0) | 2025.01.29 |
[Android] Compose Compiler Metrics로 성능 측정하기 (1) | 2024.12.27 |
[Android] 단어 단위의 개행하기 (Compose) (0) | 2024.11.22 |