앱에서 접힌 상태 인식

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

기기가 탁자 모드나 책 모드와 같은 구성을 지원하는지 여부를 파악하면 다양한 레이아웃을 지원하거나 특정 기능을 제공하는 것에 관한 결정을 내릴 수 있습니다.

창 정보

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

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

Kotlin 흐름

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 흐름을 사용하지 않고 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 흐름을 사용하지 않고 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 of 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 WindowInfoTracker 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<FoldingFeature>()
                        .firstOrNull()
                    // Use information from the foldingFeature object.
                }

        }
    }
}

자바

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 객체에 포함된 정보를 사용하여 다음을 수행할 수 있습니다. 탁자, 휴대전화가 표면에 놓여 있는 상태, 힌지가 가로 위치에 있고 폴더블 화면이 반만 열려 있습니다.

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

그림 1. 탁자 모드의 동영상 플레이어 앱

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
}

자바

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

기기가 탁자 모드임을 확인했으면 앱 레이아웃을 업데이트합니다. 변경할 수 있습니다 미디어 앱의 경우 일반적으로 재생 위치를 바로 아래에 보조 콘텐츠를 추가하여 핸즈프리 보기 또는 청취 환경입니다.

Android 15 (API 수준 35) 이상에서는 동기 API를 호출하여 현재 기기에 관계없이 기기가 탁자 상태를 지원하는지 감지 정의합니다.

이 API는 기기에서 지원하는 자세 목록을 제공합니다. 목록이 탁자 상태가 포함된 경우 앱 레이아웃을 분할하여 상태를 지원할 수 있습니다. 앱 UI에서 탁자 및 전체 화면 레이아웃의 A/B 테스트를 실행할 수 있습니다.

Kotlin

if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
    val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
    if (postures.contains(TABLE_TOP)) {
        // Device supports tabletop posture.
   }
}

자바

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
    List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures();
    if (postures.contains(SupportedPosture.TABLETOP)) {
        // Device supports tabletop posture.
    }
}

도서 상태

또 다른 고유한 폴더블 기능은 기기가 반쯤 열려 있고 힌지가 수직인 책 모드입니다. 책 모드는 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
}

자바

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까지 하위 호환됩니다.

창 크기 클래스 사용을 참고하세요.

추가 리소스

샘플

  • Jetpack WindowManager: Jetpack 사용 방법의 예 WindowManager 라이브러리
  • Jetcaster : Compose를 사용한 탁자 모드 구현

Codelab