折りたたみ式ディスプレイ モードをサポートする

折りたたみ式デバイスでは、折りたたみ式ならではの視聴エクスペリエンスを実現できます。背面ディスプレイ モードとデュアル スクリーン モードを活用した特別なディスプレイ機能を構築できます。たとえば、背面カメラ セルフィー プレビューや、外側と内側の画面で異なる表示を同時に行う機能などです。

背面ディスプレイ モード

通常、折りたたみ式デバイスを開くと、内側の画面のみがアクティブになります。 背面ディスプレイ モードでは、折りたたみ式デバイスの外側の画面にアクティビティを移動できます。外側の画面は通常、デバイスを開いた状態でユーザーとは反対の方向を向いています。インナー ディスプレイは自動的にオフになります。

新しい用途として、外側の画面にカメラ プレビューを表示して、ユーザーが背面カメラで自撮り写真を撮影できるようにしました。通常は前面カメラよりも撮影性能が格段に向上します。

背面ディスプレイ モードを有効にするには、ユーザーがダイアログに応答して、アプリに画面の切り替えを許可します。次に例を示します。

図 1. 背面ディスプレイ モードを有効にするシステム ダイアログ

ダイアログはシステムによって作成されるため、デベロッパー側での開発は必要ありません。デバイスの状態に応じて異なるダイアログが表示されます。たとえば、デバイスが閉じている場合は、デバイスを開くようシステムがユーザーに指示します。ダイアログはカスタマイズできません。また、デバイスの OEM ごとに異なる可能性があります。

Google Pixel Fold のカメラアプリで背面ディスプレイ モードを試すことができます。Codelab のJetpack WindowManager を使用して折りたたみ式デバイスでのカメラアプリを最適化するの実装例をご覧ください。

デュアル スクリーン モード

デュアル スクリーン モードでは、折りたたみ式デバイスの両方のディスプレイに同時にコンテンツを表示できます。デュアル スクリーン モードは、Android 14(API レベル 34)以降を搭載した Google Pixel Fold で利用できます。

ユースケースの一例として、デュアル スクリーンの通訳があります。

図 2. 前面ディスプレイと背面ディスプレイに異なるコンテンツを表示するデュアル スクリーンの通訳

モードをプログラマティックに有効にする

ライブラリ バージョン 1.2.0-beta03 以降では、Jetpack WindowManager API を使用して、背面ディスプレイ モードとデュアル スクリーン モードにアクセスできます。

アプリのモジュール build.gradle ファイルに WindowManager の依存関係を追加します。

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

エントリ ポイントは WindowAreaController です。これは、デバイス上のディスプレイ間またはディスプレイ領域間でのウィンドウの移動に関する情報と動作を提供します。WindowAreaController を使用すると、使用可能な WindowAreaInfo オブジェクトのリストを確認できます。

WindowAreaInfo を使用すると、アクティブなウィンドウ領域機能を表すインターフェースである WindowAreaSession にアクセスできます。WindowAreaSession を使用すると、特定の WindowAreaCapability が使用可能かどうかを判断できます。

各 capability は特定の WindowAreaCapability.Operation に関連付けられています。バージョン 1.2.0-beta03 では、Jetpack WindowManager は次の 2 種類のオペレーションをサポートしています。

アプリのメイン アクティビティで背面ディスプレイ モードとデュアル スクリーン モードの変数を宣言する方法の例を次に示します。

KotlinJava
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
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() メソッドで変数を初期化する方法を以下に示します。

KotlinJava
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
           
}
   
}
}
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;
       
}
   
}
});

オペレーションを開始する前に、特定の機能の可用性を確認します。

KotlinJava
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 available.
   
}
   
WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
     
// The selected display mode is available and can be enabled.
   
}
   
WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
     
// The selected display mode is already active.
   
}
   
else -> {
     
// The selected display mode status is unknown.
   
}
}
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 available.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE)) {
 
// The selected display mode is available and can 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.
}

デュアル スクリーン モード

次の例では、capability がすでにアクティブな場合にセッションを閉じます。そうでない場合は presentContentOnWindowArea() 関数を呼び出します。

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

アプリのメイン アクティビティを WindowAreaPresentationSessionCallback 引数として使用していることに注目してください。

この API はリスナー アプローチを使用します。折りたたみ式デバイスの反対側のディスプレイにコンテンツを表示するリクエストを行うと、リスナーの onSessionStarted() メソッドを通じて返されるセッションが開始されます。セッションを閉じると、onSessionEnded() メソッドに確認メッセージが表示されます。

リスナーを作成するには、WindowAreaPresentationSessionCallback インターフェースを実装します。

KotlinJava
class MainActivity : AppCompatActivity(), windowAreaPresentationSessionCallback
public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback

リスナーは onSessionStarted()onSessionEnded(),onContainerVisibilityChanged() の各メソッドを実装する必要があります。このコールバック メソッドにより、セッション ステータスが通知され、それに応じてアプリを更新できます。

onSessionStarted() コールバックは、引数として WindowAreaSessionPresenter を受け取ります。この引数は、ウィンドウ領域にアクセスしてコンテンツを表示できるようにするコンテナです。プレゼンテーションは、ユーザーがメインのアプリケーション ウィンドウを閉じたときに、システムによって自動的に閉じることができます。または、WindowAreaSessionPresenter#close() を呼び出してプレゼンテーションを閉じることもできます。

他のコールバックについては、わかりやすくするために、関数本体でエラーの有無を確認し、状態をログに記録します。

KotlinJava
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")
}
@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);
}

エコシステム全体で一貫性を保つため、デュアル スクリーンの公式アイコンを使用して、デュアル スクリーン モードを有効または無効にする方法をユーザーに示します。

実際のサンプルについては、DualScreenActivity.kt をご覧ください。

背面ディスプレイ モード

デュアル スクリーン モードの例と同様に、次の toggleRearDisplayMode() 関数の例では、capability がすでにアクティブな場合はセッションを閉じます。そうでない場合は transferActivityToWindowArea() 関数を呼び出します。

KotlinJava
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
           
)
       
}
   
}
}
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 として使用されます。コールバックはウィンドウ領域でコンテンツを表示できるプレゼンターを受け取るのではなく、アクティビティ全体を別のエリアに転送するため、実装が簡単です。

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

override fun onSessionEnded(t: Throwable?) {
   
if(t != null) {
       
Log.e(logTag, "Something was broken: ${t.message}")
   
}
}
@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}");
   
}
}

エコシステム全体で一貫性を保つため、公式の背面カメラアイコンを使用して、背面ディスプレイ モードを有効または無効にする方法をユーザーに示します。

参考情報