본문 바로가기

[Android] ScrollView 안에서 동적으로 높이가 조절되는 뷰 만들기

@Marchbreeze2025. 5. 15. 22:32
2024년 1월 18일에 작성되었던 글입니다.

 

구현 영상

 

“할일을 추가해 보세요” Empty 뷰를 스크롤되는 제목 부분을 제외한 화면 중앙 높이에 정렬되게 만들고자 했습니다.

 

그러나, 다음과 같은 문제점이 있었습니다.

    1. CoordinatorLayoutNestedScrollView를, AppBarLayoutLinearLayout을 상속받아 동작합니다.
    2. ScrollView 내부 레이아웃은 wrap_content로만 작성할 수 있었습니다.
    3. 내부 컨테이너에 top·bottom 제약을 줄 수 없어, 외부 기준으로 높이를 중앙에 붙이기 어려웠습니다.

다음과 같은 방법으로 해결했습니다.

  1. 뷰에 wrap_content, match_parent가 아닌 고정 높이를 부여하되, 고정 높이를 원하는 높이만큼 설정합니다.
  2. 화면 크기를 잰 후, 들어가야하는 크기를 구하기 위해 다른 요소들의 높이를 제거해 고정할 높이를 결정합니다.
  3. 스크롤 리스너를 달아서 스크롤이 진행될 때마다 고정 높이를 변경해줌으로서 동적으로 높이를 변경시킵니다.

 

 


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 뷰가 스크롤 제목 영역을 제외한 나머지 화면 중앙에 정확히 정렬되도록 구현했습니다.

 


참고 자료:

WindowManager  |  Android Developers

Marchbreeze
Marchbreeze

안드로이드 개발자, 김상호입니다.

https://github.com/Marchbreeze

목차