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

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

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

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

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

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

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

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

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

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

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

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

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

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Perform logic on the size class to decide whether to show the top app bar.
    val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT

    // MyScreen knows nothing about window sizes, and performs logic based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@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 תמיד הוא נדרש, ללא קשר לרוחב הזמין.

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

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

מידע נוסף

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

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

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

סרטונים