1. 始める前に
折りたたみ式デバイスの特別な点
折りたたみ式デバイスは一世代に一度のイノベーションで、ユニークな体験を提供します。ハンズフリー操作のテーブルトップ UI など、差別化された機能でユーザーに喜ばれる貴重な機会をもたらします。
前提条件
- Android アプリ開発に関する基本的な知識
- Hilt 依存関係インジェクション フレームワークに関する基本的な知識
作成するアプリの概要
この Codelab では、折りたたみ式デバイス向けに最適化されたレイアウトを備えるカメラアプリを作成します。
まず、基本的なカメラアプリから始めましょう。このアプリはデバイス形状の変化に対応していません。また、背面カメラで高度な自撮りを行うこともできません。このソースコードを更新して、デバイスを広げたときにプレビューを小さい画面に移動し、スマートフォンをテーブルトップ モードに設定するようにします。
この Codelab で扱う API の説明にはカメラアプリが最も適した使用事例ですが、ここで学習する機能はどのアプリにも応用できるものです。
学習内容
- Jetpack Window Manager を使用して、形状の変化に対応できるようにする方法
- 折りたたみ式デバイスの小さな画面にアプリを移動する方法
必要なもの
- 最新バージョンの Android Studio
- 折りたたみ式デバイスまたは折りたたみ式エミュレータ
2. 設定する
開始用コードを取得する
- Git がインストールされている場合は、以下のコマンドをそのまま実行できます。Git がインストールされているかどうかを確認するには、ターミナルまたはコマンドラインで「
git --version
」と入力し、正しく実行されることを確認します。
git clone https://github.com/android/large-screen-codelabs.git
最初のモジュールを開く
- Android Studio で、
/step1
にある最初のモジュールを開きます。
最新バージョンの Gradle を使用するよう求められた場合は、更新してください。
3. 実行して観察する
- モジュール
step1
のコードを実行します。
ご覧のとおり、これはシンプルなカメラアプリで、前面カメラと背面カメラを切り替え、アスペクト比を調整できます。左側の最初のボタンは何も実行しませんが、これは背面自撮りモードのエントリ ポイントです。
- デバイスを半開きの状態にしてみましょう。ヒンジは完全にフラットな状態でも、閉じている状態でもありません。90 度の角度で開いています。
ご覧のとおり、アプリはデバイスの形状の変化に反応しません。レイアウトは変わらず、ヒンジがビューファインダーの中央に残っています。
4. Jetpack WindowManager について学習する
Jetpack WindowManager は、折りたたみ式デバイス向けに最適化されたエクスペリエンスの開発に役立つライブラリです。この中に、フレキシブル ディスプレイの折りたたみや 2 つの物理ディスプレイ パネル間のヒンジを記述する FoldingFeature
クラスがあります。このクラスの API は、次のように、デバイスに関する重要な情報へのアクセスを提供します。
- ヒンジが 180 度開いている場合、
state()
はFLAT
を返します。それ以外の場合はHALF_OPENED
を返します。 FoldingFeature
の幅が高さより大きい場合、orientation()
はFoldingFeature.Orientation.HORIZONTAL
を返します。それ以外の場合はFoldingFeature.Orientation.VERTICAL
を返します。bounds()
は、FoldingFeature
の境界をRect
形式で提供します。
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"
これで、アプリの FoldingFeature
と WindowAreaController
の両方のクラスにアクセスできるようになりました。これらのクラスを利用して、折りたたみ式デバイスで究極のカメラ体験を実現しましょう。
5. 背面自撮りモードを実装する
リア表示モードから始めます。このモードを有効にする API は WindowAreaControllerJavaAdapter
です。これは Executor
を必要とし、現在の状態が保存された WindowAreaSession
を返します。この WindowAreaSession
は、Activity
が破棄されて再作成されるときに保持されている必要があります。構成が変更されても安全に保存されるように、この値は ViewModel
に保存します。
- これらの変数を
MainActivity
に宣言します。
step1/MainActivity.kt
private lateinit var windowAreaController: WindowAreaControllerJavaAdapter
private lateinit var displayExecutor: Executor
- これらを
onCreate()
メソッドで初期化します。
step1/MainActivity.kt
windowInfoTracker = WindowInfoTracker.getOrCreate(this)
displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaControllerJavaAdapter(WindowAreaController.getOrCreate())
Activity
でコンテンツを小さい画面に移動できるようになりましたが、セッションを保存する必要があります。
- セッションを保存するには、
CameraViewModel
を開いてこの変数を宣言します。
step1/CameraViewModel.kt
var rearDisplaySession: WindowAreaSession? = null
private set
rearDisplaySession
は、作成するたびに変わるため、変数にする必要があります。ただし、この値を必要なときに更新するメソッドを作成したので、この値が外部から変更されないようにします。
- 次のコードを
CameraViewModel
内に貼り付けます。
step1/CameraViewModel.kt
fun updateSession(newSession: WindowAreaSession? = null) {
rearDisplaySession = newSession
}
このメソッドは、セッションの更新が必要になるたびに呼び出されるので、単一のアクセス ポイントにカプセル化するとよいでしょう。
Rear Display API はリスナー アプローチと連携します。小さい画面へのコンテンツの移動をリクエストするときに、リスナーの onSessionStarted()
メソッドによって戻されるセッションを開始します。内部の(大きい)画面に戻したい場合は、セッションを閉じます。onSessionEnded()
メソッドから確認を受け取ります。これらのメソッドは、CameraViewModel
内の rearDisplaySession
を更新するために使用します。このようなリスナーを作成するには、WindowAreaSessionCallback
インターフェースの実装が必要です。
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)
}
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
リスナーとして使用されます。
- では、アプリをビルドして実行してみましょう。デバイスを広げて背面ディスプレイのボタンをタップすると、次のようなメッセージが表示されます。
- [Switch screens now] をクリックして、コンテンツがアウター ディスプレイに移動したことを確認します。
6. テーブルトップ モードを実装する
次に、アプリを折りたたみ対応にします。折りたたみの方向に応じて、デバイスのヒンジの横または上にコンテンツを移動します。この処理は FoldingStateActor
の内部で行います。読みやすくするため、開発するコードを Activity
から切り離して示します。
この API のコア部分は WindowInfoTracker
インターフェースにあります。これは、Activity
を必要とする静的メソッドで作成されています。
step1/CameraCodelabDependencies.kt
@Provides
fun provideWindowInfoTracker(activity: Activity) =
WindowInfoTracker.getOrCreate(activity)
このコードはすでに存在するため、記述する必要はありませんが、WindowInfoTracker
がどのように作成されているのかを理解しておくと役に立ちます。
- ウィンドウの変更をリッスンするには、これらの変更を
Activity
のonResume()
メソッドでリッスンします。
step1/MainActivity.kt
lifecycleScope.launch {
foldingStateActor.checkFoldingState(
this@MainActivity,
binding.viewFinder
)
}
FoldingStateActor
ファイルを開いて、checkFoldingState()
メソッドを入力します。
すでに見たように、これは Activity
の RESUMED
フェーズで実行され、レイアウトの変更をリッスンするために WindowInfoTracker
を利用します。
step1/FoldingStateActor.kt
windowInfoTracker.windowLayoutInfo(activity)
.collect { newLayoutInfo ->
activeWindowLayoutInfo = newLayoutInfo
updateLayoutByFoldingState(cameraViewfinder)
}
WindowInfoTracker
インターフェースを使用すると、windowLayoutInfo()
を呼び出して、DisplayFeature
で使用可能なすべての情報を含む WindowLayoutInfo
の Flow
を収集できます。
最後に、この変化に反応してコンテンツを移動するようにします。この処理は updateLayoutByFoldingState()
メソッド内で一度に 1 ステップずつ実行します。
activityLayoutInfo
コンテナに一部のDisplayFeature
プロパティが含まれるようにします。少なくともFoldingFeature
は含め、必要でないものは含めないようにします。
step1/FoldingStateActor.kt
val foldingFeature = activeWindowLayoutInfo?.displayFeatures
?.firstOrNull { it is FoldingFeature } as FoldingFeature?
?: return
- デバイスの位置がレイアウトに影響し、階層の境界外にないように、折りたたみの位置を計算します。
step1/FoldingStateActor.kt
val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
foldingFeature,
cameraViewfinder.parent as View
) ?: return
レイアウトに影響を及ぼす FoldingFeature
があるので、コンテンツを動かしてみましょう。
FoldingFeature
がHALF_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
の場合、コンテンツは右側に移動します。それ以外の場合は折りたたみ位置の上部に移動します。
- アプリをビルドして実行します。デバイスを広げてテーブルトップ モードにし、コンテンツが正しく移動していることを確認します。
7. 完了
この Codelab では、折りたたみ式デバイスで特別な点、形状の変化、Rear Display API について学習しました。