대형 화면을 위한 안내서

Android는 별 5개 평점에 준하는 대형 화면용 앱에 필요한 모든 재료를 제공합니다. 이 안내서의 레시피에서는 선택한 재료를 선별하고 결합하여 특정 개발 문제를 해결하는 방법을 제시합니다. 각 레시피에는 권장사항, 양질의 코드 샘플, 단계별 안내가 포함되어 있어 대형 화면의 최고 실력자가 되는 데 활용할 수 있습니다.

별표 평점

레시피는 대형 화면 앱 품질 가이드라인을 얼마나 잘 준수하는지에 따라 별표로 평점을 표시합니다.

별 5개 평점 등급 1, 대형 화면 차별화 기준을 충족합니다.
별 4개 평점 등급 2, 대형 화면 최적화 기준을 충족합니다.
별 3개 평점 등급 3, 대형 화면 준비 완료 기준을 충족합니다.
별 2개 평점 대형 화면 기능을 일부 지원하지만, 대형 화면 앱 품질 가이드라인의 일부를 충족하지 않습니다.
별 1개 평점 특정 사용 사례의 요구사항을 충족하지만, 대형 화면을 제대로 지원하지 않습니다.

Chromebook 카메라 지원

별 3개 평점

Google Play에서 Chromebook 사용자의 관심을 사로잡으세요.

카메라 앱이 기본 카메라 기능으로만 작동할 수 있는 경우에도, 고급형 휴대전화에서 사용되는 고급 카메라 기능을 실수로 지정하는 바람에 앱 스토어에서 Chromebook 사용자가 앱을 설치하지 못하게 되어서는 안 됩니다.

Chromebook에는 화상 회의, 스냅샷, 기타 애플리케이션에서 사용하기에 적합한 전면(안쪽) 카메라가 내장되어 있습니다. 하지만 모든 Chromebook에 후면(바깥쪽) 카메라가 있는 것은 아니며 Chromebook의 전면에 위치한 대부분의 카메라는 자동 초점이나 플래시 기능을 지원하지 않습니다.

권장사항

다목적으로 사용할 수 있는 카메라 앱은 카메라 구성에 관계없이 전면 카메라, 후면 카메라, USB로 연결된 외장 카메라 등 모든 기기를 지원합니다.

앱 스토어에서 최대한 많은 기기에 앱을 제공할 수 있도록 하려면 앱에서 사용하는 모든 카메라 기능을 항상 선언하고 특정 기능이 필요한지 여부를 명시해야 합니다.

재료

  • CAMERA 권한: 앱이 기기의 카메라에 액세스하도록 허용합니다.
  • <uses-feature> 매니페스트 요소: 앱 스토어에 앱에서 어떤 기능을 사용하는지 알립니다.
  • required 속성: 앱 스토어에 특정 기능이 없어도 앱 작동이 가능한지 여부를 알립니다.

단계

요약

CAMERA 권한을 선언합니다. 기본적인 카메라 지원을 제공하는 카메라 기능을 선언합니다. 각 기능이 필요한지 여부를 명시합니다.

1. CAMERA 권한 선언

앱 매니페스트에 다음 권한을 추가합니다.

<uses-permission android:name="android.permission.CAMERA" />
2. 기본 카메라 기능 선언

앱 매니페스트에 다음 기능을 추가합니다.

<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
3. 각 기능이 필요한지 여부 지정

내장 카메라 또는 외장 카메라가 있는 기기나 카메라가 없는 기기에서 앱에 액세스할 수 있도록 android.hardware.camera.any 기능에 android:required="false"를 설정합니다.

다른 기능의 경우 android:required="false"를 설정하여 후면 카메라, 자동 초점, 플래시가 없는 Chromebook과 같은 기기에서도 앱 스토어의 앱에 액세스할 수 있도록 합니다.

결과

Chromebook 사용자가 Google Play 및 다른 앱 스토어에서 앱을 다운로드하고 설치할 수 있습니다. 휴대전화와 같이 모든 기능을 제공하는 카메라를 지원하는 기기는 카메라 기능에서 제한을 받지 않습니다.

앱에서 지원하는 카메라 기능을 명시적으로 설정하고 앱에 필요한 기능을 명시하면 최대한 많은 기기에 앱을 제공할 수 있습니다.

추가 리소스

자세한 내용은 <uses-feature> 문서의 카메라 하드웨어 기능을 참고하세요.

휴대전화에서는 앱 방향이 제한되지만 대형 화면 기기에서는 제한되지 않음

별 2개 평점

앱이 세로 모드의 휴대전화에서 잘 작동하므로 앱을 세로 모드로만 사용하도록 제한했습니다. 하지만 대형 화면의 가로 모드 방향으로 더 많은 작업을 할 수 있는 경우가 있습니다.

작은 화면에서는 세로 모드 방향으로 앱을 제한하고 대형 화면에서는 가로 모드를 사용할 수 있도록 두 가지 방법을 모두 제공하려면 어떻게 해야 할까요?

권장사항

우수한 앱은 기기 방향과 같은 사용자 선호도를 중요하게 생각합니다.

대형 화면 앱 품질 가이드라인에서는 앱이 세로 모드 방향, 가로 모드 방향, 멀티 윈도우 모드, 폴더블 기기의 접힌 상태와 펼친 상태를 비롯하여 모든 기기 설정을 지원할 것을 권장합니다. 앱은 다양한 구성에 맞게 레이아웃과 사용자 인터페이스를 최적화해야 하며, 구성을 변경하는 중에 상태를 저장하고 복원해야 합니다.

이 레시피는 대형 화면을 지원하기 위한 임시 조치입니다. 모든 기기 구성을 완벽하게 지원하도록 앱을 개선할 수 있게 되기까지 이 레시피를 사용하세요.

재료

  • screenOrientation: 앱이 기기 방향 변경에 응답하는 방식을 지정할 수 있는 앱 매니페스트 설정
  • Jetpack WindowManager: 앱 창의 크기와 가로세로 비율을 결정할 수 있는 라이브러리 세트로 API 수준 14와 하위 호환됩니다.
  • Activity#setRequestedOrientation(): 런타임 시 앱 방향을 변경할 수 있게 해주는 메서드

단계

요약

기본적으로 앱이 방향 변경을 처리하도록 앱 매니페스트에 사용 설정합니다. 런타임 시 앱 창 크기를 결정합니다. 앱 창이 작은 경우 매니페스트 방향 설정을 재정의하여 앱 방향을 제한합니다.

1. 앱 매니페스트에 방향 설정 지정

앱 매니페스트의 screenOrientation 요소를 선언하지 않거나(이 경우 방향 기본값이 unspecified가 됨) 화면 방향을 fullUser로 설정할 수 있습니다. 사용자가 센서 기반 회전을 잠그지 않은 경우 앱은 모든 기기 방향을 지원합니다.

<activity
    android:name=".MyActivity"
    android:screenOrientation="fullUser">

unspecifiedfullUser의 차이점은 미묘하지만 중요합니다. screenOrientation 값을 선언하지 않으면 시스템이 방향을 선택하며, 시스템에서 방향을 정의하는 데 사용하는 정책은 기기마다 다를 수 있습니다. 반면에 fullUser를 지정하면 사용자가 기기에 정의한 동작에 더 가까워집니다. 사용자가 센서 기반 회전을 잠근 경우에는 앱이 사용자 환경설정을 따르고, 그러지 않은 경우에는 시스템이 네 가지 화면 방향(세로, 가로, 세로 반전, 가로 반전)을 허용합니다. android:screenOrientation을 참고하세요.

2. 화면 크기 확인

사용자가 허용하는 모든 방향을 지원하도록 매니페스트를 설정하면 화면 크기에 따라 프로그래매틱 방식으로 앱 방향을 지정할 수 있습니다.

Jetpack WindowManager 라이브러리를 모듈의 build.gradle 또는 build.gradle.kts 파일에 추가합니다.

Kotlin

implementation("androidx.window:window:version")
implementation("androidx.window:window-core:version")

Groovy

implementation 'androidx.window:window:version'
implementation 'androidx.window:window-core:version'

Jetpack WindowManager WindowMetricsCalculator#computeMaximumWindowMetrics() 메서드를 사용하여 기기 화면 크기를 WindowMetrics 객체로 가져옵니다. 창 측정항목을 창 크기 클래스와 비교하여 방향을 제한할 시점을 결정할 수 있습니다.

창 크기 클래스는 작은 화면과 큰 화면 사이에 중단점을 제공합니다.

WindowWidthSizeClass#COMPACTWindowHeightSizeClass#COMPACT 중단점을 사용하여 화면 크기를 결정합니다.

Kotlin

/** Determines whether the device has a compact screen. **/
fun compactScreen() : Boolean {
    val metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this)
    val width = metrics.bounds.width()
    val height = metrics.bounds.height()
    val density = resources.displayMetrics.density
    val windowSizeClass = WindowSizeClass.compute(width/density, height/density)

    return windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT ||
        windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT
}

Java

/** Determines whether the device has a compact screen. **/
private boolean compactScreen() {
    WindowMetrics metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this);
    int width = metrics.getBounds().width();
    int height = metrics.getBounds().height();
    float density = getResources().getDisplayMetrics().density;
    WindowSizeClass windowSizeClass = WindowSizeClass.compute(width/density, height/density);
    return windowSizeClass.getWindowWidthSizeClass() == WindowWidthSizeClass.COMPACT ||
                windowSizeClass.getWindowHeightSizeClass() == WindowHeightSizeClass.COMPACT;
}
    참고:
  • 위 예는 활동의 메서드로 구현되었습니다. 따라서 활동은 computeMaximumWindowMetrics()의 인수에서 this로 역참조됩니다.
  • 앱이 멀티 윈도우 모드에서 실행될 수 있으므로 computeMaximumWindowMetrics()computeCurrentWindowMetrics() 대신 사용됩니다. 멀티 윈도우 모드에서는 화면 방향 설정을 무시되기 때문입니다. 앱 창이 기기 전체 화면을 차지하지 않는다면 앱 창 크기를 결정하고 방향 설정을 재정의하는 것은 의미가 없습니다.

앱에서 computeMaximumWindowMetrics() 메서드를 사용할 수 있도록 종속 항목을 선언하는 방법은 WindowManager를 참고하세요.

3. 앱 매니페스트 설정 재정의

기기의 화면이 작다는 것을 확인했다면 Activity#setRequestedOrientation()을 호출하여 매니페스트의 screenOrientation 설정을 재정의할 수 있습니다.

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    requestedOrientation = if (compactScreen())
        ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else
        ActivityInfo.SCREEN_ORIENTATION_FULL_USER
    ...
    // Replace with a known container that you can safely add a
    // view to where the view won't affect the layout and the view
    // won't be replaced.
    val container: ViewGroup = binding.container

    // Add a utility view to the container to hook into
    // View.onConfigurationChanged. This is required for all
    // activities, even those that don't handle configuration
    // changes. You can't use Activity.onConfigurationChanged,
    // since there are situations where that won't be called when
    // the configuration changes. View.onConfigurationChanged is
    // called in those scenarios.
    container.addView(object : View(this) {
        override fun onConfigurationChanged(newConfig: Configuration?) {
            super.onConfigurationChanged(newConfig)
            requestedOrientation = if (compactScreen())
                ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else
                ActivityInfo.SCREEN_ORIENTATION_FULL_USER
        }
    })
}

Java

@Override
protected void onCreate(Bundle savedInstance) {
    super.onCreate(savedInstanceState);
    if (compactScreen()) {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    } else {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER);
    }
    ...
    // Replace with a known container that you can safely add a
    // view to where the view won't affect the layout and the view
    // won't be replaced.
    ViewGroup container = binding.container;

    // Add a utility view to the container to hook into
    // View.onConfigurationChanged. This is required for all
    // activities, even those that don't handle configuration
    // changes. You can't use Activity.onConfigurationChanged,
    // since there are situations where that won't be called when
    // the configuration changes. View.onConfigurationChanged is
    // called in those scenarios.
    container.addView(new View(this) {
        @Override
        protected void onConfigurationChanged(Configuration newConfig) {
            super.onConfigurationChanged(newConfig);
            if (compactScreen()) {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            } else {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER);
            }
        }
    });
}

onCreate()View.onConfigurationChanged() 메서드에 로직을 추가하면 활동이 디스플레이 간에 크기가 조절되거나 이동할 때마다(예: 기기 회전 후 또는 폴더블 기기가 접히거나 펼쳐질 때) 최대 창 측정항목을 가져오고 방향 설정을 재정의할 수 있습니다. 구성 변경이 발생하는 시점과 이러한 변경사항이 활동 재생성을 유발하는 시점에 관한 자세한 내용은 구성 변경사항 처리를 참고하세요.

결과

이제 앱이 작은 화면에서 기기 회전과 관계없이 세로 모드 방향으로 유지될 것입니다. 대형 화면에서는 앱이 가로 모드 방향과 세로 모드 방향을 지원합니다.

추가 리소스

항상 모든 기기 설정을 지원하도록 앱을 업그레이드하는 방법은 다음을 참고하세요.

외부 키보드 스페이스바를 사용하여 미디어 재생 일시중지 및 다시 시작

별 4개 평점

대형 화면 최적화에는 스페이스바를 누르는 동작에 반응하여 동영상 및 기타 미디어의 재생을 일시중지하거나 재개하는 등의 외부 키보드 입력을 처리하는 기능이 포함됩니다. 이는 주로 외부 키보드에 연결되는 태블릿과 일반적으로 외부 키보드와 함께 제공되지만 태블릿 모드로 사용할 수 있는 Chromebook에 유용합니다.

미디어가 창의 유일한 요소인 경우(예: 전체 화면 동영상 재생) 활동 수준 또는 Jetpack Compose에서는 화면 수준에서 키 누름 이벤트에 응답합니다.

권장사항

앱에서 미디어 파일을 재생할 때마다 사용자는 실제 키보드에서 스페이스바를 눌러 재생을 일시중지하거나 재개할 수 있어야 합니다.

재료

Compose

  • onPreviewKeyEvent: 구성요소(또는 그 하위 요소 중 하나)에 포커스가 있을 때 구성요소에서 하드웨어 키 이벤트를 가로채도록 하는 Modifier입니다.
  • onKeyEvent: onPreviewKeyEvent와 마찬가지로 이 Modifier를 사용하면 구성요소(또는 그 하위 요소 중 하나)에 포커스가 있을 때 구성요소에서 하드웨어 키 이벤트를 가로챌 수 있습니다.

  • onKeyUp(): 키가 해제되고 활동 내의 뷰에서 처리하지 않을 때 호출됩니다.

단계

요약

뷰 기반 앱과 Jetpack Compose 기반의 앱은 키보드 키 누름에 비슷한 방식으로 응답합니다. 앱은 키 누름 이벤트를 수신 대기하고, 이벤트를 필터링하고, 스페이스바 키 누름과 같은 선택된 키 누름에 응답해야 합니다.

1. 키보드 이벤트 수신 대기

앱의 활동에서 onKeyUp() 메서드를 다음과 같이 재정의합니다.

Kotlin

override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
    ...
}

Java

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
    ...
}

이 메서드는 눌린 키가 해제될 때마다 호출되므로 키 입력마다 정확하게 한 번씩 실행됩니다.

Compose

Jetpack Compose를 사용하면 화면 내에서 키 입력을 관리하는 onPreviewKeyEvent 또는 onKeyEvent 수정자를 활용할 수 있습니다.

Column(modifier = Modifier.onPreviewKeyEvent { event ->
    if (event.type == KeyEventType.KeyUp) {
        ...
    }
    ...
})

또는

Column(modifier = Modifier.onKeyEvent { event ->
    if (event.type == KeyEventType.KeyUp) {
        ...
    }
    ...
})

2. 스페이스바 누르기 필터링

onKeyUp() 메서드 또는 Compose onPreviewKeyEventonKeyEvent 수정자 메서드 내에서 KeyEvent.KEYCODE_SPACE를 필터링하여 올바른 이벤트를 미디어 구성요소에 전송합니다.

Kotlin

if (keyCode == KeyEvent.KEYCODE_SPACE) {
    togglePlayback()
    return true
}
return false

Java

if (keyCode == KeyEvent.KEYCODE_SPACE) {
    togglePlayback();
    return true;
}
return false;

Compose

Column(modifier = Modifier.onPreviewKeyEvent { event ->
    if (event.type == KeyEventType.KeyUp && event.key == Key.Spacebar) {
        ...
    }
    ...
})

또는

Column(modifier = Modifier.onKeyEvent { event ->
    if (event.type == KeyEventType.KeyUp && event.key == Key.Spacebar) {
        ...
    }
    ...
})

결과

이제 앱이 스페이스바 키를 누를 때 이에 응답하여 동영상이나 다른 미디어를 일시중지하거나 재개할 수 있습니다.

추가 리소스

키보드 이벤트 및 관리 방법에 관한 자세한 내용은 키보드 입력 처리를 참고하세요.

스타일러스 손바닥 움직임 무시

별 5개 평점

스타일러스는 대형 화면에서 생산성과 창의성이 뛰어난 도구가 될 수 있습니다. 하지만 사용자는 스타일러스를 사용하여 앱에서 그리거나 쓰거나 상호작용할 때 손바닥으로 화면을 터치하기도 합니다. 시스템이 이러한 이벤트를 의도치 않은 손바닥 터치로 인식하여 무시하기 전에 터치 이벤트가 앱에 보고될 수 있습니다.

권장사항

앱은 관련 없는 터치 이벤트를 식별하여 무시해야 합니다. Android는 MotionEvent 객체를 전달하여 손바닥 터치를 취소합니다. ACTION_CANCEL 또는 ACTION_POINTER_UPFLAG_CANCELED 객체를 확인하여 손바닥 터치로 인한 동작을 무시할지 결정합니다.

재료

  • MotionEvent: 터치 및 이동 이벤트를 나타냅니다. 이벤트를 무시할지 결정하는 데 필요한 정보가 포함되어 있습니다.
  • OnTouchListener#onTouch(): MotionEvent 객체를 수신합니다.
  • MotionEvent#getActionMasked(): 모션 이벤트와 관련된 작업을 반환합니다.
  • ACTION_CANCEL: 동작이 실행취소되어야 함을 나타내는 MotionEvent 상수입니다.
  • ACTION_POINTER_UP: 첫 번째 포인터 이외의 포인터가 소실되었음을, 즉 기기 화면과의 접촉을 포기했음을 나타내는 MotionEvent 상수입니다.
  • FLAG_CANCELED: 포인터가 소실되어 의도치 않은 터치 이벤트가 발생했음을 나타내는 MotionEvent 상수입니다. Android 13(API 수준 33) 및 이후 버전의 ACTION_POINTER_UPACTION_CANCEL 이벤트에 추가되었습니다.

걸음수

요약

앱에 전달된 MotionEvent 객체를 검사합니다. MotionEvent API를 사용하여 이벤트 특성을 판단합니다.

  • 단일 포인터 이벤트: ACTION_CANCEL을 확인합니다. Android 13 이상에서는 FLAG_CANCELED도 확인합니다.
  • 멀티 포인터 이벤트: Android 13 이상에서는 ACTION_POINTER_UPFLAG_CANCELED를 확인합니다.

ACTION_CANCELACTION_POINTER_UP/FLAG_CANCELED 이벤트에 응답합니다.

1. 모션 이벤트 객체 획득

앱에 OnTouchListener를 추가합니다.

Kotlin

val myView = findViewById<View>(R.id.myView).apply {
    setOnTouchListener { view, event ->
        // Process motion event.
    }
}

Java

View myView = findViewById(R.id.myView);
myView.setOnTouchListener( (view, event) -> {
    // Process motion event.
});
2. 이벤트 작업 및 플래그 확인

모든 API 수준에서 단일 포인터 이벤트를 나타내는 ACTION_CANCEL을 확인합니다. Android 13 이상에서는 ACTION_POINTER_UP에서 FLAG_CANCELED.를 확인합니다.

Kotlin

val myView = findViewById<View>(R.id.myView).apply {
    setOnTouchListener { view, event ->
        when (event.actionMasked) {
            MotionEvent.ACTION_CANCEL -> {
                //Process canceled single-pointer motion event for all SDK versions.
            }
            MotionEvent.ACTION_POINTER_UP -> {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
                   (event.flags and MotionEvent.FLAG_CANCELED) == MotionEvent.FLAG_CANCELED) {
                    //Process canceled multi-pointer motion event for Android 13 and higher.
                }
            }
        }
        true
    }
}

Java

View myView = findViewById(R.id.myView);
myView.setOnTouchListener( (view, event) -> {
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_CANCEL:
            // Process canceled single-pointer motion event for all SDK versions.
        case MotionEvent.ACTION_UP:
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
               (event.getFlags() & MotionEvent.FLAG_CANCELED) == MotionEvent.FLAG_CANCELED) {
                //Process canceled multi-pointer motion event for Android 13 and higher.
            }
    }
    return true;
});
3. 동작 실행취소

손바닥 터치를 식별한 후에는 동작이 화면에 미치는 효과를 실행취소할 수 있습니다.

손바닥 터치와 같은 의도하지 않은 입력이 실행취소될 수 있도록 앱에서는 사용자 작업 기록을 유지해야 합니다. 예를 보려면 Android 앱에서 스타일러스 지원 개선 Codelab의 기본 그리기 앱 구현을 참고하세요.

결과

이제 앱은 Android 13 이상 API 수준의 멀티 포인터 이벤트와 모든 API 수준의 단일 포인터 이벤트에서 손바닥 터치를 식별하고 무시할 수 있습니다.

추가 리소스

자세한 내용은 다음을 참고하세요.

WebView 상태 관리

별 3개 평점

WebView는 일반적으로 사용되는 구성요소로, 상태 관리를 위한 고급 시스템을 제공합니다. WebView는 구성 변경 시 상태와 스크롤 위치를 유지해야 합니다. WebView는 사용자가 기기를 회전하거나 폴더블 휴대전화를 펼칠 때 스크롤하던 위치를 잃을 수 있습니다. 이런 경우 사용자가 WebView의 상단에서 이전 스크롤 위치로 다시 스크롤해야 합니다.

권장사항

WebView가 다시 생성되는 횟수를 최소화합니다. WebView는 상태를 관리하는 데 능숙하며 가능한 한 많은 구성 변경을 관리하여야 이 기능의 장점을 활용할 수 있습니다. Activity 재생성(시스템에서 구성 변경을 처리하는 방식) 시 WebView도 다시 생성하므로 WebView에서 상태가 손실될 수 있기 때문에 앱이 구성 변경을 처리해야 합니다.

재료

  • android:configChanges: 매니페스트 <activity> 요소의 속성입니다. 활동이 처리하는 구성 변경을 나열합니다.
  • View#invalidate(): 뷰를 다시 그리는 메서드입니다. WebView에서 상속됩니다.

단계

요약

WebView 상태를 저장하려면 Activity 재생성을 최대한 방지하고 그런 다음 상태를 유지하면서 크기를 조절할 수 있도록 WebView를 무효화합니다.

1. 앱의 AndroidManifest.xml 파일에 구성 변경사항 추가

시스템이 아닌 앱에서 처리하는 구성 변경을 지정하여 활동 재생성을 방지합니다.

<activity
  android:name=".MyActivity"
  android:configChanges="screenLayout|orientation|screenSize
      |keyboard|keyboardHidden|smallestScreenSize" />

2. 앱이 구성 변경을 수신할 때마다 WebView를 무효화

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    webView.invalidate()
}

Java

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    webview.invalidate();
}

이 단계는 Jetpack Compose가 Composable 요소의 크기를 올바르게 조절하기 위해 어떤 것을 무효화할 필요가 없으므로 뷰 시스템에만 적용됩니다. 그러나 WebView를 올바르게 관리하지 않으면 Compose에서 다시 만들기도 합니다. Accompanist WebView 래퍼를 사용하여 Compose 앱에 WebView 상태를 저장하고 복원합니다.

결과

이제 앱의 WebView 구성요소가 크기 조절, 방향 변경, 접기 및 펼치기 등 여러 구성 변경사항에 대해 상태 및 스크롤 위치를 유지합니다.

추가 리소스

구성 변경사항 및 관리 방법에 대한 자세한 내용은 구성 변경 처리를 참고하세요.

RecyclerView 상태 관리

별 3개 평점

RecyclerView는 최소한의 그래픽 리소스를 사용하여 대량의 데이터를 표시할 수 있습니다. RecyclerView가 항목 목록을 스크롤하면 RecyclerView는 화면에서 스크롤된 항목의 View 인스턴스를 재사용하여 화면을 스크롤할 때 새 항목을 만듭니다. 그러나 기기 회전과 같은 구성 변경으로 인해 RecyclerView의 상태가 재설정되어 사용자가 RecyclerView 항목 목록에서 이전 위치로 다시 스크롤해야 할 수 있습니다.

권장사항

RecyclerView는 모든 구성 변경사항 중에 상태(특히 스크롤 위치)와 목록 요소의 상태를 유지해야 합니다.

재료

걸음수

요약

RecyclerView.Adapter의 상태 복원 정책을 설정하여 RecyclerView 스크롤 위치를 저장합니다. RecyclerView 목록 항목의 상태를 저장합니다. 목록 항목의 상태를 RecyclerView 어댑터에 추가하고 목록 항목이 ViewHolder에 바인딩되었을 때 목록 항목의 상태를 복원합니다.

1. Adapter 상태 복원 정책 사용 설정

구성 변경 후에도 RecyclerView의 스크롤 위치가 유지되도록 RecyclerView 어댑터의 상태 복원 정책을 사용 설정합니다. 어댑터 생성자에 정책 사양을 추가합니다.

Kotlin

class MyAdapter() : RecyclerView.Adapter() {
    init {
        stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
    }
    ...
}

Java

class MyAdapter extends RecyclerView.Adapter {

    public Adapter() {
        setStateRestorationPolicy(StateRestorationPolicy.PREVENT_WHEN_EMPTY);
    }
    ...
}

2. 스테이트풀(Stateful) 목록 항목 상태 저장

EditText 요소가 포함된 항목과 같이 복잡한 RecyclerView 목록 항목의 상태를 저장합니다. 예를 들어 EditText의 상태를 저장하려면 onClick 핸들러와 유사한 콜백을 추가하여 텍스트 변경사항을 캡처합니다. 다음과 같이 콜백 내에서 저장할 데이터를 정의합니다.

Kotlin

input.addTextChangedListener(
    afterTextChanged = { text ->
        text?.let {
            // Save state here.
        }
    }
)

Java

input.addTextChangedListener(new TextWatcher() {
    
    ...

    @Override
    public void afterTextChanged(Editable s) {
        // Save state here.
    }
});

Activity 또는 Fragment에서 콜백을 선언합니다. ViewModel을 사용하여 상태를 저장합니다.

3. Adapter에 목록 항목 상태 추가

RecyclerView.Adapter에 목록 항목의 상태를 추가합니다. 호스트 Activity 또는 Fragment가 생성되면 항목 상태를 어댑터 생성자에 전달합니다.

Kotlin

val adapter = MyAdapter(items, viewModel.retrieveState())

Java

MyAdapter adapter = new MyAdapter(items, viewModel.retrieveState());

4. 어댑터의 ViewHolder에서 목록 항목 상태 복구

ViewHolder를 항목에 바인딩하면 RecyclerView.Adapter에서 항목의 상태를 복원합니다.

Kotlin

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    ...
    val item = items[position]
    val state = states.firstOrNull { it.item == item }

    if (state != null) {
        holder.restore(state)
    }
}

Java

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
    ...
    Item item = items[position];
    Arrays.stream(states).filter(state -> state.item == item)
        .findFirst()
        .ifPresent(state -> holder.restore(state));
}

결과

이제 RecyclerView에서 스크롤 위치 및 RecyclerView 목록에 있는 모든 항목의 상태를 복원할 수 있습니다.

추가 리소스

탈착 가능한 키보드 관리

별 3개 평점

분리형 키보드를 지원하여 대형 화면 기기에서 사용자 생산성을 극대화할 수 있습니다. Android는 키보드가 기기에 연결되거나 기기에서 분리될 때마다 구성 변경을 트리거하며, 이로 인해 UI 상태가 손실될 수 있습니다. 앱은 상태를 저장하고 복원하여 시스템이 활동 재생성을 처리하도록 하거나 키보드 구성 변경과 관련하여 활동 재생성을 제한할 수 있습니다. 모든 경우 키보드와 관련된 모든 데이터는 Configuration 객체에 저장됩니다. 구성 객체의 keyboardkeyboardHidden 필드에는 키보드 유형과 사용 가능 여부에 관한 정보가 포함됩니다.

권장사항

대형 화면에 최적화된 앱은 소프트웨어 및 하드웨어 키보드부터 스타일러스, 마우스, 트랙패드, 기타 주변기기에 이르기까지 모든 유형의 입력 기기를 지원합니다.

외부 키보드 지원에는 다음 두 가지 방법 중 하나로 관리할 수 있는 구성 변경이 포함됩니다.

  1. 시스템이 현재 실행 중인 활동을 다시 생성하도록 하면 개발자가 앱 상태 관리를 처리합니다.
  2. 구성 변경을 직접 관리합니다 (활동이 다시 생성되지 않음).
    • 모든 키보드 관련 구성 값 선언
    • 구성 변경 핸들러 만들기

텍스트 입력 및 기타 입력을 위해 UI를 세밀하게 제어해야 하는 경우가 많은 생산성 앱은 구성 변경을 처리하는 DIY 접근 방식을 활용할 수 있습니다.

예를 들어 하드웨어 키보드가 연결되거나 분리될 때 도구나 수정 창을 위한 공간을 더 확보하기 위해 앱 레이아웃을 변경하는 것이 좋습니다.

구성 변경사항을 수신 대기하는 신뢰할 수 있는 유일한 방법은 뷰의 onConfigurationChanged() 메서드를 재정의하는 것이므로 새 뷰 인스턴스를 앱 활동에 추가하고 뷰의 onConfigurationChanged() 핸들러에서 키보드 연결 또는 분리로 인한 구성 변경에 응답할 수 있습니다.

재료

  • android:configChanges: 앱 매니페스트의 <activity> 요소 속성입니다. 앱이 관리하는 구성 변경을 시스템에 알립니다.
  • View#onConfigurationChanged(): 새 앱 구성의 전파에 반응하는 메서드입니다.

걸음수

요약

configChanges 속성을 선언하고 키보드 관련 값을 추가합니다. 활동의 뷰 계층 구조에 View를 추가하고 구성 변경사항을 수신 대기합니다.

1. configChanges 속성 선언

이미 관리되는 구성 변경사항 목록에 keyboard|keyboardHidden 값을 추가하여 앱 매니페스트에서 <activity> 요소를 업데이트합니다.

<activity
      …
      android:configChanges="...|keyboard|keyboardHidden">

2. 뷰 계층 구조에 빈 뷰 추가

새 뷰를 선언하고 뷰의 onConfigurationChanged() 메서드 내에 핸들러 코드를 추가합니다.

Kotlin

val v = object : View(this) {
  override fun onConfigurationChanged(newConfig: Configuration?) {
    super.onConfigurationChanged(newConfig)
    // Handler code here.
  }
}

Java

View v = new View(this) {
    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        // Handler code here.
    }
};

결과

이제 앱이 현재 실행 중인 활동을 다시 만들지 않고 연결되거나 분리되는 외부 키보드에 응답합니다.

추가 리소스

키보드 연결 또는 분리와 같은 구성 변경 중에 앱의 UI 상태를 저장하는 방법을 알아보려면 UI 상태 저장을 참고하세요.