• Android 15 StatusBar, NavigationBar 확장으로 발생하는 문제 해결

    2025. 6. 11.

    by. 하루플스토리

    안녕하세요, 하루플입니다.

     

    최근 회사 프로젝트를 진행하면서 Android 15 버전을 대응하게 되었는데 간단(?)할 줄 알았으나 생각보다 시간을 써서 문제 해결 과정을 적어봅니다.

     

    Android15 부터 더 넓은 화면을 표시하도록 하는 edge to edge 함수가 모든 화면에 적용되고, 구글에서도 더 넓은 화면으로 개발하기를 권장하고 있습니다.

    상단 StatusBar 영역과, 하단 Navigation Bar 이 투명하게 확장되었습니다.

    이로인해 기존 앱 UI에 문제가 생겼습니다.

     

     

    Android 14 / Android 15

     

    Android 15에서는 StatusBar 부분이 투명해지면서 UI가 전체적으로 올라가게 되었고, 뒤로가기 버튼 등 여러 UI가 StatusBar, NavigationBar와 겹쳐지게 되었습니다.

     

     

    위 문제를 구글에서는 Inset 정보를 가져와서 margin이나 padding을 적용해서 해결하도록 권장하고 있습니다.

    ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
        val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
        val navBarHeight = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
        insets
    }

     

    저희 프로젝트는 대부분 Activity 하나에 여러개의 Fragment로 이루어져있고, 일부 Compose로 전환된 상태입니다.

    위 코드를 Activity와 Fragment에서 사용했을 때 아래의 2가지 문제가 발생했습니다.

     

    1. Fragment 에서 setOnApplyWindowInsetsListener 의 Inset 값이 호출되지 않는 문제 발생.
    2. 여러화면에서 동시에 setOnApplyWindowInsetsListener 호출시 가장 마지막에 호출된 리스너만 등록되는 문제 발생.

     

     

    사실 더 쉬운 방법으로 statusBar 와 navigationBar height를 가져오는 방법을 알고 있었지만, 앞으로의 구글 업데이트에 요긴하게 대처하고 구글이 권장하는 방법을 따르기 위해 Inset 을 사용하는 방법으로 계속 개발을 진행했습니다.

     

     

     

    1. StatusBar, NavBar Height 구하는 유틸리티 함수

    /**
     * Android 15 statusBar, navBar Deprecated
     * 이를 대비하여 최상단, 최하단 뷰에 마진을 추가하기 휘해 높이를 구하는 함수 입니다.
     */
    fun onSystemBarInsetsChanged(
        view: View,
        onChanged: (statusBarHeight: Int, navBarHeight: Int) -> Unit
    ) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
                val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
                val navBarHeight = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
                onChanged(statusBarHeight, navBarHeight)
                insets
            }
        } else {
            onChanged(0, 0)
        }
    }

    유틸리티 클래스에 statusBar와 navBar Height를 구하는 함수를 먼저 만들었습니다.

    리스너에서 Inset의 변화를 감지하면 람다함수로 전달하도록 했습니다.

     

     

     

     

    2.  뷰에 마진을 더하는 유틸리티 함수 개발

    private val baseTopMargins = mutableMapOf<View, Int>() // 함수를 여러번 실행해도 높이가 중복으로 증가하지 않도록 첫 마진값 저장
    
    fun applyStatusBarMargin(height:Int, views: List<View>) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            views.forEach { view ->
                val baseMargin = baseTopMargins.getOrPut(view) { view.marginTop }
                view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
                    topMargin = baseMargin + height
                }
            }
        }
    }
    
    private val baseBottomMargins = mutableMapOf<View, Int>() // 함수를 여러번 실행해도 높이가 중복으로 증가하지 않도록 첫 마진값 저장
    
    fun applyNavigationBarMargin(height: Int, views: List<View>) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            views.forEach { view ->
                val baseMargin = baseBottomMargins.getOrPut(view) { view.marginBottom }
                view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
                    bottomMargin = baseMargin + height
                }
            }
        }
    }

    함수를 여러번 실행했을 때 View의 마진이 중복으로 늘어나지 않도록 baseMargins 라는 전역변수를 만들어두었습니다.

     

     

     

     

    3. viewModel 에 값 저장

    viewModel 에 아래와 같이 height를 저장하는 변수를 선언합니다.

    val statusBarHeight = MutableLiveData<Int>().apply { value = 0 }
    val navBarHeight = MutableLiveData<Int>().apply { value = 0 }

     

    Activity 에 아래와 같이 height 정보를 ViewModel에 저장하도록 합니다.

    onSystemBarInsetsChanged(binding.root) { statusBarHeight, navBarHeight ->
        addProfileVM.statusBarHeight.value = statusBarHeight
        addProfileVM.navBarHeight.value = navBarHeight
    }

     

     

     

    4. observe로 margin 업데이트

    addProfileVM.statusBarHeight.observe(viewLifecycleOwner) {
        applyStatusBarMargin(height = it, views = listOf(binding.ivBack))
    }
    addProfileVM.navBarHeight.observe(viewLifecycleOwner) {
        applyNavigationBarMargin(height = it, views = listOf(binding.btnNext))
    }

    아까 만들어둔 마진 업데이트 유틸리티 함수를 활용해 뷰를 업데이트 합니다.

     

    NavigationBar에 가려진 RecyclerView

    RecyclerView의 경우 항목의 최하단에 padding이 추가되어야 Navigation Bar에 가려지지 않으므로 아래 코드를 작성해줍니다.

    binding.recyclerView.setPadding(
        binding.recyclerView.paddingLeft,
        binding.recyclerView.paddingTop,
        binding.recyclerView.paddingRight,
        navBarHeight // 하단 padding 추가
    )
    binding.recyclerView.clipToPadding = false // padding 영역까지 스크롤되도록 설정

     

     

     

    이렇게 Activity에서만 Inset을 얻어오고, 실제 뷰가 존재하는 Fragment에서 뷰를 업데이트 하는 간단하고 짧은 코드로 모든 화면을 Android 15에 대응하게 되었습니다.

     

     

    댓글