Jetpack Compose for XR で空間 UI を開発する

対応する XR デバイス
このガイダンスは、次のようなタイプの XR デバイス向けのエクスペリエンスを構築する際に役立ちます。
[
XR Headsets
]
有線 XR グラス

Jetpack Compose for XR を使用すると、行や列などの使い慣れた Compose のコンセプトを使用して、空間 UI とレイアウトを宣言的に構築できます。これにより、既存の Android UI を 3D 空間に拡張したり、没入型の 3D アプリケーションを新たに構築したりできます。

既存の Android Views ベースのアプリを空間化する場合は、いくつかの開発オプションがあります。相互運用性 API を使用したり、Compose と Views を組み合わせて使用したり、SceneCore ライブラリを直接操作したりできます。詳しくは、ビューの操作に関するガイドをご覧ください。

サブスペースと空間化されたコンポーネントについて

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 は、アプリ のコンテンツを表示できるサブスペース コンポーザブルです。たとえば、動画再生、静止画、その他の コンテンツを空間パネルに表示できます。

空間 UI パネルの例

次の例に示すように、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
        )
    }
}

コードに関する主なポイント

  • SpatialPanel API はサブスペース コンポーザブルであるため、 をSubspace内で呼び出す必要があります。サブスペースの外部で呼び出すと、例外がスローされます。
  • SpatialPanel のサイズは、SubspaceModifierheight 仕様と width 仕様を使用して設定されています。これらの仕様を省略すると、パネルのサイズはコンテンツの測定値によって決まります。
  • movable サブスペース修飾子を追加して、ユーザーがパネルを移動できるようにします。
  • resizable サブスペース 修飾子を追加して、ユーザーがパネルのサイズを変更できるようにします。
  • サイズ設定と 配置について詳しくは、空間パネルのデザイン ガイダンスをご覧ください。コード実装の詳細については、リファレンス ドキュメントをご覧ください。

movable 修飾子の仕組み

ユーザーがパネルを遠ざけると、デフォルトでは、movable 修飾子 は、ホームスペースでシステムによってパネルのサイズが変更されるのと同様の方法でパネルをスケーリングします。すべての子コンテンツはこの動作を継承します。これを無効にするには、shouldScaleWithDistance パラメータを false に設定します。

オービターを作成する

オービターは空間 UI コンポーネントです。対応する空間パネルまたは空間レイアウト コンポーネントにアタッチするように設計されています。 SpatialColumnSpatialRow、または 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 レイアウトでは、アプリはオービター内のコンテンツのみをレンダリングし、オービター自体は無視します。
  • オービターの使用方法と 設計について詳しくは、デザイン ガイダンスをご覧ください。

複数の空間パネルを空間レイアウトに追加する

複数の空間パネルを作成し、空間レイアウト内に SpatialRowSpatialColumnSpatialBox、および 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
        )
    }
}

コードに関する主なポイント

  • SpatialRowSpatialColumnSpatialBox、および 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 レイアウト システムに返します。

その他の情報

画像または動画コンテンツのサーフェスを追加する

SpatialExternalSurface は、アプリが画像や 動画 などのコンテンツを描画できる Surface を作成して 管理するサブスペース コンポーザブルです。SpatialExternalSurface は、ステレオスコピック コンテンツとモノスコピック コンテンツのどちらにも対応しています。

この例では、 Media3 ExoplayerSpatialExternalSurface を使用して、サイドバイサイドのステレオスコピック動画を読み込む方法を示します。

@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() }
        }
    }
}

コードに関する主なポイント

    • レンダリングするコンテンツのタイプに応じて、StereoModeMonoSideBySideTopBottom のいずれかに設定します。
    • Mono: 画像または動画フレームは、両目に表示される単一の同一の画像で構成されます。
    • SideBySide: 画像または動画フレームには、左右に配置された画像のペアまたは動画フレームが含まれます。左側の画像またはフレームは左目のビューを表し、右側の画像またはフレームは右目のビューを表します。
    • TopBottom: 画像または動画フレームには、上下に積み重ねられた画像のペアまたは動画フレームが含まれます。上側の画像またはフレームは左目のビューを表し、下側の画像またはフレームは右目のビューを表します。
  • SpatialExternalSurface は、長方形のサーフェスのみをサポートしています。
  • この Surface は入力イベントをキャプチャしません。
  • StereoMode の変更をアプリケーション レンダリングや動画デコードと同期することはできません。
  • このコンポーザブルは他のパネルの前面にレンダリングできないため、レイアウトに他のパネルがある場合は MovePolicy を使用しないでください。

DRM で保護された動画コンテンツのサーフェスを追加する

SpatialExternalSurface は、DRM で保護された動画ストリームの再生もサポートしています。これを有効にするには、保護されたグラフィック バッファにレンダリングする安全なサーフェスを作成する必要があります。これにより、コンテンツが画面録画されたり、安全でないシステム コンポーネントからアクセスされたりするのを防ぐことができます。

安全なサーフェスを作成するには、surfaceProtection パラメータを SurfaceProtection.Protected に設定します。SpatialExternalSurface コンポーザブルで。 また、ライセンス サーバーからライセンスを取得できるように、適切な DRM 情報を使用して Media3 Exoplayer を構成する必要があります。

次の例では、DRM で保護された動画ストリームを再生するように SpatialExternalSurfaceExoPlayer を構成する方法を示します。

@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 コンテンツに適した安全なバッファによってバックアップされるように、SpatialExternalSurfacesurfaceProtection = SurfaceProtection.Protected を設定することが重要です。
  • DRM 構成: DRM スキーム(C.WIDEVINE_UUID など)とライセンス サーバーの URI を使用して MediaItem を構成する必要があります。ExoPlayer はこの情報を使用して DRM セッションを管理します。
  • 安全なコンテンツ: 保護されたサーフェスにレンダリングする場合、動画コンテンツはデコードされ、安全なパスに表示されます。これにより、コンテンツ ライセンス要件を満たすことができます。また、コンテンツがスクリーン キャプチャに表示されるのを防ぐこともできます。

他の空間 UI コンポーネントを追加する

空間 UI コンポーネントは、アプリケーションの UI 階層内の任意の場所に配置できます。 これらの要素は 2D UI で再利用でき、空間機能が有効になっている場合にのみ空間属性が表示されます。これにより、コードを 2 回記述することなく、メニューやダイアログなどのコンポーネントに高度を追加できます。これらの要素の使用方法について詳しくは、空間 UI の次の例をご覧ください。

UI コンポーネント

空間化が有効になっている場合

2D 環境

SpatialDialog

パネルは z 深度でわずかに後方に移動し、高度の高いダイアログを表示します

2D の Dialog にフォールバックします。

SpatialPopup

パネルは z 深度でわずかに後方に移動し、高度の高いポップアップを表示します

2D の Popup にフォールバックします。

SpatialElevation

SpatialElevationLevel を設定して高度を追加できます。

空間高度なしで表示されます。

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")
                }
            }
        }
    }
}

コードに関する主なポイント

カスタムパネルとレイアウトを作成する

Compose for XR でサポートされていないカスタムパネルを作成するには、 PanelEntity インスタンスとシーングラフを SceneCore API を使用して直接操作します。

オービターを空間パネルとレイアウトに固定する

オービターを、Compose で宣言された SpatialPanels と空間レイアウト コンポーネントに固定できます。これには、SpatialRowSpatialColumnSpatialBox などの 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 の上部に固定されます。
  • SpatialRowSpatialColumnSpatialBox などの空間レイアウトには、コンテンツのないエンティティが関連付けられています。そのため、空間レイアウトで宣言されたオービターはそのレイアウトに固定されます。

関連情報