안드로이드 API 35 버전에서는 Edge-To-Edge UI 디자인이 강제로 적용됩니다.
35버전 이전의 디바이스와 35버전 이상의 디바이스는 같은 레이아웃 코드를 작성해도 Edge-To-Edge UI 로 인해 다르게 보이게 됩니다.
그렇기에 35버전의 디바이스와 같은 UI로 보이고 싶다면 Edge To Edge UI를 적용해야만 합니다.
Edge-To-Edge UI
액티비티는 기본적으로 디스플레이 화면 전체를 사용하지 않습니다. 상태표시줄(Status bar)과 네비게이션바(Navigation bar) 영역을 제외한 부분만을 사용합니다. 예전에는 화면전체를 사용하려면 WindowManager 를 통해 Flag 설정을 직접 해야 했습니다. 하지만 안드로이드 버전이 변경되면 기존 Flag 설정으로 적용되지 않는 등의 문제가 있어 개발자들을 조금 불편하게 했습니다.
구글은 컴포즈 개발방법을 도입하면서 UI 적으로 화면 전체를 사용하게 함으로서 사용자에게 몰입감을 주고자 화면을 상단 끝(Edge)에서 하단 끝(Edge) 까지 사용하는 것을 권장합니다. 그래서 화면 전체 사용을 편하게 적용하게 하기 위해 enableEdgeToEdge() 기능을 제공하며 이를 기본 보일러플레이트 코드(미리 작성되어 있는 코드)로 추가하였습니다.
Android 15(api 35)버전 부터는 명시적인 enableEdgeToEdge() 사용 여부와 상관없이 강제로 적용됩니다.
위 그림에서 보듯이 Edge to Edge UI는 액티비티 영역이 시스템바(status bar & navigation bar) 영역까지 확장되었기에 화면의 상단 또는 하단에 뷰를 배치하면 시스템바에 의해 가려지는 문제가 발생합니다. 즉, 시스템바는 없어진 것은 아니고 액티비티 위에 오버레이 되어 그려집니다. 그렇기에 배경색이나 배경 이미지를 제외한 뷰들의 배치는 통상적으로 시스템바에 가려지지 않도록 최상위뷰(root view - id 가 R.id.main)에 안쪽 여백을 설정하는 padding 을 적용해줍니다.
다만, 네비게이션 바의 종류(제스처, 2버튼, 3버튼)나 디바이스의 종류에 따라 시스템바의 사이즈가 다르기에 리스너를 통해 시스템바의 사이즈를 동적으로 얻어와서 패딩을 적용합니다. 즉. 레이아웃 xml 파일에서 지정하지 않고 코틀린파일에서 코드로 지정하는 방식을 사용합니다. 이 또한 프로젝트를 생성하면 자동으로 작성되어 있는 보일러플레이트 코드로 되어 있습니다.
덕분에 코드가 지저분해 보이기도 합니다.
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//시스템바(status bar, navigation bar) 영역까지 액티비티의 영역을 확장(api35버전 부터 명시적 적용여부와 상관없이 강제로 적용)
enableEdgeToEdge()
//액티비티가 보여줄 레이아웃 뷰를 지정
setContentView(R.layout.activity_main)
//액티비티가 시스템바 영역까지 확대되면 시스템바에 의해 뷰들이 가려짐. 이에 최상위 뷰(R.id.main)를 찾아와서 시스템바 사이즈를 동적으로 얻어와 패딩을 적용.
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
}
새로 추가된 EdgeToEdge.enable(), setOnApplyWinowInsetsListener() 메소드의 역할을 조금 더 잘 이해하기 위해 예전처럼 레이아웃 뷰를 설정하는 setContentView()만 두고 나머지는 지우고 실행해서 차이점을 확인해 보겠습니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//액티비티가 보여줄 레이아웃 뷰를 지정
setContentView(R.layout.activity_main)
}
}
EdgeToEdge 코드를 제거했기에 api 34 버전까지의 디바이스에서는 시스템바(status bar, navigation bar)의 영역을 제외한 부분만 액티비티가 사용됩니다. api 35 버전에서는 EdgeToEdge 의 명시적인 적용여부와 상관없이 무조건 반영되기에 시스템바 영역까지 액티비티의 영역이 확장됩니다. 적용여부의 차이를 시각적으로 구분하기 위해 AVD 2대를 동시에 실해하여 비교해 보겠습니다. Android Studio는 한번에 여러대의 phone 을 실행할 수 있습니다. 다만 기본적으로는 웹 브라이저의 탭처럼 어려탭으로 나뉘어 한번에 하나의 디바이스 화면만 보입니다. 이를 동시에 좌우로 보기위해 애뮬레이터의 탭제목 영역을 우클릭하여 spilt 된 좌우 화면으로 볼 수 있습니다.
♣ 2대의 디바이스를 동시에 실행하고 좌우로 분리하여 보기
이제 준비가 되었으니 EdgeToEdge 가 적용되지 않았을때 35버전 이전과 이후가 다른 점을 확인해 보겠습니다. 동시에 실행해야 하는 만큼 디바이스 target 을 변경하면서 run 을 하겠습니다.
♣ api 34 버전과 api 35 버전의 실행결과 차이 확인
위 그림 좌측에 보여지는 34 버전의 경우에는 화면 상단의 보라색 영역인 상태표시줄과 화면 하단의 검정색 네비게이션바 영역을 제외한 부분만 액티비티가 차지하기에 하얀색 배경 테마가 시스템바를 제외한 부분만 차지합니다. EdgeToEdge 가 적용되지 않았을때 모습입니다.
이에 반해 그림 우측에 보여지는 35 버전의 경우에는 하얀색 테마의 액티비티가 하면 상단,하단의 시스템바(status bar, navigation bar)영역까지 확장되어 있어 화면 전체를 사용하는 모습을 볼 수 있습니다. 디스플레이 전체를 사용하기에 사용자가 화면에 대한 몰입감이 향상된다고 합니다.
결국, 같은 코드를 실행해도 35 버전 이전과 이후가 다르게 보이게 됩니다. 그렇기에 모든 사용자에게 같은 UI를 제공하기 위해서는 어쩔 수 없이 EdgeToEdge 를 적용해야만 합니다. 물론 설정을 통해 35 버전에서도 EdgeToEdge 를 사용하지 않도록 설정할 수도 있기는 합니다만 권장하지는 않습니다.
이제 EdgeToEdge UI 를 사용해보겠습니다. 패딩을 적용하기 위한 Insets 는 아직 적용하지 않겠습니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//시스템바(status bar, navigation bar) 영역까지 액티비티의 영역을 확장(api35버전 부터 명시적 적용여부와 상관없이 강제로 적용)
enableEdgeToEdge()
//액티비티가 보여줄 레이아웃 뷰를 지정
setContentView(R.layout.activity_main)
}
}
EdgeToEdge 를 적용했을 때 34 버전과 35 버전의 실행모습 확인
보다시피 네비게이션 바의 색상만 약간 회색일 뿐 같은 UI 구조를 보여주기에 사용자에세 통일감 있는 화면을 제공하기에 이제 Edge 적용은 선택이 아닌 필수라고 할 수 있습니다.
다만 액티비티의 영역이 화면 상단과 하단까지 차지하기에 시스템바에 의해 가려지는 부분이 발생합니다. 그렇기에 뷰의 배치를 상단 또는 하단에 위치하면 시스템바에 의해 가려지는 문제가 발생합니다.
확인을 위해 TextView를 화면 하단으로 이동하겠습니다. 글씨가 잘 보이도록 글씨 크기를 조금 키우도록 하겠습니다.
TextVeiw의 constraint 속성 중 app:layout_constraintTop_toTopOf="parent" 를 제거 하도록 하겠습니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textSize="64sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
EdgeToEdge UI 로 인해 텍스트뷰를 가리는 네비게이션바
이를 해결하기 위해 시스템바 영역을 제외한 크기만큼 최상위 뷰(R.id.main)에 padding 을 적용해 줘야 합니다. 다만, 시스템 바의 사이즈는 디바이스나 네비게이션바의 종류에 따라 달라 질 수 있기에 xml 레이아웃파일에서 적용하지 않고 코틀린코드에서 동적으로 적용해 줘야 합니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//시스템바(status bar, navigation bar) 영역까지 액티비티의 영역을 확장(api35버전 부터 명시적 적용여부와 상관없이 강제로 적용)
enableEdgeToEdge()
//액티비티가 보여줄 레이아웃 뷰를 지정
setContentView(R.layout.activity_main)
//액티비티가 시스템바 영역까지 확대되면 시스템바에 의해 뷰들이 가려짐. 이에 최상위 뷰(R.id.main)를 찾아와서 시스템바 사이즈를 동적으로 얻어와 패딩을 적용.
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
}
이제 시스템바의 사이즈 만큼 패딩이 적용되어 TextView 가 가려지지 않는 것을 확인할 수 있습니다.
참고로 setContentView() 와 setOnApplyWindowInsetsListener()의 적용 순서는 바뀌면 안됩니다. setContentView()가 실행되어야 xml 에 작성한 뷰 객체가 만들어 지고 그래야 Insets 를 적용할 최상위 뷰 객체를 찾아올 수 있기에 순서가 바뀌면 null point 예외가 발생하게 됩니다. 즉, 최초 프로젝트를 생성할 때 작성되어 있는 코드를 수정하지 않고 그냥 사용하시면 됩니다.
※ 종합 정리
android api 35 버전의 디바이스 부터는 명시적인 사용설정 여부와 상관없이 강제로 화면을 모두 사용하는 EdgeToEdge UI 가 적용됩니다. 그렇기에 enableEdgeToEdge() 를 적용하지 않는 앱을 만들게 되면 같은 앱을 다운받은 사용자들이라고 하더라고 사용하는 폰의 안드로이드 버전에 따라 다르게 보일 수 있게 됩니다.
그렇기에 이제는 선택의 여지 없이 enableEdgeToEdge()를 적용 할 수 밖에 없다고 보는게 좋을 것 같습니다.