カメラ エクスペリエンスを広げる

1. 始める前に

折りたたみ式デバイスの特別な点

折りたたみ式デバイスは一世代に一度のイノベーションで、ユニークな体験を提供します。ハンズフリー操作のテーブルトップ UI など、差別化された機能でユーザーに喜ばれる貴重な機会をもたらします。

前提条件

  • Android アプリ開発に関する基本的な知識
  • Hilt 依存関係インジェクション フレームワークに関する基本的な知識

作成するアプリの概要

この Codelab では、折りたたみ式デバイス向けに最適化されたレイアウトを備えるカメラアプリを作成します。

実行中のアプリのスクリーンショット

まず、基本的なカメラアプリから始めましょう。このアプリはデバイス形状の変化に対応していません。また、背面カメラで高度な自撮りを行うこともできません。このソースコードを更新して、デバイスを広げたときにプレビューを小さい画面に移動し、スマートフォンをテーブルトップ モードに設定するようにします。

この Codelab で扱う API の説明にはカメラアプリが最も適した使用事例ですが、ここで学習する機能はどのアプリにも応用できるものです。

学習内容

  • Jetpack Window Manager を使用して、形状の変化に対応できるようにする方法
  • 折りたたみ式デバイスの小さな画面にアプリを移動する方法

必要なもの

  • 最新バージョンの Android Studio
  • 折りたたみ式デバイスまたは折りたたみ式エミュレータ

2. 設定する

開始用コードを取得する

  1. Git がインストールされている場合は、以下のコマンドをそのまま実行できます。Git がインストールされているかどうかを確認するには、ターミナルまたはコマンドラインで「git --version」と入力し、正しく実行されることを確認します。
git clone https://github.com/android/large-screen-codelabs.git
  1. 省略可: git がない場合は、次のボタンをクリックして、この Codelab のすべてのコードをダウンロードできます。

最初のモジュールを開く

  • Android Studio で、/step1 にある最初のモジュールを開きます。

この Codelab に関連するコードが表示されている Android Studio のスクリーンショット

最新バージョンの Gradle を使用するよう求められた場合は、更新してください。

3. 実行して観察する

  1. モジュール step1 のコードを実行します。

ご覧のとおり、これはシンプルなカメラアプリで、前面カメラと背面カメラを切り替え、アスペクト比を調整できます。左側の最初のボタンは何も実行しませんが、これは背面自撮りモードのエントリ ポイントです。

背面自撮りモードのアイコンがハイライト表示されているアプリのスクリーンショット

  1. デバイスを半開きの状態にしてみましょう。ヒンジは完全にフラットな状態でも、閉じている状態でもありません。90 度の角度で開いています。

ご覧のとおり、アプリはデバイスの形状の変化に反応しません。レイアウトは変わらず、ヒンジがビューファインダーの中央に残っています。

4. Jetpack WindowManager について学習する

Jetpack WindowManager は、折りたたみ式デバイス向けに最適化されたエクスペリエンスの開発に役立つライブラリです。この中に、フレキシブル ディスプレイの折りたたみや 2 つの物理ディスプレイ パネル間のヒンジを記述する FoldingFeature クラスがあります。このクラスの API は、次のように、デバイスに関する重要な情報へのアクセスを提供します。

FoldingFeature クラスには occlusionType()isSeparating() などの追加情報も含まれますが、この Codelabo では詳しく説明しません。

バージョン 1.1.0-beta01 から、このライブラリは WindowAreaController を使用しています。この API はリア表示モードを有効にし、背面カメラ用に調整されたディスプレイに現在のウィンドウを移動します。これは、背面カメラでの自撮りなど、多くの用途に利用できます。

依存関係を追加する

  • アプリで Jetpack WindowManager を使用するには、次の依存関係をモジュールレベルの build.gradle ファイルに追加する必要があります。

step1/build.gradle

def work_version = '1.1.0-beta01'
implementation "androidx.window:window:$work_version"
implementation "androidx.window:window-java:$work_version"
implementation "androidx.window:window-core:$work_version"

これで、アプリの FoldingFeatureWindowAreaController の両方のクラスにアクセスできるようになりました。これらのクラスを利用して、折りたたみ式デバイスで究極のカメラ体験を実現しましょう。

5. 背面自撮りモードを実装する

リア表示モードから始めます。このモードを有効にする API は WindowAreaControllerJavaAdapter です。これは Executor を必要とし、現在の状態が保存された WindowAreaSession を返します。この WindowAreaSession は、Activity が破棄されて再作成されるときに保持されている必要があります。構成が変更されても安全に保存されるように、この値は ViewModel に保存します。

  1. これらの変数を MainActivity に宣言します。

step1/MainActivity.kt

private lateinit var windowAreaController: WindowAreaControllerJavaAdapter
private lateinit var displayExecutor: Executor
  1. これらを onCreate() メソッドで初期化します。

step1/MainActivity.kt

windowInfoTracker = WindowInfoTracker.getOrCreate(this)
displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaControllerJavaAdapter(WindowAreaController.getOrCreate())

Activity でコンテンツを小さい画面に移動できるようになりましたが、セッションを保存する必要があります。

  1. セッションを保存するには、CameraViewModel を開いてこの変数を宣言します。

step1/CameraViewModel.kt

var rearDisplaySession: WindowAreaSession? = null
        private set

rearDisplaySession は、作成するたびに変わるため、変数にする必要があります。ただし、この値を必要なときに更新するメソッドを作成したので、この値が外部から変更されないようにします。

  1. 次のコードを CameraViewModel 内に貼り付けます。

step1/CameraViewModel.kt

fun updateSession(newSession: WindowAreaSession? = null) {
        rearDisplaySession = newSession
}

このメソッドは、セッションの更新が必要になるたびに呼び出されるので、単一のアクセス ポイントにカプセル化するとよいでしょう。

Rear Display API はリスナー アプローチと連携します。小さい画面へのコンテンツの移動をリクエストするときに、リスナーの onSessionStarted() メソッドによって戻されるセッションを開始します。内部の(大きい)画面に戻したい場合は、セッションを閉じます。onSessionEnded() メソッドから確認を受け取ります。これらのメソッドは、CameraViewModel 内の rearDisplaySession を更新するために使用します。このようなリスナーを作成するには、WindowAreaSessionCallback インターフェースの実装が必要です。

  1. WindowAreaSessionCallback インターフェースを実装するように、MainActivity 宣言を変更します。

step1/MainActivity.kt

class MainActivity : AppCompatActivity(), WindowAreaSessionCallback

では、MainActivity 内に onSessionStarted メソッドと onSessionEnded メソッドを実装してみましょう。最初のメソッドで WindowAreaSession を保存し、2 番目のメソッドでこれを null にリセットします。これは、WindowAreaSession の存在でセッションを開始するのか、既存のセッションを終了するのかを判断する場合に特に有効です。

step1/MainActivity.kt

override fun onSessionEnded() {
    viewModel.updateSession(null)
}

override fun onSessionStarted(session: WindowAreaSession) {
    viewModel.updateSession(session)
}
  1. MainActivity.kt ファイルに、この API に必要な最後のコード部分を記述します。

step1/MainActivity.kt

private fun startRearDisplayMode() {
   if (viewModel.rearDisplaySession != null) {
      viewModel.rearDisplaySession?.close()
   } else {
      windowAreaController.startRearDisplayModeSession(
         this,
         displayExecutor,
         this
      )
   }
}

前述のように、実行されるアクションを把握するため CameraViewModel 内で rearDisplaySession の存在を確認します。null でない場合は、セッションがすでに存在するため、セッションを終了します。一方、null の場合は、windowAreaController を使用して新しいセッションを開始します。Activity を 2 回渡します。最初は Context として使用され、2 回目は WindowAreaSessionCallback リスナーとして使用されます。

  1. では、アプリをビルドして実行してみましょう。デバイスを広げて背面ディスプレイのボタンをタップすると、次のようなメッセージが表示されます。

リア表示モード開始時のユーザー プロンプトのスクリーンショット

  1. [Switch screens now] をクリックして、コンテンツがアウター ディスプレイに移動したことを確認します。

6. テーブルトップ モードを実装する

次に、アプリを折りたたみ対応にします。折りたたみの方向に応じて、デバイスのヒンジの横または上にコンテンツを移動します。この処理は FoldingStateActor の内部で行います。読みやすくするため、開発するコードを Activity から切り離して示します。

この API のコア部分は WindowInfoTracker インターフェースにあります。これは、Activity を必要とする静的メソッドで作成されています。

step1/CameraCodelabDependencies.kt

@Provides
fun provideWindowInfoTracker(activity: Activity) =
        WindowInfoTracker.getOrCreate(activity)

このコードはすでに存在するため、記述する必要はありませんが、WindowInfoTracker がどのように作成されているのかを理解しておくと役に立ちます。

  1. ウィンドウの変更をリッスンするには、これらの変更を ActivityonResume() メソッドでリッスンします。

step1/MainActivity.kt

lifecycleScope.launch {
    foldingStateActor.checkFoldingState(
         this@MainActivity,
         binding.viewFinder
    )
}
  1. FoldingStateActor ファイルを開いて、checkFoldingState() メソッドを入力します。

すでに見たように、これは ActivityRESUMED フェーズで実行され、レイアウトの変更をリッスンするために WindowInfoTracker を利用します。

step1/FoldingStateActor.kt

windowInfoTracker.windowLayoutInfo(activity)
      .collect { newLayoutInfo ->
         activeWindowLayoutInfo = newLayoutInfo
         updateLayoutByFoldingState(cameraViewfinder)
      }

WindowInfoTracker インターフェースを使用すると、windowLayoutInfo() を呼び出して、DisplayFeature で使用可能なすべての情報を含む WindowLayoutInfoFlow を収集できます。

最後に、この変化に反応してコンテンツを移動するようにします。この処理は updateLayoutByFoldingState() メソッド内で一度に 1 ステップずつ実行します。

  1. activityLayoutInfo コンテナに一部の DisplayFeature プロパティが含まれるようにします。少なくとも FoldingFeature は含め、必要でないものは含めないようにします。

step1/FoldingStateActor.kt

val foldingFeature = activeWindowLayoutInfo?.displayFeatures
            ?.firstOrNull { it is FoldingFeature } as FoldingFeature?
            ?: return
  1. デバイスの位置がレイアウトに影響し、階層の境界外にないように、折りたたみの位置を計算します。

step1/FoldingStateActor.kt

val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
            foldingFeature,
            cameraViewfinder.parent as View
        ) ?: return

レイアウトに影響を及ぼす FoldingFeature があるので、コンテンツを動かしてみましょう。

  1. FoldingFeatureHALF_OPEN であることを確認します。そうでない場合は、コンテンツの位置を戻します。HALF_OPEN の場合は、別のチェックを実行します。ここでは、折りたたみの向きに応じて異なるアクションを実行する必要があります。

step1/FoldingStateActor.kt

if (foldingFeature.state == FoldingFeature.State.HALF_OPENED) {
    when (foldingFeature.orientation) {
        FoldingFeature.Orientation.VERTICAL -> {
            cameraViewfinder.moveToRightOf(foldPosition)
        }
        FoldingFeature.Orientation.HORIZONTAL -> {
            cameraViewfinder.moveToTopOf(foldPosition)
        }
    }
} else {
    cameraViewfinder.restore()
}

折りたたみが VERTICAL の場合、コンテンツは右側に移動します。それ以外の場合は折りたたみ位置の上部に移動します。

  1. アプリをビルドして実行します。デバイスを広げてテーブルトップ モードにし、コンテンツが正しく移動していることを確認します。

7. 完了

この Codelab では、折りたたみ式デバイスで特別な点、形状の変化、Rear Display API について学習しました。

参考資料

リファレンス