앱에서 접힌 상태 인식

펼친 상태의 대형 디스플레이와 고유한 접힌 상태는 폴더블 기기에서 새로운 사용자 환경을 제공합니다. 앱이 기기의 접힌 상태를 인식하도록 하려면 접힘 및 힌지와 같은 폴더블 기기 창 기능에 API 표시 영역을 제공하는 라이브러리인 Jetpack WindowManager를 사용합니다. 앱이 접힌 상태를 인식하는 경우 접힘 또는 힌지 영역에 중요한 콘텐츠를 배치하지 않고 접힘과 힌지를 자연스러운 구분선으로 사용하도록 레이아웃을 조정할 수 있습니다.

창 정보

Jetpack WindowManager의 WindowInfoTracker 인터페이스는 창 레이아웃 정보를 노출합니다. 인터페이스의 windowLayoutInfo() 메서드는 앱에 폴더블 기기의 접힌 상태를 알려주는 WindowLayoutInfo 데이터 스트림을 반환합니다. WindowInfoTracker getOrCreate() 메서드는 WindowInfoTracker 인스턴스를 만듭니다.

WindowManager는 Kotlin Flow 및 Java 콜백을 사용하여 WindowLayoutInfo 데이터를 수집하도록 지원합니다.

Kotlin Flow

WindowLayoutInfo 데이터 수집을 시작하고 중지하려면 재시작 가능한 수명 주기 인식 코루틴을 사용하면 됩니다. 코루틴의 repeatOnLifecycle 코드 블록은 수명 주기가 STARTED 이상이면 실행되고 수명 주기가 STOPPED이면 중지됩니다. 수명 주기가 다시 STARTED가 되면 자동으로 코드 블록의 실행이 다시 시작됩니다. 다음 예에서 코드 블록은 WindowLayoutInfo 데이터를 수집하고 사용합니다.

class DisplayFeaturesActivity : AppCompatActivity() {

    private lateinit var binding: ActivityDisplayFeaturesBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
        setContentView(binding.root)

        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
                    .windowLayoutInfo(this@DisplayFeaturesActivity)
                    .collect { newLayoutInfo ->
                        // Use newLayoutInfo to update the layout.
                    }
            }
        }
    }
}

Java 콜백

androidx.window:window-java 종속 항목에 포함된 콜백 호환성 레이어를 사용하면 Kotlin Flow를 사용하지 않고도 WindowLayoutInfo 업데이트를 수집할 수 있습니다. 아티팩트에는 WindowInfoTrackerCallbackAdapter 클래스가 포함됩니다. 이 클래스는 WindowInfoTracker를 조정하여 WindowLayoutInfo 업데이트를 수신하는 등록(및 등록 취소) 콜백을 지원합니다. 예를 들면 다음과 같습니다.

public class SplitLayoutActivity extends AppCompatActivity {

    private WindowInfoTrackerCallbackAdapter windowInfoTracker;
    private ActivitySplitLayoutBinding binding;
    private final LayoutStateChangeCallback layoutStateChangeCallback =
            new LayoutStateChangeCallback();

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

       windowInfoTracker =
                new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
   }

   @Override
   protected void onStart() {
       super.onStart();
       windowInfoTracker.addWindowLayoutInfoListener(
                this, Runnable::run, layoutStateChangeCallback);
   }

   @Override
   protected void onStop() {
       super.onStop();
       windowInfoTracker
           .removeWindowLayoutInfoListener(layoutStateChangeCallback);
   }

   class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
       @Override
       public void accept(WindowLayoutInfo newLayoutInfo) {
           SplitLayoutActivity.this.runOnUiThread( () -> {
               // Use newLayoutInfo to update the layout.
           });
       }
   }
}

RxJava 지원

이미 RxJava(버전 2 또는 3)를 사용 중인 경우 Observable 또는 Flowable을 사용하여 Kotlin Flow를 사용하지 않고도 WindowLayoutInfo 업데이트를 수집할 수 있는 아티팩트를 사용할 수 있습니다.

androidx.window:window-rxjava2androidx.window:window-rxjava3 종속 항목에서 제공하는 호환성 계층에는 WindowInfoTracker#windowLayoutInfoFlowable()WindowInfoTracker#windowLayoutInfoObservable() 메서드가 포함되며 이러한 메서드를 통해 앱이 WindowLayoutInfo 업데이트를 수신할 수 있습니다. 예를 들면 다음과 같습니다.

class RxActivity: AppCompatActivity {

    private lateinit var binding: ActivityRxBinding

    private var disposable: Disposable? = null
    private lateinit var observable: Observable<WindowLayoutInfo>

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

        // Create a new observable
        observable = WindowInfoTracker.getOrCreate(this@RxActivity)
            .windowLayoutInfoObservable(this@RxActivity)
   }

   @Override
   protected void onStart() {
       super.onStart();

        // Subscribe to receive WindowLayoutInfo updates
        disposable?.dispose()
        disposable = observable
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { newLayoutInfo ->
            // Use newLayoutInfo to update the layout
        }
   }

   @Override
   protected void onStop() {
       super.onStop();

        // Dispose the WindowLayoutInfo observable
        disposable?.dispose()
   }
}

폴더블 디스플레이 기능

Jetpack WindowManager의 WindowLayoutInfo 클래스는 디스플레이 창의 기능을 DisplayFeature 요소 목록으로 사용할 수 있도록 해 줍니다.

FoldingFeature는 다음을 비롯하여 폴더블 디스플레이에 관한 정보를 제공하는 DisplayFeature 유형입니다.

  • state: 기기의 접힌 상태(FLAT 또는 HALF_OPENED)
  • orientation: 접힘 또는 힌지의 방향(HORIZONTAL 또는 VERTICAL)
  • occlusionType: 접힘 또는 힌지가 디스플레이의 일부를 가리는지 여부(NONE 또는 FULL)
  • isSeparating: 접힘 또는 힌지가 두 개의 논리 디스플레이 영역을 생성하는지 여부(true 또는 false)

HALF_OPENED인 폴더블 기기는 화면이 두 개의 디스플레이 영역으로 분리되므로 항상 isSeparating을 true로 보고합니다. 듀얼 화면 기기에서 애플리케이션이 두 화면에 걸쳐 있는 경우 isSeparating은 항상 true입니다.

FoldingFeature bounds 속성(DisplayFeature에서 상속됨)은 접힘 또는 힌지와 같은 접기 기능의 경계 직사각형을 나타냅니다. 경계는 접기 기능을 기준으로 화면에 요소를 배치하는 데 사용할 수 있습니다.

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Safely collects from windowInfoRepo when the lifecycle is STARTED
            // and stops collection when the lifecycle is STOPPED
            WindowInfoTracker.getOrCreate(this@MainActivity)
                .windowLayoutInfo(this@MainActivity)
                .collect { layoutInfo ->
                    // New posture information
                    val foldingFeature = layoutInfo.displayFeatures
                        .filterIsInstance()
                        .firstOrNull()
                    // Use information from the foldingFeature object
                }

        }
    }
}

Java

private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private final LayoutStateChangeCallback layoutStateChangeCallback =
                new LayoutStateChangeCallback();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    windowInfoTracker =
            new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}

@Override
protected void onStart() {
    super.onStart();
    windowInfoTracker.addWindowLayoutInfoListener(
            this, Runnable::run, layoutStateChangeCallback);
}

@Override
protected void onStop() {
    super.onStop();
    windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}

class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
    @Override
    public void accept(WindowLayoutInfo newLayoutInfo) {
        // Use newLayoutInfo to update the Layout
        List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures();
        for (DisplayFeature feature : displayFeatures) {
            if (feature instanceof FoldingFeature) {
                // Use information from the feature object
            }
        }
    }
}

탁자 모드

FoldingFeature 객체에 포함된 정보를 사용하여 앱은 휴대전화가 표면에 놓여 있고 힌지가 가로 위치에 있으며 폴더블 화면이 반만 열려 있는 테이블탑 모드와 같은 상태를 지원할 수 있습니다.

탁자 모드는 사용자가 휴대전화를 손에 들고 있지 않아도 휴대전화를 조작할 수 있는 편리함을 제공합니다. 탁자 모드는 미디어를 보고, 사진을 찍고, 영상 통화를 할 때 적합합니다.

탁자 모드의 동영상 플레이어 앱

FoldingFeature.StateFoldingFeature.Orientation를 사용하여 기기가 탁자 모드인지 확인합니다.

Kotlin


fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}

Java


boolean isTableTopPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}

기기가 탁자 모드임을 알면 그에 따라 앱 레이아웃을 업데이트합니다. 미디어 앱의 경우 일반적으로 재생 버튼을 스크롤 없이 볼 수 있는 부분에 배치하고 배치 컨트롤 및 보조 콘텐츠를 바로 아래에 배치하여 핸즈프리로 보거나 음악을 들을 수 있도록 합니다.

  • MediaPlayerActivity 앱: Media3 ExoplayerWindowManager를 사용하여 폴더블 인식 동영상 플레이어를 만드는 방법을 참고하세요.

  • 카메라 환경 펼치기 Codelab: 사진 앱용 테이블탑 모드를 구현하는 방법을 알아봅니다. 뷰파인더는 화면의 상단, 스크롤 없이 볼 수 있는 부분에, 컨트롤은 화면의 하단, 스크롤해야 볼 수 있는 부분에 표시합니다.

책 모드

또 다른 고유한 폴더블 상태는 기기가 반만 열려 있고 힌지가 세로인 책 모드입니다. 책 모드는 eBook을 읽을 때 유용합니다. 책 모드는 제본된 책처럼 대형 화면 폴더블에서 열린 두 페이지 레이아웃을 사용하여 실제 책을 읽는 경험을 캡처합니다.

핸즈프리로 사진을 찍을 때 다른 가로세로 비율을 캡처하려는 경우 사진에도 사용할 수 있습니다.

탁자 모드에 사용된 것과 동일한 기법으로 책 모드를 구현합니다. 유일한 차이점은 코드에서 접기 기능의 방향이 가로가 아닌 세로인지 확인해야 한다는 것입니다.

Kotlin

fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}

Java

boolean isBookPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}

창 크기 변경

기기 구성이 변경되면(예: 기기가 접히거나 펼쳐지는 경우, 회전되는 경우, 멀티 윈도우 모드에서 창 크기가 조절되는 경우) 앱의 디스플레이 영역이 변경될 수 있습니다.

Jetpack WindowManager의 WindowMetricsCalculator 클래스를 사용하면 현재 및 최대 창 측정항목을 검색할 수 있습니다. WindowManager WindowMetrics는 API 수준 30에서 도입된 플랫폼 WindowMetrics와 마찬가지로 창 경계를 제공하지만 API는 API 수준 14까지 하위 호환됩니다.

다양한 화면 크기 지원에서 다양한 창 크기를 지원하는 방법을 참고하세요.

추가 리소스

샘플

Codelab