三つ折りと横向きの折りたたみ式をサポート

横向きの折りたたみ式デバイスが閉じた状態と完全に開いた状態、横に並んだ三つ折りデバイスが閉じた状態と完全に開いた状態。

デベロッパーは、折りたたみ式デバイス(特に Samsung Trifold や初代 Google Pixel Fold など、横向きで開くデバイス(rotation_0 = landscape))用のアプリを作成する際に、特有の困難に直面することがよくあります。デベロッパーのミスには次のようなものがあります。

  • デバイスの向きに関する誤った想定
  • 見落とされたユースケース
  • 構成の変更をまたいで値を再計算またはキャッシュに保存できない

デバイス関連の具体的な問題には、次のようなものがあります。

  • カバー ディスプレイとインナー ディスプレイのデバイスの自然な向きが一致しない(rotation_0 = 縦向きに基づく想定)ため、折りたたみと展開の過程でアプリが失敗する
  • 画面密度の違いと density 構成変更の誤った処理
  • カメラセンサーが自然な向きに依存していることが原因で発生するカメラ プレビューの問題

折りたたみ式デバイスで高品質のユーザー エクスペリエンスを提供するには、次の重要な領域に焦点を当てます。

  • デバイスの物理的な向きではなく、アプリが占有する実際の画面領域に基づいてアプリの向きを決定する
  • カメラ プレビューを更新して、デバイスの向きとアスペクト比を正しく管理し、横向きのプレビューを回避し、画像が引き伸ばされたりトリミングされたりしないようにする
  • ViewModel または同様のアプローチで状態を保持するか、画面密度と向きの変更を手動で処理することで、デバイスの折りたたみまたは展開中にアプリの継続性を維持し、アプリの再起動や状態の損失を回避する
  • モーション センサーを利用するアプリでは、座標系を調整して画面の現在の向きに合わせ、rotation_0 = 縦向きに基づく想定を回避することで、正確なユーザー操作を保証します。

アダプティブ ビルド

アプリがすでにアダプティブであり、大画面アプリの品質に関するガイドラインで説明されている最適化レベル(Tier 2)に準拠している場合、アプリは折りたたみ式デバイスで適切に動作します。それ以外の場合は、三つ折りと横向きの折りたたみ式デバイスの具体的な詳細を再確認する前に、Android のアダプティブ開発の次の基本的なコンセプトを確認してください。

アダプティブ レイアウト

UI は、さまざまな画面サイズだけでなく、アスペクト比のリアルタイムな変化(折りたたみ式デバイスを広げる、マルチウィンドウ モードやデスクトップ ウィンドウ モードに入るなど)にも対応する必要があります。詳しくは、アダプティブ レイアウトについてをご覧ください。

  • アダプティブ レイアウトを設計して実装する
  • ウィンドウ サイズに基づいてアプリのメイン ナビゲーションを調整する
  • ウィンドウ サイズクラスを使用してアプリの UI を調整する
  • Jetpack API を使用して、リストと詳細などの正規レイアウトの実装を簡素化
開いた折りたたみ式デバイスでレターボックス表示されたアプリと、開いた別の折りたたみ式デバイスでアダプティブ レイアウトで全画面表示された同じアプリ。
図 1. 非アダプティブ(レターボックス)レイアウトとアダプティブ レイアウトの違い。

ウィンドウ サイズクラス

横向き折りたたみ式デバイスや三つ折りデバイスなどの折りたたみ式デバイスでは、コンパクト、中程度、拡大幅のウィンドウ サイズクラスを瞬時に切り替えることができます。これらのクラスを理解して実装することで、アプリが現在のデバイスの状態に適したナビゲーション コンポーネントとコンテンツ密度を表示できるようになります。

コンパクト、中程度、拡大のウィンドウ サイズクラスに合わせたサイズのデバイスでアプリを表示した様子。
図 2. ウィンドウ サイズクラス。

次の例では、マテリアル 3 アダプティブ ライブラリを使用して、まず currentWindowAdaptiveInfo() 関数を呼び出し、3 つのウィンドウ サイズクラスに対応するレイアウトを使用して、アプリで使用可能なスペースを判断しています。

val adaptiveInfo = currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)
val windowSizeClass = adaptiveInfo.windowSizeClass

when {
  windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND) -> // Large
  windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND) -> // Medium
  else -> // Compact
}

詳しくは、ウィンドウ サイズ クラスを使用するをご覧ください。

大画面のアプリの品質

大画面アプリの品質に関するガイドラインの Tier 2(大画面向けに最適化)または Tier 1(大画面によって差別化)に準拠することで、アプリが三つ折りデバイス、横向きの折りたたみ式デバイス、その他の大画面デバイスで魅力的なユーザー エクスペリエンスを提供できるようになります。このガイドラインでは、複数の階層レベルにわたる重要なチェックについて説明し、アダプティブ対応から差別化されたエクスペリエンスへと移行する方法を解説します。

Android 16 以降

Android 16(API レベル 36)以上をターゲットとするアプリの場合、システムは、最小幅が 600 dp 以上のディスプレイでの向き、サイズ変更、アスペクト比の制限を無視します。アスペクト比やユーザーが希望する向きに関係なく、アプリはディスプレイ ウィンドウ全体に表示され、レターボックス表示互換モードは使用されなくなります。

その他の考慮事項

三つ折りと横向き折りたたみ式では、センサー、カメラ プレビュー、構成の継続性(折りたたみ、展開、サイズ変更時の状態の保持)に関して、特別な処理が必要となる独自のハードウェア動作が導入されています。

カメラ プレビュー

横向きの折りたたみ式デバイスやアスペクト比の計算(マルチ ウィンドウ、デスクトップ ウィンドウ、接続されたディスプレイなどのシナリオ)でよくある問題は、カメラ プレビューが引き伸ばされたり、横向きになったり、切り取られたり、回転したりして表示されることです。

前提条件の不一致

この問題は、大画面デバイスや折りたたみ式デバイスでよく発生します。アプリが、アスペクト比やセンサーの向きなどのカメラ機能と、デバイスの向きや自然な向きなどのデバイス機能の間に一定の関係があることを想定しているためです。

新しいフォーム ファクタは、この前提に異議を唱えています。折りたたみ式デバイスでは、デバイスの回転を変更せずにディスプレイのサイズとアスペクト比を変更できます。たとえば、デバイスを開くとアスペクト比は変わりますが、ユーザーがデバイスを回転させなければ、回転は変わりません。アスペクト比がデバイスの回転に関連しているとアプリが想定している場合、カメラ プレビューが誤って回転または拡大縮小される可能性があります。アプリがカメラセンサーの向きが縦向きのデバイスの向きと一致することを前提としている場合も同様のことが起こります。横向きの折りたたみ式デバイスでは、必ずしもそうとは限りません。

解決方法 1: Jetpack CameraX(推奨)

最もシンプルで堅牢なソリューションは、Jetpack CameraX ライブラリを使用することです。PreviewView UI 要素は、すべてのプレビューの複雑さを自動的に処理するように設計されています。

  • PreviewView は、センサーの向き、デバイスの回転、スケーリングを正しく調整します。
  • 通常は中央に配置して切り抜く(FILL_CENTER)ことで、カメラ画像のアスペクト比を維持します。
  • 必要に応じて、スケールタイプを FIT_CENTER に設定してプレビューをレターボックス表示にできます。

詳しくは、CameraX ドキュメントのプレビューを実装するをご覧ください。

解決策 2: CameraViewfinder

既存の Camera2 コードベースを使用している場合は、CameraViewfinder ライブラリ(API レベル 21 との下位互換性あり)も最新のソリューションです。TextureView または SurfaceView を使用してカメラフィードの表示を簡素化し、必要な変換(アスペクト比、スケール、回転)をすべて適用します。

詳しくは、カメラ ビューファインダーの概要のブログ投稿とカメラ プレビューのデベロッパー ガイドをご覧ください。

解決策 3: 手動の Camera2 実装

CameraX または CameraViewfinder を使用できない場合は、向きとアスペクト比を手動で計算し、構成が変更されるたびに計算が更新されるようにする必要があります。

  • CameraCharacteristics からカメラセンサーの向き(0、90、180、270 度など)を取得します。
  • デバイスの現在のディスプレイの回転を取得します(0、90、180、270 度など)。
  • これらの 2 つの値を使用して、SurfaceView または TextureView に必要な変換を決定します。
  • 歪みを防ぐため、出力 Surface のアスペクト比がカメラ プレビューのアスペクト比と一致するようにします。
  • カメラアプリは、マルチ ウィンドウ モードまたはデスクトップ ウィンドウ モードで画面の一部で実行されているか、接続されたディスプレイで実行されている可能性があります。そのため、画面サイズを使用してカメラのファインダーの寸法を決定するべきではありません。代わりに、ウィンドウ指標を使用してください。

詳しくは、カメラ プレビューのデベロッパー ガイドと、さまざまなフォーム ファクタに対応したカメラアプリの動画をご覧ください。

解決策 4: インテントを使用して基本的なカメラ操作を行う

カメラの機能がそれほど必要ない場合は、デバイスのデフォルトのカメラ アプリケーションで写真や動画の撮影などの基本的なカメラ操作を行うという、シンプルでわかりやすい解決策があります。カメラ ライブラリと統合する必要はありません。代わりに Intent を使用します。

詳しくは、カメラ インテントをご覧ください。

構成と連続性

折りたたみ式デバイスは UI の汎用性を高めますが、折りたたみ式でないデバイスよりも多くの構成変更を開始する可能性があります。アプリは、デバイスの回転、折りたたみ/展開、マルチウィンドウ モードまたはデスクトップ モードでのウィンドウのサイズ変更など、これらの構成の変更とその組み合わせを管理しながら、アプリの状態を保持または復元する必要があります。たとえば、アプリは次の継続性を維持する必要があります。

  • クラッシュしたり、ユーザーに混乱を招くような変更(画面の切り替えやアプリをバックグラウンドに送るなど)を引き起こしたりすることなく、アプリの状態を保持する
  • スクロール可能なフィールドのスクロール位置
  • テキスト フィールドに入力されたテキストとキーボードの状態
  • 設定の変更が開始されたときに中断した時点から再生を再開するためのメディア再生位置

頻繁にトリガーされる構成変更には、screenSizesmallestScreenSizescreenLayoutorientationdensityfontScaletouchscreenkeyboard があります。

android:configChanges構成の変更に対処するをご覧ください。アプリの状態の管理について詳しくは、UI の状態を保存するをご覧ください。

密度構成の変更

三つ折りデバイスや横向きの折りたたみ式デバイスの外側画面と内側画面では、ピクセル密度が異なる場合があります。そのため、density の構成変更の管理には特に注意が必要です。通常、Android はディスプレイの密度が変化するとアクティビティを再起動するため、データが失われる可能性があります。システムによるアクティビティの再起動を防ぐには、マニフェストで密度処理を宣言し、アプリで構成変更をプログラムで管理します。

AndroidManifest.xml の構成

  • density: アプリが画面密度の変化を処理することを宣言します
  • その他の構成変更: screenSizeorientationkeyboardHiddenfontScale など、頻繁に発生するその他の構成変更も宣言することをおすすめします。

密度(およびその他の構成変更)を宣言すると、システムはアクティビティを再起動せず、代わりに onConfigurationChanged() を呼び出します。

onConfigurationChanged() の実装

密度が変更された場合は、コールバックでリソースを更新する必要があります(ビットマップの再読み込みやレイアウト サイズの再計算など)。

  • DPI が newConfig.densityDpi に変更されたことを確認します。
  • カスタムビューやカスタム ドローアブルなどを新しい密度にリセット

処理するリソース アイテム

  • 画像リソース: ビットマップとドローアブルを密度固有のリソースに置き換えるか、スケールを直接調整する
  • レイアウト単位(dp から px への変換): ビューのサイズ、マージン、パディングを再計算します。
  • フォントとテキストのサイズ: sp 単位のテキストサイズを再適用
  • カスタム View/Canvas の描画: Canvas の描画に使用されるピクセルベースの値を更新

アプリの向きを決定する

アダプティブを構築する際は、物理デバイスの回転に依存しないでください。大画面デバイスでは無視されます。また、マルチウィンドウ モードのアプリは、デバイスとは異なる向きになる可能性があります。代わりに、Configuration.orientation または WindowMetrics を使用して、ウィンドウ サイズに基づいてアプリが現在横向きか縦向きかを特定します。

解決策 1: Configuration.orientation を使用する

このプロパティは、アプリが現在表示されている向きを識別します。

解決策 2: WindowMetrics#getBounds() を使用する

アプリの現在のディスプレイ境界を取得し、その幅と高さを確認して向きを判断できます。

スマートフォン(または折りたたみ式デバイスの外部ディスプレイ)ではアプリの向きを制限するが、大画面デバイスでは制限しない必要がある場合は、スマートフォンでアプリの向きを制限するをご覧ください。

ポスチャーとディスプレイ モード

テーブルトップや HALF_OPENED などの折りたたみ式デバイスの形状と状態は、縦向きの折りたたみ式デバイスと横向きの折りたたみ式デバイスの両方でサポートされています。ただし、三つ折りは卓上姿勢をサポートしていないため、HALF_OPENED では使用できません。一方、三つ折りは、完全に開いたときに大画面で独自のユーザー エクスペリエンスを提供します。

HALF_OPENED をサポートする折りたたみ式デバイスでアプリを差別化するには、FoldingFeature などの Jetpack WindowManager API を使用します。

折りたたみ式デバイスの姿勢、状態、カメラ プレビューのサポートについて詳しくは、以下のデベロッパー ガイドをご覧ください。

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

画面の向きを自然なセンサーの向きに固定する

非常に特殊なユースケース(特に、デバイスの折りたたみ状態とは関係なく画面全体を占有する必要があるアプリ)では、nosensor フラグを使用すると、アプリをデバイスの自然な向きにロックできます。たとえば、Google Pixel Fold では、折りたたんだ状態のデバイスの自然な向きは縦向きですが、広げた状態の自然な向きは横向きです。nosensor フラグを追加すると、アプリは外側のディスプレイで実行されている場合は縦向きに、内側のディスプレイで実行されている場合は横向きにロックされます。

<activity
  android:name=".MainActivity"
  android:screenOrientation="nosensor">

ゲームと XR センサーのリマッピング

ゲームや XR アプリの場合、センサーの生データ(ジャイロスコープや加速度計など)はデバイス固定の座標系で提供されます。ユーザーがデバイスを回転させて横向きでゲームをプレイすると、センサーの軸が画面に合わせて回転しないため、ゲームの操作が正しく行われません。

この問題を修正するには、現在の Display.getRotation() を確認し、それに応じて軸を再マッピングします。

  • 回転 0: x=x, y=y
  • Rotation 90: x=-y, y=x
  • 180 度回転: x=-x、y=-y
  • 回転 270: x=y、y=-x

回転ベクトル(コンパス アプリや XR アプリで使用)の場合は、SensorManager.remapCoordinateSystem() を使用して、現在の回転に基づいてカメラレンズの方向または画面の上部を新しい軸にマッピングします。

アプリの互換性

すべてのフォーム ファクタと接続されたディスプレイとの互換性を保証するため、アプリはアプリの品質に関するガイドラインに準拠する必要があります。アプリがガイドラインに準拠できない場合、デバイス メーカーは互換性処理を実装できますが、ユーザー エクスペリエンスが低下する可能性があります。

詳細については、プラットフォームで提供される互換性回避策の包括的なリスト(特に、カメラ プレビューオーバーライドAndroid 16 API の変更点に関連する回避策)を確認してください。これらの回避策は、アプリの動作を変更する可能性があります。

アダプティブ アプリの構築について詳しくは、大画面のアプリの品質をご覧ください。