פיתוח ממשק משתמש באמצעות 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
        )
    }
}

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

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

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

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

אפשר לקשר אובייקט מסוג orbiter לכל ישות שהוגדרה ב-Compose. לשם כך, צריך להצהיר על רכיב מסלול במיקום מרחבי של רכיבי ממשק משתמש, כמו SpatialRow,‏ SpatialColumn או SpatialBox. ה-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)
            )
        }
    }
}

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

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

למידע נוסף