상세 컨텐츠

본문 제목

스크롤뷰 안에서 동적으로 뷰의 높이가 조절되도록 만들어보자

Android

by Marchbreeze 2024. 1. 19. 05:10

본문

참고자료 : WindowManager  |  Android Developers

 

WindowManager  |  Android Developers

android.inputmethodservice

developer.android.com

 

우선 구현이 완료된 영상을 확인해보자 ~

 

연습 영상 / 실제 활용 영상

 

Trouble

구현하고자 했던 뷰는 다음과 같았다.

“할일을 추가해 보세요” Empty 뷰가, 스크롤되는 제목 부분을 제외하고 가운데 높이에 정렬되도록 만들어야 했다.

 

그러나

  • ScrollView 내부의 레이아웃은 wrap_content로 작성되어야 하며
  • ScrollView 내부의 레이아웃의 내부에서, 외부에 top & bottom constraint를 부여할 수 없고
  • 내 뷰였던 CoordinatorLayout은 NestedScrollView를, AppbarLayout은 LinearLayout을 상속받았다.

 

그래서 다음과 같은 방법으로 해결하고자 했다.

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

 

TroubleShoot

(1) 화면 크기에서 높이를 측정하는 방법

fun Activity.getWindowHeight(): Int {
    val windowManager = this.getSystemService(Context.WINDOW_SERVICE) as WindowManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        val windowMetrics = windowManager.currentWindowMetrics
        val insets = windowMetrics.windowInsets
            .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
        return windowMetrics.bounds.height() - insets.bottom - insets.top
    } else {
        val displayMetrics = DisplayMetrics()
        windowManager.defaultDisplay.getMetrics(displayMetrics)
        return displayMetrics.heightPixels
    }
}
  • 재사용을 위해서 액티비티를 가져오는 확장함수로 빼주어서 작성했다.
  • 분기 처리를 진행해주어야 한다.
    • getDefaultDisplay() 함수는 This method was deprecated in API level 30 이기 때문에, 버전 별로 분기처리를 진행해주어야 하며
    • 대신 이후 버전부터는 getCurrentWindowMetrics() 를 활용해서 크기 측정 진행해야 한다.
  • 코드 설명 :
    1. windowManager
      • 현재 안드로이드 시스템에서 WINDOW_SERVICE에 대한 참조를 가져와 WindowManager 인스턴스를 생성한다.
      • WindowManager는 애플리케이션 윈도우를 관리하는 데 사용된다.
    2. windowMetrics
      • WindowManager 인스턴스를 통해 현재 윈도우의 메트릭스를 가져왔다.
      • 이 메트릭스에는 화면의 크기, 밀도 등 화면에 대한 다양한 정보가 포함된다.
    3. insets
      • windowMetrics에서 windowInsets를 가져와 시스템 UI(상태 바, 내비게이션 바 등)를 무시하는 실제 화면 영역을 계산한다.
    4. windowMetrics.bounds.height() - insets.bottom - insets.top
      • 현재 화면의 전체 높이에서 상단과 하단 인셋을 뺀 값을 반환한다.
      • 이 값은 앱이 실제로 사용할 수 있는 화면의 높이를 나타냈다.

 

(2) 레이아웃 크기를 비교해 뷰 크기를 고정하는 방법

binding.appbarMyTodo.viewTreeObserver.addOnGlobalLayoutListener(object :
    ViewTreeObserver.OnGlobalLayoutListener {

    override fun onGlobalLayout() {
        binding.appbarOurTodo.viewTreeObserver.removeOnGlobalLayoutListener(this)
        val displayHeight = activity?.getWindowHeight() ?: return
        val toolbarHeight = binding.toolbarMyTodo.height
        val appBarHeight = binding.appbarMyTodo.totalScrollRange
        binding.layoutOurTodoEmpty.layoutParams = (binding.layoutOurTodoEmpty.layoutParams).also {
            it.height = displayHeight - toolbarHeight - appBarHeight
        }
    }
})
  1. addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener
    • 앱바의 레이아웃이 변경될 때마다 이를 감지하기 위해 OnGlobalLayoutListener를 추가한다.
    • 뷰의 레이아웃이 변경될 때마다 호출되는 콜백 인터페이스이다.
  2. override fun onGlobalLayout()
    • 뷰가 그려질 때, 뷰의 크기나 위치가 변경될 때, 뷰가 추가되거나 제거될 때 등 뷰의 레이아웃이나 트리 구조에 변화가 생기면 호출되는 콜백 메서드이다.
  3. removeOnGlobalLayoutListener(this)
    • 이미 레이아웃 변경을 감지하고 동작을 수행했기 때문에, 더 이상 필요하지 않은 리스너를 제거한다.
    • OnGlobalLayoutListener는 뷰 레이아웃의 모든 변경사항을 감지하기 때문에 호출 빈도가 높아 성능에 영향을 줄 수 있기 때문에, 불필요하게 호출되는 것을 방지하기 위해 사용 후 제거해주어야 한다.
  4. displayHeight
    • 상위 액티비티의 현재 화면의 유효한 높이를 가져왔다.
    • 액티비티가 존재하지 않는 경우를 대비해서 앨리스 연산자 설정해주었다.
  5. toolbarHeight
    • View의 픽셀 높이값을 가져온다.
  6. appBarHeight
    • View의 스크롤 가능 범위를 가져온다.
  7. (binding.layoutOurTodoEmpty.layoutParams).also { it.height = displayHeight - toolbarHeight - appBarHeight }
    • layoutOurTodoEmpty 뷰의 높이를 전체에서 display, toolbar, appbar의 높이를 제외한 높이로 고정시켜준다.

 

(3) 스크롤 리스너에 적용시키는 방법

binding.appbarOurTodo.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
    val displayHeight = activity?.getWindowHeight() ?: return@addOnOffsetChangedListener
    val toolbarHeight = binding.toolbarOurTodo.height
    val appBarHeight = appBarLayout.totalScrollRange + verticalOffset
    binding.layoutOurTodoEmpty.layoutParams = (binding.layoutOurTodoEmpty.layoutParams).also {
        it.height = displayHeight - toolbarHeight - appBarHeight
    }
}
  • addOnOffsetChangedListener
    • AppBarLayout의 offset이 변경될 때마다 이를 감지하기 위해 OnOffsetChangedListener를 추가한다.
  • offset 
    • = 특정 위치나 점으로부터의 거리나 방향을 나타내는 값
    • verticalOffset = AppBarLayout의 수직 위치를 나타내는 값
    • AppBarLayout이 완전히 펼쳐져 있을 때의 offset = 0
    • AppBarLayout이 완전히 축소되었을 때의 offset = - (AppBarLayout의 totalScrollRange)

 

아자아자~~

관련글 더보기