edge-to-edge 처리하기

김민태·2024년 10월 20일
post-thumbnail

0. 발단

안드로이드 15(API 35) 타겟 부터는 엣지 투 엣지(edge-to-edge) 디자인이 기본값으로 적용되었습니다. 추후 스토어에 등록된 앱들이 API 35 버전으로 강제 될 예정이기 때문에 대응하는 과정을 작성해보겠습니다.

참고!
이 글은 build.gradle (:app)상에서 targetSdk = 35 환경에서 대응 작업을 했습니다.


1. 문제

이전 버전까지는 Status Bar, 하단의 Navigation Bar 영역만큼 자동으로 패딩이 적용되어 UI가 가려지지 않았지만, 안드로이드 15부터는 별도의 처리를 하지 않으면 시스템 바 아래에 UI가 위치해 가려질 수 있습니다.

Status Bar 예시

targetSdk 34이하 미대응
34 35
정상 상단바와 겹치는 문제 발생

Navigation Bar 예시

targetSdk 34이하 미대응
34 35
정상 하단 네비게이션과 겹치는 문제 발생

2. XML을 사용하는 환경에서 대응

  1. fragment 없이 복수의 activity 로 구성된 프로젝트
  2. activity를 두고 복수의 fragment간 이동하는 Single Activity Architecture 프로젝트
  3. WebView를 사용하는 Hybrid 프로젝트
  4. xml + Compose 혼합된 고도화 중인 프로젝트

등의 구조에서 사용할 수 있습니다.

2-1. XML에서

대응이 필요한 UI에 해당하는 xml 의 루트에 해당하는 레이아웃의 속성으로

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
	추가 ⬇️
    android:fitsSystemWindows="true"
    ... 생략            
    >

fitsSystemWindows="true" 지정하면 시스템 영역을 피해서 UI요소들이 배치됩니다.

다만 Android 15 환경에서는, fitsSystemWindows 사용은 더 이상 권장되지 않으며, WindowInsets 기반의 수동 처리가 공식적으로 안내되고 있습니다.

2-2. Activity / Fragment insets 처리

BaseActivity(Fragment) 같은 공통 코드의 onCreate()ViewCompat를 통해 처리합니다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding = ActivitySplashBinding.inflate(layoutInflater)
    setContentView(binding.root)

    ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
        val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
        view.setPadding(
            systemInsets.left,
            systemInsets.top,
            systemInsets.right,
            systemInsets.bottom
        )
        insets
    }
}

단 유의할 점은 신규 개발이 아닌 대응을 하는 시점에서 기존에 지정한 xml 상의 android:paddingHorizontal 같은 좌우 여백 값들이 무시됩니다.

그렇기 때문에 기존 view의 설정된 여백 값을 가져와 적용해주거나 여백 관리 방식을 바꿔야 합니다.

val initialPaddingLeft = view.paddingLeft
val initialPaddingRight = view.paddingRight

ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
    val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
    v.setPadding(
        initialPaddingLeft,
        systemInsets.top,
        initialPaddingRight,
        systemInsets.bottom
    )
    insets
}

위처럼 정의된 값을 가져와 그대로 적용하거나 initialPaddingLeft + systemInsets.left 이런식으로 기존 값과 더하는 방식으로 처리하면 기존 값을 살리면서 의도에 맞게 대응하게 됩니다.


3. Compose를 사용하는 환경에서 대응

3-1. Modifier 처리

컴포저블의 Modifier에 붙여 처리하는 방식이며 두가지 처리가 있습니다

  Column(
    modifier = Modifier
    // 1번 개별 처리
    .statusBarsPadding()
    .navigationBarsPadding()  
    // 2번 일괄 처리
	.windowInsetsPadding(WindowInsets.safeDrawing
    //.verticalScroll() 등등.. 
    )

3-1-1. 개별 처리

1번의 경우 상/하단 Inset 처리를 개별적으로 해주는 방식입니다.

하단 네비게이션을 사용하는 프로젝트에서 material3.NavigationBar 같은 컴포넌트를 사용하는 경우 컴포넌트가 자체적으로 하단 Inset을 고려해서 그려지기 때문에 별도의 하단 처리가 필요 없어질 수도 있습니다. 그럴 때 statusBarsPadding() 만 사용하여 개별 처리 할 수 있겠습니다.

3-1-2. 일괄 처리

2번의 경우 일괄적으로 처리하나 파라미터 값에 따라 처리되는 범위가 달라지며 대표적으로는 가장 넓은 범위의 안전영역을 대응할때 쓰는 safeDrawing가 있습니다.

그외:

  • WindowInsets.statusBars: 상태바(상단 시스템바) 영역
  • WindowInsets.navigationBars: 내비게이션바(하단 시스템바) 영역
  • WindowInsets.ime: 소프트 키보드(IME) 영역
  • WindowInsets.systemBars: 상태바 + 내비게이션바를 모두 포함
  • WindowInsets.displayCutout: 노치, 홀 등 디스플레이 컷아웃 영역
  • WindowInsets.captionBar: 캡션바(일부 기기에서만 사용)

3-2. Scaffold의 innerPadding

ScaffoldsnackbarHost, topBar, bottomBar 등 기본 UI를 그리기에 유리하다는 장점을 가지고 있으며, 시스템 인셋에 대한 처리를 지원합니다.

Scaffold(
  content = { innerPadding ->
    Box(
      modifier = Modifier
        .fillMaxSize()
        // 추가 ⬇️
        .padding(innerPadding)
    )
    ...
  }
 )

Scaffold 필수 슬롯인 content는 메인 콘텐츠를 할당하는 영역이며, 람다 파라미터로 제공하는 PaddingValues타입의 innerPadding를 통해서 위 처럼 시스템 인셋 처리를 할 수 있겠습니다.


4. 참조

동작 변경사항: Android 15
뷰에서 더 넓은 화면에 콘텐츠 표시
Compose의 더 넓은 화면
지원 중단된 API

0개의 댓글