פיתוח ממשק משתמש באמצעות Jetpack Compose ל-XR

באמצעות Jetpack Compose for XR, אפשר ליצור באופן דקלרטיבי את הפריסה ואת ממשק המשתמש המרחבי באמצעות מושגים מוכרים של Compose, כמו שורות ועמודות. כך תוכלו להרחיב את ממשק המשתמש הקיים של Android למרחב תלת-ממדי או ליצור אפליקציות תלת-ממדיות חדשות לגמרי.

אם אתם מפתחים אפליקציה קיימת שמבוססת על תצוגות של Android, יש לכם כמה אפשרויות פיתוח. אפשר להשתמש בממשקי API לתאימות הדדית, להשתמש ב-Compose וב-Views יחד או לעבוד ישירות עם ספריית SceneCore. מידע נוסף זמין במדריך לעבודה עם תצוגות.

מידע על מרחבים משניים ורכיבים במרחב

כשכותבים אפליקציה ל-Android XR, חשוב להבין את המושגים מרחב משנה ורכיבים מרחבי.

מידע על מרחב משנה

כשמפתחים ל-Android XR, צריך להוסיף מרחב משנה לאפליקציה או לפריסה. מרחב משנה הוא מחיצה של מרחב תלת-ממדי באפליקציה, שבה אפשר למקם תוכן תלת-ממדי, ליצור פריסות תלת-ממדיות ולהוסיף עומק לתוכן דו-ממדי. המרחב המשני ימומש רק כשהמיקום במרחב הווירטואלי יופעל. במרחב הבית או במכשירים שאינם XR, המערכת מתעלמת מכל קוד במרחב המשנה הזה.

יש שתי דרכים ליצור מרחב משנה:

  • setSubspaceContent(): הפונקציה הזו יוצרת מרחבים משניים ברמת האפליקציה. אפשר להפעיל את הפונקציה הזו בפעילות הראשית באותו אופן שבו משתמשים ב-setContent(). המרחב המשנה ברמת האפליקציה לא מוגבל בגובה, ברוחב ובעומק, ובעצם מספק קנבס אינסופי לתוכן מרחבי.
  • Subspace: אפשר למקם את הרכיב הזה בכל מקום בהיררכיית ממשק המשתמש של האפליקציה, וכך לשמור על פריסות לממשק משתמש דו-ממדי ומרחבי בלי לאבד את ההקשר בין הקבצים. כך קל יותר לשתף דברים כמו ארכיטקטורת אפליקציה קיימת בין XR לבין גורמי צורה אחרים, בלי שתצטרכו להעביר את המצב דרך כל עץ ממשק המשתמש או לתכנן מחדש את האפליקציה.

מידע נוסף זמין במאמר הוספת מרחב משנה לאפליקציה.

מידע על רכיבים במרחב

רכיבים שניתנים ליצירה במרחב משנה: אפשר להציג את הרכיבים האלה רק במרחב משנה. צריך להוסיף את התגים האלה בתוך Subspace או setSubspaceContent() לפני שממקמים אותם בפריסה דו-ממדית. SubspaceModifier מאפשר להוסיף מאפיינים כמו עומק, סטייה ומיקום לרכיבים הניתנים לשילוב במרחב המשנה.

רכיבים אחרים במרחב לא מחייבים קריאה בתוך מרחבים משניים. הם מורכבים מרכיבים דו-ממדיים רגילים שמקובצים בתוך מאגר מרחבי. אפשר להשתמש ברכיבים האלה בפריסות דו-ממדיות או תלת-ממדיות, אם הם מוגדרים בשתיהן. אם התכונה לא מופעלת, המערכת תתעלם מהתכונות המרחביות שלהם והן יחזרו לגרסאות הדו-ממדיות שלהן.

יצירת לוח מרחבי

SpatialPanel הוא מרחב משנה שאפשר ליצור ממנו רכיבים, ומאפשר להציג תוכן של אפליקציות. לדוגמה, אפשר להציג הפעלת וידאו, תמונות סטילס או כל תוכן אחר בחלונית מרחבית.

דוגמה ללוח של ממשק משתמש מרחבי

אפשר להשתמש ב-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
        )
    }
}

נקודות עיקריות לגבי הקוד

  • מאחר שממשקי ה-API של SpatialPanel הם רכיבים שניתנים ליצירה במרחב משנה, צריך להפעיל אותם בתוך Subspace או setSubspaceContent(). קריאה אליהן מחוץ למרחב משנה תגרום להשלכת חריגה.
  • הגודל של SpatialPanel הוגדר באמצעות המפרטים height ו-width ב-SubspaceModifier. אם משמיטים את המפרטים האלה, הגודל של החלונית נקבע לפי המדידות של התוכן שלה.
  • מאפשרים למשתמש לשנות את הגודל או להזיז את החלונית על ידי הוספת המשתנים המשנה movable או resizable.
  • הנחיות לעיצוב של חלוניות מרחבי – מידע על הגודל והמיקום. מידע ספציפי יותר על הטמעת קוד זמין במאמרי העזרה שלנו.

איך פועלת פונקציית שינוי של מרחבים משניים ניידים

כשמשתמש מעביר חלונית הרחק ממנו, ברירת המחדל היא שמשתנה של מרחבים משניים ניידים ישנה את הגודל של החלונית באופן דומה לאופן שבו המערכת משנה את הגודל של חלוניות במרחב הבית. כל תוכן הצאצאים יורש את ההתנהגות הזו. כדי להשבית את האפשרות הזו, מגדירים את הפרמטר scaleWithDistance לערך false.

יצירת כלי ניווט במסלול

רכיב מסלול הוא רכיב של ממשק משתמש מרחבי. הוא מיועד להתחבר ללוח מרחבי, לפריסה או לישות אחרת תואמת. בדרך כלל, רכיב מסלול מכיל פריטים של ניווט ופעולות לפי הקשר שקשורים לישות שאליה הוא מקובע. לדוגמה, אם יצרתם לוח מרחבי כדי להציג תוכן וידאו, תוכלו להוסיף לחצני בקרה להפעלת וידאו בתוך כלי ניווט.

דוגמה למכשיר אורביטר

כפי שמוצג בדוגמה הבאה, קוראים ל-orbiter בתוך הפריסה הדו-ממדית ב-SpatialPanel כדי לעטוף את אמצעי הבקרה של המשתמש, כמו ניווט. הפעולה הזו מחלצת אותם מתצוגת ה-2D ומצרפת אותם לחלונית המרחבית בהתאם להגדרה שלכם.

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

@Composable
fun OrbiterExample() {
    Orbiter(
        position = OrbiterEdge.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
                )
            }
        }
    }
}

נקודות עיקריות לגבי הקוד

  • מאחר שרכיבי Orbiter הם רכיבים מרחביים של ממשק המשתמש, אפשר לעשות שימוש חוזר בקוד בתצוגות 2D או 3D. בפריסה דו-ממדית, האפליקציה תיצור רק את התוכן בתוך הלוויין ותתעלם מהלוויין עצמו.
  • מידע נוסף על השימוש ב-Orbiters ועל תכנון שלהם זמין בהנחיות העיצוב שלנו.

הוספת כמה פאנלים מרחביים לפריסה מרחבית

אפשר ליצור כמה לוחות מרחביים ולהציב אותם בפריסה מרחבית באמצעות 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 כדי להתאים אישית את הפריסה.
  • בפריסות עם כמה פאנלים בשורה, מומלץ להגדיר רדיוס עקומה של 825dp באמצעות SubspaceModifier כדי שהפאנלים יקיפו את המשתמש. לפרטים נוספים, אפשר לעיין בהנחיות לעיצוב.

שימוש בנפח כדי למקם אובייקט תלת-ממדי בפריסה

כדי להציב אובייקט תלת-ממדי בפריסה, צריך להשתמש במרחב משנה שאפשר ליצור ממנו אובייקטים שנקרא נפח אחסון. דוגמה לכך מופיעה בהמשך.

דוגמה לאובייקט תלת-ממדי בפריסה

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

@Composable
fun ObjectInAVolume(show3DObject: Boolean) {

מידע נוסף

הוספת משטח לתוכן של תמונות או סרטונים

SpatialExternalSurface הוא מרחב משנה שאפשר ליצור ולנהל בו את Surface, שבו האפליקציה יכולה לצייר תוכן, כמו תמונה או סרטון. SpatialExternalSurface תומך בתוכן סטריאופוליפי או מונוסקופי.

בדוגמה הזו מוסבר איך לטעון סרטון סטריאופוני זה לצד זה באמצעות Media3 Exoplayer ו-SpatialExternalSurface:

@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 עם רינדור של אפליקציות או עם פענוח של וידאו.
  • לא ניתן להציג את הרכיב הזה מול חלוניות אחרות, לכן לא כדאי להשתמש במודיפיקטורים שניתן להזיז אם יש חלוניות אחרות בפריסה.

הוספת רכיבים אחרים של ממשק משתמש מרחבי

אפשר למקם רכיבי ממשק משתמש מרחבי בכל מקום בהיררכיית ממשק המשתמש של האפליקציה. אפשר לעשות שימוש חוזר ברכיבים האלה בממשק המשתמש הדו-מימדי, והמאפיינים המרחביים שלהם יהיו גלויים רק כשהיכולות המרחביות מופעלות. כך תוכלו להוסיף הרשאות גישה לתפריטים, לתיבת דו-שיח ולרכיבים אחרים בלי לכתוב את הקוד פעמיים. כדאי לעיין בדוגמאות הבאות לממשק משתמש מרחבי כדי להבין טוב יותר איך להשתמש ברכיבים האלה.

רכיב UI

כשהמיקום מופעל

בסביבה דו-ממדית

SpatialDialog

החלונית תזוז מעט לאחור ב-z-depth כדי להציג תיבת דו-שיח מוגבהת

מעבר לתצוגה דו-ממדית Dialog.

SpatialPopup

החלונית תזוז מעט לאחור ב-z-depth כדי להציג חלון קופץ מוגבה

המערכת חוזרת לתצוגה דו-ממדית Popup.

SpatialElevation

אפשר להגדיר את SpatialElevationLevel כדי להוסיף גובה.

תוכניות ללא תצוגה מרחבית.

SpatialDialog

זו דוגמה לתיבת דו-שיח שנפתחת לאחר השהיה קצרה. כשמשתמשים ב-SpatialDialog, תיבת הדו-שיח מופיעה באותו עומק z כמו החלונית המרחבית, והחלונית מוסטת לאחור ב-125dp כשהמיקום המרחבי מופעל. אפשר להשתמש ב-SpatialDialog גם כשהמיקום הגיאוגרפי לא מופעל. במקרה כזה, 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 ועם תרשים הסצינה באמצעות ממשקי ה-API של SceneCore.

עיגון של רכיבי מעקב אחר תנועה למיקומים במרחב ולישויות אחרות

אפשר לקשר אובייקט מסוג orbiter לכל ישות שהוגדרה ב-Compose. לשם כך, צריך להצהיר על רכיב מסלול במיקום מרחבי של רכיבי ממשק משתמש, כמו SpatialRow,‏ SpatialColumn או SpatialBox. ה-orbiter יאחז בישות ההורה הקרובה ביותר למיקום שבו הכרזתם עליו.

ההתנהגות של הלוויין נקבעת לפי המיקום שבו מגדירים אותו:

  • בפריסה דו-ממדית שמקיפה את SpatialPanel (כפי שמוצג בקטע הקוד הקוד הקודם), ה-Orbiter מוצמד ל-SpatialPanel הזה.
  • ב-Subspace, ה-orbiter מקובע לישות ההורה הקרובה ביותר, שהיא הפריסה המרחבית שבה ה-orbiter מוצהר.

הדוגמה הבאה מראה איך לקשר כלי ניווט למסגרת מרובע במרחב:

Subspace {
    SpatialRow {
        Orbiter(
            position = OrbiterEdge.Top,
            offset = EdgeOffset.inner(8.dp),
            shape = SpatialRoundedCornerShape(size = CornerSize(50))
        ) {
            Text(
                "Hello World!",
                style = MaterialTheme.typography.h2,
                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 משויכות ישויות ללא תוכן. לכן, עצם שמוצג במסלול שהוכרז בפריסה מרחבית מקובע לפריסה הזו.

למידע נוסף