折りたたみ式デバイスでは、折りたたみ式ならではの視聴エクスペリエンスを実現できます。たとえば、背面カメラ セルフィー プレビューや、外側と内側の画面で異なる表示を同時に行う機能など、背面ディスプレイ モードとデュアル スクリーン モードを活用した特別なディスプレイ機能を構築できます。
背面ディスプレイ モード
通常、折りたたみ式デバイスを開くと、内側の画面のみがアクティブになります。 背面ディスプレイ モードでは、折りたたみ式デバイスの外側の画面にアクティビティを移動できます。外側の画面は、通常、デバイスが開かれている間はユーザーとは反対の方向を向いています。インナー ディスプレイは自動的にオフになります。
新しい用法として、外側の画面にカメラ プレビューを表示できます。背面カメラで自撮り写真を撮影できるため、多くの場合前面カメラよりも撮影パフォーマンスが大幅に向上します。
背面ディスプレイ モードを有効にするには、ユーザーがダイアログに応答して、アプリに画面の切り替えを許可します。次に例を示します。
ダイアログはシステムによって作成されるため、デベロッパー側での開発は必要ありません。デバイスの状態に応じて異なるダイアログが表示されます。たとえば、デバイスが閉じている場合は、デバイスを開くようシステムがユーザーに指示します。ダイアログはカスタマイズできませんが、デバイスの OEM ごとに異なる場合があります。
Google Pixel Fold のカメラアプリで背面ディスプレイ モードを試すことができます。Codelab の Jetpack WindowManager を使用して折りたたみ式デバイスのカメラアプリを最適化するで実装例をご覧ください。
デュアル スクリーン モード
デュアル スクリーン モードでは、折りたたみ式デバイスの両方のディスプレイに同時にコンテンツを表示できます。デュアル スクリーン モードは、Android 14(API レベル 34)以降を搭載した Google Pixel Fold で利用できます。
ユースケースの一例として、デュアル スクリーンの通訳があります。
モードをプログラマティックに有効にする
ライブラリ バージョン 1.2.0-beta03 以降では、Jetpack WindowManager API を使用して、背面ディスプレイ モードとデュアル スクリーン モードにアクセスできます。
アプリのモジュール build.gradle ファイルに WindowManager の依存関係を追加します。
Kotlin
dependencies {
// Define window_version in your project's build configuration.
implementation("androidx.window:window:$window_version")
}
Groovy
dependencies {
// TODO: Define window_version in your project's build configuration.
implementation "androidx.window:window:$window_version"
}
エントリー ポイントは WindowAreaController です。これは、デバイス上のディスプレイ間またはディスプレイ領域間でのウィンドウの移動に関する情報を提供し、動作を制御します。WindowAreaController を使用すると、使用可能な WindowAreaInfo オブジェクトのリストを確認できます。
WindowAreaInfo を使用すると、アクティブなウィンドウ領域機能を表すインターフェースである WindowAreaSession にアクセスできます。WindowAreaSession を使用すると、特定の WindowAreaCapability が使用可能かどうかを判断できます。
各 capability は特定の WindowAreaCapability.Operation に関連付けられています。バージョン 1.2.0-beta03 では、Jetpack WindowManager は次の 2 種類のオペレーションをサポートしています。
WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA: デュアル スクリーン モードを開始します。WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA: 背面ディスプレイ モードを開始します。
アプリのメイン Activity で背面ディスプレイ モードとデュアル スクリーン モードの変数を宣言する方法の例を次に示します。
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;
}
}
});
オペレーションを開始する前に、特定の capability について使用可否を確認します。
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 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. } }
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 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() 関数を呼び出します。
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 ExampleActivity : ComponentActivity(), WindowAreaPresentationSessionCallback {
Java
public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback
リスナーは onSessionStarted()、onSessionEnded(),、onContainerVisibilityChanged() の各メソッドを実装する必要があります。このコールバック メソッドにより、セッション ステータスが通知され、それに応じてアプリを更新できます。
onSessionStarted() コールバックは、引数として WindowAreaSessionPresenter を受け取ります。この引数は、ウィンドウ領域にアクセスしてコンテンツを表示できるようにするコンテナです。プレゼンテーションは、ユーザーがメインのアプリケーション ウィンドウを閉じたときに、システムによって自動的に閉じることができます。または、WindowAreaSessionPresenter#close() を呼び出してプレゼンテーションを閉じることもできます。
他のコールバックでは、わかりやすくするために、関数本体でエラーを確認し、状態をログに記録します。
Kotlin
override fun onSessionStarted(session: WindowAreaSessionPresenter) { windowAreaSession = session session.setContentView(ComposeView(session.context).apply { setContent { MyScreen() } }) } 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);
}
エコシステム全体で一貫性を保つため、デュアル スクリーンの公式アイコンを使用して、デュアル スクリーン モードを有効または無効にする方法をユーザーに示します。
実際のサンプルについては、DualScreenActivity.kt をご覧ください。
背面ディスプレイ モード
デュアル スクリーン モードの例と同様に、次の toggleRearDisplayMode() 関数の例では、capability がすでにアクティブな場合はセッションを閉じます。そうでない場合は 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 toggleRearDisplayMode() {
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 として使用されます。
Rear Display API はリスナー アプローチで動作します。コンテンツを他のディスプレイに移動するようリクエストしたら、リスナーの onSessionStarted() メソッドによって返されるセッションを開始します。内部の(大きい)画面に戻したい場合は、セッションを閉じます。onSessionEnded() メソッドから確認を受け取ります。
Kotlin
override fun onSessionStarted(session: WindowAreaSession) { 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}");
}
}
エコシステム全体で一貫性を保つため、背面カメラの公式アイコンを使用して、背面ディスプレイ モードを有効または無効にする方法をユーザーに示します。
参考情報
- Jetpack WindowManager を使用して折りたたみ式デバイスのカメラアプリを最適化する Codelab
androidx.window.areaパッケージの概要- Jetpack WindowManager サンプルコード: