사전 요구 사항
- 프로젝트 반드시 Android Jetpack Compose를 사용해야 합니다.
- 프로젝트 반드시 SDK 35 이상을 대상(target)으로 해야 합니다. SDK가 35보다 낮으면 SDK를 35로 올리세요.
1단계: 계획
- 모든 Activity 클래스를 찾아 분석하여 엣지 투 엣지 지원이 이미 있는지 확인합니다. 엣지 투 엣지가 없는 모든 Activity에 대해 각 Activity를 엣지 투 엣지로 만들 계획을 세웁니다.
- 각 Activity에서 모든 리스트와 FAB 구성 요소를 찾아 분석하여 엣지 투 엣지 지원이 이미 있는지 확인합니다. 엣지 투 엣지 지원이 없는 각 구성 요소에 대해 이 구성 요소를 엣지 투 엣지로 만들 계획을 세웁니다.
- 각 Activity에서
TextField,OutlinedTextField또는BasicTextField를 검색합니다. 발견되면 이 스킬의 IME 섹션을 따라 IME가 입력 필드를 가리지 않는지 반드시 확인해야 합니다.
2단계: 엣지 투 엣지 지원 추가
- 아직
enableEdgeToEdge를 호출하지 않는 각 Activity의onCreate에서setContent전에enableEdgeToEdge를 추가합니다. - 소프트 키보드를 사용하는 모든 Activity에 대해
AndroidManifest.xml에android:windowSoftInputMode="adjustResize"를 추가합니다.
3단계: 삽입(insets) 적용
-
앱은 반드시 시스템 삽입을 적용하거나 콘텐츠를 눈금자(rulers)에 정렬하여 중요한 UI를 터치 가능한 상태로 유지해야 합니다. 이중 패딩을 방지하려면 한 가지 방법만 선택하세요.
- 권장: 사용 가능한 경우
Scaffold를 사용하고PaddingValues를 콘텐츠 람다에 전달하세요.
Scaffold { innerPadding -> // innerPadding은 시스템 표시줄과 모든 Scaffold 구성 요소를 고려합니다. LazyColumn( modifier = Modifier .fillMaxSize() .consumeWindowInsets(innerPadding), contentPadding = innerPadding ) { /* 콘텐츠 */ } }-
권장: 사용 가능한 경우 머티리얼 구성 요소의 자동 삽입 처리 또는 패딩 수정자를 사용하세요.
- Material 3 구성 요소는 자체 구성 요소의 안전 영역을 관리합니다. 여기에는 다음이 포함됩니다:
TopAppBarSmallTopAppBarCenterAlignedTopAppBarMediumTopAppBarLargeTopAppBarBottomAppBarModalDrawerSheetDismissibleDrawerSheetPermanentDrawerSheetModalBottomSheetNavigationBarNavigationRail
- Material 2 구성 요소의 경우
windowInsets매개변수를 사용하여BottomAppBar,TopAppBar및BottomNavigation에 수동으로 삽입을 적용하세요. 상위 컨테이너에 패딩을 적용하지 마세요. 대신 App Bar 구성 요소에 직접 삽입을 전달하세요. 상위 컨테이너에 패딩을 적용하면 App Bar 배경이 시스템 표시줄 영역으로 그려지는 것을 방지합니다. 예를 들어TopAppBar의 경우 다음 옵션 중 하나만 선택하세요:- 권장:
TopAppBar(windowInsets = AppBarDefaults.topAppBarWindowInsets) TopAppBar(windowInsets = WindowInsets.systemBars.exclude(WindowInsets.navigationBars))TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))
- 권장:
- Material 3 구성 요소는 자체 구성 요소의 안전 영역을 관리합니다. 여기에는 다음이 포함됩니다:
-
Scaffold 외부의 구성 요소의 경우 패딩 수정자(예:
Modifier.safeDrawingPadding()또는Modifier.windowInsetsPadding(WindowInsets.safeDrawing))를 사용하세요.Box( modifier = Modifier .fillMaxSize() .safeDrawingPadding() ) { Button( onClick = {}, modifier = Modifier.align(Alignment.BottomCenter) ) { Text("로그인") } } -
과도한 패딩이 있는 깊이 중첩된 구성 요소의 경우
WindowInsetsRulers(예:Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current))를 사용하세요. 코드 샘플은 IME 섹션을 참조하세요. -
요소(예: 사용자 정의 헤더 또는 장식용 스크림)가 시스템 표시줄의 치수와 같아야 하는 경우 삽입 크기 수정자(예:
Modifier.windowInsetsTopHeight(WindowInsets.systemBars))를 사용하세요. 코드 샘플은 리스트 섹션을 참조하세요.
- 권장: 사용 가능한 경우
적응형 스캐폴드(Scaffolds)
NavigationSuiteScaffold는NavigationRail또는NavigationBar와 같은 자체 구성 요소의 안전 영역을 관리합니다. 그러나 적응형 스캐폴드(예:NavigationSuiteScaffold,ListDetailPaneScaffold)는PaddingValues를 내부 콘텐츠로 전파하지 않습니다. 3단계에 설명된 대로 개별 화면 또는 구성 요소(예: 리스트contentPadding또는 FAB 패딩)에 삽입을 반드시 적용해야 합니다.NavigationSuiteScaffold상위에safeDrawingPadding또는 유사한 수정자를 적용하지 마세요. 이렇게 하면 클리핑이 발생하고 엣지 투 엣지 화면이 방지됩니다.
IME
- 소프트 키보드가 있는 각 Activity에 대해
AndroidManifest.xml에android:windowSoftInputMode="adjustResize"가 설정되어 있는지 확인하세요.SOFT_INPUT_ADJUST_RESIZE는 더 이상 사용되지 않으므로 사용하지 마세요. 그런 다음 입력 필드에 포커스를 유지하세요. 다음 중 하나를 선택하세요:-
- 권장: 콘텐츠 컨테이너에
Modifier.fitInside(WindowInsetsRulers.Ime.current)를 추가하세요. 계층 구조 상류에서 삽입을 소비하는 것을 잊어서 발생하는 버벅임과 추가 패딩을 줄이므로imePadding()보다 권장됩니다.
- 권장: 콘텐츠 컨테이너에
-
- 콘텐츠 컨테이너에
imePadding을 추가하세요. 패딩 수정자는Modifier.verticalScroll()전에 배치되어야 합니다. 상위 항목이 이미contentWindowInsets(예:contentWindowInsets = WindowInsets.safeDrawing)으로 IME를 처리하는 경우Modifier.imePadding()을 사용하지 마세요. 그렇게 하면 이중 패딩이 발생합니다.
- 콘텐츠 컨테이너에
-
Scaffold가 있는 IME 코드 패턴
올바름
contentWindowInsets에 IME 삽입이 포함되어 있으므로 올바릅니다. 이는 innerPadding으로 콘텐츠 람다에 전달됩니다.
// 올바름
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.verticalScroll(rememberScrollState())
) { /* 콘텐츠 */ }
}
contentWindowInsets에 관계없이 fitInside가 콘텐츠를 IME 삽입에 맞추므로 올바릅니다.
// 올바름
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.fitInside(WindowInsetsRulers.Ime.current)
.verticalScroll(rememberScrollState())
) { /* 콘텐츠 */ }
}
기본 contentWindowInsets는 IME 삽입을 포함하지 않고 imePadding()이 IME 삽입을 적용하므로 올바릅니다.
// 올바름
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* 콘텐츠 */ }
}
잘못됨
IME가 열릴 때 초과 패딩이 발생하므로 잘못되었습니다. IME 삽입이 두 번 적용됩니다. 한 번은 전달된 contentWindowInsets 값의 IME 삽입을 포함하는 innerPadding으로, 다른 한 번은 imePadding으로 적용됩니다.
// 잘못됨
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* 콘텐츠 */ }
}
IME가 콘텐츠를 덮게 되므로 잘못되었습니다. Scaffold의 기본 contentWindowInsets는 IME 삽입을 포함하지 않습니다.
// 잘못됨
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
) { /* 콘텐츠 */ }
}
Scaffold가 없는 IME 코드 패턴
올바름
다음 코드 샘플은 과도한 패딩을 발생시키지 않습니다.
// 올바름
Box(
// 삽입 소비됨
modifier = Modifier.safeDrawingPadding() // 또는 imePadding(), safeContentPadding(), safeGesturesPadding()
) {
Column(
modifier = Modifier.imePadding()
) { /* 콘텐츠 */ }
}
// 올바름
Box(
// 삽입 소비됨
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // 또는 WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures
) {
Column(
modifier = Modifier.imePadding()
) { /* 콘텐츠 */ }
}
// 올바름
Box(
// 삽입이 소비되지는 않지만 fitInside 때문에 관련 없음
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // 또는 WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier
.fillMaxSize()
.fitInside(WindowInsetsRulers.Ime.current)
) { /* 콘텐츠 */ }
}
잘못됨
다음 코드 샘플은 IME 삽입이 두 번 적용되어 과도한 패딩을 발생시킵니다.
// 잘못됨
Box(
// 삽입이 소비되지 않음
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // 또는 WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier.imePadding()
) { /* 콘텐츠 */ }
}
내비게이션 바 대비 및 시스템 표시줄 아이콘
-
Activity가
WindowCompat의enableEdgeToEdge를 사용하는 경우, 밝은 테마와 어두운 테마를 모두 지원하는 앱의 경우isAppearanceLightNavigationBars및isAppearanceLightStatusBars를 장치 테마의 반대로 설정하여 시스템 표시줄 아이콘을 읽을 수 있게 반드시 해야 합니다. 테마 파일에서 이 작업을 수행하는 것이 좋습니다. Activity가ComponentActivity의enableEdgeToEdge를 사용하는 경우 이 작업을 수행하지 마세요. 아이콘 색상을 자동으로 처리하기 때문입니다.// `WindowCompat`에서 `enableEdgeToEdge`를 호출하는 경우에만 사용하세요. // 테마 파일에 적용하세요. @Composable fun MyTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { val view = LocalView.current if (!view.isInEditMode) { SideEffect { val window = (view.context as? Activity)?.window ?: return@SideEffect val controller = WindowCompat.getInsetsController(window, view) // 밝은 모드(!darkTheme)에는 어두운 아이콘, 어두운 모드에는 밝은 아이콘 controller.isAppearanceLightStatusBars = !darkTheme controller.isAppearanceLightNavigationBars = !darkTheme } } MaterialTheme(content = content) } -
화면이 하단 바(예:
BottomAppBar,NavigationBar)와 함께Scaffold또는NavigationSuiteScaffold를 사용하는 경우, SDK 29+의 해당 Activity에서window.isNavigationBarContrastEnforced = false를 설정하세요. 이렇게 하면 시스템이 네비게이션 바에 반투명 배경을 추가하는 것을 방지하여 하단 바 색상이 화면 하단까지 확장되도록 합니다.
리스트
- 스크롤 가능한 구성 요소(예:
LazyColumn,LazyRow)의contentPadding매개변수에 삽입 패딩(예:Scaffold의innerPadding)을 적용하세요. 리스트의 상위 컨테이너에Modifier.padding()으로 적용하지 마세요. 이렇게 하면 콘텐츠가 잘리고 시스템 표시줄 뒤로 스크롤되는 것이 방지됩니다. - 시스템 표시줄을 덮는 반투명 컴포저블을 만들어 아이콘이 계속 읽기 쉽도록 하세요.
class SystemBarProtectionSnippets : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// enableEdgeToEdge는 window.isNavigationBarContrastEnforced = true로 설정합니다.
// 이는 세 버튼 내비게이션에 반투명 스크림을 추가하는 데 사용됩니다.
enableEdgeToEdge()
setContent {
MyTheme {
// 주요 콘텐츠
MyContent()
// 주요 콘텐츠를 그린 후 상태 표시줄 보호 그리기
StatusBarProtection()
}
}
}
}
@Composable
private fun StatusBarProtection(
color: Color = MaterialTheme.colorScheme.surfaceContainer,
) {
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(
with(LocalDensity.current) {
(WindowInsets.statusBars.getTop(this) * 1.2f).toDp()
}
)
.background(
brush = Brush.verticalGradient(
colors = listOf(
color.copy(alpha = 1f),
color.copy(alpha = 0.8f),
Color.Transparent
)
)
)
)
}
다이얼로그
다음 두 조건이 모두 참이면 다이얼로그는 전체 화면이며 엣지 투 엣지로 만들어야 합니다.
DialogProperties에usePlatformDefaultWidth = false가 포함되어 있습니다.- 다이얼로그가
Modifier.fillMaxSize()를 호출합니다.
전체 화면 다이얼로그를 엣지 투 엣지로 만들려면 DialogProperties에서 decorFitsSystemWindows = false를 설정하세요.
Dialog(
onDismissRequest = { /* 닫기 처리 */ },
properties = DialogProperties(
// 1. 다이얼로그가 화면 전체 너비로 확장되도록 허용
usePlatformDefaultWidth = false,
// 2. 다이얼로그가 상태 및 내비게이션 표시줄 뒤에 그려지도록 허용
decorFitsSystemWindows = false
)
) { /* 콘텐츠 */ }
체크리스트
- 모든
Activity가enableEdgeToEdge()를 호출합니까? -
AndroidManifest.xml에adjustResize가 설정되어 있습니까? - 모든
TextField,OutlinedTextField또는BasicTextField에imePadding(),fitInside,Modifier.safeDrawingPadding(),Modifier.safeContentPadding(),Modifier.safeGesturesPadding()을 사용하는 부모가 있거나contentWindowInsets가WindowInsets.safeDrawing또는WindowInsets.ime으로 설정되어 있습니까? - 첫 번째 및 마지막 리스트 항목이 삽입을
contentPadding에 전달하여 시스템 표시줄에서 멀어지게 그려집니까? - FAB가 Scaffold 내부에 있거나
Modifier.safeDrawingPadding()을 적용하여 내비게이션 바 위에 그려집니까? - 프로젝트가 빌드됩니까?
./gradlew build를 실행하여 확인하세요.