توسعه رابط کاربری با Jetpack Compose برای XR

با Jetpack Compose برای XR، می‌توانید رابط کاربری و طرح‌بندی فضایی خود را با استفاده از مفاهیم آشنای Compose مانند ردیف‌ها و ستون‌ها، به صورت اعلانی بسازید. این به شما امکان می‌دهد رابط کاربری اندروید موجود خود را به فضای سه‌بعدی گسترش دهید یا برنامه‌های سه‌بعدی فراگیر کاملاً جدیدی بسازید.

اگر در حال فضاسازی یک برنامه مبتنی بر Views اندروید موجود هستید، گزینه‌های توسعه متعددی دارید. می‌توانید از APIهای قابلیت همکاری استفاده کنید، Compose و Views را با هم استفاده کنید، یا مستقیماً با کتابخانه SceneCore کار کنید. برای جزئیات بیشتر به راهنمای ما در مورد کار با Views مراجعه کنید.

درباره زیرفضاها و اجزای فضایی‌شده

وقتی دارید برنامه‌تان را برای اندروید XR می‌نویسید، درک مفاهیم زیرفضا (subspace) و اجزای فضایی‌شده (spatialized components) مهم است.

درباره زیرفضا

هنگام توسعه برای اندروید XR، باید یک Subspace به برنامه یا طرح‌بندی خود اضافه کنید. Subspace یک پارتیشن از فضای سه‌بعدی درون برنامه شماست که می‌توانید محتوای سه‌بعدی را در آن قرار دهید، طرح‌بندی‌های سه‌بعدی بسازید و به محتوای دوبعدی عمق اضافه کنید. Subspace فقط زمانی رندر می‌شود که قابلیت فضایی‌سازی فعال باشد. در Home Space یا در دستگاه‌های غیر XR، هر کدی که درون آن Subspace باشد نادیده گرفته می‌شود.

دو روش برای ایجاد زیرفضا وجود دارد:

  • Subspace : این ترکیب‌پذیر را می‌توان در هر جایی از سلسله مراتب رابط کاربری برنامه شما قرار داد و به شما امکان می‌دهد طرح‌بندی‌های رابط کاربری دوبعدی و فضایی را بدون از دست دادن زمینه بین فایل‌ها حفظ کنید. این امر اشتراک‌گذاری چیزهایی مانند معماری برنامه موجود بین XR و سایر عوامل فرم را بدون نیاز به انتقال وضعیت در کل درخت رابط کاربری یا معماری مجدد برنامه، آسان‌تر می‌کند.
  • ApplicationSubspace : این تابع فقط زیرفضای سطح برنامه ایجاد می‌کند و باید در بالاترین سطح در سلسله مراتب رابط کاربری مکانی برنامه شما قرار گیرد. ApplicationSubspace محتوای مکانی را با VolumeConstraints اختیاری رندر می‌کند. برخلاف Subspace ، ApplicationSubspace نمی‌توان درون Subspace یا ApplicationSubspace دیگری تودرتو کرد.

برای اطلاعات بیشتر، به افزودن یک زیرفضا به برنامه خود مراجعه کنید.

درباره اجزای فضایی

کامپوننت‌های Subspace : این کامپوننت‌ها فقط می‌توانند در یک Subspace رندر شوند. آن‌ها باید قبل از قرار گرفتن در یک طرح‌بندی دوبعدی، درون Subspace محصور شوند. SubspaceModifier به شما امکان می‌دهد ویژگی‌هایی مانند عمق، افست و موقعیت‌یابی را به کامپوننت‌های Subspace خود اضافه کنید.

سایر اجزای فضایی نیازی به فراخوانی درون یک زیرفضا ندارند. آن‌ها از عناصر دوبعدی مرسوم تشکیل شده‌اند که درون یک ظرف فضایی قرار گرفته‌اند. این عناصر می‌توانند در طرح‌بندی‌های دوبعدی یا سه‌بعدی استفاده شوند، اگر برای هر دو تعریف شده باشند. وقتی فضایی‌سازی فعال نباشد، ویژگی‌های فضایی آن‌ها نادیده گرفته می‌شوند و به معادل‌های دوبعدی خود برمی‌گردند.

یک پنل فضایی ایجاد کنید

SpatialPanel یک زیرفضای قابل ترکیب است که به شما امکان نمایش محتوای برنامه را می‌دهد - برای مثال، می‌توانید پخش ویدیو، تصاویر ثابت یا هر محتوای دیگری را در یک پنل فضایی نمایش دهید.

نمونه‌ای از یک پنل رابط کاربری فضایی

شما می‌توانید SubspaceModifier برای تغییر اندازه، رفتار و موقعیت‌یابی پنل spatial استفاده کنید، همانطور که در مثال زیر نشان داده شده است.

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 قابل ترکیب با subspace هستند، باید آنها را داخل Subspace فراخوانی کنید. فراخوانی آنها خارج از subspace باعث ایجاد خطا می‌شود.
  • اندازه SpatialPanel با استفاده از مشخصات height و width در SubspaceModifier تنظیم شده است. حذف این مشخصات باعث می‌شود اندازه پنل با اندازه‌گیری محتویات آن تعیین شود.
  • با اضافه کردن MovePolicy به کاربر اجازه دهید یک پنل را جابجا کند.
  • با اضافه کردن ResizePolicy به کاربر اجازه دهید اندازه یک پنل را تغییر دهد.
  • برای جزئیات بیشتر در مورد اندازه و موقعیت قرارگیری ، به راهنمای طراحی پنل فضایی ما مراجعه کنید. برای جزئیات بیشتر در مورد پیاده‌سازی کد، به مستندات مرجع ما مراجعه کنید.

نحوه کار MovePolicy

وقتی کاربر یک پنل را از خود دور می‌کند، به طور پیش‌فرض، MovePolicy پنل را به روشی مشابه تغییر اندازه پنل‌ها توسط سیستم در فضای خانه ، مقیاس‌بندی می‌کند. همه محتوای فرزند این رفتار را به ارث می‌برند. برای غیرفعال کردن این، پارامتر shouldScaleWithDistance را روی false تنظیم کنید.

یک مدارگرد ایجاد کنید

یک مدارگرد یک کامپوننت رابط کاربری فضایی است. این کامپوننت به گونه‌ای طراحی شده است که به یک پنل فضایی، طرح‌بندی یا موجودیت دیگر مربوطه متصل شود. یک مدارگرد معمولاً شامل آیتم‌های ناوبری و اقدامات زمینه‌ای مرتبط با موجودیتی است که به آن متصل شده است. به عنوان مثال، اگر یک پنل فضایی برای نمایش محتوای ویدیویی ایجاد کرده‌اید، می‌توانید کنترل‌های پخش ویدیو را درون یک مدارگرد اضافه کنید.

نمونه‌ای از یک مدارگرد

همانطور که در مثال زیر نشان داده شده است، یک مدارگرد را درون طرح دوبعدی در SpatialPanel فراخوانی کنید تا کنترل‌های کاربر مانند ناوبری را در بر بگیرد. انجام این کار آنها را از طرح دوبعدی شما استخراج کرده و مطابق پیکربندی شما به پنل فضایی متصل می‌کند.

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

نکات کلیدی در مورد کد

  • از آنجا که مدارگردها اجزای رابط کاربری فضایی هستند، کد را می‌توان در طرح‌بندی‌های دوبعدی یا سه‌بعدی دوباره استفاده کرد. در یک طرح‌بندی دوبعدی، برنامه شما فقط محتوای داخل مدارگرد را رندر می‌کند و خود مدارگرد را نادیده می‌گیرد.
  • برای اطلاعات بیشتر در مورد نحوه استفاده و طراحی مدارگردها، راهنمای طراحی ما را بررسی کنید.

چندین پنل فضایی را به یک طرح فضایی اضافه کنید

شما می‌توانید چندین پنل فضایی ایجاد کنید و آن‌ها را با استفاده از SpatialRow ، SpatialColumn ، SpatialBox و SpatialLayoutSpacer درون یک طرح‌بندی فضایی قرار دهید.

نمونه‌ای از چندین پنل فضایی در یک طرح فضایی

نمونه کد زیر نحوه انجام این کار را نشان می‌دهد.

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 و SpatialLayoutSpacer همگی ترکیبات زیرفضا هستند و باید درون یک زیرفضا قرار گیرند.
  • برای سفارشی‌سازی طرح‌بندی خود SubspaceModifier استفاده کنید.
  • برای طرح‌بندی‌هایی با چندین پنل در یک ردیف، توصیه می‌کنیم با استفاده از SubspaceModifier ، شعاع منحنی را روی ۸۲۵dp تنظیم کنید تا پنل‌ها کاربر شما را احاطه کنند. برای جزئیات بیشتر به راهنمای طراحی ما مراجعه کنید.

یک سطح برای محتوای تصویر یا ویدیو اضافه کنید

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

نکات کلیدی در مورد کد

  • بسته به نوع محتوایی که رندر می‌کنید، StereoMode روی Mono ، SideBySide یا TopBottom تنظیم کنید:
    • Mono : تصویر یا فریم ویدیویی شامل یک تصویر واحد و یکسان است که به هر دو چشم نشان داده می‌شود.
    • SideBySide : تصویر یا فریم ویدیویی شامل یک جفت تصویر یا فریم ویدیویی است که در کنار هم قرار گرفته‌اند، به طوری که تصویر یا فریم سمت چپ نمایانگر نمای چشم چپ و تصویر یا فریم سمت راست نمایانگر نمای چشم راست است.
    • TopBottom : تصویر یا فریم ویدیویی شامل یک جفت تصویر یا فریم ویدیویی است که به صورت عمودی روی هم قرار گرفته‌اند، که تصویر یا فریم بالا نشان دهنده نمای چشم چپ و تصویر یا فریم پایین نشان دهنده نمای چشم راست است.
  • SpatialExternalSurface فقط از سطوح مستطیلی پشتیبانی می‌کند.
  • این Surface رویدادهای ورودی را ثبت نمی‌کند.
  • همگام‌سازی تغییرات StereoMode با رندر برنامه یا رمزگشایی ویدیو امکان‌پذیر نیست.
  • این ترکیب‌پذیر نمی‌تواند جلوی پنل‌های دیگر رندر شود، بنابراین اگر پنل‌های دیگری در طرح‌بندی وجود دارد، نباید از MovePolicy استفاده کنید.

یک سطح برای محتوای ویدیویی محافظت‌شده با DRM اضافه کنید

SpatialExternalSurface همچنین از پخش جریان‌های ویدیویی محافظت‌شده با DRM پشتیبانی می‌کند. برای فعال کردن این قابلیت، باید یک سطح امن ایجاد کنید که در بافرهای گرافیکی محافظت‌شده رندر شود. این امر مانع از ضبط محتوا روی صفحه یا دسترسی اجزای سیستم غیرایمن به آن می‌شود.

برای ایجاد یک سطح امن، پارامتر surfaceProtection را روی SurfaceProtection.Protected در SpatialExternalSurface composable تنظیم کنید. علاوه بر این، باید 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 = SurfaceProtection.Protected روی SpatialExternalSurface ضروری است تا Surface زیرین توسط بافرهای امن مناسب برای محتوای DRM پشتیبانی شود.
  • پیکربندی DRM: شما باید MediaItem با طرح DRM (برای مثال، C.WIDEVINE_UUID ) و URI سرور لایسنس خود پیکربندی کنید. ExoPlayer از این اطلاعات برای مدیریت جلسه DRM استفاده می‌کند.
  • محتوای امن: هنگام رندر کردن روی یک سطح محافظت‌شده، محتوای ویدیو رمزگشایی شده و در یک مسیر امن نمایش داده می‌شود که به برآورده شدن الزامات مجوز محتوا کمک می‌کند. این امر همچنین از نمایش محتوا در تصاویر گرفته شده از صفحه نمایش جلوگیری می‌کند.

افزودن سایر اجزای رابط کاربری فضایی

اجزای رابط کاربری فضایی را می‌توان در هر جایی از سلسله مراتب رابط کاربری برنامه شما قرار داد. این عناصر را می‌توان در رابط کاربری دوبعدی شما مجدداً استفاده کرد و ویژگی‌های مکانی آنها فقط زمانی قابل مشاهده خواهد بود که قابلیت‌های مکانی فعال باشند. این به شما امکان می‌دهد بدون نیاز به نوشتن مجدد کد، به منوها، دیالوگ‌ها و سایر اجزا، ارتفاع اضافه کنید. برای درک بهتر نحوه استفاده از این عناصر، به مثال‌های زیر از رابط کاربری فضایی مراجعه کنید.

کامپوننت رابط کاربری

وقتی فضاسازی فعال می‌شود

در محیط دوبعدی

SpatialDialog

پنل کمی در عمق z به عقب فشار داده می‌شود تا یک کادر محاوره‌ای با ارتفاع بالا نمایش داده شود.

به Dialog دوبعدی برمی‌گردد.

SpatialPopup

پنل در عمق z کمی به عقب فشار داده می‌شود تا یک پنجره بازشو (popup) در ارتفاع بالا نمایش داده شود.

به یک Popup دوبعدی برمی‌گردد.

SpatialElevation

می‌توان SpatialElevationLevel برای افزودن ارتفاع تنظیم کرد.

بدون ارتفاع مکانی نشان می‌دهد.

گفتگوی فضایی

این نمونه‌ای از یک کادر محاوره‌ای است که پس از یک تأخیر کوتاه باز می‌شود. وقتی از SpatialDialog استفاده می‌شود، کادر محاوره‌ای در همان عمق z پنل spatial ظاهر می‌شود و وقتی spatialization فعال باشد، پنل به اندازه ۱۲۵dp به عقب رانده می‌شود. SpatialDialog همچنین می‌تواند زمانی که spatialization فعال نیست استفاده شود، که در این صورت SpatialDialog به معادل دوبعدی خود، 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 کار کنید.

اتصال مدارگردها به طرح‌های فضایی و سایر نهادها

شما می‌توانید یک مدارگرد را به هر موجودیتی که در Compose تعریف شده است، متصل کنید. این شامل تعریف یک مدارگرد در یک طرح‌بندی فضایی از عناصر رابط کاربری مانند SpatialRow ، SpatialColumn یا SpatialBox می‌شود. مدارگرد به نزدیکترین موجودیت والد به جایی که آن را تعریف کرده‌اید، متصل می‌شود.

رفتار مدارگرد با توجه به جایی که آن را اعلام می‌کنید تعیین می‌شود:

  • در یک طرح دوبعدی که در یک 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)
            )
        }
    }
}

نکات کلیدی در مورد کد

  • وقتی یک مدارگرد را خارج از یک طرح دوبعدی تعریف می‌کنید، مدارگرد به نزدیکترین موجودیت والد خود متصل می‌شود. در این حالت، مدارگرد به بالای SpatialRow که در آن تعریف شده است، متصل می‌شود.
  • طرح‌بندی‌های فضایی مانند SpatialRow ، SpatialColumn ، SpatialBox همگی دارای موجودیت‌های بدون محتوا هستند. بنابراین، یک مدارگرد که در یک طرح‌بندی فضایی اعلام شده است، به آن طرح‌بندی متصل می‌شود.

همچنین ببینید