使用 Jetpack Compose for XR 開發空間 UI

適用的 XR 裝置
這份指南可協助您為這類 XR 裝置打造體驗。
XR 頭戴式裝置
有線 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 的版面配置。PlanarEmbeddedSubspace 會遵守父項的限制和定位。然後,系統會根據這個 2D 定義區域,放置其中的 3D 內容。

詳情請參閱「在應用程式中新增子空間」。

關於空間化元件

子空間可組合函式:這些元件只能在子空間中算繪。 必須先以 Subspace 括住,才能放置在 2D 版面配置中。 SubspaceModifier 可讓您為子空間可組合函式新增深度、位移和定位等屬性。

其他空間化元件不需要在子空間內呼叫。這類元素由包裝在空間容器中的傳統 2D 元素組成。如果同時為 2D 和 3D 版面配置定義這些元素,即可在其中使用。如果未啟用空間化功能,系統會忽略空間化特徵,並改用 2D 對應特徵。

建立空間面板

SpatialPanel 是子空間可組合項,可顯示應用程式內容,例如在空間面板中顯示影片播放、靜態圖片或任何其他內容。

空間 UI 面板範例

您可以使用 SubspaceModifier 變更空間面板的大小、行為和位置,如下列範例所示。

Subspace {
    SpatialPanel(
        SubspaceModifier
            .height(824.dp)
            .width(1400.dp)
            .movable(),
        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 內呼叫這些 API。在子空間外部呼叫這些函式會擲回例外狀況。
  • SpatialPanel 的大小已在 SubspaceModifier 上使用 heightwidth 規格設定。如果省略這些規格,系統會根據內容的測量結果決定面板大小。
  • 新增 movable 子空間修飾符,允許使用者移動面板。
  • 新增 resizable 子空間修飾符,允許使用者調整面板大小。
  • 如要瞭解大小和位置的詳細資訊,請參閱空間面板設計指南。如要進一步瞭解程式碼導入作業,請參閱參考文件

movable 修飾符的運作方式

根據預設,當使用者將面板移開時,movable 修飾符會以類似於系統在住家空間中調整面板大小的方式,縮放面板。所有子項內容都會沿用這項行為。如要停用這項功能,請將 shouldScaleWithDistance 參數設為 false

建立軌道器

軌道器是空間 UI 元件。這項函式旨在附加至對應的空間面板或空間版面配置元件,例如 SpatialColumnSpatialRowSpatialBox。軌跡球通常包含與所錨定實體相關的導覽和關聯動作項目。舉例來說,如果您已建立空間面板來顯示影片內容,可以在軌跡球內新增影片播放控制項。

軌道飛行器範例

如下列範例所示,在 2D 版面配置中呼叫軌跡球,以在 SpatialPanel 中包裝導覽等使用者控制項。這樣做會從 2D 版面配置中擷取這些項目,並根據設定附加至空間面板。

Subspace {
    SpatialPanel(
        SubspaceModifier
            .height(824.dp)
            .width(1400.dp)
            .resizable()
            .movable(),
    ) {
        SpatialPanelContent()
        OrbiterExample()
    }
}

@Composable
fun OrbiterExample() {
    Orbiter(
        anchorPoint = OrbiterAnchorPoint.Bottom,
        offset = DpVolumeOffset(y = 96.dp),
    ) {
        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 版面配置中,應用程式只會算繪軌道器內的內容,並忽略軌道器本身。
  • 如要進一步瞭解如何使用及設計軌跡球,請參閱設計指南

在空間配置中新增多個空間面板

您可以使用 SpatialRowSpatialColumnSpatialBoxSpatialSpacer,建立多個空間面板並放置在空間版面配置中。

空間版面配置中的多個空間面板範例

以下程式碼範例說明如何執行這項操作。

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

程式碼重點

使用 SpatialGltfModel 將 3D 物件新增至版面配置

Android XR 支援 glTF 格式的 3D 模型,通常會儲存為 .glb 檔案。如要將這些物件新增至版面配置,請使用 SpatialGltfModel 可組合函式。這個 API 可簡化載入資產及管理資產狀態的程序。

如要顯示模型,請先使用 rememberSpatialGltfModelState 定義模型的來源和狀態。您可以從應用程式的 assets 資料夾、URIraw data 載入模型。

val modelState = rememberSpatialGltfModelState(
    source = SpatialGltfModelSource.fromPath(
        Paths.get("models/model_name.glb")
    )
)

定義狀態後,請使用 SpatialGltfModel 可組合函式,在 Subspace 中算繪狀態。

SpatialGltfModel(state = modelState, modifier = SubspaceModifier)

程式碼重點

  • 非同步載入:模型會以非同步方式載入。在初始組合期間,其內在大小可能為零;模型準備就緒後,版面配置會重新測量。
  • 控制狀態:使用 SpatialGltfModelState.status 查詢載入狀態或控制動畫。
  • 大小和縮放:根據預設,版面配置大小會與資產的定界框相符。您可以透過 SubspaceModifier.size 覆寫這項設定,將模型統一縮放至指定界限內。

使用 SceneCoreEntity 將實體放在版面配置中

SceneCoreEntity 可組合函式會連結 Jetpack SceneCoreCompose for XR 程式庫,讓您在 Compose 版面配置中使用 SceneCore 建構的實體。這可讓您建構較低層級的實體和自訂元件,同時允許 Compose 調整大小、放置、重新設定父項、新增子項,以及對這些實體套用修飾符。

val density = LocalDensity.current
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 =
            object : SceneCoreEntitySizeAdapter<SurfaceEntity> {
                override fun onLayoutSizeChanged(
                    entity: SurfaceEntity,
                    size: IntVolumeSize
                ) {
                    val extents = FloatSize2d(
                        Meter.fromPixel(size.width.toFloat(), density).toM(),
                        Meter.fromPixel(size.height.toFloat(), density).toM(),
                    )
                    entity.shape = SurfaceEntity.Shape.Quad(extents)
                }
            },
    ) {
        // Content here will be children of the SceneCoreEntity
        // in the scene graph.
    }
}

程式碼重點

  • 工廠區塊:您可以在工廠區塊中初始化基礎 SceneCore 實體。
  • 更新區塊:使用更新區塊修改實體的屬性,以回應 Compose 狀態的變更。
  • 大小調整:sizeAdapter 會將實體的尺寸傳回 Compose 版面配置系統。

其他資訊

新增圖片或影片內容的介面

SpatialExternalSurface 是可組合的子空間,可建立及管理應用程式可繪製內容 (例如圖片或影片) 的 SurfaceSpatialExternalSurface 支援立體或單視內容。

這個範例說明如何使用 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() }
        }
    }
}

程式碼重點

  • 根據您要算繪的內容類型,將 StereoMode 設為 MonoSideBySideTopBottom
    • Mono:影像或影片畫面由單一相同圖像組成,並顯示在雙眼。
    • SideBySide:圖片或影片影格包含並排排列的一對圖片或影片影格,左側的圖片或影格代表左眼視角,右側的圖片或影格代表右眼視角。
    • TopBottom:圖片或影片畫面包含一組垂直堆疊的圖片或影片畫面,其中頂端的圖片或畫面代表左眼視角,底部的圖片或畫面則代表右眼視角。
  • SpatialExternalSurface 僅支援矩形表面。
  • 這個 Surface 不會擷取輸入事件。
  • 無法將 StereoMode 變更內容與應用程式算繪或影片解碼同步處理。
  • 這個可組合項無法在其他面板前方算繪,因此如果版面配置中有其他面板,就不應使用 MovePolicy

為受 DRM 保護的影片內容新增介面

SpatialExternalSurface 也支援播放受數位版權管理保護的影片串流。如要啟用這項功能,您必須建立安全介面,並將其算繪至受保護的圖像緩衝區。這樣可防止內容遭到螢幕錄影,或遭不安全的系統元件存取。

如要建立安全介面,請將 SpatialExternalSurface 可組合函式的 SpatialExternalSurfaceProtection 參數設為 SpatialExternalSurfaceProtection.Protected。此外,您必須使用適當的 DRM 資訊設定 Media3 Exoplayer,才能處理從授權伺服器取得授權的程序。

以下範例說明如何設定 SpatialExternalSurfaceExoPlayer,播放受 DRM 保護的影片串流:

@OptIn(ExperimentalComposeApi::class)
@Composable
fun DrmSpatialVideoPlayer() {
    val context = LocalContext.current
    Subspace {
        SpatialExternalSurface(
            modifier = SubspaceModifier
                .width(1200.dp)
                .height(676.dp),
            stereoMode = StereoMode.SideBySide,
            surfaceProtection = SpatialExternalSurfaceProtection.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:設定 surfaceProtection = SpatialExternalSurfaceProtection.Protected on SpatialExternalSurface 至關重要,這樣底層的 Surface 才能由適用於 DRM 內容的安全緩衝區支援。
  • DRM 設定:您必須使用 DRM 方案 (例如 C.WIDEVINE_UUID) 和授權伺服器的 URI 設定 MediaItem。ExoPlayer 會使用這項資訊管理 DRM 工作階段。
  • 安全內容:轉譯至受保護的介面時,影片內容會透過安全路徑解碼及顯示,有助於滿足內容授權需求。這麼做也能防止內容出現在螢幕截圖中。

新增其他空間 UI 元件

空間 UI 元件可以放在應用程式 UI 階層中的任何位置。 這些元素可在 2D UI 中重複使用,且只有在啟用空間功能時,才會顯示空間屬性。這樣一來,您就能為選單、對話方塊和其他元件新增高度,不必重複編寫程式碼。請參閱下列空間 UI 範例,進一步瞭解如何使用這些元素。

UI 元件

啟用空間化功能時

在 2D 環境中

SpatialDialog

面板會稍微向後推移,顯示高架對話方塊

改為顯示 2D Dialog

SpatialPopup

面板會稍微向後推移,顯示高架彈出式視窗

改用 2D Popup

SpatialElevation

SpatialElevationLevel 可設定為新增海拔高度。

沒有空間升高的節目。

SpatialDialog

這是延遲一小段時間後開啟的對話方塊範例。使用 SpatialDialog 時,對話方塊會顯示在與空間面板相同的 z 深度,且啟用空間化時,面板會往後推 125 dp。如果未啟用空間化功能,也可以使用 SpatialDialog,此時 SpatialDialog 會改用 2D 對應項目 Dialog

@Composable
fun DelayedDialog() {
    var showDialog by remember { mutableStateOf(false) }
    LaunchedEffect(Unit) {
        delay(3000.milliseconds)
        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 不支援的自訂面板,可以使用 SceneCore API 直接處理 PanelEntity 執行個體和場景圖。

將軌跡球錨定至空間面板和版面配置

您可以將軌跡球錨定至 Compose 中宣告的 SpatialPanels 和空間版面配置元件。這包括在 UI 元素 (例如 SpatialRowSpatialColumnSpatialBox) 的空間布局中宣告軌道器。軌域會錨定至最靠近您宣告位置的父項。

軌跡球的行為取決於您宣告的位置:

  • 在以 SpatialPanel 包裝的 2D 版面配置中 (如前述程式碼片段所示),軌跡球會錨定至該 SpatialPanel
  • Subspace 中,軌道器會錨定至最近的父系實體,也就是宣告軌道器的空間版面配置。

以下範例說明如何將軌跡球錨定至空間列:

Subspace {
    SpatialRow {
        Orbiter(
            anchorPoint = OrbiterAnchorPoint.Top,
            offset = DpVolumeOffset(y = 8.dp),
            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 等空間版面配置都與沒有內容的實體相關聯。因此,在空間版面配置中宣告的軌跡球會錨定至該版面配置。

另請參閱