תמיכה בגדלים שונים של מסכים

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

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

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

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

הגדרת שינויים גדולים בפריסה של רכיבים קומפוזביליים ברמת התוכן

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

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

איור 1. גורמי צורה של טלפון, טלפון מתקפל, טאבלט ומחשב נייד

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

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

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

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

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

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true).windowSizeClass
) {
    // Decide whether to show the top app bar based on window size class.
    val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND)

    // MyScreen logic is based on the showTopAppBar boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

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

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

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

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

אפליקציה שמציגה שני חלוניות זו לצד זו.
איור 2. אפליקציה שמציגה פריסה אופיינית של רשימה ופרטים – 1 הוא אזור הרשימה, 2 הוא אזור הפרטים.

ההחלטה לגבי רשימה מפורטת צריכה להיות חלק מהפריסה הכוללת של האפליקציה, כך שההחלטה מועברת מרכיב שאפשר להרכיב ברמת התוכן:

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

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

איור 3. כרטיס צר שמציג רק סמל וכותרת, וכרטיס רחב יותר שמציג את הסמל, הכותרת ותיאור קצר.

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

מכיוון שרכיב ה-Composable הוא לא רכיב Composable ברמת התוכן, אל תשתמשו במדדים של החלון הנוכחי באופן ישיר.

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

משתמשים ברוחב שמוקצה בפועל לרכיב הקומפוזבילי כדי לעבד את עצמו. יש שתי אפשרויות לקבל את הרוחב הזה:

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

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

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

הפיכת כל הנתונים לזמינים לגדלים שונים של מסכים

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

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

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

בדוגמה Card, שימו לב שהערך description תמיד מועבר אל Card. למרות שהתג description משמש רק כשהרוחב מאפשר להציג אותו, התג Card תמיד דורש את התג description, ללא קשר לרוחב הזמין.

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

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

לדוגמה, אפשר להעלות showMore דגל בוליאני כדי לשמור את מצב האפליקציה כששינוי הגודל של התצוגה גורם לפריסה לעבור בין הסתרה להצגה של התוכן:

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

מידע נוסף

מידע נוסף על פריסות דינמיות ב-Compose זמין במקורות המידע הבאים:

אפליקציות לדוגמה

  • CanonicalLayouts הוא מאגר של דפוסי עיצוב מוכחים שמספקים חוויית משתמש אופטימלית במסכים גדולים
  • ב-JetNews מוסבר איך לעצב אפליקציה שמתאימה את ממשק המשתמש שלה כדי לנצל את שטח המסך הזמין.
  • Reply הוא דוגמה אדפטיבית לתמיכה בניידים, בטאבלטים ובמכשירים מתקפלים
  • Now in Android היא אפליקציה שמשתמשת בפריסות דינמיות כדי לתמוך בגדלים שונים של מסכים

סרטונים