
안드로이드는 상태바나 네비게이션바와 같은 시스템 UI를 그립니다. 이 시스템 UI는 사용자가 어떤 앱을 사용하고 있는지와 관계없이 표시됩니다. WindowInsets는 앱이 시스템 UI와 겹칠 수 있는 화면 부분을 나타냅니다. 앱이 올바른 영역에 표시되고 시스템 UI에 의해 앱이 가려지지 않도록 시스템 UI에 관한 정보를 제공합니다.
Android 14 (API 수준 34) 이하에서는 기본적으로 앱의 UI가 시스템바와 디스플레이 컷아웃 뒤에 그려지지 않습니다. Android 15(API 수준 35)부터 앱의 상단 및 하단 영역이 상태줄와 네비게이션바,디스플레이 컷아웃 뒤에 배치됩니다. 상태바와 네비게이션바를 합쳐 시스템바라고 합니다.
시스템바는 일반적으로 알림 표시, 기기 상태 전달, 기기 네비게이션으로 사용되는 영역입니다. 디스플레이 컷아웃은 휴대폰의 노치를 의미합니다. 따라서 사용자 환경이 더욱 원활해지고 앱에서 사용할 수 있는 창 공간을 최대한 활용할 수 있습니다.
시스템 UI 뒤에 콘텐츠를 표시하는 것을 더 넓은 화면을 활용한다고 합니다(edge-to-edge). 만약 edge-to-edge의 처리를 하지 않는 경우 앱의 일부가 시스템 UI에 의해 가려질 수 있습니다. Material 3의 Scaffold 구성요소를 사용하면 edge-to-edge 처리를 쉽게 대응 할 수 있습니다.
앱이 더 넓은 화면에 콘텐츠를 표시하는 경우 중요한 콘텐츠와의 상호작용이 시스템 UI에 의해 가려지지 않도록 해야 합니다. 예를 들어 버튼이 네비게이션바 뒤에 배치되면 사용자가 버튼을 클릭하지 못할 수 있습니다. 시스템 UI의 크기와 위치에 관한 정보는 inset을 통해 지정됩니다. UI 영역과 겹친다는건 시스템 UI 위에 앱 콘텐츠가 표시된다는 의미일 수도 있고, 시스템이 제스처에 대한 정보를 앱에 제공한다는 의미일 수도 있습니다.
기본적으로, 앱 창을 전체 화면에 걸쳐 확장하고 시스템 바 뒤까지 앱 콘텐츠를 그리는 edge-to-edge 방식이 활성화됩니다. 이때 시스템 바 뒤에 중요한 콘텐츠나 터치 대상이 표시되지 않도록 여백을 사용해야 합니다.
영화나 이미지와 같은 콘텐츠를 표시하는 앱은 몰입도 높은 환경을 위해 시스템바를 숨김. 시스템바는 기기를 탐색하고 기기 상태를 확인하는 일반적인 방법. 시스템바를 숨기는게 사용자 요구와 기대에 맞는지 고려 필요
Display cutout은 일부 장치에서 디스플레이 표면으로 확장되어 장치 전면에 센서 공간을 제공하는 영역. 중요한 콘텐츠가 컷아웃 영역과 겹치지 않도록 위치를 쿼리하기 위해 앱은 디스플레이 컷아웃(노치)을 지원.
키보드 전환은 window inset이 동적으로 업데이트되는 일반적인 예. 앱은 현재 키보드 상태를 관찰, 프로그래매틱 방식으로 상태를 전환, window inset의 애니메이션을 지원, 앱 콘텐츠가 원활하게 키보드 전환 애니메이션 처리
Compose에서는 시스템 UI의 각 부분과 대응되는 inset 유형이 있습니다. 예를 들어 상태바의 inset은 상태바의 크기와 위치를 제공하는 반면 네비게이션바 inset은 네비게이션바의 크기와 위치를 제공합니다. 각 inset 유형은 상단, 왼쪽, 오른쪽, 하단의 네 가지 픽셀 크기로 구성됩니다. 이러한 수치들은 시스템 UI가 앱 창의 각 측면으로 얼마나 들어와서 UI가 겹쳐져 있는지 나타냅니다. 따라서 시스템 UI와 겹치지 않도록 앱 UI를 해당 양만큼 여백을 주어 inset의 처리를 해야 합니다.이러한 기본 제공 안드로이드 inset 유형은 WindowInsets을 통해 사용할 수 있습니다.
WindowInsets.statusBars: 상태바를 설명하는 inset. 알림 아이콘과 기타 표시기가 포함된 상단 시스템 UI바
WindowInsets.statusBarsIgnoringVisibility : statusBars는 상태바가 보여질 때의 inset으로 몰입형이나 전체화면 모드로 상태바가 숨겨져 있으면 비어있는 값을 반환. statusBarsIgnoringVisibility는 비어있지 않는, 상태바가 보여질 때의 inset을 여전히 반환
WindowInsets.navigationBars : 네비게이션바를 설명하는 inset. 태스크바나 네비게이션 아이콘을 의미하는, 기기의 왼쪽이나 오른쪽 또는 하단에 있는 시스템 UI바. 이러한 값은 사용자가 선호하는 네비게이션 방법과 태스크바와의 상호작용에 따라 런타임에 변경.
WindowInsets.navigationBarsIgnoringVisibility:
navigationBars는 네비게이션바가 보여질 때의 inset으로 몰입형이나 전체화면 모드로 네비게이션바가 숨겨져 있으면 비어있는 값을 반환. navigationBarsIgnoringVisibility는 비어있지 않는, 네비게이션바가 보여질 때의 inset을 여전히 반환.
WindowInsets.captionBar : 앱 창의 상단에 표시되는 시스템 타이틀 바 영역의 inset. 이는 freeform 창 모드에서 적용되며, freeform 모드는 PC의 프로그램 창처럼 앱 창의 크기나 위치를 자유롭게 조절할 수 있는 환경. 이 모드에서는 앱이 독립된 창처럼 표시되기 때문에 상단에 시스템 타이틀 바가 생성.
WindowInsets.captionBarIgnoringVisibility : caption bar가 보여지든 숨겨지든 상관없이 반환하는 inset 값
WindowInsets.systemBars : 상태바, 네비게이션바, caption bar을 포함하는 시스템바 inset의 합집합
WindowInsets.systemBarsIgnoringVisibility : 시스템바가 보여지든 숨겨지든 상관없이 반환하는 inset 값
WindowInsets.ime : 소프트 키보드가 화면 하단에서 차지하는 높이만큼의 공간 크기를 설명하는 inset
WindowInsets.imeAnimationSource : 현재 키보드 애니메이션이 시작되기 직전에 소프트 키보드가 차지하고 있던 공간의 inset. 키보드가 사라지고 있는 중이면 키보드가 완전히 올라와 있던 상태의 공간 영역 의미. 키보드가 나타나는 중이면 키보드가 숨겨져 있을 때의 공간.
WindowInsets.imeAnimationTarget : 현재 키보드 애니메이션이 시작되기 직후에 소프트 키보드가 차지하고 있던 공간의 inset. 키보드가 사라지고 있는 중이면 키보드가 완전히 사라져 있던 상태의 공간 의미. 키보드가 나타나는 중이면 키보드가 올라와 있을 때의 공간 영역.
WindowInsets.tappableElement : 네비게이션 UI에 대한 더 구체적인 정보를 제공하는 inset. 앱이 아닌 시스템이 '탭'을 처리하는 영역의 크기를 의미. 제스처로 네비게이션 하는 투명한 네비게이션 바의 경우, 시스템 네비게이션 UI 위로 앱 요소가 보일 수 있지만, 이 영역에서 발생하는 터치는 시스템이 처리하므로 앱 UI와 겹치지 않도록 주의 필요
WindowInsets.tappableElementIgnoringVisibility : 탭할 수 있는 요소가 보여지든 숨겨지든 상관없이 반환하는 inset 값
WindowInsets.systemGestures : 이 inset은 시스템이 제스처 탐색(예: 뒤로가기 스와이프)을 처리하는 화면 영역을 의미. 기본적으로 이 영역 내의 제스처는 시스템에서 처리하지만 앱은 Modifier.systemGestureExclusion를 통해 일부 영역을 예외로 지정하여 제스처를 직접 제어 가능
WindowInsets.mandatorySystemGestures : 시스템 제스처 중에서도 무조건 시스템이 처리해야 하는 제스처 영역. 이 영역에 대해서는 앱이 제스처 처리 우선권을 가질 수 없음. Modifier.systemGestureExclusion을 통해 예외 지정 불가능
WindowInsets.displayCutout : 디스플레이 컷아웃 (노치 또는 핀홀)과의 중복 방지에 필요한 간격의 양을 나타내는 inset
WindowInsets.waterfall : waterfall 디스플레이의 곡선 영역을 나타내는 inset. 화면의 좌우 또는 상하 가장자리가 둥글게 말려있는 디스플레이 의미. 화면 가장자리의 곡면 부분 때문에 앱 UI가 가려지거나 터치 불가 영역이 생길 수 있어, UI 배치 시 고려해야 하는 영역
콘텐츠가 가려지지 않도록 하는 세 가지 safe inset 유형 존재. 이러한 safe inset 유형은 기본 플랫폼 inset에 따라 다양한 방식으로 콘텐츠를 보호합니다.
WindowInsets.safeDrawing : 시스템 UI 아래에 그려지면 안 되는 콘텐츠를 보호. inset을 사용하는 가장 일반적인 방법으로, 시스템 UI에 의해 일부 또는 완전히 가려지는 콘텐츠가 그려지지 않도록 방지.
WindowInsets.safeGestures : 제스처 충돌을 방지하며 콘텐츠를 보호. 시스템 제스처와 앱 내 제스처(예: 바텀 시트, 캐러셀(슬라이드 쇼 형태의 UI), 게임 내 제스처)가 겹치지 않도록 보장.
WindowInsets.safeContent : safeDrawing과 safeGestures를 결합. 시각적 겹침과 제스처 충돌 모두 없도록 콘텐츠를 보호
많은 앱에는 상단 앱 바가 있습니다. 상단 앱 바는 화면의 상단 가장자리까지 늘어나 상태바 밑에 표시됩니다. 선택적으로 콘텐츠가 스크롤될 때 상단 앱 바가 상태바 높이로 축소될 수 있습니다.
하단 앱 바 또는 하단 네비게이션바도 있습니다. 이러한 바는 화면 하단 가장자리까지 늘어나 네비게이션바 밑에 표시됩니다. 그렇지 않으면 앱은 네비게이션바 뒤에 스크롤의 콘텐츠가 보이도록 해야 합니다.

앱에서 edge-to-edge 레이아웃을 구현할 때는 edge-to-edge 화면 설정, 시각적 중복 처리, 시스템 바 뒤의 스크림 표시에 유의해야합니다. 스크림(scrim)이란 콘텐츠 사이의 명확성 구분이나 가독성, 포커스 조정 등을 위해 반투명한 색상의 밝기를 조절하는 겁니다.
앱이 SDK 35 이상을 타겟팅하는 경우 Android 15 이상 기기에서는 edge-to-edge이 자동으로 사용 설정됩니다. 이전 Android 버전에서도 edge-to-edge를 사용하려면 Activity의 onCreate() 에서 enableEdgeToEdge()을 수동으로 호출하면 됩니다.
기본적으로 enableEdgeToEdge()는 시스템바를 투명하게 만듭니다. 단, 3버튼 네비게이션 모드에는 예외로 상태바를 반투명 스크림으로 적용합니다. 시스템 아이콘과 스크림의 색상은 시스템 밝은 테마 또는 어두운 테마에 따라 조정됩니다.
//java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WindowCompat.enableEdgeToEdge(getWindow());
...
}
//kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.enableEdgeToEdge(window)
...
}
앱의 일부 뷰는 시스템바 뒤에 그려질 수 있습니다. inset을 이용하면 겹쳐진 영역을 해결할 수 있습니다. 앱을 edge-to-edge 화면으로 표시하는 데 적용되는 inset 유형은 다음과 같습니다.
inset: 앱보다 우선순위가 높은 시스템에서 사용하는 제스처 탐색 영역inset: 탭할 수 있고 시스템바에 의해 시각적으로 가려서는 안 되는 뷰에 가장 적합inset: 기기 모양으로 인해 화면 컷아웃이 있을 수 있는 영역
시스템 제스처 inset은 시스템 제스처가 앱보다 우선하는 창 영역으로 황색으로 표시된 부분입니다.
WindowInsetsCompat.Type.systemGestures()와 함께 getInsets(int)를 사용하여 시스템 제스처 inset이 겹치지 않도록 할 수 있습니다. 이러한 inset을 사용하여 스와이프 가능한 뷰의 위치 자체를 가장자리로부터 멀리 옮기거나 패딩을 줍니다. 일반적인 사용 사례로는 하단 시트, 게임에서의 스와이프, ViewPager2를 사용하여 구현된 캐러셀이 있습니다.

Android 10 이상에서 시스템 제스처 inset에는 홈 제스처를 위한 하단 inset과 뒤로 동작을 위한 왼쪽/오른쪽 inset이 포함됩니다. 다음 코드 예에서는 시스템 제스처 inset을 구현하는 방법을 보여줍니다.
//java
ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures());
// view에 padding으로 inset 적용
// 원하는 수치 적용 가능
//원할 경우 margin으로 적용 가능
view.setPadding(insets.left, insets.top, insets.right, insets.bottom);
// window inset을 자식 view에게 전달하고 싶지 않으면 CONSUMED으로 리턴
return WindowInsetsCompat.CONSUMED;
});
//kotlin
ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures());
// view에 padding으로 inset 적용
// 원하는 수치 적용 가능
//원할 경우 margin으로 적용 가능
view.setPadding(insets.left, insets.top, insets.right, insets.bottom);
// window inset을 자식 view에게 전달하고 싶지 않으면 CONSUMED으로 리턴
return WindowInsetsCompat.CONSUMED;
});

inset이 하위 뷰로 디스패치되는 것을 중지하고 과도한 패딩을 방지하려면 WindowInsetsCompat.CONSUMED 상수를 사용하여 inset을 사용하면 됩니다.
하지만 Android 10 (API 수준 29 이하)을 실행하는 기기에서는 WindowInsetsCompat.CONSUMED를 호출한 후에도 inset이 형제 요소에 디스패치되지 않아 의도하지 않은 시각적 중복이 발생할 수 있습니다. 지원되는 모든 Android 버전에서 inset가 형제 요소에도 전달되도록 하려면, AndroidX Core 및 Core-ktx 1.16.0-alpha01 이상에서는 inset을 사용하기 전에 ViewGroupCompat.installCompatInsetsDispatch()를 호출해야 합니다
//java
// R.id.main처럼 루트 view의 레이아웃 적용
LinearLayout rootView = findViewById(R.id.main);
// inset 소비하기 전에 호출
ViewGroupCompat.installCompatInsetsDispatch(rootView);
//kotlin
// R.id.main처럼 루트 view의 레이아웃 적용
val rootView = findViewById(R.id.main)
// inset 소비하기 전에 호출
ViewGroupCompat.installCompatInsetsDispatch(rootView)


inset 을 처리하고 clipToPadding 을 false로 설정하면 RecyclerView 또는 NestedScrollView의 마지막 목록 항목이 시스템바에 가려지지 않습니다. 위의 gif는 edge-to-edge가 적용되지 않는 RecyclerView(왼쪽)와 적용된 RecyclerView (오른쪽)를 보여줍니다.
전체 화면 대화상자를 더 넓게 만들려면 대화상자에서 enableEdgeToEdge() 을 호출합니다.
//java
public class MyAlertDialogFragment extends DialogFragment {
@Override
public void onStart() {
super.onStart();
Dialog dialog = getDialog();
if (dialog != null) {
Window window = dialog.getWindow();
if (window != null) {
WindowCompat.enableEdgeToEdge(window);
}
}
}
...
}
//kotlin
class MyAlertDialogFragment : DialogFragment() {
override fun onStart(){
super.onStart()
dialog?.window?.let { WindowCompat.enableEdgeToEdge(it) }
}
...
}
앱의 UI 위치를 제대로 제어하기 위해서는 다음 단계를 따라야 합니다. 그렇지 않으면 앱이 시스템 UI 뒤에 검은색/단색으로 채우거나 소프트웨어 키보드와 동기화되지 않을 수 있습니다.
inset 을 처리하여 앱의 UI를 조정 가능. Activity.onCreate() 에서 enableEdgeToEdge() 를 호출하여 이전 Android 버전에서도 앱에 edge-to-edge 적용 android:windowSoftInputMode="adjustResize" 을 설정. 앱이 소프트웨어 키보드(IME)의 크기를 inset 으로 받을 수 있게 됨. 이를 통해 적절한 레이아웃과 패딩 적용 가능<activity
android:name=".ui.MainActivity"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.MyApplication"
android:exported="true">
개발자가 activity에서 모든 inset 처리를 직접 제어하고 싶으면 Compose API를 활용하면 됩니다. 콘텐츠가 시스템 UI에 가려지지 않도록 하고 인터랙션 가능한 요소들이 시스템 UI와 겹치지 않도록 할 수 있습니다. 이러한 API는 앱의 레이아웃이 inset 변경에 맞춰 동기화되도록 도와줍니다. 예를 들어 전체 앱의 콘텐츠에 inset 을 적용하는 가장 기본적인 방법은 다음과 같습니다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Box(Modifier.safeDrawingPadding()) {
// the rest of the app
}
}
}
위의 예제는 safeDrawing window inset 을 앱 전체 콘텐츠의 패딩으로 적용합니다. 이렇게 하면 인터랙션 가능한 요소가 시스템 UI와 겹치지 않도록 보장되지만, 앱이 시스템 UI 아래로 그려지는 edge-to-edge 효과를 내는 건 아닙니다. 전체 창을 온전히 활용하려면, inset 을 화면 단위 또는 컴포넌트 단위로 정밀하게 적용해야 있습니다. 이러한 모든 inset 유형은 API 21이상이면 IME 애니메이션을 통해 자동으로 애니메이션 효과를 냅니다. 따라서 이러한 inset 을 사용하는 레이아웃도 inset 값 변화에 따라 자동으로 애니메이션됩니다. inset 값을 Composable 레이아웃에 적용하는 주요 방법은 두 가지입니다. 패딩 modifier 와 inset 크기 modifier 입니다.
Modifier.windowInsetsPadding(windowInsets: WindowInsets) inset 을 패딩으로 적용. Modifier.padding 과 똑같이 작동 safeDrawing 을 전달하면 inset 을 4면 모두에 패딩으로 적용 inset 유형에 대한 여러 내장 유틸리티 메서드 존재 Modifier.safeDrawingPadding() . Modifier.windowInsetsPadding(WindowInsets.safeDrawing) 와 동일한 메서드 중 하나. inset 유형에도 유사한 modifier 존재다음 modifier 는 구성요소의 inset 만큼의 크기를 적용합니다.
Modifier.windowInsetsStartWidth(windowInsets: WindowInsets) : start 방향의 windowInsets 크기를 컴포넌트 너비로 적용 (예: Modifier.width ).
Modifier.windowInsetsEndWidth(windowInsets: WindowInsets) : end 방향의 windowInsets 크기를 컴포넌트 너비로 적용 (예: Modifier.width ).
Modifier.windowInsetsTopHeight(windowInsets: WindowInsets) : top 방향의 windowInsets 크기를 컴포넌트 높이로 적용
(예: Modifier.height ).
Modifier.windowInsetsBottomHeight(windowInsets: WindowInsets) : bottom 방향의 windowInsets 크기를 컴포넌트 높이로 적용 (예:Modifier.height ).
이러한 modifier 는 inset 의 공간을 차지하는 Spacer 의 크기를 조정하는 데 특히 유용합니다.
LazyColumn(
Modifier.imePadding()
) {
// Other content
item {
Spacer(
Modifier.windowInsetsBottomHeight(
WindowInsets.systemBars
)
)
}
}
Modifier.imePadding() 는 IME가 화면에 나타나면 IME 크기만큼의 패딩을 하단에 적용하는 modifier 입니다.
inset 패딩 modifier ( windowInsetsPadding 및 safeDrawingPadding 같은 헬퍼들)는 일반 패딩 modifier 과 달리, 적용된 inset 영역을 자동으로 소비(consume)합니다. 컴포지션 트리의 더 깊은 위치로 내려갈수록, 중첩된 inset 패딩 modifier 나 inset 크기 modifier 는 바깥쪽에서 일부 inset 이 이미 소비되었음을 인식하고, 동일한 inset 영역을 중복으로 적용하지 않도록 처리합니다. 그렇지 않으면 불필요하게 공간이 많이 생길 수 있기 때문입니다.
inset 크기 modifier 들도, inset 이 이미 소비된 경우 중복 적용을 피하도록 동작합니다. 하지만 이들은 크기를 직접 조절할 뿐이므로 자체적으로 inset 을 consume하지는 않습니다. 그렇기에 해당 inset 정보를 다른 컴포저블에도 사용할 수 있습니다.
결론으로, 중첩된 패딩 modifier 는 각 Composable에 적용되는 패딩 양을 자동으로 조절합니다.

이전의 inset 크기 modifier 예제를 보면, Modifier.imePadding()에 의해 LazyColumn이 크기가 조정되고, LazyColumn 내부에서 마지막 아이템(Spacer)은 하단 시스템 바의 높이만큼의 크기로 바뀝니다.
IME가 닫히면 IME에 높이가 없으므로 Modifier.imePadding()는 패딩을 적용하지 않습니다. Modifier.imePadding()는 패딩을 적용하지 않으므로 inset이 사용되지 않으며 Spacer의 높이는 하단 시스템바의 크기가 됩니다.
IME가 열리면 IME inset이 IME 크기에 맞게 애니메이션으로 표시되고 Modifier.imePadding()가 IME가 열릴 때 LazyColumn의 크기를 조절하기 위해 하단 패딩을 적용하기 시작합니다. Modifier.imePadding()가 하단 패딩을 적용하기 시작하면 해당 크기의 inset도 사용하기 시작합니다. 따라서 시스템바와의 일부 여백이 Modifier.imePadding()에 의해 적용되었으므로 Spacer의 높이가 감소하기 시작합니다. Modifier.imePadding()가 시스템바보다 큰 하단 패딩을 적용하면 Spacer의 높이는 0입니다.
IME가 닫히면 변경사항이 반대로 적용됩니다. imePadding()이 시스템바의 하단보다 적게 적용되면 Spacer이 높이 0에서 커지기 시작하여 IME가 완전히 애니메이션으로 표시되지 않을 때까지 Spacer는 하단 시스템바의 높이와 일치합니다.
contentPadding 대신 Spacer를 사용하는 이유는 IME가 마지막 요소를 가릴 수 있기 때문입니다.
이 동작은 모든 Modifier.imePadding() 간의 inset 소비 상태 소통을 통해 이뤄지며, 다른 방법들로도 조정될 수 있습니다.
Modifier.consumeWindowInsets(WindowInsets)는 windowInsetsPadding처럼 inset을 소비하지만, 패딩을 적용하지는 않습니다. 이건 inset 크기 modifier와 함께 사용할 때 유용합니다. 즉, 형제 컴포넌트에게 일정 inset이 이미 처리됐다는 걸 알려주는 용도입니다. 시각적으로 패딩은 없지만 논리적으로는 inset을 처리한 것으로 취급됩니다
Column(Modifier.verticalScroll(rememberScrollState())) {
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))
Column(
Modifier.consumeWindowInsets(
WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
)
) {
// content
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}
PaddingValues를 받는 consumeWindowInsets()는 위와 거의 동일하게 동작하지만,
임의의 PaddingValues을 inset처럼 소비합니다. Modifier.padding이나 Spacer 같은 일반 패딩 방식으로 여백을 줬을 때, 하위 컴포넌트에 "이 여백은 inset으로 간주해" 라고 알려줄 때 유용합니다.
Column(Modifier.padding(16.dp).consumeWindowInsets(PaddingValues(16.dp))) {
// content
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}
inset 값을 소비하지 않고 사용하려면 WindowInsets 자체를 사용하거나,
WindowInsets.asPaddingValues()를 써서 소비되지 않은 패딩을 얻으면 됩니다.
다만 명확한 제어를 위해 가능하면 windowInsetsPadding이나 size modifier를 사용하는 걸 권장합니다.
Compose는 AndroidX core API를 기반으로 inset을 업데이트하고 애니메이션 처리하며, 이 AndroidX core API는 다시 플랫폼의 inset 관리 API를 사용합니다. inset 값은 composition 단계 이후, layout 단계 이전에 업데이트됩니다. 즉, composition 중에 inset 값을 읽으면 한 프레임 이전의 값을 읽게 되는 경우가 많습니다. 위에서 설명한 내장 modifier들은 inset 값을 layout 단계까지 지연시키도록 설계되어있기 때문에 업데이트된 inset 값을 사용할 수 있게 합니다.
많은 뷰 기반 Android Material의 구성요소는 사용 편의성을 위해 BottomAppBar, BottomNavigationView, NavigationRailView, NavigationView를 비롯한 inset을 자동으로 처리합니다. 하지만 AppBarLayout는 inset을 자동으로 처리하지 않습니다. 상단 inset을 처리하기 위해 android:fitsSystemWindows="true"를 추가합니다. 다음은 inset을 자동으로 처리하는 Material 구성요소 목록입니다.
앱 바
TopAppBar/ SmallTopAppBar/ CenterAlignedTopAppBar/ MediumTopAppBar/ LargeTopAppBar: 화면창 상단에 사용되므로 상단 및 horizontal 측면의 시스템바에 패딩 적용BottomAppBar: 하단 및 horizontal 측면의 시스템바에 패딩 적용콘텐츠 컨테이너
ModalDrawerSheet / DismissibleDrawerSheet / PermanentDrawerSheet (modal navigation drawer 내부의 콘텐츠): 콘텐츠에 vertical 및 start inset을 적용ModalBottomSheet: 하단 inset을 적용NavigationBar : 하단 및 horizontal inset을 적용NavigationRail: vertical 및 start inset을 적용Scaffold

Scaffold는 inset을 매개변수 paddingValues로 제공하여 소비하고 사용할 수 있게 함. 이 inset 값을 content에 자동으로 적용하지 않으며 inset 값을 사용하는 건 개발자의 선택. 예를 들어 Scaffold 내에서 LazyColumn로 이러한 inset을 사용하려면 다음을 실행.Scaffold { innerPadding ->
// 사용하고 적용할 수 있는 inset정보가 포함된 innerPadding
LazyColumn(
// 기본적으로 scaffold inset 자동 적용X
modifier = Modifier.consumeWindowInsets(innerPadding),
contentPadding = innerPadding
) {
// ..
}
}
Material 2 구성요소는 inset 자체를 자동으로 처리하지 않습니다. 하지만 inset에 액세스하여 수동으로 적용할 수 있습니다. androidx.compose.material 1.6.0 이상에서 windowInsets 매개변수를 사용하여 BottomAppBar, TopAppBar, BottomNavigation, NavigationRail에 inset을 수동으로 적용합니다. 마찬가지로 Scaffold에는 contentWindowInsets 매개변수를 사용합니다. 그 외에는 inset을 패딩으로 적용합니다.
컴포저블에 전달된 windowInsets 매개변수를 변경하여 컴포저블의 동작을 구성할 수 있습니다. 이 매개변수는 다른 종류의 window inset일 수도 있고 빈 인스턴스 WindowInsets(0, 0, 0, 0)를 전달하여 사용 중지할 수도 있습니다. 예를 들어 LargeTopAppBar에서 inset 처리를 사용 중지하려면 windowInsets 매개변수를 빈 인스턴스로 설정합니다
LargeTopAppBar(
windowInsets = WindowInsets(0, 0, 0, 0),
title = {
Text("Hi")
}
)
앱에 Compose 코드와 뷰 코드가 모두 포함된 경우 각 코드가 사용할 시스템 inset을 명시하고 inset이 형제 뷰에 디스패치되도록 해야 할 수 있습니다. 화면에 동일한 계층 구조에 뷰와 Compose 코드가 모두 있는 경우 기본 inset을 재정의해야 할 수 있습니다. 이 경우 inset을 사용해야 하는 항목과 무시해야 하는 항목을 명시해야 합니다.
가장 바깥쪽 레이아웃이 Android View 레이아웃인 , View 시스템에서 inset을 사용하고 Compose에서는 무시해야 합니다.
가장 바깥쪽 레이아웃이 컴포저블인 경우, Compose에서 inset을 사용하고 AndroidView 컴포저블을 적절히 패딩해야 합니다.
기본적으로 각 ComposeView는 WindowInsetsCompat 수준에서 모든 inset을 소비합니다. 이 기본 동작을 변경하려면 AbstractComposeView.consumeWindowInsets을 false로 설정합니다.
앱에 Views 코드가 포함된 경우 Android 10 (API 수준 29) 이하를 실행하는 기기에서 inset이 형제 뷰에 디스패치되는지 확인해야합니다.
Android 12(API 레벨 31)부터 RoundedCorner와 WindowInsets.getRoundedCorner(int position)을 사용하여 기기 화면의 둥근 모서리에 대한 반지름과 중심점을 가져올 수 있습니다. 이 API들은 둥근 모서리가 있는 화면에서 앱의 UI 요소가 잘리는 것을 방지합니다.

프레임워크는 getPrivacyIndicatorBounds() API도 제공하는데, 이는 표시 중인 마이크 및 카메라 표시기의 경계 사각형을 반환합니다.
이 API들은 둥근 모서리가 없는 기기에서는 아무런 효과를 주지 않습니다.

둥근 모서리 처리를 구현하려면 WindowInsets.getRoundedCorner(int position)을 사용하여 앱의 영역 기준으로 RoundedCorner 정보를 가져옵니다.
앱이 전체 화면을 차지하지 않는 경우, API는 앱의 창 경계를 기준으로 둥근 모서리의 중심점을 계산해 적용합니다. 다음 코드는 RoundedCorner에서 가져온 정보를 기반으로 뷰에 여백(margin)을 설정하여 UI가 잘리지 않도록 하는 방법을 보여줍니다. 오른쪽 상단의 둥근 모서리에 대해 처리합니다.

//java
//상단 오른쪽 모서리 WindowInsets 얻기
val insets = rootWindowInsets
val topRight = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT) ?: return
// 창에서 닫기 버튼 위치 좌표 얻기
val location = IntArray(2)
closeButton!!.getLocationInWindow(location)
val buttonRightInWindow = location[0] + closeButton.width
val buttonTopInWindow = location[1]
// 45도 지점의 원호 경계 계산
val offset = (topRight.radius * Math.sin(Math.toRadians(45.0))).toInt()
val topBoundary = topRight.center.y - offset
val rightBoundary = topRight.center.x + offset
// 버튼이 경계를 벗어났는지 확인
if (buttonRightInWindow < rightBoundary << buttonTopInWindow > topBoundary) {
return
}
//겹치지 않도록 마진 설정
val parentLocation = IntArray(2)
getLocationInWindow(parentLocation)
val lp = closeButton.layoutParams as FrameLayout.LayoutParams
lp.rightMargin = Math.max(buttonRightInWindow - rightBoundary, 0)
lp.topMargin = Math.max(topBoundary - buttonTopInWindow, 0)
closeButton.layoutParams = lp
//kotlin
//상단 오른쪽 모서리 WindowInsets 얻기
val insets = rootWindowInsets
val topRight = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT) ?: return
// 창에서 닫기 버튼 위치 좌표 얻기
val location = IntArray(2)
closeButton!!.getLocationInWindow(location)
val buttonRightInWindow = location[0] + closeButton.width
val buttonTopInWindow = location[1]
// 45도 지점의 원호 경계 계산
val offset = (topRight.radius * Math.sin(Math.toRadians(45.0))).toInt()
val topBoundary = topRight.center.y - offset
val rightBoundary = topRight.center.x + offset
// 버튼이 경계를 벗어났는지 확인
if (buttonRightInWindow < rightBoundary << buttonTopInWindow > topBoundary) {
return
}
//겹치지 않도록 마진 설정
val parentLocation = IntArray(2)
getLocationInWindow(parentLocation)
val lp = closeButton.layoutParams as FrameLayout.LayoutParams
lp.rightMargin = Math.max(buttonRightInWindow - rightBoundary, 0)
lp.topMargin = Math.max(topBoundary - buttonTopInWindow, 0)
closeButton.layoutParams = lp
디스플레이 전체를 채우는 UI의 경우, 둥근 모서리때문에 콘텐츠가 잘리는(clipping) 문제가 발생할 수 있습니다.

예를 들어 시스템 바 뒤에 레이아웃이 그려진 상태에서 화면 구석에 아이콘이 위치해 있고, 이 아이콘은 둥근 모서리에 의해 일부 가려집니다. 이 문제를 해결하기 위해서는 둥근 모서리가 있는지 확인하고, 앱 콘텐츠가 기기 모서리를 침범하지 않도록 패딩을 적용해야 합니다.

//java
public class InsetsLayout extends FrameLayout {
public InsetsLayout(@NonNull Context context) {
super(context);
}
public InsetsLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
WindowInsets insets = getRootWindowInsets();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && insets != null) {
applyRoundedCornerPadding(insets);
}
super.onLayout(changed, left, top, right, bottom);
}
@RequiresApi(Build.VERSION_CODES.S)
private void applyRoundedCornerPadding(WindowInsets insets) {
RoundedCorner topLeft = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT);
RoundedCorner topRight = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT);
RoundedCorner bottomLeft = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT);
RoundedCorner bottomRight = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT);
int radiusTopLeft = 0;
int radiusTopRight = 0;
int radiusBottomLeft = 0;
int radiusBottomRight = 0;
if (topLeft != null) radiusTopLeft = topLeft.getRadius();
if (topRight != null) radiusTopRight = topRight.getRadius();
if (bottomLeft != null) radiusBottomLeft = bottomLeft.getRadius();
if (bottomRight != null) radiusBottomRight = bottomRight.getRadius();
int leftRadius = Math.max(radiusTopLeft, radiusBottomLeft);
int topRadius = Math.max(radiusTopLeft, radiusTopRight);
int rightRadius = Math.max(radiusTopRight, radiusBottomRight);
int bottomRadius = Math.max(radiusBottomLeft, radiusBottomRight);
WindowManager windowManager =
(WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
Rect windowBounds = windowManager.getCurrentWindowMetrics().getBounds();
Rect safeArea = new Rect(
windowBounds.left + leftRadius,
windowBounds.top + topRadius,
windowBounds.right - rightRadius,
windowBounds.bottom - bottomRadius
);
int[] location = {0, 0};
getLocationInWindow(location);
int leftMargin = location[0] - windowBounds.left;
int topMargin = location[1] - windowBounds.top;
int rightMargin = windowBounds.right - getRight() - location[0];
int bottomMargin = windowBounds.bottom - getBottom() - location[1];
Rect layoutBounds = new Rect(
location[0] + getPaddingLeft(),
location[1] + getPaddingTop(),
location[0] + getWidth() - getPaddingRight(),
location[1] + getHeight() - getPaddingBottom()
);
if (!layoutBounds.equals(safeArea) && layoutBounds.contains(safeArea)) {
setPadding(
calculatePadding(radiusTopLeft, radiusBottomLeft,
leftMargin, getPaddingLeft()),
calculatePadding(radiusTopLeft, radiusTopRight,
topMargin, getPaddingTop()),
calculatePadding(radiusTopRight, radiusBottomRight,
rightMargin, getPaddingRight()),
calculatePadding(radiusBottomLeft, radiusBottomRight,
bottomMargin, getPaddingBottom())
);
}
}
private int calculatePadding(int radius1, int radius2, int margin, int padding) {
return Math.max(Math.max(radius1, radius2) - margin - padding, 0);
}
}
//kotlin
class InsetsLayout(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) {
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
val insets = rootWindowInsets
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && insets != null) {
applyRoundedCornerPadding(insets)
}
super.onLayout(changed, left, top, right, bottom)
}
@RequiresApi(Build.VERSION_CODES.S)
private fun applyRoundedCornerPadding(insets: WindowInsets) {
val topLeft = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)
val topRight = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT)
val bottomLeft = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT)
val bottomRight = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT)
val leftRadius = max(topLeft?.radius ?: 0, bottomLeft?.radius ?: 0)
val topRadius = max(topLeft?.radius ?: 0, topRight?.radius ?: 0)
val rightRadius = max(topRight?.radius ?: 0, bottomRight?.radius ?: 0)
val bottomRadius = max(bottomLeft?.radius ?: 0, bottomRight?.radius ?: 0)
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val windowBounds = windowManager.currentWindowMetrics.bounds
val safeArea = Rect(
windowBounds.left + leftRadius,
windowBounds.top + topRadius,
windowBounds.right - rightRadius,
windowBounds.bottom - bottomRadius
)
val location = intArrayOf(0, 0)
getLocationInWindow(location)
val leftMargin = location[0] - windowBounds.left
val topMargin = location[1] - windowBounds.top
val rightMargin = windowBounds.right - right - location[0]
val bottomMargin = windowBounds.bottom - bottom - location[1]
val layoutBounds = Rect(
location[0] + paddingLeft,
location[1] + paddingTop,
location[0] + width - paddingRight,
location[1] + height - paddingBottom
)
if (layoutBounds != safeArea && layoutBounds.contains(safeArea)) {
setPadding(
calculatePadding(leftRadius, leftMargin, paddingLeft),
calculatePadding(topRadius, topMargin, paddingTop),
calculatePadding(rightRadius, rightMargin, paddingRight),
calculatePadding(bottomRadius, bottomMargin, paddingBottom)
)
}
}
private fun calculatePadding(radius1: Int?, radius2: Int?, margin: Int, padding: Int): Int =
(max(radius1 ?: 0, radius2 ?: 0) - margin - padding).coerceAtLeast(0)
}
예시 코드에서는 UI가 둥근 모서리 영역까지 위치하였는지 판단하고, 겹쳐진 경우 해당 위치에 패딩을 추가합니다.

"Show layout bounds(레이아웃 경계 표시)" 개발자 옵션을 활성화해서 실제 패딩이 적용된 상태를 더 명확히 확인할 수 있습니다.
패딩을 추가하기 위해서는 두 개의 사각형 영역을 계산해야 합니다.
layoutBounds가 safeArea를 완전히 포함하면, layoutBounds가 더 크면 콘텐츠가 잘릴 수 있기 때문에 패딩을 추가해야 합니다. 반대로, 레이아웃이 화면 가장자리까지 확장되지 않아서 둥근 모서리 영역에 닿지 않는다면, 굳이 패딩을 추가하지 않아도 됩니다.

레이아웃이 내비게이션 바 뒤로 그려지지 않는 경우를 보여주며, 이 경우에는 둥근 모서리도 내비게이션 바 영역 안에 있어서 추가적인 패딩이 필요하지 않습니다

동영상, 게임, 이미지 갤러리, 책, 프레젠테이션 슬라이드 등은 상태바나 네비게이션바의 표시 없이 전체 화면으로 이용하는 게 좋습니다. 이를 몰입형 모드라고 합니다. 몰입형 모드를 사용하면 게임 중에 실수로 종료되지 않고 이미지, 동영상, 도서를 즐길 수 있는 몰입형 환경을 제공합니다. 하지만 사용자가 알림을 확인하거나, 즉흥적인 검색을 실행하거나 다른 작업을 수행하기 위해 얼마나 자주 앱을 시작하고 종료하는지 고려해야 합니다. 몰입형 모드를 사용하면 사용자가 시스템 네비게이션에 쉽게 액세스할 수 없으므로 사용자 환경에 미치는 이점이 단순 화면 확장 이상인 경우에만 몰입형 모드를 사용해야 합니다.
WindowInsetsControllerCompat.hide()을 사용하여 시스템바를 숨기고 WindowInsetsControllerCompat.show()을 사용하여 다시 표시합니다.
시스템바 inset은 가장 일반적으로 사용되는 inset 유형입니다. 탭할 수 있고 시스템바에 의해 시각적으로 가려서는 안 되는 앱의 뷰를 이동하거나 패딩 적용에 가장 적합합니다. WindowInsetsController 및 WindowInsetsControllerCompat 라이브러리를 사용하여 몰입형 모드의 시스템바를 숨길 수 있습니다. 다음 코드는 시스템바를 숨기고 표시하는 버튼을 구성하는 예를 보여줍니다. 숨길 시스템바의 유형을 지정하고 사용자가 시스템바와 상호작용할 때의 동작을 결정할 수 있습니다.
//java
@Override
protected void onCreate(Bundle savedInstanceState) {
...
WindowInsetsControllerCompat windowInsetsController =
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
// 시스템 바 숨기기 동작 구성
windowInsetsController.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
);
// 시스템 바 숨기거나 보여줄 때 전체화면에 대한 토글 버튼의 동작 listener 추가
ViewCompat.setOnApplyWindowInsetsListener(
getWindow().getDecorView(),
(view, windowInsets) -> {
//다른 시스템바는 보이고 캡션 바만 숨길 수 있음
// 이를 위해, systemBars()의 가시성을 확인하는 대신
//navigationBars()와 statusBars()의 가시성을 명시적으로 확인
if (windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars())
|| windowInsets.isVisible(WindowInsetsCompat.Type.statusBars())) {
binding.toggleFullscreenButton.setOnClickListener(v -> {
// 상태바와 네비게이션 바 모두 숨기기
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
});
} else {
binding.toggleFullscreenButton.setOnClickListener(v -> {
/// 상태바와 네비게이션 바 모두 보여주기
windowInsetsController.show(WindowInsetsCompat.Type.systemBars());
});
}
return ViewCompat.onApplyWindowInsets(view, windowInsets);
});
}
//kotlin
override fun onCreate(savedInstanceState: Bundle?) {
...
val windowInsetsController =
WindowCompat.getInsetsController(window, window.decorView)
// 시스템 바 숨기기 동작 구성
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// 시스템 바 숨기거나 보여줄 때 전체화면에 대한 토글 버튼의 동작 listener 추가
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets ->
//다른 시스템바는 보이고 캡션 바만 숨길 수 있습니다
// 이를 위해, systemBars()의 가시성을 확인하는 대신
//navigationBars()와 statusBars()의 가시성을 명시적으로 확인
if (windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars())
|| windowInsets.isVisible(WindowInsetsCompat.Type.statusBars())) {
binding.toggleFullscreenButton.setOnClickListener {
// 상태바와 네비게이션 바 모두 숨기기
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
}
} else {
binding.toggleFullscreenButton.setOnClickListener {
// 상태바와 네비게이션 바 모두 보여주기
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
}
}
ViewCompat.onApplyWindowInsets(view, windowInsets)
}
}
숨길 시스템바의 유형을 지정하려면 다음 매개변수 중 하나를 WindowInsetsControllerCompat.hide()에 전달하세요.
WindowInsetsCompat.Type.systemBars() : 시스템바 모두 숨김WindowInsetsCompat.Type.statusBars() : 상태바만 숨김WindowInsetsCompat.Type.navigationBars() : 네비게이션바만 숨김WindowInsetsControllerCompat.setSystemBarsBehavior()를 사용하여 사용자가 숨겨진 시스템바와 상호작용할 때 시스템바가 어떻게 작동하는지 지정합니다.
WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH : 해당 디스플레이에서 모든 사용자 상호작용 시 숨겨진 시스템바 표시
WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE : 시스템바가 숨겨진 화면 가장자리에서 스와이프하는 등 시스템 동작에서 숨겨진 시스템바 표시
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE : 시스템 동작으로 숨겨진 시스템바를 일시적으로 표시합니다(예: 바가 숨겨진 화면 가장자리에서 스와이프). 이러한 일시적인 시스템바는 앱 콘텐츠에 오버레이되고 어느 정도 투명할 수 있으며 짧은 제한 시간이 지나면 자동으로 숨겨짐.

플로팅 작업 버튼 (FAB)은 네비게이션바에 의해 부분적으로 가려집니다. 제스처 모드나 버튼 모드에서 이러한 시각적 중복을 방지하려면 WindowInsetsCompat.Type.systemBars()과 함께 getInsets(int)을 사용하여 뷰의 여백을 늘리면 됩니다. 다음 코드 예는 시스템바 inset을 구현하는 방법을 보여줍니다.
//java
ViewCompat.setOnApplyWindowInsetsListener(fab, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
// view에 margin으로서 inset적용
// 해당 코드는 bottom, left, and right수치만 적용했지만 원하는데로 값 부여 가능
//원할경우 padding으로 적용 가능
MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams();
mlp.leftMargin = insets.left;
mlp.bottomMargin = insets.bottom;
mlp.rightMargin = insets.right;
v.setLayoutParams(mlp);
// window inset을 자식 view에게 전달하고 싶지 않으면 CONSUMED으로 리턴
return WindowInsetsCompat.CONSUMED;
});
//kotlin
ViewCompat.setOnApplyWindowInsetsListener(fab) { v, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
// view에 margin으로서 inset적용
// 해당 코드는 bottom, left, and right수치만 적용했지만 원하는데로 값 부여 가능
//원할경우 padding으로 적용 가능
v.updateLayoutParams<MarginLayoutParams> {
leftMargin = insets.left
bottomMargin = insets.bottom
rightMargin = insets.right
}
// window inset을 자식 view에게 전달하고 싶지 않으면 CONSUMED으로 리턴
WindowInsetsCompat.CONSUMED
}

위의 코드를 적용하면 버튼 모드에서 시각적 중복이 발생하지 않습니다.

제스처 탐색 모드에도 동일하게 적용됩니다.
앱이 SDK 35 이상을 타겟팅하면 edge-to-edge 화면이 적용됩니다. 시스템 상태바와 제스처 네비게이션바가 투명해줍니다. 하지만 제스처 네비게이션 바는 시각적 명확성을 위해 반투명을 권장합니다.
앱이 enableEdgeToEdge()를 호출하면 기본으로 3버튼 네비게이션바는 반투명합니다. window.isNavigationBarContrastEnforced = true 입니다.
반투명 스크림을 적용하고 싶지 않는 경우는Window.setNavigationBarContrastEnforced을 false로 설정하면 됩니다. ProtectionView로 감싸진 레이아웃이 있다면setProtections 메서드에 ColorProtection 또는 GradientProtection을 추가로 전달하면 됩니다.
반투명 상태바를 만들려면 다음을 실행하세요.
1. androidx-core 종속 항목을 1.16.0-beta01 이상으로 업데이트합니다.
2. XML 레이아웃을 androidx.core.view.insets.ProtectionLayout로 래핑하고 ID를 할당합니다.
3. 프로그래매틱 방식으로 ProtectionLayout에 액세스하여 보호를 설정하고 상태바의 측면과 GradientProtection을 지정합니다.
<androidx.core.view.insets.ProtectionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list_protection"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/item_list"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--items-->
</ScrollView>
</androidx.core.view.insets.ProtectionLayout>
findViewById<ProtectionLayout>(R.id.list_protection)
.setProtections(
listOf(
GradientProtection(
WindowInsetsCompat.Side.TOP,
// Ideally, this is the pane's background color
paneBackgroundColor
)
)
)
GradientProtection에 전달된 ColorInt이 콘텐츠 배경과 일치하는지 확인합니다.

목록과 세부정보를 함께 보여주는 레이아웃에 목록 패널과 세부정보 패널의 색상이 서로 다른 GradientProtections을 적용할 수 있습니다.
enableEdgeToEdge()를 호출하면 이전 버전에도 적용할 수 있습니다. 하지만 시스템 기본값이 모든 사용 사례에 적합하지 않을 수 있습니다. 아래 디자인 권장 사이트를 참고하여 투명 또는 반투명바를 사용할지 결정하면 됩니다.
🛑🛑🛑
enableEdgeToEdge()를 호출하면 기기 테마가 변경될 때 시스템바 아이콘 색상이 업데이트됩니다. Edge-to-edge 화면으로 전환하는 동안 앱의 배경과 대비되도록 시스템바 아이콘 색상을 수동으로 업데이트할 수 있습니다. 예를 들어 밝은 상태바 아이콘을 만들려면 다음을 실행합니다.
//java
WindowCompat.getInsetsController(window, window.getDecorView())
.setAppearanceLightStatusBars(false);
//kotlin
WindowCompat.getInsetsController(window, window.decorView)
.isAppearanceLightStatusBars = false

반투명 시스템바를 만드는 경우 기본 콘텐츠와 겹치게 만들고 inset으로 덮인 영역에는 그라데이션을 그리는 컴포저블을 만들면 됩니다. 적응형 앱의 경우 edge-to-edge 디자인처럼 각 창의 색상에 맞는 컴포저블을 만들면 됩니다. 반투명 네비게이션바를 만들려면 Window.setNavigationBarContrastEnforced를 true로 설정합니다.
class SystemBarProtectionSnippets : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// window.isNavigationBarContrastEnforced = true하는 메서드
// 3버튼 내비게이션 바에 스크림 적용
enableEdgeToEdge()
setContent {
MyTheme {
// Main content
MyContent()
// After drawing main content, draw status bar protection
StatusBarProtection()
}
}
}
}
@Composable
private fun StatusBarProtection(
color: Color = MaterialTheme.colorScheme.surfaceContainer,
heightProvider: () -> Float = calculateGradientHeight(),
) {
Canvas(Modifier.fillMaxSize()) {
val calculatedHeight = heightProvider()
val gradient = Brush.verticalGradient(
colors = listOf(
color.copy(alpha = 1f),
color.copy(alpha = .8f),
Color.Transparent
),
startY = 0f,
endY = calculatedHeight
)
drawRect(
brush = gradient,
size = Size(size.width, calculatedHeight),
)
}
}
@Composable
fun calculateGradientHeight(): () -> Float {
val statusBars = WindowInsets.statusBars
val density = LocalDensity.current
return { statusBars.getTop(density).times(1.2f) }
}

디스플레이 컷아웃(display cutout)은 일부 기기에서 화면 영역 안으로 파고든 부분을 말합니다. 이는 화면 가장자리까지 콘텐츠를 표시하는 edge-to-edge 경험을 제공하면서도, 전면 카메라나 센서 같은 중요한 하드웨어를 위한 공간을 확보하기 위한 구조입니다. 그렇기에 컷아웃 영역에 앱 콘텐츠를 표시하기 전에, 앱이 edge-to-edge 콘텐츠 표시를 지원하도록 구성되어 있는지 확인해야 합니다. Android는 Android 9(API 레벨 28) 이상에서 디스플레이 컷아웃을 기본 지원합니다. 하지만 일부 제조사는 Android 8.1 이하에서도 컷아웃을 지원할 수 있습니다.
일반적으로 컷아웃은 화면 상단에 있으며 상태바에 포함됩니다. 기기 화면이 가로 모드인 경우 컷아웃이 세로 가장자리에 있을 수 있습니다. 앱이 화면에 표시하는 콘텐츠에 따라 디스플레이 컷아웃을 피하기 위해 패딩을 구현해야 합니다. 기본적으로 앱은 디스플레이 컷아웃에 그려지기 때문입니다.
컷아웃 영역에 콘텐츠를 렌더링하려는 경우, WindowInsetsCompat.getDisplayCutout()을 사용해 DisplayCutout 객체를 가져올 수 있습니다. 이 객체는 각 컷아웃의 safe inset과 경계 상자(bounding box)를 제공합니다. 이 API를 통해 콘텐츠가 컷아웃과 겹치는지 확인하고, 필요 시 위치를 조정할 수 있습니다. 또한 콘텐츠가 컷아웃 뒤에 배치되었는지도 확인할 수 있습니다.
많은 앱 화면에서 리스트를 사용하는데 디스플레이 컷아웃이나 시스템바로 리스트의 항목을 가리면 안됩니다. 시스템바와 디스플레이 컷아웃 유형에 논리 연산 or을 적용하여 WindowInsetsCompat 값을 결정합니다.
//java
ViewCompat.setOnApplyWindowInsetsListener(mBinding.recyclerView, (v, insets) -> {
Insets bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
| WindowInsetsCompat.Type.displayCutout()
);
v.setPadding(bars.left, bars.top, bars.right, bars.bottom);
return WindowInsetsCompat.CONSUMED;
});
//kotlin
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
v.updatePadding(
left = bars.left,
top = bars.top,
right = bars.right,
bottom = bars.bottom,
)
WindowInsetsCompat.CONSUMED
}
패딩이 목록 항목과 함께 스크롤되도록 RecyclerView에 clipToPadding을 설정합니다. 이렇게 하면 사용자가 스크롤할 때 항목이 시스템바 뒤로 이동할 수 있습니다.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
layoutInDisplayCutoutMode 속성은 콘텐츠가 컷아웃 영역에 어떻게 그려지는지를 제어하는 윈도우 속성입니다. 이 속성에는 다음과 같은 값 중 하나를 설정할 수 있습니다.
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
FLAG_FULLSCREEN, SYSTEM_UI_FLAG_FULLSCREEN, SYSTEM_UI_FLAG_HIDE_NAVIGATION를 설정했거나 전체 화면, 가로 모드에서는 컷아웃을 피해서 콘텐츠 배치ALWAYS로 해석LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
Window.setDecorFitsSystemWindows(false)를 통해 시스템바에 맞춰 자동 패딩 주는 것을 막을 수 있음. 시스템이 inset 정보만 넘겨주어 레이아웃 위치는 앱이 직접 제어할 수 있기에 컷아웃 영역 위에 직접 콘텐츠를 그릴 수 있음LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES도 사실상 ALWAYS로 해석됨 (non-floating window 기준).


LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER는 non-floating 윈도우에 대해 ALWAYS로 해석. 즉, 실질적으로 컷아웃 영역까지 콘텐츠가 확장. (기존 의미와 다르게 작동).

컷아웃 모드는 코드에서 프로그래밍적으로 설정할 수도 있고, Activity에 적용할 스타일 속성으로도 지정할 수 있습니다. 예시는 LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 속성을 Activity에 스타일로 지정하는 방법을 보여줍니다.
<style name="ActivityTheme">
<item name="android:windowLayoutInDisplayCutoutMode">
shortEdges <!-- default, shortEdges, or never -->
</item>
</style>
앱이 SDK 35을 타겟하고 Android 15 기기에서 실행 중이라면,
LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS가 기본 동작이며
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT도 사실상 ALWAYS로 해석됩니다 (단, non-floating window일 경우).
SDK 35 미만이거나 Android 15 미만인 경우,
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT이 기본값으로 작동합니다.
중요한 텍스트, 컨트롤, 혹은 정밀한 터치 인식이 필요한 인터랙티브 요소를 가리지 않도록 컷아웃 영역은 직접 컷아웃 정보를 처리해야 합니다. 컷아웃 영역은 터치 감도가 낮을 수 있으므로 주의해야 합니다. 컷아웃을 처리할 때 상태바 높이를 하드코딩하지 말아야 합니다. 그렇지 않으면 콘텐츠가 겹치거나 잘릴 수 있습니다. 아래 방법 중 하나를 통해 컷아웃을 처리해야합니다.
WindowInsets.displayCutout, WindowInsets.safeContent, WindowInsets.safeDrawing사용LocalView.current.rootWindowInsets.displayCutout로 컷아웃 Path 객체 접근Jetpack Compose에서는 displayCutout, safeContent, 또는 safeDrawing을 이용해 컷아웃 inset을 처리해야합니다. 컷 아웃이 필요 없는 곳이라면 처리하지 않아도 됩니다.
UI의 핵심 요소 배치에 주의. 컷아웃 영역이 중요한 텍스트, 컨트롤, 기타 정보를 가리지 않도록 할 것.
정밀한 터치가 필요한 인터랙션 요소는 컷아웃 영역에 두지 말 것. 컷아웃 영역은 터치 민감도가 낮을 수 있음.
가능한 WindowInsetsCompat를 사용해 상태바 높이를 가져와 콘텐츠에 적절한 패딩 적용. 상태바 높이를 하드코딩하지 말 것 .겹치거나 잘리는 문제가 생길 수 있음.

앱이 차지하는 실제 window 영역을 확인할 때는 View.getLocationInWindow() 사용. 전체 window를 사용한다고 가정하지 말고 View.getLocationOnScreen()은 사용하지 말 것.
몰입형 모드 전환이 필요한 경우 always, shortEdges, never 모드를 명시적으로 설정. default 모드는 시스템 바가 표시될 때는 컷아웃에 콘텐츠가 그려지지만, 몰입형 모드에서는 그려지지 않음 .이로 인해 전환 중 콘텐츠가 위아래로 움직이는 현상 발생.

몰입형 모드에서는 window 좌표와 screen 좌표를 주의해서 사용해야 함. 레터박스 상태에서는 전체 화면을 쓰지 않으므로 window 기준 좌표와 screen 기준 좌표는 다름. 필요 시 getLocationOnScreen()을 사용해 screen 좌표를 뷰 좌표로 변환할 것.

MotionEvent 처리 시 getX() / getY() 사용. getRawX() / getRawY()는 위 좌표 이슈로 인해 사용하지 말 것.
앱의 모든 화면과 사용자 경험을 테스트해야 합니다. 가능하다면 다양한 형태의 컷아웃이 있는 기기에서의 테스트를 권장합니다. 컷아웃이 있는 실제 기기가 없다면, Android 9 이상이 실행되는 기기나 에뮬레이터에서 일반적인 컷아웃 구성을 다음 방법으로 시뮬레이션할 수 있습니다.

1. 개발자 옵션(Developer options) 활성화.
2. 개발자 옵션 화면에서 Drawing 섹션으로 스크롤.
3. Simulate a display with a cutout(컷아웃이 있는 디스플레이 시뮬레이션) 항목 선택.
4. 원하는 컷아웃 유형을 선택.

WindowInsetsCompat를 사용하면 앱이 시스템바와 상호작용하는 방식과 유사하게 터치 키보드 (IME)를 쿼리하고 제어할 수 있습니다. 앱은 WindowInsetsAnimationCompat을 사용하여 소프트 키보드가 열리거나 닫힐 때 원활한 전환을 만들 수도 있습니다. WindowInsets을 사용하여 소프트웨어 키보드 상태를 확인합니다.
//java
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
//kotlin
val insets = ViewCompat.getRootWindowInsets(view) ?: return
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
또는 ViewCompat.setOnApplyWindowInsetsListener를 사용하여 소프트 키보드 표시 상태의 변경사항을 관찰할 수 있습니다.
//java
ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
return insets;
});
//kotlin
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
insets
}
AndroidX 구현과의 최상의 하위 호환성을 달성하려면 AndroidManifest.xml 파일에서 android:windowSoftInputMode="adjustResize"을 설정해야 합니다.

사용자가 텍스트 입력란을 탭하면 키보드가 화면 하단에서 슬라이드되어 제자리에 배치됩니다. '동기화되지 않음'이라는 라벨이 지정된 예에서는 Android 10 (API 수준 29)의 기본 동작을 보여줍니다. 여기서는 텍스트 필드와 앱 콘텐츠가 키보드의 애니메이션과 동기화되지 않고 제자리에 스냅됩니다. 이 동작은 시각적으로 거슬릴 수 있습니다.
Android 11 (API 수준 30) 이상에서는 WindowInsetsAnimationCompat를 사용하여 화면 하단에서 키보드가 위아래로 슬라이드하는 것과 앱의 전환을 동기화할 수 있습니다. '동기화됨' 라벨이 지정된 예와 같이 더 부드럽게 표시됩니다.
Android 10 이하에서는 상위 ViewGroup 객체에 setWindowInsetsApplyListener에서 WindowInsets를 직접 사용하지 마세요. 대신 WindowInsetsAnimatorCompat를 사용해야 합니다.
키보드 애니메이션과 동기화할 뷰로 WindowInsetsAnimationCompat.Callback를 구성합니다.
//java
ViewCompat.setWindowInsetsAnimationCallback(
view,
new WindowInsetsAnimationCompat.Callback(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP
) {
// Override methods.
});
//kotlin
ViewCompat.setWindowInsetsAnimationCallback(
view,
new WindowInsetsAnimationCompat.Callback(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP
) {
// Override methods.
});
WindowInsetsAnimationCompat.Callback에는 onPrepare(), onStart(), onProgress(), onEnd() 등 재정의할 메서드가 여러 개 있습니다. 레이아웃을 변경하기 전에 첫 번째로 onPrepare()가 호출됩니다.

onPrepare()는 inset 애니메이션이 시작될 때, 애니메이션으로 인해 뷰가 다시 배치되기 전에 호출됩니다. 이를 사용하여 시작 상태(이 경우 뷰의 하단 좌표)를 저장할 수 있습니다. 다음 코드는 onPrepare() 호출의 예를 보여줍니다.
//java
float startBottom;
@Override
public void onPrepare(
@NonNull WindowInsetsAnimationCompat animation
) {
startBottom = view.getBottom();
}
//kotlin
var startBottom = 0f
override fun onPrepare(
animation: WindowInsetsAnimationCompat
) {
startBottom = view.bottom.toFloat()
}

inset 애니메이션이 시작되면 onStart()가 호출됩니다. 이 시점에서 레이아웃 변경의 최종 상태로 모든 뷰 속성을 설정할 수 있습니다. 뷰에 OnApplyWindowInsetsListener 콜백이 설정되어 있다면, 이 시점에서 이미 해당 콜백이 호출된 상태입니다. 따라서 이 시점은 뷰 속성의 최종 상태를 저장하기에 적절한 타이밍입니다 다음 코드는 onStart() 호출의 예를 보여줍니다.
//java
float endBottom;
@NonNull
@Override
public WindowInsetsAnimationCompat.BoundsCompat onStart(
@NonNull WindowInsetsAnimationCompat animation,
@NonNull WindowInsetsAnimationCompat.BoundsCompat bounds
) {
endBottom = view.getBottom();
return bounds;
}
//kotlin
var endBottom = 0f
override fun onStart(
animation: WindowInsetsAnimationCompat,
bounds: WindowInsetsAnimationCompat.BoundsCompat
): WindowInsetsAnimationCompat.BoundsCompat {
// IME 전환 후 뷰의 위치 기록
endBottom = view.bottom.toFloat()
return bounds
}

onProgress()는 애니메이션을 실행하는 과정에서 inset이 변경될 때 호출되므로 이를 재정의하여 키보드 애니메이션 중에 모든 프레임에서 알림을 받을 수 있습니다. 키보드와 동기화되어 뷰가 애니메이션으로 표시되도록 뷰 속성을 업데이트합니다. 이 시점에서 모든 레이아웃 변경이 완료됩니다. 예를 들어 View.translationY를 사용하여 뷰를 이동하면 이 메서드를 호출할 때마다 값이 점차 감소하여 결국 원래 레이아웃 위치인 0에 도달합니다. 다음 코드는 onProgress() 호출의 예를 보여줍니다.
//java
@NonNull
@Override
public WindowInsetsCompat onProgress(
@NonNull WindowInsetsCompat insets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations
) {
// IME 애니메이션 찾기
WindowInsetsAnimationCompat imeAnimation = null;
for (WindowInsetsAnimationCompat animation : runningAnimations) {
if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {
imeAnimation = animation;
break;
}
}
if (imeAnimation != null) {
// 애니메이션의 보간된 비율에 따라 뷰를 이동
view.setTranslationY((startBottom - endBottom)
* (1 - imeAnimation.getInterpolatedFraction()));
}
return insets;
}
//kotlin
override fun onProgress(
insets: WindowInsetsCompat,
runningAnimations: MutableList<WindowInsetsAnimationCompat>
): WindowInsetsCompat {
// IME 애니메이션 찾기
val imeAnimation = runningAnimations.find {
it.typeMask and WindowInsetsCompat.Type.ime() != 0
} ?: return insets
// 애니메이션의 보간된 비율에 따라 뷰를 이동
view.translationY =
(startBottom - endBottom) * (1 - imeAnimation.interpolatedFraction)
return insets
}
선택적으로 onEnd()를 재정의할 수 있습니다. 이 메서드는 애니메이션이 끝난 후 호출됩니다. 이때 임시 변경사항을 정리하는 것이 좋습니다.
compose에서는 스크롤 컨테이너에 Modifier.imeNestedScroll()를 적용하여 컨테이너 하단으로 스크롤할 때 IME를 자동으로 열고 닫을 수 있습니다.
class WindowInsetsExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
MaterialTheme {
MyScreen()
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MyScreen() {
Box {
LazyColumn(
modifier = Modifier
.fillMaxSize() // 전체 창 채우기
.imePadding() // IME 하단 padding
.imeNestedScroll(), // 하단으로 IME 스크롤
content = { }
)
FloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp) // normal 16dp of padding for FABs
.navigationBarsPadding() // 네비게이션바 패딩
.imePadding(), // IME이 나타났을 때 패딩
onClick = { }
) {
Icon(imageVector = Icons.Filled.Add, contentDescription = "Add")
}
}
}

시스템 바(상태바·내비게이션 바)를 투명하게 만들고 콘텐츠를 edge-to-edge로 확장하는 역할입니다. 시스템이 콘텐츠 영역을 시스템 바를 피해서 배치하지 않도록 힙니다. 즉, 콘텐츠가 상태바, 내비게이션 바 아래로 그려질 수 있도록 허용하여 edge-to-edge 설정 핵심 역할을 합니다. SDK 버전에 따라 다르게 처리되는데 공통적으로 WindowCompat.setDecorFitsSystemWindows(window, false) 사용됩니다.
FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_NAVIGATION 플래그로 시스템 바를 반투명 처리 (구버전 방식), 배경만 투명해지며 시스템 바 영역을 피하지 않도록 설정.WindowInsetsControllerCompat를 이용해 라이트/다크 아이콘 여부 설정 (라이트 바 여부 제어 가능해짐)getScrim()으로 설정 가능. WindowInsetsControllerCompat를 통해 색상바와 내비게이션 바 아이콘 색도 제어.layoutInDisplayCutoutMode 를 SHORT_EDGES로 설정. 세로 및 가로 모드 모두에서 짧은 변 컷아웃에 콘텐츠 확장 허용.isStatusBarContrastEnforced가 true가 되어 시스템이 자체적으로 명암 대비를 강제 적용. false이면 앱이 지정한 색상 그대로 사용. 상태바/내비게이션 바 색상은 getScrimWithEnforcedContrast()로 명암 대비를 고려하여 적절한 색상을 계산해서 반환layoutInDisplayCutoutMode = ALWAYS. cutout이 있는 모든 화면 가장자리까지 콘텐츠 확장 허용.| 클래스 | SDK | 주요 처리 | 컷아웃 지원 | 시스템 바 투명 | 아이콘 색 설정 | 명암 대비 |
|---|---|---|---|---|---|---|
| Base | <21 | 없음 | ❌ | ❌ | ❌ | ❌ |
| Api21 | 21+ | 투명 플래그 | ❌ | ✅ (old way) | ❌ | ❌ |
| Api23 | 23+ | 색 설정 + 아이콘 색 | ❌ | ✅ | ✅ | ❌ |
| Api26 | 26+ | 내비게이션 바도 설정 | ❌ | ✅ | ✅ | ❌ |
| Api28 | 28+ | 컷아웃: SHORT_EDGES | ✅ | ✅ | ✅ | ❌ |
| Api29 | 29+ | 명암 대비 제어 | ✅ | ✅ | ✅ | ✅ |
| Api30 | 30+ | 컷아웃: ALWAYS | ✅ (모든 변) | ✅ | ✅ | ✅ |
ViewCompat.setOnApplyWindowInsetsListener 안의 코드는 화면 회전이나 상태바, 네비게이션 바 크기 변경 등으로 inset 값이 바뀔 때마다 호출됩니다.
insets.getInsets(WindowInsetsCompat.Type.systemBars())로 시스템바 영역 크기를 받아온 후, v.setPadding으로 뷰에 패딩을 줘서 콘텐츠가 시스템 바와 겹치지 않도록 합니다.
//화면 회전같이 inset 변경마다 호출
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(R.id.main), (v, insets) -> {
Insets systemBars =
//insets 정보(크기) 가져오기
insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(
systemBars.left, systemBars.top,
systemBars.right, systemBars.bottom
);
//추가 처리를 위한 inset 리턴
return insets;
}