Jetpack Compose for XR を使用すると、行や列など、使い慣れた Compose のコンセプトを使用して、空間 UI とレイアウトを宣言的に構築できます。これにより、既存の Android UI を 3D 空間に拡張したり、没入型の 3D アプリケーションを新たに構築したりできます。
既存の Android View ベースのアプリを空間化する場合は、いくつかの開発オプションがあります。相互運用性 API を使用したり、Compose と View を組み合わせて使用したり、SceneCore ライブラリを直接操作したりできます。詳しくは、View の操作に関するガイドをご覧ください 。
サブスペースと空間化されたコンポーネントについて
Android XR 用のアプリを作成する場合は、 サブスペースと 空間化されたコンポーネントのコンセプトを理解することが重要です。
サブスペースについて
Android XR 向けに開発する場合は、アプリまたはレイアウトにサブスペースを追加する必要があります。サブスペースは、アプリ内の 3D 空間を区切ったパーティションです。これにより、3D コンテンツの配置、3D レイアウトの構築、2D コンテンツへの奥行き追加ができるようになります。サブスペースは、空間化が有効になっている場合にのみレンダリングされます。ホームスペースまたは非 XR デバイスでは、そのサブスペース内のコードは無視されます。
サブスペースを作成する方法はいくつかあります。
Subspace: このコンポーザブルは、新しい独立した空間 UI 階層を作成します。ネストされている親Subspaceの空間位置、向き、スケールは継承されません。Subspaceは、システムの推奨コンテンツ ボックスによって自動的にバインドされます。PlanarEmbeddedSubspace: このコンポーザブルは、アプリの UI 階層内に配置できるため、2D UI と空間 UI のレイアウトを維持できます。PlanarEmbeddedSubspaceは、親の制約と配置を尊重します。その中に配置された 3D コンテンツは、この 2D で定義された領域を基準に配置されます。
詳しくは、アプリにサブスペースを追加するをご覧ください。
空間化されたコンポーネントについて
サブスペース コンポーザブル: これらのコンポーネントは、サブスペースでのみレンダリングできます。
2D レイアウト内に配置する前に、Subspace で囲む必要があります。
SubspaceModifier を使用すると、奥行き、オフセット、
配置などの属性をサブスペース コンポーザブルに追加できます。
他の空間化されたコンポーネントは、サブスペース内で呼び出す必要はありません。空間コンテナでラップされた従来の 2D 要素で構成されます。これらの要素は、2D レイアウトと 3D レイアウトの両方で定義されている場合は、どちらのレイアウトでも使用できます。空間化が有効になっていない場合、空間化された機能は無視され、2D の対応する機能にフォールバックされます。
空間パネルを作成する
SpatialPanel は、アプリ
のコンテンツを表示できるサブスペース コンポーザブルです。たとえば、動画再生、静止画、その他の
コンテンツを空間パネルに表示できます。

次の例に示すように、SubspaceModifier を使用して、空間パネルのサイズ、動作、配置を変更できます。
Subspace { SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp), dragPolicy = MovePolicy(), resizePolicy = ResizePolicy(), ) { 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 ) } }
コードに関する主なポイント
SpatialPanelAPI はサブスペース コンポーザブルであるため、 をSubspace内で呼び出す必要があります。サブスペースの外部で呼び出すと、例外がスローされます。SpatialPanelのサイズは、SubspaceModifierのheight仕様とwidth仕様を使用して設定されています。これらの仕様を省略すると、パネルのサイズはコンテンツの測定値によって決まります。movableサブスペース修飾子を追加して、ユーザーがパネルを移動できるようにします。resizableサブスペース 修飾子を追加して、ユーザーがパネルのサイズを変更できるようにします。- サイズ設定と 配置について詳しくは、空間パネルのデザイン ガイダンスをご覧ください。コード実装の詳細については、リファレンス ドキュメントをご覧ください。
movable 修飾子の仕組み
ユーザーがパネルを遠ざけると、デフォルトでは、movable 修飾子
は、ホームスペースでシステムによってパネルのサイズが変更されるのと同様に、パネルのサイズを変更します。すべての子コンテンツはこの動作を継承します。これを無効にするには、shouldScaleWithDistance パラメータを false に設定します。
オービターを作成する
オービターは空間 UI コンポーネントです。SpatialColumn、SpatialRow、SpatialBox などの対応する空間パネルまたは空間レイアウト コンポーネントにアタッチするように設計されています。通常、オービターには、固定されているエンティティに関連するナビゲーション項目とコンテキスト アクション項目が含まれます。たとえば、動画コンテンツを表示する空間パネルを作成した場合は、オービター内に動画再生コントロールを追加できます。

次の例に示すように、SpatialPanel の 2D レイアウト内でオービターを呼び出して、ナビゲーションなどのユーザー コントロールをラップします。これにより、2D レイアウトから抽出され、構成に応じて空間パネルにアタッチされます。
Subspace { SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp), dragPolicy = MovePolicy(), resizePolicy = ResizePolicy(), ) { SpatialPanelContent() OrbiterExample() } }
@Composable fun OrbiterExample() { Orbiter( position = ContentEdge.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`、および`SpatialSpacer` を使用して、複数の空間パネルを作成し、空間レイアウト内に配置できます。

次のコード例は、これを行う方法を示しています。
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、およびSpatialSpacerはすべてサブスペース コンポーザブルであり、 サブスペース内に配置する必要があります。SubspaceModifierを使用してレイアウトをカスタマイズします。- 1 行に複数のパネルがあるレイアウトの場合は、
SubspaceModifierを使用して 825dp の曲線半径を設定することをおすすめします。これにより、パネルがユーザーを囲むようになります。詳しくは、デザイン ガイダンスをご覧ください。
SpatialGltfModel を使用して 3D オブジェクトをレイアウトに追加する
Android XR は、3D モデルの glTF 形式をサポートしています。通常、
.glb ファイルとして保存されます。これらのオブジェクトをレイアウトに追加するには、
SpatialGltfModel コンポーザブルを使用する必要があります。この API を使用すると、アセットの読み込みと状態の管理が簡単になります。
モデルを表示するには、まず
rememberSpatialGltfModelStateを使用してソースと状態を定義します。モデルは、アプリの assets フォルダ、URI、または
raw dataから読み込むことができます。
val modelState = rememberSpatialGltfModelState( source = SpatialGltfModelSource.fromPath( Paths.get("models/model_name.glb") ) )
状態が定義されたら、SpatialGltfModel コンポーザブルを使用して、サブスペース内にレンダリングします。
SpatialGltfModel(state = modelState, modifier = SubspaceModifier)
コードに関する主なポイント
- 非同期読み込み: モデルは非同期で読み込まれます。最初のコンポジションでは、固有サイズがゼロになることがあります。モデルの準備が完了すると、レイアウトが再測定されます。
- 状態の制御:
SpatialGltfModelState.statusを使用して、読み込み ステータスを照会したり、アニメーションを制御したりします。 - サイズ設定とスケーリング: デフォルトでは、レイアウト サイズはアセットの境界ボックスと一致します。これを
SubspaceModifier.sizeでオーバーライドして、指定した境界内に収まるようにモデルを均一にスケーリングできます。
SceneCoreEntity を使用してレイアウトにエンティティを配置する
SceneCoreEntity コンポーザブルは、Jetpack
SceneCore ライブラリと Compose for XR ライブラリをブリッジします。これにより、SceneCore で構築された
エンティティを Compose レイアウトで使用できます。これにより、Compose でエンティティのサイズ設定、配置、親の変更、子の追加、修飾子の適用を行うことができます。
Subspace { SceneCoreEntity( modifier = SubspaceModifier.offset(x = 50.dp), factory = { SurfaceEntity.create( session = session, pose = Pose.Identity, stereoMode = SurfaceEntity.StereoMode.MONO ) }, update = { entity -> // compose state changes may be applied to the // SceneCore entity here. entity.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE }, sizeAdapter = SceneCoreEntitySizeAdapter({ IntSize2d(it.width, it.height) }), ) { // Content here will be children of the SceneCoreEntity // in the scene graph. } }
コードに関する主なポイント
- ファクトリ ブロック: ファクトリ ブロックは、基盤となる
SceneCoreエンティティを初期化する場所です。 - 更新ブロック: 更新ブロックを使用して、Compose の状態の変化に応じてエンティティのプロパティを変更します。
- サイズ適応:
sizeAdapterは、エンティティの寸法を Compose レイアウト システムに返します。
その他の情報
SceneCoreEntity内で 3D コンテンツを読み込む方法について詳しくは、アプリに 3D モデルを追加するをご覧ください。
画像または動画コンテンツのサーフェスを追加する
SpatialExternalSurface は、アプリが画像や 動画 などのコンテンツを描画できる Surface を作成して
管理するサブスペース コンポーザブルです。SpatialExternalSurface は、ステレオスコピック コンテンツとモノスコピック コンテンツのどちらもサポートしています。
この例では、
Media3 Exoplayer と SpatialExternalSurface を使用して、サイドバイサイドのステレオスコピック動画を読み込む方法を示します。
@OptIn(ExperimentalComposeApi::class) @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() } } } }
コードに関する主なポイント
-
レンダリングするコンテンツのタイプに応じて、
Mono: 画像または動画フレームは、両目に表示される単一の同一の画像で構成されます。SideBySide: 画像または動画フレームには、左右に配置された画像または動画フレームのペアが含まれます。左側の画像またはフレームは左目のビューを表し、右側の画像またはフレームは右目のビューを表します。TopBottom: 画像または動画フレームには、上下に積み重ねられた画像または動画フレームのペアが含まれます。上部の画像またはフレームは左目のビューを表し、下部の画像またはフレームは右目のビューを表します。
StereoModeをMono、SideBySide、TopBottomのいずれかに設定します。SpatialExternalSurfaceは、長方形のサーフェスのみをサポートしています。- この
Surfaceは入力イベントをキャプチャしません。 StereoModeの変更をアプリケーション レンダリングや動画デコードと同期することはできません。- このコンポーザブルは他のパネルの前面にレンダリングできないため、レイアウトに他のパネルがある場合は
MovePolicyを使用しないでください。
DRM で保護された動画コンテンツのサーフェスを追加する
SpatialExternalSurface は、DRM で保護された動画ストリームの再生もサポートしています。これを有効にするには、保護されたグラフィック バッファにレンダリングする安全なサーフェスを作成する必要があります。これにより、コンテンツが画面録画されたり、安全でないシステム コンポーネントからアクセスされたりするのを防ぐことができます。
安全なサーフェスを作成するには、SpatialExternalSurface コンポーザブルで SpatialExternalSurfaceProtection パラメータ
を SpatialExternalSurfaceProtection.Protected に設定します。また、ライセンス サーバーからライセンスを取得できるように、適切な DRM 情報を使用して
Media3 Exoplayerを構成する必要があります。
次の例は、DRM で保護された動画ストリームを再生するように SpatialExternalSurface と ExoPlayer を構成する方法を示しています。
@OptIn(ExperimentalComposeApi::class) @Composable fun DrmSpatialVideoPlayer() { val context = LocalContext.current Subspace { SpatialExternalSurface( modifier = SubspaceModifier .width(1200.dp) .height(676.dp), stereoMode = StereoMode.SideBySide, surfaceProtection = SurfaceProtection.Protected ) { val exoPlayer = remember { ExoPlayer.Builder(context).build() } // Define the URI for your DRM-protected content and license server. val videoUri = "https://your-content-provider.com/video.mpd" val drmLicenseUrl = "https://your-license-server.com/license" // Build a MediaItem with the necessary DRM configuration. val mediaItem = MediaItem.Builder() .setUri(videoUri) .setDrmConfiguration( MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID) .setLicenseUri(drmLicenseUrl) .build() ) .build() onSurfaceCreated { surface -> // The created surface is secure and can be used by the player. exoPlayer.setVideoSurface(surface) exoPlayer.setMediaItem(mediaItem) exoPlayer.prepare() exoPlayer.play() } onSurfaceDestroyed { exoPlayer.release() } } } }
コードに関する主なポイント
- 保護されたサーフェス: 基盤となる
Surfaceが DRM コンテンツに適した安全なバッファによってバックアップされるように、SpatialExternalSurfaceでsurfaceProtection = SpatialExternalSurfaceProtection.Protectedを設定することが重要です。 - DRM 構成: DRM スキーム(
C.WIDEVINE_UUIDなど)とライセンス サーバーの URI を使用してMediaItemを構成する必要があります。ExoPlayer はこの情報を使用して DRM セッションを管理します。 - 安全なコンテンツ: 保護されたサーフェスにレンダリングする場合、動画コンテンツはデコードされ、安全なパスに表示されます。これにより、コンテンツ ライセンス要件を満たすことができます。また、コンテンツがスクリーン キャプチャに表示されるのを防ぐこともできます。
その他の空間 UI コンポーネントを追加する
空間 UI コンポーネントは、アプリケーションの UI 階層内の任意の場所に配置できます。 これらの要素は 2D UI で再利用でき、空間機能が有効になっている場合にのみ空間属性が表示されます。これにより、コードを 2 回記述することなく、メニューやダイアログなどのコンポーネントに高度を追加できます。これらの要素の使用方法を理解するには、空間 UI の次の例をご覧ください。
UI コンポーネント |
空間化が有効になっている場合 |
2D 環境 |
|---|---|---|
|
パネルは z 深度で少し後ろに移動し、高度の高いダイアログを表示します |
2D の |
|
パネルは z 深度で少し後ろに移動し、高度の高いポップアップを表示します |
2D の |
|
|
空間的な高度なしで表示されます。 |
SpatialDialog
これは、短い遅延の後に開くダイアログの例です。
SpatialDialog を使用すると、ダイアログは
空間パネルと同じ z 深度で表示され、空間化が
有効になっている場合はパネルが 125dp 後ろに移動します。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 でサポートされていないカスタムパネルを作成するには、 PanelEntity インスタンスとシーングラフを SceneCore API を使用して直接操作します。
オービターを空間パネルとレイアウトに固定する
オービターを、Compose で宣言された SpatialPanels と空間レイアウト コンポーネントに固定できます。これには、SpatialRow、SpatialColumn、SpatialBox などの UI 要素の空間レイアウトでオービターを宣言します。オービターは、宣言した場所に最も近い親に固定されます。
オービターの動作は、宣言する場所によって決まります。
- (
前のコード スニペットに示すように)
SpatialPanelでラップされた 2D レイアウトでは、オービターはそのSpatialPanelに固定されます。 Subspaceでは、オービターは最も近い親エンティティ(オービターが宣言されている空間レイアウト)に固定されます。
次の例は、オービターを空間行に固定する方法を示しています。
Subspace { SpatialRow { Orbiter( position = ContentEdge.Top, offset = 8.dp, offsetType = OrbiterOffsetType.InnerEdge, shape = SpatialRoundedCornerShape(size = CornerSize(50)) ) { Text( "Hello World!", style = MaterialTheme.typography.titleMedium, 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などの空間レイアウトには、コンテンツのないエンティティが関連付けられています。したがって、空間レイアウトで宣言されたオービターはそのレイアウトに固定されます。