[Android] Android 15 대응 - EdgeToEdge: 1. Inset 적용

TL; DR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import android.view.View
import android.view.Window
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat

/**
* EdgeToEdge(API 30부터 지원) 적용에 따른 인셋을 적용한다.
*/
fun Window.setupWindowInsets() {
WindowCompat.getInsetsController(this, this.decorView).apply {
isAppearanceLightStatusBars = true
isAppearanceLightNavigationBars = true
}

ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { view, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}

/**
* EdgeToEdge(API 30부터 지원) 적용에 따른 인셋을 적용한다.
* 추가로, 키보드가 올라올 때 (IME 인셋이 있을 때) 버튼을 이동시킨다.
* @param viewForKeyboard 키보드가 올라올 때 이동시킬 View
*/
fun Window.setupWindowInsetsKeyboard(viewForKeyboard: View) {
WindowCompat.getInsetsController(this, this.decorView).apply {
isAppearanceLightStatusBars = false
isAppearanceLightNavigationBars = true
}

ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { view, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())

// 키보드가 올라올 때 (IME 인셋이 있을 때) 버튼을 이동
viewForKeyboard.translationY = if (imeInsets.bottom > 0) {
-(imeInsets.bottom - systemBars.bottom).toFloat()
} else {
0f
}

view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}

EdgeToEdge 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainActivity : ComponentActivitiy() {

private fun setupWindowInsets() {
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { view, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}

override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() // 반드시 onCreate보다 먼저 호출해야 한다.
super.onCreate(savedInstanceState)
setupWindowInsets()
...
}
...
}

setDecorFitsSystemWindows

WindowInsetsCompat에 따라 decorView가 루트 수준 콘텐츠 뷰에 맞출 것인지 여부를 설정한다.

false로 설정하면 프레임워크는 콘텐츠 뷰를 인셋에 맞추지 않고 WindowInsetsCompat을 통해 콘텐츠 뷰에 전달한다.

API 35 미만에서는 시스템 바까지 확장된 풀스크린(edgeToEdge)이 decorFitsSystemWindows 옵션을 통해 선택적이었다.

decorFitsSystemWindows는 내부적으로 API 16-29에서 setSystemUiVisibility를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static class Api16Impl {

private Api16Impl() {
// This class is not instantiable.
}

static void setDecorFitsSystemWindows(
@NonNull Window window,
final boolean decorFitsSystemWindows
) {
final int decorFitsFlags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
final View decorView = window.getDecorView();
final int sysUiVis = decorView.getSystemUiVisibility();
decorView.setSystemUiVisibility(decorFitsSystemWindows
? sysUiVis & ~decorFitsFlags
: sysUiVis | decorFitsFlags);
}
}

WindowCompat.setDecorFitsSystemWindows(window, boolean)은 API에 맞는 동작으로 하도록 구성되어, API 30 이상과 API 16-29가 분기처리되어 있다.

1
2
3
4
5
6
7
8
public static void setDecorFitsSystemWindows(@NonNull Window window,
final boolean decorFitsSystemWindows) {
if (Build.VERSION.SDK_INT >= 30) {
Api30Impl.setDecorFitsSystemWindows(window, decorFitsSystemWindows);
} else {
Api16Impl.setDecorFitsSystemWindows(window, decorFitsSystemWindows);
}
}

그런데, API 35에서 이를 아우를 수 있는, 즉 decorFitsSystemWindows나 setSystemUiVisibility를 개발자가 사용하지 않고 enableEdgeToEdge() 함수 호출만으로 풀스크린을 구현할 수 있게 되었다.

문제는 그것 뿐만이 아니라 풀스크린이 기본 기기 설정이 된 것이다. enableEdgeToEdge()를 설정하지 않아도 targetSDK가 35인 앱은 풀스크린으로 실행된다. 따라서 그 기본 설정을 위해 enableEdgeToEdge()를 호출해줘야 하는 것이다.

enableEdgeToEdge()를 사용하기 이전에는 decorFitsSystemWindows를 사용하여 시스템 표시줄 뒤에 앱을 배치하였다.

1
2
3
4
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
}
1
2
3
<RelativeLayout
...
android:fitsSystemWindows="true">

그런데 enableEdgeToEdge()의 내부 코드를 보면 내부적으로 모두 setDecorFitsSystemWindows(window, false)를 호출하고 있음을 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@JvmName("enable")
@JvmOverloads
fun ComponentActivity.enableEdgeToEdge(
statusBarStyle: SystemBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
navigationBarStyle: SystemBarStyle = SystemBarStyle.auto(DefaultLightScrim, DefaultDarkScrim)
) {
val view = window.decorView
val statusBarIsDark = statusBarStyle.detectDarkMode(view.resources)
val navigationBarIsDark = navigationBarStyle.detectDarkMode(view.resources)
val impl = Impl ?: if (Build.VERSION.SDK_INT >= 29) {
EdgeToEdgeApi29()
} else if (Build.VERSION.SDK_INT >= 26) {
EdgeToEdgeApi26()
} else if (Build.VERSION.SDK_INT >= 23) {
EdgeToEdgeApi23()
} else if (Build.VERSION.SDK_INT >= 21) {
EdgeToEdgeApi21()
} else {
EdgeToEdgeBase()
}.also { Impl = it }
impl.setUp(
statusBarStyle, navigationBarStyle, window, view, statusBarIsDark, navigationBarIsDark
)
}

@RequiresApi(21)
private class EdgeToEdgeApi21 : EdgeToEdgeImpl {
@Suppress("DEPRECATION")
@DoNotInline
override fun setUp(...) {
WindowCompat.setDecorFitsSystemWindows(window, false)
...
}
}

@RequiresApi(23)
private class EdgeToEdgeApi23 : EdgeToEdgeImpl {
@DoNotInline
override fun setUp(...) {
WindowCompat.setDecorFitsSystemWindows(window, false)
...
}
}

@RequiresApi(26)
private class EdgeToEdgeApi26 : EdgeToEdgeImpl {
@DoNotInline
override fun setUp(...) {
WindowCompat.setDecorFitsSystemWindows(window, false)
...
}
}

@RequiresApi(29)
private class EdgeToEdgeApi29 : EdgeToEdgeImpl {
@DoNotInline
override fun setUp(...) {
WindowCompat.setDecorFitsSystemWindows(window, false)
...
}
}

링크 Scrim은 연극 용어이다. 앞쪽에 빛을 비추면 불투명하고, 뒤쪽에 빛을 비추면 투명하거나 반투명하게 보이는 theater drop을 의미한다.

키보드가 나타났을 때의 인셋 대응 - adjustResize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* EdgeToEdge(API 30부터 지원) 적용에 따른 인셋을 적용한다.
* 추가로, 키보드가 올라올 때 (IME 인셋이 있을 때) 버튼을 이동시킨다.
* @param viewForKeyboard 키보드가 올라올 때 이동시킬 View
*/
fun Window.setupWindowInsetsKeyboard(viewForKeyboard: View) {
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { view, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())

// 키보드가 올라올 때 (IME 인셋이 있을 때) 버튼을 이동
viewForKeyboard.translationY = if (imeInsets.bottom > 0) {
-(imeInsets.bottom - systemBars.bottom).toFloat()
} else {
0f
}

view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}

[Android] Android 15 대응 - EdgeToEdge: 1. Inset 적용

https://dl137584.github.io/2025/06/25/033-android15-edge-to-edge-01-inset/

Author

LEEJS

Posted on

2025-06-25

Updated on

2026-01-10

Licensed under

댓글