1. 개요
안드로이드 애플리케이션에서는 상태 표시줄과 탐색 바를 숨기는 전체 화면 요구사항이 자주 발생합니다. 전체 화면 몰입 처리를 통해 애플리케이션이 더 나은 디스플레이 효과를 얻을 수 있습니다. 안드로이드 4.1 이전에는 상태 표시줄만 숨길 수 있었고, 4.1 이후부터는 SystemUI를 제어하는 방법이 제공되었습니다. 안드로이드 P에서는 노치 스크린 처리가 추가되어 애플리케이션이 노치 스크린에 대한 적응이 필요해졌습니다. 안드로이드 Q에서는 전체 화면 제스처 탐색이 추가되어 애플리케이션이 전체 화면 제스처 탐색에 대한 적응도 필요해졌습니다. 안드로이드 R부터는 WindowInsetsController가 추가되어 Window 효과를 제어합니다.
2. 안드로이드 4.4 이전 상태 표시줄 숨기기
2.1 코드를 통한 설정
Activity에 대해 WindowManager 플래그를 사용하여 동적으로 설정할 수 있습니다:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 젤리빈보다 낮은 안드로이드 버전의 경우, 이 호출을 사용하여
// 상태 표시줄을 숨깁니다.
if (Build.VERSION.SDK_INT < 16) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
setContentView(R.layout.activity_main);
}
}
FLAG_LAYOUT_IN_SCREEN을 사용하여 activity 레이아웃이 동일한 화면 영역을 사용하도록 설정할 수 있습니다. FLAG_FULLSCREEN을 활성화하면 동일한 화면 영역을 사용할 수 있으며, 이는 상태 표시줄이 숨겨지고 표시될 때 콘텐츠 크기가 조정되는 것을 방지합니다.
2.2 XML 구성
activity의 상태 표시줄을 항상 숨기려면 manifest에서 style을 설정하는 것이 좋습니다:
<application
...
android:theme="@android:style/Theme.Holo.NoActionBar.Fullscreen" >
...
</application>
manifest 설정 방식의 장점:
- 프로그래밍 방식보다 유지보수가 쉽고 오류 발생 가능성이 적습니다.
- 더 부드러운 전환 효과가 있으며, activity 인스턴스화 전에 시스템이 UI 렌더링에 필요한 정보를 이미 가지고 있기 때문입니다.
안드로이드 4.1 이전의 전체 화면 경험은 매우 좋지 않으며, 심각한 단편화로 인해 일부 기기에서는 예상된 효과가 나타나지 않을 수 있습니다.
3. 안드로이드 4.4에서 전체 화면 설정
안드로이드 4.1 이후부터는 setSystemUiVisibility를 통해 SystemUI를 제어하며, 사용되는 플래그는 다음과 같습니다:
SystemBar 관련 제어:
SYSTEM_UI_FLAG_FULLSCREEN
SYSTEM_UI_FLAG_HIDE_NAVIGATION
SYSTEM_UI_FLAG_LOW_PROFILE
레이아웃 관련:
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
SYSTEM_UI_FLAG_LAYOUT_STABLE
몰입형 고정 관련(안드로이드 4.4 도입):
SYSTEM_UI_IMMERSIVE
SYSTEM_UI_IMMERSIVE_STICKY
3.1 SystemBar 제어
SYSTEM_UI_FLAG_FULLSCREEN
이 속성은 상태 표시줄을 숨기는 데 사용됩니다
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
getSupportActionBar().hide(); // ActionBar를 숨깁니다
위 코드를 통해 상태 표시줄을 숨길 수 있습니다. 전체 화면 효과를 보여주기 위해 ActionBar도 함께 숨깁니다. SYSTEM_UI_FLAG_FULLSCREEN 속성만 설정한 경우의 표시 효과:
- system bar를 슬라이드하거나 홈 키, 메뉴 키를 누르면 플래그가 초기화되어 상태 표시줄이 다시 표시됩니다.
- 레이아웃도 상태 표시줄의 표시/숨김에 따라 재조정되고 다시 그려집니다.
SYSTEM_UI_HIDE_NAVIGATION
이 속성은 탐색 바를 숨기는 데 사용됩니다
View decorView2 = getWindow().getDecorView();
int uiOptions2 = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView2.setSystemUiVisibility(uiOptions2);
getSupportActionBar().hide();
여기서는 상태 표시줄과 탐색 바를 모두 숨깁니다. 효과:
- 상태 표시줄 숨김과 다른 점은 레이아웃 내 임의 위치를 클릭해도 탐색 바가 다시 표시된다는 것입니다.
- 레이아웃도 상태 표시줄과 탐색 바의 표시/숨김에 따라 재조정되고 다시 그려집니다.
SYSTEM_UI_LOW_PROFILE
이 속성은 SystemBar를 시각적으로 흐릿하게 만들어 중요도를 낮추는 기능입니다. 구체적으로는 상태 표시줄 아이콘이 배터리 시간 등 핵심 아이콘만 남고 어두워지며, 탐색 바 아이콘이 세 점으로 변경되거나 어두워집니다. 이 플래그는 거의 사용되지 않습니다.
View decorView7 = getWindow().getDecorView();
int uiOptions7 = View.SYSTEM_UI_FLAG_LOW_PROFILE;
decorView7.setSystemUiVisibility(uiOptions7);
3.2 레이아웃 관련
새로운 안드로이드 4.1 및 이후의 SystemUI 설정에서 상태 표시줄과 탐색 바 숨김 플래그만 단독으로 설정하면 레이아웃이 다시 그려지므로, 상태 표시줄과 탐색 바의 표시/숨김 시 레이아웃의 안정적인 표시 효과를 유지하려면 다음 속성이 필요합니다.
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
레이아웃이 상태 표시줄 위치까지 확장되도록 합니다. 상태 표시줄이 숨겨지고 표시되기 전후로 레이아웃이 안정적으로 상태 표시줄 뒤에 표시됩니다(상태 표시줄 표시 시 레이아웃이 상태 표시줄 뒤에 있고, 상태 표시줄 숨김 시 레이아웃도 변경되지 않음).
View decorView3 = getWindow().getDecorView();
int uiOptions3 = View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView3.setSystemUiVisibility(uiOptions3);
getSupportActionBar().hide();
위 코드의 표시 효과는 이전 코드와 비교하여 레이아웃이 상태 표시줄 위치까지 확장됩니다:
- systembar를 슬라이드하거나 홈 키, 메뉴 키를 누르면 플래그가 초기화되어 상태 표시줄이 다시 표시됩니다.
- 레이아웃은 상태 표시줄의 표시/숨김에 따라 조정되지 않습니다.
SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
레이아웃이 탐색 바 위치까지 확장되도록 합니다. 탐색 바가 숨겨지고 표시되기 전후로 레이아웃이 안정적으로 탐색 바 뒤에 표시됩니다(탐색 바 표시 시 레이아웃이 탐색 바 뒤에 있고, 탐색 바 숨김 시 레이아웃도 변경되지 않음).
View decorView4 = getWindow().getDecorView();
int uiOptions4 = View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView4.setSystemUiVisibility(uiOptions4);
getSupportActionBar().hide();
위 코드의 표시 효과는 상태 표시줄과 탐색 바가 숨겨지고, 레이아웃이 상태 표시줄과 탐색 바 위치까지 확장됩니다:
- 임의 레이아웃을 클릭하면 플래그가 초기화되어 상태 표시줄과 탐색 바가 다시 표시됩니다.
- 레이아웃은 상태 표시줄과 탐색 바의 표시/숨김에 따라 조정되지 않습니다.
SYSTEM_UI_FLAG_LAYOUT_STABLE
이 플래그는 레이아웃 안정성을 유지하여 상태 표시줄과 탐색 바의 표시/숨김 시 레이아웃 변경을 방지합니다.
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION두 플래그의 사용을 보조하여 애플리케이션이 전체 화면 상태에서 레이아웃이 상태 표시줄과 탐색 바의 표시/숨김에 따라 변경되지 않도록 합니다. 이 두 플래그와 함께 사용하지 않고도 레이아웃 안정성 유지를 달성할 수 있지만, 전체 화면을 실현할 수 없고 상태 표시줄과 탐색 바 위치가 남게 됩니다.
View decorView3 = getWindow().getDecorView();
int uiOptions3 = View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView3.setSystemUiVisibility(uiOptions3);
getSupportActionBar().hide();
위 코드의 표시 효과는 이전 코드와 비교하여 레이아웃이 상태 표시줄 위치까지 확장됩니다:
- systembar를 슬라이드하거나 홈 키, 메뉴 키를 누르면 플래그가 초기화되어 상태 표시줄이 다시 표시됩니다.
- 레이아웃은 상태 표시줄의 표시/숨김에 따라 조정되지 않습니다.
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN, SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 플래그를 설정한 상태에서 상태 표시줄과 탐색 바 색상을 투명하게 설정하면 투명한 상태 표시줄과 탐색 바가 레이아웃 위에 겹쳐지는 효과가 있습니다. 이는 레이아웃이 실제로 상태 표시줄과 탐색 바 위치까지 확장되었음을 증명합니다.
3.3 몰입형 고정 관련
위 플래그 조합 설정에서 계속 존재하는 문제(홈 키, 메뉴 키 등을 클릭하면 플래그가 초기화되고, 인터페이스를 클릭하면 플래그가 초기화되어 효과가 사라지는 문제)가 있습니다. 대부분의 경우 효과가 안정적으로 표시되기를 원하며, 간단한 작업 후에 사라지기를 원하지 않습니다. 다음 두 속성이 이 문제를 해결합니다.
SYSTEM_UI_IMMERSIVE
위 플래그 설정 기반에 이 속성을 설정하면 홈 키, 메뉴 키를 클릭해도 상태가 손실되지 않습니다. 하지만 수동으로 systembar를 호출하면 관련 플래그가 초기화됩니다.
View decorView5 = getWindow().getDecorView();
int uiOptions5 = View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_IMMERSIVE;
decorView5.setSystemUiVisibility(uiOptions5);
getSupportActionBar().hide();
위 코드의 표시 효과:
- 상태 표시줄, 탐색 바를 숨깁니다.
- 레이아웃이 상태 표시줄, 탐색 바 위치까지 확장됩니다.
- 레이아웃이 안정적으로 표시되어 상태 표시줄의 표시/숨김으로 인한 레이아웃 조정이 없습니다.
- 수동으로 상태 표시줄과 탐색 바를 호출할 때만 플래그가 초기화됩니다.
SYSTEM_UI_IMMERSIVE_STICKY
이 속성을 설정하면 상태 표시줄이 숨겨진 상태에서 수동으로 상태 표시줄과 탐색 바를 호출할 때 잠시 표시되었다가 다시 숨겨집니다. 이 속성을 설정하면 플래그가 초기화되지 않으며, 상대적으로 자주 사용되는 속성입니다. 하지만 페이지를 떠나면 플래그가 초기화되어 효과가 사라집니다. 위 모든 플래그 설정에서 이런 현상이 발생합니다.
View decorView6 = getWindow().getDecorView();
int uiOptions6 = View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
decorView6.setSystemUiVisibility(uiOptions6);
getSupportActionBar().hide();
위 코드의 표시 효과:
- 상태 표시줄, 탐색 바를 숨깁니다.
- 레이아웃이 상태 표시줄, 탐색 바 위치까지 확장됩니다.
- 레이아웃이 안정적으로 표시되어 상태 표시줄의 표시/숨김으로 인한 레이아웃 조정이 없습니다.
- 수동으로 호출된 상태 표시줄과 탐색 바가 반투명하게 표시되어 인터페이스 위에 겹쳐진 후 다시 숨겨집니다.
- 페이지를 떠나면 플래그가 초기화되어 효과가 사라집니다.
3.4 전체 화면 설명
3.4.1 플래그 초기화 문제
전체 화면 표시 효과를 실현하기 위해 다양한 플래그로 window를 제어하는데, 이러한 플래그는 초기화될 수 있습니다. 플래그를 능동적으로 초기화하려면 setSystemUiVisibility(0)을 직접 호출할 수 있습니다.
플래그 초기화 문제를 해결하기 위해 재설정 위치를 onWindowFocusChanged에 둘 수 있습니다. 일반적인做法은 onWindowFocusChanged 콜백에서 다시 설정하는 것입니다.
class MainActivity : AppCompatActivity() {
...
override fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
}
}
...
}
3.4.2 전체 화면 경험
실제로 상태 표시줄과 탐색 바를 숨겨야 하는 시나리오는 많지 않으며, 게임, 비디오 등 애플리케이션에서 필요할 수 있습니다. 대부분의 경우 뷰 콘텐츠를 상태 표시줄과 탐색 바 뒤로 확장시켜(정말로 필요할까?) 더 강력한 시각적 충격 효과를 얻는 것만으로도 충분합니다.
이때 상태 표시줄과 탐색 바 색상을 투명하게 설정해야 합니다. 안드로이드 Api 21은 해당 인터페이스를 제공하며, 예제 코드는 다음과 같습니다:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
}
뷰 콘텐츠를 상태 표시줄과 탐색 바 뒤로 확장시키는 것이 정말로 필요한지, 그리고 발생할 수 있는 상호작용 충돌을 어떻게 해결할지에 대해서도 고려해야 합니다.
안드로이드 10은 전체 화면 제스처 탐색을 지원하며, 탐색 바 자체의 크기와 돌출 정도가 매우 작기 때문에 일반적으로 콘텐츠를 탐색 바 뒤로 확장시키는 것을 권장합니다.
안드로이드 9 및 이하 기기에서는 탐색 바 뒤에 콘텐츠를 그리는 것이 선택적이며, 상황에 따라 적절히 선택해야 합니다.
상태 표시줄 뒤에 콘텐츠를 그리는 것은 완전히 개발자에게 달려 있으며, 콘텐츠가 배경 이미지나 지도 애플리케이션인 경우에는 뷰 콘텐츠를 상태 표시줄 뒤로 확장시킬 수 있지만, UI 페이지 상단이 Toolbar인 경우는 적절하지 않습니다.
안드로이드 10의 전체 화면 경험에 대해서는 5절에서 자세히 다룹니다.
3.4.3 혼란스러운 fitsSystemWindows 속성
공식 문서에 따르면 어떤 View의 fitsSystemWindows가 true로 설정되면 해당 View의 padding 속성이 시스템에 의해 설정되며, 사용자가 레이아웃 파일에서 설정한 padding은 무시됩니다. 시스템은 해당 View에 paddingTop을 설정하며, 값은 statusbar의 높이입니다. fitsSystemWindows의 기본값은 false입니다.
중요 설명
- 뷰 콘텐츠를 상태 표시줄이나 탐색 바 뒤로 확장시키는 경우(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 또는 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 플래그 설정)에만 fitsSystemWindows가 작동합니다. 그렇지 않으면 systemBar 공간이 사용자 처리를 기다리지 않고 ContentView의 부모 컨트롤이 처리하며, HierarchyView 도구로 확인하면 ContentView 부모 컨트롤의 paddingTop 또는 paddingBottom이 설정된 것을 볼 수 있습니다.
- 여러 view가 동시에 fitsSystemWindows를 설정하면 첫 번째 것만 작동합니다.
4. 안드로이드 P 노치 스크린 적응
안드로이드의 노치 스크린에는 노치 스크린, 물방울 스크린, 구멍 스크린이 포함되며, 안드로이드 9.0(API Level 28)부터 안드로이드 공식적으로 노치 스크린 적응 지원이 나왔습니다. 아래는 공식 노치 스크린 적응 방안입니다.
4.1 공식 노치 스크린 기기 요구사항
일관성과 애플리케이션 호환성을 보장하기 위해 공식적으로 안드로이드 9를 탑재한 기기에 다음과 같은 요구사항이 있습니다:
- 한 가장자리에 최대 하나의 노치만 포함할 수 있습니다.
- 한 기기에 두 개 이상의 노치가 있을 수 없습니다.
- 기기의 두 긴 가장자리에 노치가 있을 수 없습니다.
- 특별한 플래그를 설정하지 않은 세로 모드에서 상태 표시줄의 높이는 노치의 높이 이상이어야 합니다.
- 기본적으로 전체 화면 모드나 가로 모드에서 전체 노치 영역은 검은 테두리를 표시해야 합니다.
4.2 시스템 상태 표시줄을 숨기지 않는 경우
애플리케이션의 모든 인터페이스가 상태 표시줄을 숨기지 않는 경우, 즉 애플리케이션이 시스템 상태 표시줄과 겹치지 않는 경우 노치 스크린 적응이 필요 없으며, 시스템 상태 표시줄이 자동으로 노치 절개 위치를 차지합니다.
세로 모드와 가로 모드에서의 효과:
4.3 시스템 상태 표시줄을 숨기는 경우
4.3.1 애플리케이션이 노치 절개 영역을 처리하는 방법 구성
시스템 상태 표시줄을 숨기면 애플리케이션의 콘텐츠가 시스템 상태 표시줄의 원래 위치로 확장되며, 시스템은 노치 절개 영역에 콘텐츠를 표시할지 여부를 제어하는 구성 옵션을 제공합니다. 이 구성은 Window 수준 속성이며, 속성 이름은 layoutInDisplayCutoutMode입니다. 세 가지 선택 가능한 값이 있으며, 각각:
- WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
이것이 기본 동작입니다. 공식 설명에 따르면 세로 모드에서 콘텐츠가 노치 영역에 표시되지만 가로 모드에서는 콘텐츠가 검은 테두리를 표시합니다. - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
세로 모드와 가로 모드 모두에서 콘텐츠가 노치 영역에 표시됩니다. - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
콘텐츠가 절대로 노치 영역에 표시되지 않습니다.
이 속성 구성 방식은 두 가지가 있습니다:
1. xml 구성, 테마 스타일 파일을 통한 구성:
테마 스타일 파일에서 android:windowLayoutInDisplayCutoutMode를 정의하며, 예제 코드는 다음과 같습니다:
<style name="Theme.DisplayCutoutDemo" parent="...">
...
<item name="android.windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
2. 코드를 통한 정의
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_FULLSCREEN // 상태 표시줄 숨김
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // 탐색 바 숨김
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // 뷰 콘텐츠가 상태 표시줄 영역으로 확장되도록 허용
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // 뷰가 탐색 바 영역으로 확장되도록 허용
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val lp = window.attributes
lp.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
window.attributes = lp
}
안드로이드 시스템은 뷰 콘텐츠가 시스템 상태 표시줄과 탐색 바 영역과 겹치는 것을 허용하지 않으므로, 뷰 콘텐츠가 시스템 상태 표시줄과 탐색 바 영역으로 강제 확장되도록 하려면 systemUiVisibility를 적절히 설정해야 합니다.
4.3.2 노치 절개 영역의 뷰 적응
뷰 콘텐츠를 노치 절개로 확장하면 노치 절개가 일부 콘텐츠를 가릴 수 있습니다. 가려진 부분이 경험에 영향을 주지 않으면 처리할 필요가 없습니다. 하지만 가려진 부분이 텍스트 콘텐츠이거나 사용자가 상호작용할 수 있는 작업이라면 적응이 필요합니다. 구체적인 적응 방식은 노치 절개의 위치와 크기를 얻어 처리하는 것입니다. 시스템은 WindowInsets.getDisplayCutout() 인터페이스를 제공하여 DisplayCutout 객체를 얻을 수 있으며, 이 객체는 화면의 모든 절개 위치와 크기를 설명합니다.
DisplayCutout 객체를 얻는 방법
DisplayCutout 객체를 얻으려면 먼저 WindowInsets 객체를 얻어야 합니다. WindowInsets 객체는 직접 얻는 방법이 없으며, 리스너나 콜백 메서드를 통해서만 얻을 수 있습니다. WindowInsets 객체를 얻는 두 가지 방법:
- 사용자 정의 View 내부에서
View.onApplyWindowInsets(WindowInsets)메서드를 재정의하여 얻기
class MyView(context: Context): View(context) {
override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets {
return super.onApplyWindowInsets(insets)
}
}
주의사항: View.onApplyWindowInsets(WindowInsets) 메서드를 재정의하여 얻는 경우 WindowInsets가 이전에 소비되지 않았는지 확인하고, View의 부모나 View에 View.OnApplyWindowInsetsListener 리스너를 설정하지 않았는지 확인해야 합니다.
- View에
View.onApplyWindowInsetsListener리스너를 설정하여 얻기
window.decorView.apply{
window.decorView.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
setOnApplyWindowInsetsListener { v, insets ->
insets
}
}
}
}
화면 노치 절개 영역을 얻는 방법
WindowInsets 객체를 얻었으면 WindowInsets.getDisplayCutout()을 통해 절개 관련 정보를 얻을 수 있습니다.
API Level 28에서 이 객체는 화면의 안전 영역만 지원하며, 다음 메서드를 통해 각 변의 안전 간격을 얻을 수 있습니다:
WindowInsets.getSafeInsetBottom()
WindowInsets.getSafeInsetLeft()
WindowInsets.getSafeInsetRight()
WindowInsets.getSafeInsetTop()
API Level 29부터는 구체적인 절개 위치와 크기 정보도 지원하며, 방법은 다음과 같습니다:
WindowInsets.getBoundingRectBottom()
WindowInsets.getBoundingRectLeft()
WindowInsets.getBoundingRectRight()
WindowInsets.getBoundingRectTop()
각 변의 절개 정보를 얻어 절개 정보를 알면 더 정밀하게 표시를 제어할 수 있습니다.
API 21부터 WindowInsets 객체를 통해 상태 표시줄과 탐색 바의 높이를 얻을 수 있으며, 이 인터페이스는 다음과 같습니다:
WindowInsets.getStableInsetBottom()
WindowInsets.getStableInsetLeft()
WindowInsets.getStableInsetRight()
WindowInsets.getStableInsetTop()
API Level 30부터 이 인터페이스는 폐기되었으며, 다음 방법으로 대체됩니다:
WindowInsets.getInsetsIgnoringVisibility (int typeMask)
이 방법을 사용하면 상태 표시줄과 탐색 바가 숨겨진 상태에서도 영향을 받지 않고 얻을 수 있습니다.
예제
window.decorView.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
setOnApplyWindowInsetsListener { v, insets ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
insets.displayCutout?.apply {
binding.tvTest.setPadding(safeInsetLeft, safeInsetTop, safeInsetRight, safeInsetBottom)
}
}
insets
}
}
}
적응 전후 효과도:
4.3.3 노치 스크린 적응 주의사항
- 노치 절개 영역이 중요한 텍스트, 컨트롤 또는 기타 정보를 가리지 않도록 하세요.
- 노치 절개 영역에 미세한 터치 인식이 필요한 상호작용 요소를 배치하거나 확장하지 마세요. 노치 절개 영역의 터치 감도가 다른 영역보다 낮을 수 있습니다.
- 상태 표시줄 높이를 하드코딩하지 마세요. 그렇지 않으면 콘텐츠가 겹치거나 잘릴 수 있습니다.
- 애플리케이션이 전체 화면 모드에 진입해야 하는 경우 shortEdges 모드를 사용하고, 전체 화면을 종료할 때 never 모드를 사용하세요.
- 전체 화면 모드에서 창 좌표와 화면 좌표를 사용할 때 주의하세요. 검은 테두리를 표시하는 경우 창이 전체 화면을 차지하지 않기 때문에 화면 원점(화면 왼쪽 상단)에서 얻은 좌표와 창 원점(창 왼쪽 상단)에서 얻은 좌표가 같지 않습니다. 필요에 따라
View.getLocationOnScreen()인터페이스를 사용하여 화면 좌표를 뷰 좌표로 변환할 수 있습니다. MotionEvent를 처리할 때MotionEvent.getX()와MotionEvent.getY()를 사용하여 유사한 좌표 문제를 피하세요.MotionEvent.getRawX()나MotionEvent.getRawY()를 사용하지 마세요.
5. 안드로이드 Q 전체 화면 및 제스처 탐색
안드로이드 10에서는 새로운 시스템 탐색 모드가 추가되어 사용자가 제스처 상호작용을 통해 뒤로 가기, 홈 화면으로 돌아가기, 장치 도우미 열기 등의 작업을 수행할 수 있습니다. 제스처 상호작용을 사용하여 시스템 탐색을 수행함으로써 애플리케이션은 더 많은 화면 공간을 활용할 수 있으며, 이를 통해 사용자에게 더 몰입감 있는 경험을 제공할 수 있습니다.
5.1 안드로이드 10의 전체 화면
안드로이드 10에서 전체 화면을 구현하는 것은 세 단계로 나뉩니다:
1. 전체 화면 레이아웃 요청
3절에서 안드로이드 10에서는 뷰 콘텐츠를 탐색 바로 확장시키는 것을 권장한다고 했으며, 여기서는 주로 탐색 바에 집중합니다. 이 부분의 내용은 3절과 일치하며, 뷰의 setSystemUiVisibility() 메서드를 사용하면 됩니다. 주요 관심사는:
view.systemUiVisibility =
// 시스템에 창이 콘텐츠를 가장 극단적인 시나리오로 배치하기를 원한다고 알림
// 구체적인 내용은 문서 참조
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
// 시스템에 창이 탐색 바가 숨겨진 것처럼 콘텐츠를 배치하기를 원한다고 알림
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
여기서는 더 설명하지 않습니다.
효과:
2. 시스템 바 색상 변경
안드로이드 10에서는 시스템 바 색상을 완전히 투명하게 설정하면 됩니다:
XML을 통해 설정:
<style name="Theme.MyApp">
<item name="android:navigationBarColor">
@android:color/transparent
</item>
<!-- 선택사항, 상태 표시줄도 그 뒤에 그리는 경우 -->
<item name="android:statusBarColor">
@android:color/transparent
</item>
</style>
또는 코드를 통해 설정
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
}
이때 시스템은 두 가지 작업을 수행합니다:
- 동적 색상 적응
시스템 바의 콘텐츠는 그 뒤의 콘텐츠에 따라 색상이 변경됩니다. 드래그 막대가 밝은 콘텐츠 앞에 있으면 어두워지고, 어두운 콘텐츠 앞에 있으면 밝아집니다. - 반투명 덮개
애플리케이션이 targetSdkVersion을 29로 선언하면 시스템은 시스템 바 뒤에 반투명 덮개를 배치할 수 있습니다. SDK 28 또는 더 낮은 버전에서는 시스템이 덮개를 표시하지 않고 투명한 탐색 바를 제공합니다.
물론 다른 기기 제조사들은 상황에 따라 동적 색상 적응을 비활성화할 수 있습니다. 예를 들어 휴대폰 시스템 성능이 동적 색상 적응을 지원할 수 없는 경우입니다.
안드로이드 9 또는 이전 버전에서는 탐색 바에 반투명 마스크를 직접 설정해야 합니다. 예제 코드:
<!-- values/themes.xml -->
<style name="Theme.MyApp">
<item name="android:navigationBarColor">
#B3FFFFFF
</item>
</style>
<!-- values-night/themes.xml -->
<style name="Theme.MyApp">
<item name="android:navigationBarColor">
#B3000000
</item>
</style>
5.2 여백 영역(Insets)
앞서 언급했듯이 뷰 콘텐츠를 시스템 바 뒤로 확장시킨 후 콘텐츠가 겹칠 수 있어 시각적 충돌이 발생할 수 있으며, 이러한 시각적 충돌을 어떻게 처리할까요?
먼저 용어인 여백 영역(insets)을 익혀봅시다. Insets 영역은 시스템 UI와 교차되는 화면의 부분을 설명하는 역할을 합니다. 예를 들어 탐색 바나 상태 표시줄입니다. 애플리케이션의 컨트롤이 이러한 영역에 나타나면 시스템 UI에 의해 가려질 수 있습니다. insets 영역을 사용하여 시각적 충돌을 해결할 수 있으며, 뷰를 화면 가장자리에서 적절한 위치로 이동시킬 수 있습니다.
안드로이드에서 Insets 영역은 WindowInsets 클래스로 표현되며, AndroidX에서는 WindowInsetsCompat을 사용합니다. 안드로이드 10 시스템에서 애플리케이션 레이아웃을 처리할 때 개발자는 5가지 insets 영역을 얻는 방법을 알아야 합니다.
5.2.1 시스템 창 여백 영역
메서드: getSystemWindowInsets()
시스템 창 영역은 가장 자주 사용됩니다. API 1부터 다양한 형태로 존재해왔으며, 시스템 UI가 애플리케이션 위에 겹쳐 표시될 때마다 이 메서드가 호출됩니다. 일반적인 예는 드롭다운 상태 표시줄과 탐색 바, 또는 화면 소프트 키보드(IME) 팝업입니다.
예제:
xml 코드:
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#008B8B"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button test"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
시스템 바 색상을 투명하게 설정하고 뷰 콘텐츠를 시스템 바 뒤로 확장시킵니다. java 코드:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
}
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
이 레이아웃의 표시 효과:
이제 button이 가려져 클릭하면 recent task가 트리거될 수 있어 시각적 및 상호작용 충돌이 발생합니다. 해결책은 button을 위로 이동시켜 부모 컨테이너에 padding을 설정하는 것입니다. 하지만 사용자가 가상 키 탐색 또는 전체 화면 제스처 탐색을 설정했을 수 있으므로 이 padding을 하드코딩해서는 안 되고, insets를 통해 동적으로 탐색 바의 높이를 얻어야 합니다. 앞서 노치 스크린 처리와 동일합니다. insets 객체는 직접 얻을 수 없고 콜백을 통해서만 얻을 수 있습니다.
ViewCompat.setOnApplyWindowInsetsListener(binding.btnTest) { v, insets ->
v.updatePadding(bottom = insets.systemWindowInsetBottom)
insets
}
효과:
간단히 말해, 시스템 창 영역 insets는 클릭 가능한 컨트롤에 가장 적합하며, 시스템 바가 컨트롤을 가리지 않도록 할 수 있습니다.
5.2.2 클릭 가능 영역
메서드: getTappableElementInsets()
다음은 안드로이드 10에서 새로 추가된 클릭 가능 영역 insets입니다. 위의 시스템 창 영역 insets와 매우 유사합니다. 클릭 가능 영역 insets는 시스템 클릭 동작(tap)을 트리거할 수 있는 최소 영역을 정의합니다. 클릭 가능 영역의 값을 사용하여 레이아웃을 구성할 때 여전히 자신의 컨트롤이 시스템 UI와 시각적으로 겹칠 수 있다는 점에 주의해야 합니다. 이는 시스템 창 영역 insets와 다릅니다. 후자의 값을 사용하여 자신의 컨트롤을 이동시키면 시스템/탐색 바와 시각적 겹침이 발생하지 않도록 할 수 있습니다.
실제 테스트 결과 가상 삼키 탐색에서는 차이가 명확하지 않지만 전체 화면 제스처 탐색에서는 차이가 보입니다:
시스템 창 영역 insets 처리,
ViewCompat.setOnApplyWindowInsetsListener(binding.btnTest) { v, insets ->
v.updatePadding(bottom = insets.systemWindowInsetBottom)
insets
}
클릭 가능 영역 insets 처리,
ViewCompat.setOnApplyWindowInsetsListener(binding.btnTest) { v, insets ->
v.updatePadding(bottom = insets.tappableElementInsets.bottom)
insets
}
효과도는 다음과 같습니다:
실용적인 관점에서 시스템 창 영역 insets를 사용하는 것을 권장하며, 거의 모든 클릭 가능 영역 insets 사용 사례를 더 잘 만족할 수 있습니다.
5.2.3 시스템 제스처 여백 영역
메서드: getSystemGestureInsets() & getMandatorySystemGestureInsets()
이것은 안드로이드 10에서 새로 추가된 것으로, 안드로이드 10 전체 화면 제스처 탐색을 통해 사용자가 제스처 동작을 수행할 수 있습니다:
- 화면 왼쪽/오른쪽 가장자리에서 중앙으로 스와이프하여 뒤로 가기 버튼(Back)과 동일한 효과를 얻습니다.
- 화면 하단에서 위로 스와이프하여 최근 사용 앱(Recent)을 전환할 수 있습니다.
시스템 제스처 영역에서 시스템 제스처 동작이 애플리케이션의 자체 제스처 동작보다 우선합니다. 시스템 제스처 영역을 얻는 방법은 두 가지입니다. getMandatorySystemGestureInsets()는 필수 시스템 제스처 영역만 포함하며 시스템 제스처 영역의 하위 집합입니다.
안드로이드 10에서 시스템 제스처 영역은 다음과 같습니다:
제스처 동작이 필요한 컨트롤이 시스템 제스처 영역에 나타나면 해당 값을 사용하여 이러한 컨트롤을 이동시킬 수 있습니다. 일반적인 예는 하단 탐색 메뉴, 게임의 슬라이드 상호작용, ViewPager 등입니다.
필수 시스템 제스처 여백 영역은 시스템 제스처 여백 영역의 하위 집합이며, "필수 영역"이라고 불리는 이유는 애플리케이션이 이러한 영역을 수정할 수 없기 때문입니다. 필수 시스템 제스처 여백 영역은 시스템이 예약한 영역만 포함하며, 이러한 영역에서 시스템 제스처 동작이 항상 우선합니다. 안드로이드 10에서 현재 유일한 필수 영역은 화면 하단의 홈 제스처 영역이며, 시스템은 이 영역을 예약하여 사용자가 언제든지 현재 애플리케이션에서 나갈 수 있도록 합니다.
5.2.4 안정 표시 여백 영역
메서드: getStableInsets()
이 메서드는 제스처 탐색과 관련이 적으며, 시스템 창 여백 영역과 유사합니다. 안정 표시 영역은 시스템 UI가 애플리케이션에 표시될 수 있는 위치입니다. 일부 표시 모드(예: 릴렉스 모드와 몰입 모드)에서는 시스템 UI가 상황에 따라 표시 여부가 전환될 수 있습니다(예: 게임, 사진 보기, 비디오 플레이어 등). 이때 안정 표시 영역을 사용하면 자신의 컨트롤이 "갑작스럽게 나타나는" 시스템 UI에 가려지지 않도록 할 수 있습니다.
5.3 여백 영역 충돌 처리
여백 영역 충돌 처리는 이전 절에서 이미 다루었으며, 주로 windowInsets를 사용하여 여백 영역을 얻고 충돌하는 컨트롤을 여백 영역 밖으로 이동시키는 것입니다.
앞서 노치 스크린 적응을 다룰 때 WindowInsets 객체는 직접 얻는 방법이 없고 리스너나 콜백 메서드를 통해서만 얻을 수 있다고 했습니다. 예를 들어, 특정 컨트롤에 여백을 추가하여 탐색 바에 가려지지 않도록 하려면:
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
v.updatePadding(bottom = insets.systemWindowInsets.bottom)
// insets를 반환하여 뷰 계층 구조로 계속 전달되도록 함
insets
}
시스템 창 영역의 하단 여백 값만 컨트롤의 하단 여백에 할당했습니다.
주의: ViewGroup에서 이 작업을 수행하는 경우 android:clipToPadding="false"로 설정해야 할 수도 있습니다. 기본적으로 모든 뷰는 패딩 영역 내에서 그래픽을 자르기 때문입니다.
5.4 Jetpack 사용
insets를 사용할 때 항상 Jetpack의 WindowInsetsCompat 클래스를 사용하는 것이 좋습니다. 이는 낮은 버전 SDK와 호환됩니다. Compat 버전은 모든 API 수준에서 일관된 API와 동작을 제공합니다.
API의 일부 차이점에 주의하세요:
view.setOnApplyWindowInsetsListener { v, insets ->
...
insets
}
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
...
insets
}
5.5 제스처 충돌 처리
5.5.1. 처리 불필요
뷰 컨트롤이 제스처 탐색과 충돌하지 않는 경우 처리할 필요가 없습니다.
5.5.2 해당 뷰/컨트롤을 제스처 상호작용 영역 밖으로 이동
이것은 앞서 언급한 해결책입니다. 상황에 따라 다릅니다.
5.5.3 제스처 영역 제외 API 사용
"애플리케이션이 시스템 제스처 영역에서 일부를 잘라내어 자신의 제스처 상호작용에 응답할 수 있습니다". 이것이 안드로이드 10에서 새로 도입된 제스처 영역 제외 API입니다.
애플리케이션은 안드로이드 10에서 새로 추가된 시스템 제스처 영역 제외 API를 사용하여 시스템 가장자리의 일부 영역이 시스템 제스처에 응답하지 않도록 할 수 있습니다. 시스템은 상호작용 영역을 "잘라내는" 두 가지 다른 기능을 제공합니다: View.setSystemGestureExclusionRects()와 Window.setSystemGestureExclusionRects(). 사용할 것은 애플리케이션에 따라 다릅니다: Android View를 사용하는 경우 View API를 우선적으로 선택하고, 그렇지 않으면 Window API를 사용하세요.
이 두 API의 주요 차이점은 Window API가 창(Window) 좌표계로 직사각형을 계산한다는 것입니다. View API를 사용하는 경우 뷰의 좌표계로 작업합니다. View API는 좌표 공간 간 변환 문제를 해결하는 데 도움을 줍니다.
예제
사용자 정의 View로, 위에서 제스처를 스와이프하면 해당 궤적이 그려집니다.
xml 파일:
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#008B8B"
tools:context=".MainActivity">
<com.lee.windowinsetdemo.MyView
android:layout_width="match_parent"
android:layout_height="400dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:background="#B0B0B0"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Activity 코드:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
}
}
이때 실행되는 모습은 다음과 같아야 합니다:
아래 문제가 있는데, 사용자 정의 View에서 화면 가장자리에서 중앙으로 그릴 때 그리지 못하고 시스템의 돌아가기가 트리거됩니다. 시스템 제스처 탐색 영역을 제외하여 우리가 그리는 영역을 확보해야 합니다. 아래와 유사한 메서드를 생성하며, 이 메서드는 onLayout() 및/또는 onDraw() 시 호출됩니다.
private val gestureExclusionRects = mutableListOf<Rect>()
private fun updateGestureExclusion() {
// 안드로이드 10+에서 실행 중이지 않으면 이 호출을 건너뜀
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val rootWindowInsets = rootWindowInsets ?: return
val gestureInsets = rootWindowInsets.systemGestureInsets
gestureExclusionRects.clear()
// 왼쪽 제스처 가장자리에 대한 제외 직사각형 추가
gestureExclusionRects += Rect(0, 0, gestureInsets.left, height)
// 오른쪽 제스처 가장자리에 대한 제외 직사각형 추가
gestureExclusionRects += Rect(width - gestureInsets.right, 0, width, height)
ViewCompat.setSystemGestureExclusionRects(this, gestureExclusionRects)
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
updateGestureExclusion()
...
}
이때 사용자 정의 View의 하반부는 가장자리에서 화면 중앙으로 슬라이드하여 그릴 수 있지만 상반부는 여전히 시스템의 돌아가기 동작에 응답합니다. 여기에 어떤 비밀이 있을까요?
제스처 영역 제외 제한
제스처 영역 제외 API는 모든 제스처 충돌을 해결하는 완벽한 솔루션처럼 보이지만 실제로는 그렇지 않습니다. 이 API가 사용자 습관의 작업을 어느 정도 파괴하기 때문에 시스템은 제한을 두었습니다: 화면의 각 가장자리는 애플리케이션이 최대 200dp만 절단할 수 있습니다.
왜 제한이 있을까요?
제스처 영역 제외는 애플리케이션의 제스처를 "돌아가기" 등 시스템 동작보다 중요하게 만듭니다. 애플리케이션이 화면의 전체 가장자리가 시스템 제스처에 응답하지 않도록 하면 사용자가 혼란을 느끼고 이 애플리케이션이 제거될 가능성이 큽니다. 개발자는 사용자가 시스템과 일관된 작업을 수행할 수 있도록 최대한 노력해야 하며, 예를 들어 가장자리에서 내부로 슬라이드하여 돌아가는 것과 같습니다. 전체 장치에서 일관성을 유지해야 하며, 단순히 하나의 애플리케이션에서만 일관성을 유지하는 것이 아닙니다.
시스템 탐색은 항상 일관성과 사용 가능성을 유지해야 합니다.
왜 200dp일까요?
제스처 영역 제외 API는 만不得已의 경우에만 사용해야 하며, Google 개발자는 이 메커니즘을 적용할 수 있는 터치 객체의 면적을 계산했습니다. 터치 객체의 최소 권장 크기는 48dp입니다. 4개의 터치 객체를 취하면 4 × 48dp = 192dp입니다. 약간의 여유를 더하면 200dp가 됩니다.
1. 개발자가 가장자리에서 200dp 이상의 영역을 절단하도록 요청하면 시스템은 요청 중 가장 아래 200dp만 이행합니다.
2. 뷰가 화면 내에 있지 않으면 시스템은 화면 범위 내의 절단 직사각형만 계산합니다. 뷰가 화면 내에 부분적으로만 표시되면 요청된 직사각형의 화면 내 가시 부분만 계산합니다.
예제 코드에서 사용자 정의 View의 높이를 의도적으로 400dp로 설정했는데, 바로 하반부에서 시스템 제스처 영역 제외가 유효하고 상반부에서는 유효하지 않은 이유입니다. 그런데 문제가 또 생겼습니다. 전체 View에서 그릴 수 있어야 하는데 하반부만 가장자리에서 안쪽으로 그릴 수 있고 상반부는 그릴 수 없다면 이것은 어떤가요? 답은 다음 절에서 보세요.
5.5.3 몰입 모드에서 제스처 영역 제외 API 사용
앞서 언급했듯이 몰입 모드는 콘텐츠를 전체 화면으로 표시하여 시스템 바를 숨기고 애플리케이션이 최대 화면 공간을 확보할 수 있도록 하는 방식입니다. 또한 잘못된 작업을 방지하는 기능(예: 우연히 제스처를 사용하여 애플리케이션을 떠나는 것)도 제공하여 게임에 특히 적합합니다.
- 몰입 모드는 두 가지로 나뉩니다:
1. 비고정 몰입 모드: 사용자가 시스템 바에서 위로 스와이프하여 몰입 모드를 종료할 수 있습니다.
2. 고정 몰입 모드: 사용자가 시스템 바에서 위로 스와이프하여 일시적으로 몰입 모드를 종료할 수 있습니다. 짧은 시간(몇 초) 후 자동으로 몰입 모드로 돌아갑니다.
- 이 두 모드 모두 두 가지 상태가 있습니다:
1. 시스템 바 숨김: 이 상태에서 홈 화면 제스처와 뒤로 가기 제스처가 모두 비활성화됩니다. 사용자는 먼저 가장자리에서 내측으로 스와이프하여 시스템 바를 표시해야 합니다.
2. 시스템 바 표시: 이 상태에서 홈 화면 제스처와 뒤로 가기 제스처가 정상적으로 작동합니다.
- 몰입 모드의 고정성과 비고정성
비고정(non-sticky) 몰입 모드는 전체 화면 표시가 필요하지만 화면 가장자리 근처에서 정밀한 슬라이드 제스처가 필요하지 않은 UI에 매우 적합합니다. 일반적인 예는 전체 화면 비디오 재생과 사진 보기 등입니다. 제스처 탐색 관점에서 보면 이 모드에서는 시스템 바의 표시 여부와 관계없이 각 가장자리에서 제외할 수 있는 영역 높이는 여전히 200dp로 제한됩니다.
고정(sticky) 몰입 모드는 전체 화면을 강하게 필요로 하고 전체 화면 영역에서 터치 입력이 필요한 UI에 적합합니다. 일반적인 예는 드로잉 애플리케이션과 슬라이드 작업을 사용하는 게임입니다.
1. 고정 몰입 모드를 사용하는 애플리케이션은 상호작용성이 강하므로 제스처 영역 제외 API의 제한이 제거되지만 시스템 바가 숨겨진 경우에만 해당합니다. 이는 애플리케이션이 필요에 따라 화면 왼쪽/오른쪽 가장자리를 완전히 차지할 수 있음을 의미합니다.
2. 시스템 바가 표시될 때 시스템은 모든 제외된 제스처 영역을 무시하여 사용자가 돌아갈 수 있도록 하며 애플리케이션의 간섭을 받지 않습니다. 고정 몰입 모드에서 시스템 바는 짧은 시간 동안만 표시되므로 애플리케이션의 정상적인 상호작용에 영향을 주지 않습니다.
화면 하단의 홈 제스처 영역은 여전히 정상적으로 존재하며 제외할 수 없는 "필수" 제스처 영역입니다. 고정 몰입 모드에 있는 애플리케이션은 두 개의 수직 가장자리 전체 길이를 차지할 수 있으므로 화면 하단의 주요 제스처 영역은 사용자가 시스템 바를 호출하고 애플리케이션을 종료하는 유일한 방법일 수 있습니다.
이전 예제를 개선하여 뷰가 현재 몰입 모드에 있는지 알아야 합니다:
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
updateGestureExclusion()
...
}
override fun onWindowSystemUiVisibilityChanged(visibility: Int) {
super.onWindowSystemUiVisibilityChanged(visibility)
// 안드로이드 10+에서 실행 중인 경우
// 제스처 제외 직사각형 업데이트
if (Build.VERSION.SDK_INT >= 29) {
updateGestureExclusionRects()
}
}
private fun updateGestureExclusion() {
if ((windowSystemUiVisibility and SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0) {
// 루트 창 insets가 null인 경우, 이는 호출이 첨부 및 레이아웃 전에 발생했음을 의미합니다.
// 현재 호출을 무시합니다.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val rootWindowInsets = rootWindowInsets ?: return
val gestureInsets = rootWindowInsets.systemGestureInsets
gestureExclusionRects.clear()
// 왼쪽 제스처 가장자리에 대한 제외 직사각형 추가
gestureExclusionRects += Rect(0, 0, gestureInsets.left, height)
// 오른쪽 제스처 가장자리에 대한 제외 직사각형 추가
gestureExclusionRects += Rect(width - gestureInsets.right, 0, width, height)
ViewCompat.setSystemGestureExclusionRects(this, gestureExclusionRects)
}
} else {
// 탐색 바가 표시되는 경우, 모든 가장자리를 제외하지 않습니다.
ViewCompat.setSystemGestureExclusionRects(this, emptyList())
}
}
override fun onWindowSystemUiVisibilityChanged(visible: Int) {
super.onWindowSystemUiVisibilityChanged(visible)
if (Build.VERSION.SDK_INT >= 29) {
updateGestureExclusion()
}
}
6. 안드로이드 R의 새로운 API
애플리케이션이 R 및 상위 버전을 적응하는 경우 setSystemUiVisibility() 메서드를 사용하면 해당 메서드가 더 이상 사용되지 않는다는 경고가 표시되며, 앞서 언급한 플래그도 더 이상 사용되지 않음으로 표시됩니다. 안드로이드 공식은 api30 이후 WindowInsetsController를 제공하여 window 제어 클래스로, window 제어를 단순화합니다.
코드를 먼저 보겠습니다:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.decorView.windowInsetsController?.hide(WindowInsets.Type.statusBars())
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
window.setDecorFitsSystemWindows(false)
}
위 코드는 무엇을 의미할까요?
실제로 windowInsetsController는 대응하는 show()와 hide() 메서드를 사용하여 statusbar와 navigationBar를 제어하며, 코드를 보면 한눈에 무슨 의미인지 알 수 있습니다. 이전 버전의 플래그보다 더 직관적입니다.
상태 표시줄과 탐색 바 숨기기:
// window.decorView.windowInsetsController?.hide(WindowInsets.Type.systemBars())
window.decorView.windowInsetsController?.hide(WindowInsets.Type.statusBars())
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
레이아웃 안정성, 상태 표시줄과 탐색 바의 표시/숨김 변화에 따라 변화하지 않음
플로팅 창:
// WindowManager.LayoutParams.setFitInsetsTypes(WindowInsets.Type.systemBars())
WindowManager.LayoutParams.setFitInsetsTypes(WindowInsets.Type.statusBars())
WindowManager.LayoutParams.setFitInsetsTypes(WindowInsets.Type.navigationBars())
비플로팅 창:
window.setDecorFitsSystemWindows(false)
여기서 주의해야 할 점은 플로팅 창과 비플로팅 창의 차이가 있다는 것입니다.
고정성과 비고정성
// 비고정
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_DEFAULT
// 고정
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
전체 화면을 실현하기 위해:
override fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// window.decorView.windowInsetsController?.hide(WindowInsets.Type.statusBars())
// window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
window.decorView.windowInsetsController?.hide(WindowInsets.Type.systemBars())
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
window.setDecorFitsSystemWindows(false)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
} else {
// 4.1 이하 처리
...
}
}
}