使用 Jetpack Compose 為 XR 開發 UI

透過適用於 XR 的 Jetpack Compose,您可以使用熟悉的 Compose 概念 (例如資料列和資料欄),以宣告方式建構空間 UI 和版面配置。您可以藉此將現有的 Android UI 擴展到 3D 空間,或建構全新的沉浸式 3D 應用程式。

如要將現有的 Android Views 應用程式空間化,您有幾種開發選項。您可以運用互通性 API、同時使用 Compose 和 Views,或直接使用 SceneCore 程式庫。詳情請參閱檢視畫面使用指南

關於子空間和空間化元件

為 Android XR 編寫應用程式時,請務必瞭解子空間空間化元件的概念。

關於子空間

為 Android XR 開發應用程式時,您需要在應用程式或版面配置中新增 Subspace。子空間是應用程式內 3D 空間的分區,可在其中放置 3D 內容、建構 3D 版面配置,以及為其他 2D 內容加上深度。只有在啟用空間化功能時,才會算繪子空間。在首頁空間或非 XR 裝置上,系統會忽略該子空間中的任何程式碼。

建立子空間的方法有兩種:

  • Subspace:這個可組合函式可放置在應用程式 UI 階層中的任何位置,讓您維護 2D 和空間 UI 的版面配置,同時不會遺失檔案之間的內容。這樣一來,您就能輕鬆在 XR 和其他外型規格之間共用現有的應用程式架構,不必透過整個 UI 樹狀結構提升狀態,也不必重新設計應用程式架構。
  • ApplicationSubspace:此函式只會建立應用程式層級的子空間,且必須放在應用程式空間 UI 階層的最上層。ApplicationSubspace 會使用選用的 VolumeConstraints 算繪空間內容。與 Subspace 不同,ApplicationSubspace 無法在其他 SubspaceApplicationSubspace 中巢狀顯示。

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

關於空間化元件

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

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

建立空間面板

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

空間 UI 面板範例

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

可移動子空間修飾符的運作方式

使用者將面板移開時,可移動的子空間修飾符預設會縮放面板,方式與系統在主空間中調整面板大小類似。所有子項內容都會沿用這項行為。如要停用這項功能,請將 scaleWithDistance 參數設為 false

建立軌道器

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

軌道飛行器範例

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

Subspace {     SpatialPanel(         SubspaceModifier             .height(824.dp)             .width(1400.dp)             .movable()             .resizable()     ) {         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 版面配置中,應用程式只會算繪軌道器內的內容,並忽略軌道器本身。
  • 如要進一步瞭解如何使用及設計軌跡球,請參閱設計指南

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

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

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

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

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

程式碼重點

使用體積在版面配置中放置 3D 物件

如要在版面配置中放置 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,             )         }     } }

@OptIn(ExperimentalSubspaceVolumeApi::class) @Composable fun ObjectInAVolume(show3DObject: Boolean) {

其他資訊

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

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 變更內容與應用程式算繪或影片解碼同步處理。
  • 這個可組合函式無法在其他面板前方算繪,因此如果版面配置中有其他面板,就不應使用可移動的修飾符。

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

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

如要建立安全介面,請在 SpatialExternalSurface 可組合函式中,將 surfaceProtection 參數設為 SurfaceProtection.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 = 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() }         }     } }

程式碼重點

  • 受保護的介面:設定 surfaceProtection = SurfaceProtection.Protected 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 深度,且啟用空間化時,面板會往後推 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 中宣告的任何實體。這包括在 UI 元素 (例如 SpatialRowSpatialColumnSpatialBox) 的空間布局中宣告軌道器。軌道控制器會錨定至您宣告位置附近的上層實體。

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

  • 在以 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 等空間版面配置都與沒有內容的實體相關聯。因此,在空間版面配置中宣告的軌跡物會錨定至該版面配置。

另請參閱