2024년 1월 18일에 작성되었던 글입니다.
구현 영상
“할일을 추가해 보세요” Empty 뷰를 스크롤되는 제목 부분을 제외한 화면 중앙 높이에 정렬되게 만들고자 했습니다.
그러나, 다음과 같은 문제점이 있었습니다.
- CoordinatorLayout은 NestedScrollView를, AppBarLayout은 LinearLayout을 상속받아 동작합니다.
- ScrollView 내부 레이아웃은 wrap_content로만 작성할 수 있었습니다.
- 내부 컨테이너에 top·bottom 제약을 줄 수 없어, 외부 기준으로 높이를 중앙에 붙이기 어려웠습니다.
다음과 같은 방법으로 해결했습니다.
- 뷰에 wrap_content, match_parent가 아닌 고정 높이를 부여하되, 고정 높이를 원하는 높이만큼 설정합니다.
- 화면 크기를 잰 후, 들어가야하는 크기를 구하기 위해 다른 요소들의 높이를 제거해 고정할 높이를 결정합니다.
- 스크롤 리스너를 달아서 스크롤이 진행될 때마다 고정 높이를 변경해줌으로서 동적으로 높이를 변경시킵니다.
1. 화면 전체 높이 측정
fun Activity.getWindowHeight(): Int {
val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val metrics = wm.currentWindowMetrics
val insets = metrics.windowInsets.getInsetsIgnoringVisibility(
WindowInsets.Type.systemBars()
)
return metrics.bounds.height() - insets.top - insets.bottom
} else {
val dm = DisplayMetrics()
wm.defaultDisplay.getMetrics(dm)
return dm.heightPixels
}
}
- API 레벨 분기를 통해 Android R(API 30) 이상에서는 currentWindowMetrics, 그 이하에서는 defaultDisplay를 사용했습니다.
- windowManager : 애플리케이션 윈도우를 관리하는 데 사용되며, 안드로이드 시스템에서 WINDOW_SERVICE에 대한 참조해서 인스턴스를 생성할 수 있습니다.
- windowMetrics : 현재 윈도우(앱 창)의 크기, 밀도 등 화면의 다양한 정보를 제공합니다.
- windowInsets : 시스템 UI(상태 바, 내비게이션 바 등)의 크기를 알려 주어, 실제 콘텐츠 영역을 계산할 때 사용합니다.
- windowMetrics의 bounds.height()에서 두 시스템 바 높이를 뺀 값이 실제 사용 가능한 화면 높이입니다.
2. 초기 레이아웃 시 높이 고정
binding.appbarOurTodo.viewTreeObserver
.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.appbarOurTodo.viewTreeObserver.removeOnGlobalLayoutListener(this)
val displayHeight = activity?.getWindowHeight() ?: return
val toolbarHeight = binding.toolbarOurTodo.height
val appBarHeight = binding.appbarOurTodo.totalScrollRange
binding.layoutOurTodoEmpty.layoutParams =
binding.layoutOurTodoEmpty.layoutParams.also {
it.height = displayHeight - toolbarHeight - appBarHeight
}
}
})
- OnGlobalLayoutListener : 뷰 계층 구조가 변경되거나 초기 레이아웃이 완료될 때마다 알림을 받는 리스너입니다. 레이아웃 크기나 위치가 확정된 이후에 후속 작업(높이 계산 등)을 수행할 때 유용합니다.
- removeOnGlobalLayoutListener : 한 번만 계산 후, 리스너 추가 호출을 막아 성능 이슈를 방지했습니다.
- layoutOurTodoEmpty 뷰의 높이를 전체에서 상단 화면, toolbar, appbar의 높이를 제외한 높이로 고정시키도록 구현했습니다.
3. 스크롤 변화에 따른 동적 높이 조정
binding.appbarOurTodo.addOnOffsetChangedListener { appBar, verticalOffset ->
val displayHeight = activity?.getWindowHeight() ?: return@addOnOffsetChangedListener
val toolbarHeight = binding.toolbarOurTodo.height
// verticalOffset는 음수, totalScrollRange와 더해 현재 펼쳐진 높이를 계산
val appBarExpandedHeight = appBar.totalScrollRange + verticalOffset
binding.layoutOurTodoEmpty.layoutParams =
binding.layoutOurTodoEmpty.layoutParams.also {
it.height = displayHeight - toolbarHeight - appBarExpandedHeight
}
}
- addOnOffsetChangedListener : AppBarLayout이 스크롤되면서 축소·확장될 때마다 verticalOffset을 전달받습니다.
- verticalOffset : 0일 때는 완전히 펼쳐진 상태, -totalScrollRange일 때는 완전히 축소된 상태를 나타냅니다.
- 이 값을 활용해, Empty 뷰가 항상 남은 영역의 중앙에 위치하도록 높이를 계속 업데이트했습니다.
이처럼 화면 높이 계산 → 초기 레이아웃 시 높이 고정 → 스크롤 변화에 따른 재조정의 흐름으로, Empty 뷰가 스크롤 제목 영역을 제외한 나머지 화면 중앙에 정확히 정렬되도록 구현했습니다.
참고 자료:
'XML' 카테고리의 다른 글
[Android] Fragment Navigation에 ProgressBar 연동 및 애니메이션 추가하기 (0) | 2025.05.16 |
---|---|
[Android] MultiViewType으로 Grid RecyclerView에 Header 추가하기 (0) | 2025.05.16 |
[Android] TabLayout 형태의 스크롤 가능한 Sticky Header 구현하기 (0) | 2025.05.15 |
[Android] anim 리소스로 리스트 내 아이템 삭제 애니메이션 구현하기 (0) | 2025.05.14 |
[Android] 양방향 데이터 바인딩으로 EditText 중복 여부 확인하기 (0) | 2025.05.14 |