Jetpack Compose for XR を使用すると、行や列などの使い慣れた Compose のコンセプトを使用して、空間 UI とレイアウトを宣言的に構築できます。これにより、既存の Android UI を 3D 空間に拡張したり、まったく新しい没入型 3D アプリを構築したりできます。
既存の Android ビューベースのアプリを空間化する場合は、いくつかの開発オプションがあります。相互運用 API を使用するか、Compose とビューを併用するか、SceneCore ライブラリを直接使用できます。詳しくは、ビューの操作に関するガイドをご覧ください。
サブスペースと空間化されたコンポーネントについて
Android XR 向けのアプリを作成する場合は、サブスペースと空間化されたコンポーネントの概念を理解することが重要です。
サブスペースについて
Android XR 向けに開発する場合は、アプリまたはレイアウトにサブスペースを追加する必要があります。サブスペースは、アプリ内の 3D 空間を区切ったパーティションです。これにより、3D コンテンツの配置、3D レイアウトの構築、2D コンテンツへの奥行き追加ができるようになります。サブスペースは、空間化が有効になっている場合にのみレンダリングされます。ホームスペースまたは XR 以外のデバイスでは、そのサブスペース内のコードは無視されます。
サブスペースを作成する方法は 2 つあります。
setSubspaceContent()
: この関数は、アプリレベルのサブスペースを作成します。これは、setContent()
を使用する場合と同じ方法でメイン アクティビティで呼び出すことができます。アプリレベルのサブスペースは、高さ、幅、奥行きに制限がなく、基本的に空間コンテンツ用の無限のキャンバスを提供します。Subspace
: このコンポーザブルはアプリの UI 階層内の任意の場所に配置できるため、ファイル間のコンテキストを失うことなく、2D と空間 UI のレイアウトを維持できます。これにより、UI ツリー全体で状態をホイスティングしたり、アプリを再設計したりすることなく、既存のアプリ アーキテクチャを XR と他のフォーム ファクタ間で簡単に共有できます。
詳細については、アプリにサブスペースを追加するをご覧ください。
空間化されたコンポーネントについて
サブスペース コンポーザブル: これらのコンポーネントはサブスペースでのみレンダリングできます。2D レイアウト内に配置する前に、Subspace
または setSubspaceContent
で囲む必要があります。SubspaceModifier
を使用すると、深度、オフセット、配置などの属性をサブスペース コンポーザブルに追加できます。
他の空間化されたコンポーネントは、サブスペース内で呼び出す必要はありません。空間コンテナ内にラップされた従来の 2D 要素で構成されています。これらの要素は、両方に定義されている場合は、2D または 3D レイアウト内で使用できます。空間化が有効になっていない場合、空間化された特徴は無視され、2D の対応する特徴にフォールバックします。
空間パネルを作成する
SpatialPanel
は、アプリ コンテンツを表示できるサブスペース コンポーザブルです。たとえば、動画の再生、静止画像、その他のコンテンツを空間パネルに表示できます。
次の例に示すように、SubspaceModifier
を使用して空間パネルのサイズ、動作、配置を変更できます。
Subspace { SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp) .movable() .resizable() ) { SpatialPanelContent() } }
@Composable fun SpatialPanelContent() { Box( Modifier .background(color = Color.Black) .height(500.dp) .width(500.dp), contentAlignment = Alignment.Center ) { Text( text = "Spatial Panel", color = Color.White, fontSize = 25.sp ) } }
コードに関する主なポイント
SpatialPanel
API はサブスペース コンポーザブルであるため、Subspace
またはsetSubspaceContent
内で呼び出す必要があります。サブスペースの外部から呼び出すと、例外がスローされます。movable
修飾子またはresizable
修飾子を追加して、ユーザーがパネルのサイズ変更や移動を行えるようにします。- サイズと配置について詳しくは、空間パネルの設計に関するガイダンスをご覧ください。コード実装の詳細については、リファレンス ドキュメントをご覧ください。
移動可能なサブスペース モディファイアの仕組み
ユーザーがパネルを自分から離すにつれて、デフォルトでは、移動可能なサブスペース修飾子によって、ホームスペースでシステムによってパネルのサイズが変更される方法と同様に、パネルがスケーリングされます。すべての子コンテンツはこの動作を継承します。これを無効にするには、scaleWithDistance
パラメータを false
に設定します。
オビッターを作成する
オービターは空間 UI コンポーネントです。対応する空間パネル、レイアウト、その他のエンティティに接続するように設計されています。通常、オービターには、アンカーされているエンティティに関連するナビゲーションとコンテキスト アクション アイテムが含まれています。たとえば、動画コンテンツを表示する空間パネルを作成した場合は、オービター内に動画再生コントロールを追加できます。
次の例に示すように、SpatialPanel
の 2D レイアウト内でオービターを呼び出して、ナビゲーションなどのユーザー コントロールをラップします。これにより、2D レイアウトから抽出され、構成に応じて空間パネルに接続されます。
Subspace { SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp) .movable() .resizable() ) { SpatialPanelContent() OrbiterExample() } }
@Composable fun OrbiterExample() { Orbiter( position = OrbiterEdge.Bottom, offset = 96.dp, alignment = Alignment.CenterHorizontally ) { Surface(Modifier.clip(CircleShape)) { Row( Modifier .background(color = Color.Black) .height(100.dp) .width(600.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Text( text = "Orbiter", color = Color.White, fontSize = 50.sp ) } } } }
コードに関する主なポイント
- オービターは空間 UI コンポーネントであるため、コードは 2D または 3D レイアウトで再利用できます。2D レイアウトでは、アプリはオービター内のコンテンツのみをレンダリングし、オービター自体は無視されます。
- オビッターを使用する方法と設計方法について詳しくは、設計に関するガイダンスをご覧ください。
空間レイアウトに複数の空間パネルを追加する
複数の空間パネルを作成して空間レイアウト内に配置するには、SpatialRow
、SpatialColumn
、SpatialBox
、SpatialLayoutSpacer
を使用します。
次のコードサンプルは、これを行う方法を示しています。
Subspace { SpatialRow { SpatialColumn { SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { SpatialPanelContent("Top Left") } SpatialPanel(SubspaceModifier.height(200.dp).width(400.dp)) { SpatialPanelContent("Middle Left") } SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { SpatialPanelContent("Bottom Left") } } SpatialColumn { SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { SpatialPanelContent("Top Right") } SpatialPanel(SubspaceModifier.height(200.dp).width(400.dp)) { SpatialPanelContent("Middle Right") } SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { SpatialPanelContent("Bottom Right") } } } }
@Composable fun SpatialPanelContent(text: String) { Column( Modifier .background(color = Color.Black) .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "Panel", color = Color.White, fontSize = 15.sp ) Text( text = text, color = Color.White, fontSize = 25.sp, fontWeight = FontWeight.Bold ) } }
コードに関する主なポイント
SpatialRow
、SpatialColumn
、SpatialBox
、SpatialLayoutSpacer
はすべてサブスペース コンポーザブルであり、サブスペース内に配置する必要があります。SubspaceModifier
を使用してレイアウトをカスタマイズします。- 複数のパネルが 1 行に並ぶレイアウトの場合は、
SubspaceModifier
を使用してカーブの半径を 825dp に設定し、パネルがユーザーを囲むようにすることをおすすめします。詳しくは、設計に関するガイダンスをご覧ください。
ボリュームを使用して 3D オブジェクトをレイアウトに配置する
3D オブジェクトをレイアウトに配置するには、ボリュームと呼ばれるサブスペース コンポーザブルを使用する必要があります。方法の例を次に示します。
Subspace { SpatialPanel( SubspaceModifier.height(1500.dp).width(1500.dp) .resizable().movable() ) { ObjectInAVolume(true) Box( Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text( text = "Welcome", fontSize = 50.sp, ) } } }
@Composable fun ObjectInAVolume(show3DObject: Boolean) {
その他の情報
- ボリューム内の 3D コンテンツを読み込む方法について詳しくは、アプリに 3D モデルを追加するをご覧ください。
画像または動画コンテンツのサーフェスを追加する
SpatialExternalSurface
はサブスペース コンポーザブルであり、アプリが画像や動画などのコンテンツを描画できる Surface
を作成して管理します。SpatialExternalSurface
は、立体視コンテンツまたはモノスコピック コンテンツのいずれかをサポートします。
この例では、Media3 Exoplayer と SpatialExternalSurface
を使用してサイドバイサイドの立体視動画を読み込む方法を示します。
@Composable fun SpatialExternalSurfaceContent() { val context = LocalContext.current Subspace { SpatialExternalSurface( modifier = SubspaceModifier .width(1200.dp) // Default width is 400.dp if no width modifier is specified .height(676.dp), // Default height is 400.dp if no height modifier is specified // Use StereoMode.Mono, StereoMode.SideBySide, or StereoMode.TopBottom, depending // upon which type of content you are rendering: monoscopic content, side-by-side stereo // content, or top-bottom stereo content stereoMode = StereoMode.SideBySide, ) { val exoPlayer = remember { ExoPlayer.Builder(context).build() } val videoUri = Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) // Represents a side-by-side stereo video, where each frame contains a pair of // video frames arranged side-by-side. The frame on the left represents the left // eye view, and the frame on the right represents the right eye view. .path("sbs_video.mp4") .build() val mediaItem = MediaItem.fromUri(videoUri) // onSurfaceCreated is invoked only one time, when the Surface is created onSurfaceCreated { surface -> exoPlayer.setVideoSurface(surface) exoPlayer.setMediaItem(mediaItem) exoPlayer.prepare() exoPlayer.play() } // onSurfaceDestroyed is invoked when the SpatialExternalSurface composable and its // associated Surface are destroyed onSurfaceDestroyed { exoPlayer.release() } } } }
コードに関する主なポイント
- レンダリングするコンテンツの種類に応じて、
StereoMode
をMono
、SideBySide
、またはTopBottom
に設定します。Mono
: 画像または動画フレームは、両眼に表示される 1 つの同一の画像で構成されています。SideBySide
: 画像または動画フレームには、並べて配置された画像または動画フレームのペアが含まれています。左側の画像またはフレームは左目ビューを表し、右側の画像またはフレームは右目ビューを表します。TopBottom
: 画像または動画フレームには、垂直方向に積み重ねられた画像または動画フレームのペアが含まれています。上部の画像またはフレームは左目のビューを表し、下部の画像またはフレームは右目のビューを表します。
SpatialExternalSurface
は長方形のサーフェスのみをサポートします。- この
Surface
は入力イベントをキャプチャしません。 StereoMode
の変更をアプリケーションのレンダリングや動画デコードと同期することはできません。- このコンポーザブルは他のパネルの前にレンダリングできないため、レイアウト内に他のパネルがある場合は、移動可能な修飾子を使用しないでください。
他の空間 UI コンポーネントを追加する
空間 UI コンポーネントは、アプリの UI 階層内の任意の場所に配置できます。これらの要素は 2D UI で再利用できます。空間属性は、空間機能が有効になっている場合にのみ表示されます。これにより、コードを 2 回記述しなくても、メニュー、ダイアログ、その他のコンポーネントにエレベーションを追加できます。これらの要素の使用方法を詳しく理解するには、空間 UI の次の例をご覧ください。
UI コンポーネント |
空間化が有効になっている場合 |
2D 環境の場合 |
---|---|---|
|
パネルが Z ディープネスで少し後退し、エレベートされたダイアログが表示される |
2D |
|
パネルが Z ディープネスで少し後退し、エレベートされたポップアップが表示される |
2D の |
|
|
空間的な高度のない番組。 |
SpatialDialog
以下は、少し遅れて開くダイアログの例です。SpatialDialog
を使用すると、ダイアログは空間パネルと同じ Z 深度に表示され、空間化が有効になっている場合はパネルが 125 dp 後方に移動します。SpatialDialog
は、空間化が有効になっていない場合にも使用できます。この場合、SpatialDialog
は 2D の対応する Dialog
にフォールバックします。
@Composable fun DelayedDialog() { var showDialog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(3000) showDialog = true } if (showDialog) { SpatialDialog( onDismissRequest = { showDialog = false }, SpatialDialogProperties( dismissOnBackPress = true ) ) { Box( Modifier .height(150.dp) .width(150.dp) ) { Button(onClick = { showDialog = false }) { Text("OK") } } } } }
コードに関する主なポイント
- これは
SpatialDialog
の例です。SpatialPopup
とSpatialElevation
の使用方法はよく似ています。詳しくは、API リファレンスをご覧ください。
カスタム パネルとレイアウトを作成する
Compose for XR でサポートされていないカスタム パネルを作成するには、SceneCore
API を使用して PanelEntities
とシーングラフを直接操作します。
オビッターが空間レイアウトやその他のエンティティに固定される
オビターは、Compose で宣言された任意のエンティティに固定できます。これには、SpatialRow
、SpatialColumn
、SpatialBox
などの UI 要素の空間レイアウトでオービターを宣言することが含まれます。オビターは、宣言した場所に最も近い親エンティティにアンカーされます。
オビッターは、宣言する場所によって動作が決まります。
SpatialPanel
でラップされた 2D レイアウト(上のコードスニペットに示すように)では、オービターはそのSpatialPanel
にアンカーされます。Subspace
では、オービターは最も近い親エンティティ(オービターが宣言されている空間レイアウト)にアンカーされます。
次の例は、オービターを空間行に固定する方法を示しています。
Subspace { SpatialRow { Orbiter( position = OrbiterEdge.Top, offset = EdgeOffset.inner(8.dp), shape = SpatialRoundedCornerShape(size = CornerSize(50)) ) { Text( "Hello World!", style = MaterialTheme.typography.h2, modifier = Modifier .background(Color.White) .padding(16.dp) ) } SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp) ) { Box( modifier = Modifier .background(Color.Red) ) } SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp) ) { Box( modifier = Modifier .background(Color.Blue) ) } } }
コードに関する主なポイント
- 2D レイアウトの外側でオービターを宣言すると、オービターは最も近い親エンティティにアンカーされます。この場合、オービターは宣言されている
SpatialRow
の上部にアンカーされます。 SpatialRow
、SpatialColumn
、SpatialBox
などの空間レイアウトにはすべて、コンテンツのないエンティティが関連付けられています。したがって、空間レイアウトで宣言されたオービターは、そのレイアウトにアンカーされます。