폴더블 디스플레이 모드 지원

폴더블 기기는 독특한 시청 환경을 제공합니다. 후면 디스플레이 모드와 듀얼 화면 모드를 사용하면 후면 카메라 셀카 미리보기, 내부 및 외부 화면의 다르지만 동시에 보이는 디스플레이 등 폴더블 기기의 특별한 디스플레이 기능을 빌드할 수 있습니다.

후면 디스플레이 모드

일반적으로 폴더블 기기를 펼치면 내부 화면만 활성화됩니다. 후면 디스플레이 모드를 사용하면 활동을 폴더블 기기의 외부 화면으로 이동할 수 있으며 이는 일반적으로 기기가 펼쳐져 있는 동안 사용자의 반대쪽을 향합니다. 내부 디스플레이는 자동으로 꺼집니다.

이제는 외부 화면에 카메라 미리보기를 표시할 수도 있습니다. 이를 통해 사용자는 후면 카메라로 셀카를 찍을 수 있으며, 사진 촬영 성능이 훨씬 낫습니다.

후면 디스플레이 모드를 활성화하려면 사용자는 앱이 화면을 전환하도록 허용하는 대화상자에 응답해야 합니다. 예를 들면 다음과 같습니다.

그림 1. 후면 디스플레이 모드 시작을 허용하는 시스템 대화상자

시스템에서 대화상자를 생성하므로 별도로 개발할 필요가 없습니다. 기기 상태에 따라 다른 대화상자가 표시됩니다. 예를 들어 기기가 닫혀 있으면 시스템에서 사용자에게 기기를 펼치도록 안내합니다. 대화상자를 맞춤설정할 수는 없지만 OEM의 기기에 따라 다를 수 있습니다.

Pixel Fold 카메라 앱으로 후면 디스플레이 모드를 사용해 볼 수 있습니다. 카메라 환경 펼치기 Codelab에서 샘플 구현을 확인하세요.

듀얼 화면 모드

듀얼 화면 모드를 사용하면 폴더블의 두 디스플레이에 콘텐츠를 동시에 표시할 수 있습니다. 듀얼 화면 모드는 Android 14(API 수준 34) 이상을 실행하는 Pixel Fold에서 사용할 수 있습니다.

듀얼 화면 인터프리터를 사용 사례로 들 수 있습니다.

그림 2. 전면 및 후면 디스플레이에서 서로 다른 콘텐츠를 보여주는 듀얼 화면 인터프리터

프로그래매틱 방식으로 모드 사용 설정

라이브러리 버전 1.2.0-beta03부터 Jetpack WindowManager API를 통해 후면 디스플레이 모드와 듀얼 화면 모드에 액세스할 수 있습니다.

앱의 모듈 build.gradle 파일에 WindowManager 종속 항목을 추가합니다.

Groovy

dependencies {
    implementation "androidx.window:window:1.2.0-beta03"
}

Kotlin

dependencies {
    implementation("androidx.window:window:1.2.0-beta03")
}

진입점은 WindowAreaController이며 이는 디스플레이 간 또는 기기의 디스플레이 영역 간 창 이동과 관련된 정보와 동작을 제공합니다. WindowAreaController를 사용하면 제공되는 WindowAreaInfo 객체의 목록을 쿼리할 수 있습니다.

WindowAreaInfo를 사용하여 활성 창 영역 기능을 나타내는 인터페이스인 WindowAreaSession에 액세스합니다. WindowAreaSession을 사용하여 특정 WindowAreaCapability의 사용 가능 여부를 확인합니다.

각 기능은 특정 WindowAreaCapability.Operation과 관련되어 있습니다. 버전 1.2.0-beta03에서 Jetpack WindowManager는 두 가지 작업을 지원합니다.

다음은 앱의 기본 활동에서 후면 디스플레이 모드와 듀얼 화면 모드의 변수를 선언하는 방법을 보여주는 예입니다.

Kotlin

private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var windowAreaSession: WindowAreaSession? = null
private var windowAreaInfo: WindowAreaInfo? = null
private var capabilityStatus: WindowAreaCapability.Status =
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED

private val dualScreenOperation = WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA
private val rearDisplayOperation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA

Java

private WindowAreaControllerCallbackAdapter windowAreaController = null;
private Executor displayExecutor = null;
private WindowAreaSessionPresenter windowAreaSession = null;
private WindowAreaInfo windowAreaInfo = null;
private WindowAreaCapability.Status capabilityStatus  =
        WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED;

private WindowAreaCapability.Operation dualScreenOperation =
        WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA;
private WindowAreaCapability.Operation rearDisplayOperation =
        WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA;

다음은 활동의 onCreate() 메서드에서 변수를 초기화하는 방법입니다.

Kotlin

displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaController.getOrCreate()

lifecycleScope.launch(Dispatchers.Main) {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        windowAreaController.windowAreaInfos
            .map { info -> info.firstOrNull { it.type == WindowAreaInfo.Type.TYPE_REAR_FACING } }
            .onEach { info -> windowAreaInfo = info }
            .map { it?.getCapability(operation)?.status ?: WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED }
            .distinctUntilChanged()
            .collect {
                capabilityStatus = it
            }
    }
}

Java

displayExecutor = ContextCompat.getMainExecutor(this);
windowAreaController = new WindowAreaControllerCallbackAdapter(WindowAreaController.getOrCreate());
windowAreaController.addWindowAreaInfoListListener(displayExecutor, this);

windowAreaController.addWindowAreaInfoListListener(displayExecutor,
  windowAreaInfos -> {
    for(WindowAreaInfo newInfo : windowAreaInfos){
        if(newInfo.getType().equals(WindowAreaInfo.Type.TYPE_REAR_FACING)){
            windowAreaInfo = newInfo;
            capabilityStatus = newInfo.getCapability(presentOperation).getStatus();
            break;
        }
    }
});

작업을 시작하기 전에 특정 기능을 사용할 수 있는지 확인합니다.

Kotlin

when (capabilityStatus) {
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
      // The selected display mode is not supported on this device.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
      // The selected display mode is not currently available to be enabled.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
      // The selected display mode is currently available to be enabled.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
      // The selected display mode is already active.
    }
    else -> {
      // The selected display mode status is unknown.            
    }
}

Java

if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED)) {
  // The selected display mode is not supported on this device.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE)) {
  // The selected display mode is not currently available to be enabled.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE)) {
  // The selected display mode is currently available to be enabled.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE)) {
  // The selected display mode is already active.
}
else {
  // The selected display mode status is unknown.    
}

듀얼 화면 모드

다음 예에서는 기능이 이미 활성 상태이면 세션을 닫고 그렇지 않으면 presentContentOnWindowArea() 함수를 호출합니다.

Kotlin

fun toggleDualScreenMode() {
    if (windowAreaSession != null) {
        windowAreaSession?.close()
    }
    else {
        windowAreaInfo?.token?.let { token ->
            windowAreaController.presentContentOnWindowArea(
                token = token,
                activity = this,
                executor = displayExecutor,
                windowAreaPresentationSessionCallback = this
            )
        }
    }
}

Java

private void toggleDualScreenMode() {
    if(windowAreaSession != null) {
        windowAreaSession.close();
    }
    else {
        Binder token = windowAreaInfo.getToken();
        windowAreaController.presentContentOnWindowArea( token, this, displayExecutor, this);
    }
}

앱의 기본 활동이 WindowAreaPresentationSessionCallback으로 사용된 것을 볼 수 있습니다.

API는 리스너 접근 방식을 사용합니다. 폴더블의 다른 디스플레이에 콘텐츠를 표시하도록 요청하면 리스너의 onSessionStarted() 메서드를 통해 반환되는 세션이 시작됩니다. 세션을 닫으면 onSessionEnded() 메서드에 확인 메시지가 표시됩니다.

리스너를 만들려면 다음과 같이 WindowAreaPresentationSessionCallback 인터페이스를 구현하세요.

Kotlin

class MainActivity : AppCompatActivity(), windowAreaPresentationSessionCallback

Java

public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback

리스너는 onSessionStarted(), onSessionEnded(),, onContainerVisibilityChanged() 메서드를 구현해야 합니다. 콜백 메서드는 세션 상태를 알려주므로 적절하게 앱을 업데이트할 수 있습니다.

onSessionStarted() 콜백은 WindowAreaSessionPresenter를 인수로 수신합니다. 이 인수는 창 영역에 액세스하고 콘텐츠를 표시할 수 있는 컨테이너입니다. 사용자가 기본 애플리케이션 창을 닫으면 시스템에서 프레젠테이션을 자동으로 닫을 수 있습니다. 또는 WindowAreaSessionPresenter#close()를 호출하여 프레젠테이션을 닫을 수 있습니다.

다른 콜백의 경우 편의를 위해 함수 본문에서 오류를 확인하고 상태를 기록합니다.

Kotlin

override fun onSessionStarted(session: WindowAreaSessionPresenter) {
    windowAreaSession = session
    val view = TextView(session.context)
    view.text = "Hello world!"
    session.setContentView(view)
}

override fun onSessionEnded(t: Throwable?) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}")
    }
}

override fun onContainerVisibilityChanged(isVisible: Boolean) {
    Log.d(logTag, "onContainerVisibilityChanged. isVisible = $isVisible")
}

Java

@Override
public void onSessionStarted(@NonNull WindowAreaSessionPresenter session) {
    windowAreaSession = session;
    TextView view = new TextView(session.getContext());
    view.setText("Hello world, from the other screen!");
    session.setContentView(view);
}

@Override public void onSessionEnded(@Nullable Throwable t) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}");
    }
}

@Override public void onContainerVisibilityChanged(boolean isVisible) {
    Log.d(logTag, "onContainerVisibilityChanged. isVisible = " + isVisible);
}

생태계 전체에서 일관성을 유지하려면 Dual Screen 공식 아이콘을 사용하여 사용자에게 듀얼 화면 모드를 사용 설정하거나 사용 중지하는 방법을 알려주세요.

실제로 작동하는 샘플은 DualScreenActivity.kt를 확인하세요.

후면 디스플레이 모드

듀얼 화면 모드 예와 마찬가지로 다음 toggleRearDisplayMode() 함수 예는 기능이 이미 활성 상태이면 세션을 닫고 그렇지 않으면 transferActivityToWindowArea() 함수를 호출합니다.

Kotlin

fun toggleRearDisplayMode() {
    if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
        if(windowAreaSession == null) {
            windowAreaSession = windowAreaInfo?.getActiveSession(
                operation
            )
        }
        windowAreaSession?.close()
    } else {
        windowAreaInfo?.token?.let { token ->
            windowAreaController.transferActivityToWindowArea(
                token = token,
                activity = this,
                executor = displayExecutor,
                windowAreaSessionCallback = this
            )
        }
    }
}

Java

void toggleDualScreenMode() {
    if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
        if(windowAreaSession == null) {
            windowAreaSession = windowAreaInfo.getActiveSession(
                operation
            )
        }
        windowAreaSession.close()
    }
    else {
        Binder token = windowAreaInfo.getToken();
        windowAreaController.transferActivityToWindowArea(token, this, displayExecutor, this);
    }
}

이 경우 표시된 활동은 WindowAreaSessionCallback,으로 사용되며 이는 콜백이 창 영역에 콘텐츠 표시를 허용하는 프레젠터를 수신하지 않고 대신 전체 활동을 다른 영역으로 전송하기 때문에 구현이 더 간단합니다.

Kotlin

override fun onSessionStarted() {
    Log.d(logTag, "onSessionStarted")
}

override fun onSessionEnded(t: Throwable?) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}")
    }
}

Java

@Override public void onSessionStarted(){
    Log.d(logTag, "onSessionStarted");
}

@Override public void onSessionEnded(@Nullable Throwable t) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}");
    }
}

생태계 전체에서 일관성을 유지하려면 후면 카메라 공식 아이콘을 사용하여 사용자에게 후면 디스플레이 모드를 사용 설정하거나 사용 중지하는 방법을 알려주세요.

추가 리소스