พัฒนา UI เชิงพื้นที่ด้วย Jetpack Compose สำหรับ XR

อุปกรณ์ XR ที่รองรับ
คำแนะนำนี้จะช่วยคุณสร้างประสบการณ์การใช้งานสำหรับอุปกรณ์ XR ประเภทต่างๆ
ชุดหูฟัง XR
แว่นตา XR แบบมีสาย

Jetpack Compose for XR ช่วยให้คุณสร้าง UI และเลย์เอาต์เชิงพื้นที่แบบประกาศได้โดยใช้แนวคิด Compose ที่คุ้นเคย เช่น แถวและคอลัมน์ ซึ่งจะช่วยให้คุณขยาย UI ของ Android ที่มีอยู่ไปยังพื้นที่ 3 มิติ หรือสร้างแอปพลิเคชัน 3 มิติแบบสมจริงใหม่ทั้งหมดได้

หากคุณกำลังสร้างพื้นที่ให้กับแอปที่มีอยู่ซึ่งอิงตาม Android Views คุณจะมีตัวเลือกการพัฒนาหลายอย่าง คุณสามารถใช้ API การทำงานร่วมกัน ใช้ Compose และ Views ร่วมกัน หรือทำงานกับไลบรารี SceneCore โดยตรง ดูรายละเอียดเพิ่มเติมได้ในคำแนะนำเกี่ยวกับการทำงาน กับ Views

เกี่ยวกับพื้นที่ย่อยและคอมโพเนนต์เชิงพื้นที่

เมื่อเขียนแอปสำหรับ Android XR คุณต้องทำความเข้าใจแนวคิดเรื่อง พื้นที่ย่อยและ คอมโพเนนต์เชิงพื้นที่

เกี่ยวกับพื้นที่ย่อย

เมื่อพัฒนาแอปสำหรับ Android XR คุณจะต้องเพิ่มพื้นที่ย่อยลงในแอปหรือเลย์เอาต์ พื้นที่ย่อยคือพาร์ติชันของพื้นที่ 3 มิติภายในแอปที่คุณสามารถวางเนื้อหา 3 มิติ สร้างเลย์เอาต์ 3 มิติ และเพิ่มความลึกให้กับเนื้อหา 2 มิติ ระบบจะแสดงพื้นที่ย่อยก็ต่อเมื่อเปิดใช้การสร้างพื้นที่ ใน Home Space หรือในอุปกรณ์ที่ไม่ใช่ XR ระบบจะไม่สนใจโค้ดใดๆ ภายในพื้นที่ย่อยนั้น

การสร้างพื้นที่ย่อยทำได้หลายวิธีดังนี้

  • Subspace: คอมโพสได้นี้จะสร้างลำดับชั้น UI เชิงพื้นที่ใหม่ที่เป็นอิสระ โดยจะไม่รับช่วงตำแหน่งเชิงพื้นที่ การวางแนว หรือขนาดของ Subspace ระดับบนสุดที่ซ้อนอยู่ ระบบจะผูก Subspace กับกล่องเนื้อหาที่แนะนำโดยอัตโนมัติ
  • PlanarEmbeddedSubspace: คอมโพสได้นี้สามารถวางไว้ในลำดับชั้น UI ของแอป ซึ่งช่วยให้คุณรักษาเลย์เอาต์สำหรับ UI 2 มิติและเชิงพื้นที่ได้ PlanarEmbeddedSubspace จะคำนึงถึงข้อจำกัดและการวางตำแหน่งของคอมโพเนนต์ระดับบนสุด จากนั้นระบบจะวางตำแหน่งเนื้อหา 3 มิติที่วางไว้ภายในคอมโพเนนต์นี้โดยอิงตามพื้นที่ที่กำหนดในแบบ 2 มิติ

ดูข้อมูลเพิ่มเติมได้ที่หัวข้อเพิ่มพื้นที่ย่อยลงในแอป

เกี่ยวกับคอมโพเนนต์เชิงพื้นที่

คอมโพสได้ของพื้นที่ย่อย: คอมโพเนนต์เหล่านี้จะแสดงผลได้ในพื้นที่ย่อยเท่านั้น คุณต้องใส่คอมโพเนนต์เหล่านี้ไว้ใน Subspace ก่อนที่จะวางไว้ในเลย์เอาต์ 2 มิติ A SubspaceModifier ช่วยให้คุณเพิ่มแอตทริบิวต์ต่างๆ เช่น ความลึก ออฟเซ็ต และ การวางตำแหน่ง ลงในคอมโพสได้ของพื้นที่ย่อย

คอมโพเนนต์เชิงพื้นที่อื่นๆ ไม่จำเป็นต้องเรียกใช้ภายในพื้นที่ย่อย โดยคอมโพเนนต์เหล่านี้ประกอบด้วยองค์ประกอบ 2 มิติแบบเดิมที่ห่อหุ้มไว้ในคอนเทนเนอร์เชิงพื้นที่ คุณสามารถใช้องค์ประกอบเหล่านี้ภายในเลย์เอาต์ 2 มิติหรือ 3 มิติได้หากกำหนดไว้สำหรับทั้ง 2 อย่าง เมื่อไม่ได้เปิดใช้การสร้างพื้นที่ ระบบจะไม่สนใจฟีเจอร์เชิงพื้นที่ของคอมโพเนนต์เหล่านี้ และคอมโพเนนต์จะกลับไปใช้คอมโพเนนต์ 2 มิติที่เกี่ยวข้อง

สร้างแผงเชิงพื้นที่

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

ประเด็นสำคัญเกี่ยวกับโค้ด

  • เนื่องจาก API ของ SpatialPanel เป็นคอมโพสได้ของพื้นที่ย่อย คุณจึงต้องเรียกใช้ API เหล่านี้ภายใน Subspace การเรียกใช้ API เหล่านี้ภายนอกพื้นที่ย่อยจะทำให้เกิดข้อยกเว้น
  • ขนาดของ SpatialPanel ได้รับการตั้งค่าโดยใช้ข้อกำหนด height และ width ใน SubspaceModifier การละเว้นข้อกำหนดเหล่านี้จะทำให้ขนาดของแผงกำหนดโดยการวัดขนาดของเนื้อหา
  • อนุญาตให้ผู้ใช้ย้ายแผงได้โดยเพิ่มตัวปรับแต่งพื้นที่ย่อย movable
  • อนุญาตให้ผู้ใช้ปรับขนาดแผงได้โดยเพิ่มตัวปรับแต่งพื้นที่ย่อย resizablesubspace
  • ดูรายละเอียดเกี่ยวกับการปรับขนาดและ การวางตำแหน่งได้ในคำแนะนำการออกแบบแผงเชิงพื้นที่ ดูรายละเอียดเพิ่มเติมเกี่ยวกับการติดตั้งใช้งานโค้ดได้ในเอกสารอ้างอิง

วิธีทำงานของตัวปรับแต่ง movable

เมื่อผู้ใช้ย้ายแผงออกจากตัวผู้ใช้ ตัวปรับแต่ง movable จะปรับขนาดแผงในลักษณะเดียวกับที่ระบบปรับขนาดแผงใน Home Space โดยค่าเริ่มต้น เนื้อหาที่เป็นองค์ประกอบย่อยทั้งหมดจะรับช่วงลักษณะการทำงานนี้ หากต้องการปิดใช้ลักษณะการทำงานนี้ ให้ตั้งค่าพารามิเตอร์ shouldScaleWithDistance เป็น false

สร้างออร์บิเตอร์

ออร์บิเตอร์คือคอมโพเนนต์ UI เชิงพื้นที่ โดยได้รับการออกแบบมาให้แนบกับคอมโพเนนต์แผงเชิงพื้นที่หรือเลย์เอาต์เชิงพื้นที่ที่เกี่ยวข้อง เช่น SpatialColumn, SpatialRow หรือ SpatialBox โดยปกติแล้วออร์บิเตอร์จะมีรายการการนำทางและการดำเนินการตามบริบทที่เกี่ยวข้องกับเอนทิตีที่ออร์บิเตอร์ยึดอยู่ ตัวอย่างเช่น หากคุณสร้างแผงเชิงพื้นที่เพื่อแสดงเนื้อหาวิดีโอ คุณสามารถเพิ่มการควบคุมการเล่นวิดีโอไว้ในออร์บิเตอร์ได้

ตัวอย่างยานโคจร

เรียกใช้ออร์บิเตอร์ภายในเลย์เอาต์ 2 มิติใน SpatialPanel เพื่อห่อหุ้มการควบคุมของผู้ใช้ เช่น การนำทาง ดังที่แสดงในตัวอย่างต่อไปนี้ การทำเช่นนี้จะแยกการควบคุมออกจากเลย์เอาต์ 2 มิติและแนบการควบคุมเหล่านั้นกับแผงเชิงพื้นที่ตามการกำหนดค่าของคุณ

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 เชิงพื้นที่ คุณจึงนำโค้ดไปใช้ซ้ำในเลย์เอาต์ 2 มิติหรือ 3 มิติได้ ในเลย์เอาต์ 2 มิติ แอปจะแสดงผลเฉพาะเนื้อหาภายในออร์บิเตอร์และไม่สนใจออร์บิเตอร์เอง
  • ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีใช้ออร์บิเตอร์และการออกแบบออร์บิเตอร์ได้ในคำแนะนำการออกแบบ

เพิ่มแผงเชิงพื้นที่หลายแผงลงในเลย์เอาต์เชิงพื้นที่

คุณสามารถสร้างแผงเชิงพื้นที่หลายแผงและวางแผงเหล่านั้นไว้ในเลย์เอาต์เชิงพื้นที่ได้ โดยใช้ 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 เพื่อปรับแต่งเลย์เอาต์
  • สำหรับเลย์เอาต์ที่มีหลายแผงในแถวเดียว เราขอแนะนำให้ตั้งค่ารัศมีเส้นโค้งเป็น 825dp โดยใช้ SubspaceModifier เพื่อให้แผงล้อมรอบผู้ใช้ ดูรายละเอียดได้ในคำแนะนำการออกแบบ

เพิ่มออบเจ็กต์ 3 มิติลงในเลย์เอาต์โดยใช้ SpatialGltfModel

Android XR รองรับรูปแบบ glTF สำหรับโมเดล 3 มิติ ซึ่งโดยปกติจะบันทึกเป็นไฟล์ .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)

ประเด็นสำคัญเกี่ยวกับโค้ด

  • การโหลดแบบไม่พร้อมกัน: ระบบจะโหลดโมเดลแบบไม่พร้อมกัน ในระหว่างการคอมโพสครั้งแรก ขนาดโดยธรรมชาติของโมเดลอาจเป็น 0 เลย์เอาต์จะวัดขนาดอีกครั้งเมื่อโมเดลพร้อม
  • การควบคุมสถานะ: ใช้ 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.
    }
}

ประเด็นสำคัญเกี่ยวกับโค้ด

  • บล็อก Factory: บล็อก Factory คือตำแหน่งที่คุณเริ่มต้นเอนทิตี SceneCore ที่อยู่เบื้องหลัง
  • บล็อก Update: ใช้บล็อก Update เพื่อแก้ไขพร็อพเพอร์ตี้ของเอนทิตีเพื่อตอบสนองต่อการเปลี่ยนแปลงสถานะ Compose
  • การปรับขนาด: sizeAdapter จะสื่อสารขนาดของเอนทิตีกลับไปยังระบบเลย์เอาต์ Compose

ข้อมูลเพิ่มเติม

เพิ่มพื้นผิวสำหรับเนื้อหารูปภาพหรือวิดีโอ

A SpatialExternalSurface คือคอมโพสได้ของพื้นที่ย่อยที่สร้างและ จัดการ Surface ซึ่งแอปสามารถวาดเนื้อหา เช่น รูปภาพหรือ วิดีโอ ลงใน 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() }
        }
    }
}

ประเด็นสำคัญเกี่ยวกับโค้ด

  • ตั้งค่า StereoMode เป็น Mono, SideBySide, หรือ TopBottom ขึ้นอยู่กับประเภทเนื้อหาที่คุณแสดงผล ดังนี้
    • Mono: เฟรมรูปภาพหรือวิดีโอประกอบด้วยรูปภาพเดียวที่เหมือนกันซึ่งแสดงต่อดวงตาทั้ง 2 ข้าง
    • SideBySide: เฟรมรูปภาพหรือวิดีโอมีรูปภาพหรือเฟรมวิดีโอ 2 รายการที่จัดเรียงเคียงข้างกัน โดยรูปภาพหรือเฟรมทางด้านซ้ายแสดงมุมมองของตาซ้าย และรูปภาพหรือเฟรมทางด้านขวาแสดงมุมมองของตาขวา
    • TopBottom: เฟรมรูปภาพหรือวิดีโอมีรูปภาพหรือเฟรมวิดีโอ 2 รายการที่ซ้อนกันในแนวตั้ง โดยรูปภาพหรือเฟรมด้านบนแสดงมุมมองของตาซ้าย และรูปภาพหรือเฟรมด้านล่างแสดงมุมมองของตาขวา
  • SpatialExternalSurface รองรับเฉพาะพื้นผิวสี่เหลี่ยมผืนผ้า
  • Surface นี้จะไม่บันทึกเหตุการณ์อินพุต
  • คุณไม่สามารถซิงโครไนซ์ StereoMode การเปลี่ยนแปลงกับการแสดงผลของแอปพลิเคชัน หรือการถอดรหัสวิดีโอ
  • คอมโพสได้นี้แสดงผลอยู่ด้านหน้าแผงอื่นๆ ไม่ได้ ดังนั้นคุณจึงไม่ควรใช้ MovePolicy หากมีแผงอื่นๆ ในเลย์เอาต์

เพิ่มพื้นผิวสำหรับเนื้อหาวิดีโอที่ได้รับการคุ้มครองโดย DRM

SpatialExternalSurface ยังรองรับการเล่นสตรีมวิดีโอที่ได้รับการคุ้มครองโดย DRM ด้วย หากต้องการเปิดใช้ฟีเจอร์นี้ คุณต้องสร้างพื้นผิวที่ปลอดภัยซึ่งแสดงผลไปยังบัฟเฟอร์กราฟิกที่ได้รับการคุ้มครอง ซึ่งจะป้องกันไม่ให้มีการบันทึกหน้าจอเนื้อหาหรือคอมโพเนนต์ระบบที่ไม่ปลอดภัยเข้าถึงเนื้อหา

หากต้องการสร้างพื้นผิวที่ปลอดภัย ให้ตั้งค่าพารามิเตอร์ SpatialExternalSurfaceProtection เป็น SpatialExternalSurfaceProtection.Protected ใน SpatialExternalSurface คอมโพสได้ นอกจากนี้ คุณต้องกำหนดค่า Media3 Exoplayer ด้วยข้อมูล DRM ที่เหมาะสมเพื่อจัดการการ ขอรับใบอนุญาตจากเซิร์ฟเวอร์ใบอนุญาต

ตัวอย่างต่อไปนี้แสดงวิธีกำหนดค่า SpatialExternalSurface และ ExoPlayer เพื่อเล่นสตรีมวิดีโอที่ได้รับการคุ้มครองโดย 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 = SpatialExternalSurfaceProtection.Protected บน SpatialExternalSurface เป็นสิ่งสำคัญเพื่อให้ Surfaceที่อยู่เบื้องหลังได้รับการสนับสนุนโดยบัฟเฟอร์ที่ปลอดภัยซึ่งเหมาะสำหรับเนื้อหา DRM
  • การกำหนดค่า DRM: คุณต้องกำหนดค่า MediaItem ด้วยรูปแบบ DRM (เช่น C.WIDEVINE_UUID) และ URI ของเซิร์ฟเวอร์ใบอนุญาต ExoPlayer จะใช้ข้อมูลนี้เพื่อจัดการเซสชัน DRM
  • เนื้อหาที่ปลอดภัย: เมื่อแสดงผลไปยังพื้นผิวที่ได้รับการคุ้มครอง ระบบจะถอดรหัสและแสดงเนื้อหาวิดีโอในเส้นทางที่ปลอดภัย ซึ่งช่วยให้เป็นไปตามข้อกำหนดการออกใบอนุญาตเนื้อหา นอกจากนี้ ยังป้องกันไม่ให้เนื้อหาปรากฏในภาพหน้าจอ

เพิ่มคอมโพเนนต์ UI เชิงพื้นที่อื่นๆ

คุณสามารถวางคอมโพเนนต์ UI เชิงพื้นที่ไว้ที่ใดก็ได้ในลำดับชั้น UI ของแอปพลิเคชัน คุณสามารถนำองค์ประกอบเหล่านี้ไปใช้ซ้ำใน UI 2 มิติได้ และแอตทริบิวต์เชิงพื้นที่ขององค์ประกอบจะปรากฏขึ้นเมื่อเปิดใช้ความสามารถเชิงพื้นที่เท่านั้น ซึ่งจะช่วยให้คุณเพิ่มระดับความสูงให้กับเมนู กล่องโต้ตอบ และคอมโพเนนต์อื่นๆ ได้โดยไม่ต้องเขียนโค้ด 2 ครั้ง ดูตัวอย่าง UI เชิงพื้นที่ต่อไปนี้เพื่อทำความเข้าใจวิธีใช้องค์ประกอบเหล่านี้ให้ดียิ่งขึ้น

คอมโพเนนต์ UI

เมื่อเปิดใช้การสร้างพื้นที่

ในสภาพแวดล้อม 2 มิติ

SpatialDialog

แผงจะเลื่อนถอยหลังเล็กน้อยในความลึกแกน Z เพื่อแสดงกล่องโต้ตอบที่ยกระดับ

กลับไปใช้ 2 มิติ Dialog

SpatialPopup

แผงจะเลื่อนถอยหลังเล็กน้อยในความลึกแกน Z เพื่อแสดงป๊อปอัปที่ยกระดับ

กลับไปใช้ 2 มิติ Popup

SpatialElevation

ตั้งค่า SpatialElevationLevel เพื่อเพิ่มระดับความสูงได้

แสดงโดยไม่มีระดับความสูงเชิงพื้นที่

SpatialDialog

นี่คือตัวอย่างกล่องโต้ตอบที่จะเปิดขึ้นหลังจากผ่านไปครู่หนึ่ง เมื่อใช้ SpatialDialog กล่องโต้ตอบจะปรากฏที่ความลึกแกน Z เดียวกับ แผงเชิงพื้นที่ และแผงจะเลื่อนถอยหลัง 125dp เมื่อเปิดใช้การสร้างพื้นที่ SpatialDialog ยังใช้ได้เมื่อไม่ได้เปิดใช้การสร้างพื้นที่ ใน กรณีนี้ SpatialDialog จะกลับไปใช้ Dialog ซึ่งเป็นคอมโพเนนต์ 2 มิติที่เกี่ยวข้อง

@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 และกราฟฉากได้โดยตรงโดยใช้ API ของ SceneCore

ยึดออร์บิเตอร์กับแผงและเลย์เอาต์เชิงพื้นที่

คุณสามารถยึดออร์บิเตอร์กับ SpatialPanels และคอมโพเนนต์เลย์เอาต์เชิงพื้นที่ที่ประกาศไว้ใน Compose ได้ ซึ่งเกี่ยวข้องกับการประกาศออร์บิเตอร์ในเลย์เอาต์เชิงพื้นที่ขององค์ประกอบ UI เช่น SpatialRow, SpatialColumn หรือ SpatialBox ออร์บิเตอร์จะยึดกับคอมโพเนนต์ระดับบนสุดที่อยู่ใกล้กับตำแหน่งที่คุณประกาศมากที่สุด

ลักษณะการทำงานของออร์บิเตอร์จะกำหนดโดยตำแหน่งที่คุณประกาศออร์บิเตอร์ ดังนี้

  • ในเลย์เอาต์ 2 มิติที่ห่อหุ้มไว้ใน SpatialPanel (ดังที่แสดงใน ข้อมูลโค้ดก่อนหน้า) ออร์บิเตอร์จะยึดกับ 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)
            )
        }
    }
}

ประเด็นสำคัญเกี่ยวกับโค้ด

  • เมื่อประกาศออร์บิเตอร์ภายนอกเลย์เอาต์ 2 มิติ ออร์บิเตอร์จะยึดกับเอนทิตีหลักที่อยู่ใกล้ที่สุด ในกรณีนี้ ออร์บิเตอร์จะยึดกับด้านบนของ SpatialRow ที่ประกาศไว้
  • เลย์เอาต์เชิงพื้นที่ เช่น SpatialRow, SpatialColumn, SpatialBox ทั้งหมดมีเอนทิตีที่ไม่มีเนื้อหาเชื่อมโยงอยู่ ดังนั้น ออร์บิเตอร์ที่ประกาศไว้ในเลย์เอาต์เชิงพื้นที่จะยึดกับเลย์เอาต์นั้น

ดูเพิ่มเติม