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

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

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

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

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

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

רכיבי 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. כרטיס צר שמציג רק סמל וכותרת, וכרטיס רחב יותר שמציג את הסמל, הכותרת ותיאור קצר.

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

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

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

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

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

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

סרטונים