יצירת פריסות ווידג'ט גמישות

כדאי לנסות את הדרך של Compose
‫Jetpack Compose היא ערכת הכלים המומלצת לבניית ממשק משתמש ל-Android. איך יוצרים ווידג'טים באמצעות ממשקי API בסגנון Compose

בדף הזה מוסבר על שיפורים בגודל הווידג'טים ועל גמישות רבה יותר שהוצגו ב-Android 12 (רמת API 31). בנוסף, מוסבר במאמר איך קובעים את הגודל של הווידג'ט.

שימוש בממשקי API משופרים לגדלים ולפריסות של ווידג'טים

החל מ-Android 12 (רמת API‏ 31), אפשר לספק מאפייני גודל מדויקים יותר ופריסות גמישות יותר באמצעות הפעולות הבאות, כפי שמתואר בקטעים הבאים:

  1. מציינים אילוצים נוספים לגבי גודל הווידג'ט.

  2. פריסות רספונסיביות או פריסות מדויקות.

בגרסאות קודמות של Android, אפשר לקבל את טווחי הגודל של הווידג'ט באמצעות התוספים OPTION_APPWIDGET_MIN_WIDTH,‏ OPTION_APPWIDGET_MIN_HEIGHT,‏ OPTION_APPWIDGET_MAX_WIDTH ו-OPTION_APPWIDGET_MAX_HEIGHT, ואז להעריך את הגודל של הווידג'ט, אבל הלוגיקה הזו לא פועלת בכל המצבים. לווידג'טים שמטרגטים Android 12 ואילך, מומלץ לספק פריסות דינמיות או פריסות מדויקות.

ציון אילוצים נוספים לגבי גודל הווידג'ט

ב-Android 12 נוספו ממשקי API שמאפשרים לוודא שהגודל של הווידג'ט מותאם בצורה מהימנה יותר למכשירים שונים עם גדלי מסך שונים.

בנוסף למאפיינים הקיימים minWidth,‏ minHeight,‏ minResizeWidth ו-minResizeHeight, צריך להשתמש במאפיינים החדשים הבאים: appwidget-provider

  • targetCellWidth ו-targetCellHeight: מגדירים את גודל היעד של הווידג'ט במונחים של תאי רשת של מרכז האפליקציות. אם המאפיינים האלה מוגדרים, המערכת משתמשת בהם במקום במאפיינים minWidth או minHeight.

  • maxResizeWidth ו-maxResizeHeight: מגדירים את הגודל המקסימלי שהמשתמש יכול לשנות את הגודל של הווידג'ט.

בדוגמת ה-XML הבאה אפשר לראות איך משתמשים במאפייני הגודל.

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  android:targetCellWidth="3"
  android:targetCellHeight="2"
  android:maxResizeWidth="250dp"
  android:maxResizeHeight="110dp">
</appwidget-provider>

יצירת פריסות רספונסיביות

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

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

בדוגמת הקוד הבאה אפשר לראות איך מספקים רשימה של פריסות.

override fun onUpdate(
    /* ... */
) {
    val smallView = RemoteViews(
        /* ... */
    )
    val tallView = RemoteViews(
        /* ... */
    )
    val wideView = RemoteViews(
        /* ... */
    )

    val viewMapping: Map<SizeF, RemoteViews> = mapOf(
        SizeF(150f, 100f) to smallView,
        SizeF(150f, 200f) to tallView,
        SizeF(215f, 100f) to wideView
    )
    val remoteViews = RemoteViews(viewMapping)

    appWidgetManager.updateAppWidget(id, remoteViews)
}

נניח שלווידג'ט יש את המאפיינים הבאים:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minResizeWidth="160dp"
    android:minResizeHeight="110dp"
    android:maxResizeWidth="250dp"
    android:maxResizeHeight="200dp">
</appwidget-provider>

קטע הקוד שלמעלה אומר:

  • smallView תומך בערכים מ-160dp‏ (minResizeWidth) × 110dp‏ (minResizeHeight) עד 160dp × 199dp (נקודת החיתוך הבאה – 1dp).
  • tallView תומך בערכים מ-160dp × 200dp עד 214dp (נקודת החיתוך הבאה פחות 1) × 200dp.
  • wideView תומך במידות של 215dp × 110dp (minResizeHeight) עד 250dp (maxResizeWidth) × 200dp (maxResizeHeight).

הווידג'ט צריך לתמוך בטווח הגדלים מ-‎minResizeWidth × minResizeHeight עד ‎maxResizeWidth × maxResizeHeight. בטווח הזה, אתם יכולים להחליט מה תהיה נקודת החיתוך למעבר בין פריסות.

דוגמה לפריסה רספונסיבית
איור 1. דוגמה לפריסה רספונסיבית.

הצגת פריסות מדויקות

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

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

  1. ‫Overload AppWidgetProvider.onAppWidgetOptionsChanged(), שמופעל כשקבוצת הגדלים משתנה.

  2. קוראים ל-AppWidgetManager.getAppWidgetOptions(), שמחזירה Bundle שמכילה את המידות.

  3. מגיעים למקש AppWidgetManager.OPTION_APPWIDGET_SIZES מתוך Bundle.

בדוגמה הבאה אפשר לראות איך מספקים פריסות מדויקות.

override fun onAppWidgetOptionsChanged(
    context: Context,
    appWidgetManager: AppWidgetManager,
    id: Int,
    newOptions: Bundle?
) {
    super.onAppWidgetOptionsChanged(context, appWidgetManager, id, newOptions)
    // Get the new sizes.
    val sizes = newOptions?.let {
        BundleCompat.getParcelableArrayList(
            it,
            AppWidgetManager.OPTION_APPWIDGET_SIZES,
            SizeF::class.java
        )
    }
    // Check that the list of sizes is provided by the launcher.
    if (sizes.isNullOrEmpty()) {
        return
    }
    // Map the sizes to the RemoteViews that you want.
    val remoteViews = RemoteViews(sizes.associateWith(::createRemoteViews))
    appWidgetManager.updateAppWidget(id, remoteViews)
}

// Create the RemoteViews for the given size.
private fun createRemoteViews(size: SizeF): RemoteViews {
}

קביעת הגודל של הווידג'ט

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

במסכי הבית של Android יש רשת של מקומות פנויים שבהם המשתמשים יכולים למקם ווידג'טים וסמלים. הפריסה הזו יכולה להשתנות בהתאם למכשיר. לדוגמה, בהרבה מכשירים ניידים יש פריסה של 5x4, ובטאבלטים יכולה להיות פריסה גדולה יותר. כשמוסיפים את הווידג'ט, הוא נמתח כדי לתפוס את המספר המינימלי של התאים, אופקית ואנכית, שנדרשים כדי לעמוד בדרישות של targetCellWidth ו-targetCellHeight במכשירים עם Android 12 ואילך, או בדרישות של minWidth ו-minHeight במכשירים עם Android 11 (רמת API 30) ומטה.

המידות של התאים והשוליים האוטומטיים משתנים בהתאם למכשיר. אפשר להשתמש בטבלה הבאה כדי להעריך את המידות המינימליות של הווידג'ט במכשיר טיפוסי עם רשת של 5x4, בהתאם למספר התאים ברשת שרוצים שהווידג'ט יתפוס:

מספר התאים (רוחב x גובה) הגודל הזמין במצב תצוגה לאורך (dp) הגודל הזמין במצב לרוחב (dp)
1x1 ‫57x102dp ‫127x51dp
‫2x1 ‫130x102dp 269x51dp
‫3x1 ‫203x102dp 412x51dp
4x1 ‫276x102dp ‫554x51dp
‫5x1 ‫349x102dp ‎697x51dp
‫5x2 349x220dp ‫697x117dp
5x3 349x337dp ‫697x184dp
5x4 349x455dp ‫697x250dp
... ... ...
n x m ‪(73n - 16) x (118m - 16) ‫(142n - 15) x (66m - 15)

השתמשו במידות התאים במצב אנכי כדי להזין את הערכים במאפיינים minWidth, minResizeWidth ו-maxResizeWidth. באופן דומה, צריך להשתמש בגדלים של התאים במצב לרוחב כדי להזין את הערכים במאפיינים minHeight, minResizeHeight ו-maxResizeHeight.

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

לדוגמה, אם רוצים לשנות את הרוחב של הווידג'ט עד לתא אחד ב-Google Pixel 4, צריך להגדיר את minResizeWidth ל-56dp לכל היותר, כדי לוודא שהערך של המאפיין minResizeWidth קטן מ-57dp – כי הרוחב של תא הוא לפחות 57dp במצב אנכי. באופן דומה, אם רוצים לשנות את הגובה של הווידג'ט בתא אחד באותו מכשיר, צריך להגדיר את minResizeHeight ל-50dp לכל היותר, כדי לוודא שהערך של המאפיין minResizeHeight קטן מ-51dp – כי הגובה של תא אחד הוא לפחות 51dp במצב לרוחב.

אפשר לשנות את הגודל של כל ווידג'ט בטווח הגדלים שבין המאפיינים minResizeWidth/minResizeHeight לבין maxResizeWidth/maxResizeHeight, כלומר הוא צריך להתאים לכל טווח גדלים ביניהם.

לדוגמה, כדי להגדיר את גודל ברירת המחדל של הווידג'ט במיקום, אפשר להגדיר את המאפיינים הבאים:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:targetCellWidth="3"
    android:targetCellHeight="2"
    android:minWidth="180dp"
    android:minHeight="110dp">
</appwidget-provider>

כלומר, גודל ברירת המחדל של הווידג'ט הוא 3x2 תאים, כפי שצוין במאפיינים targetCellWidth ו-targetCellHeight, או 180×110dp, כפי שצוין במאפיינים minWidth ו-minHeight למכשירים עם Android מגרסה 11 ומטה. במקרה האחרון, הגודל בתאים יכול להשתנות בהתאם למכשיר.

כדי להגדיר את טווחי הגדלים הנתמכים של הווידג'ט, אפשר להגדיר גם את המאפיינים הבאים:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minResizeWidth="180dp"
    android:minResizeHeight="110dp"
    android:maxResizeWidth="530dp"
    android:maxResizeHeight="450dp">
</appwidget-provider>

כמו שצוין במאפיינים הקודמים, אפשר לשנות את הרוחב של הווידג'ט מ-180dp ל-530dp, ואת הגובה מ-110dp ל-450dp. אחרי זה אפשר לשנות את הגודל של הווידג'ט מ-3x2 לתאים בגודל 5x2, כל עוד מתקיימים התנאים הבאים:

val smallView = RemoteViews(context.packageName, R.layout.widget_weather_forecast_small)
val mediumView = RemoteViews(context.packageName, R.layout.widget_weather_forecast_medium)
val largeView = RemoteViews(context.packageName, R.layout.widget_weather_forecast_large)

val viewMapping: Map<SizeF, RemoteViews> = mapOf(
    SizeF(180f, 110f) to smallView,
    SizeF(270f, 110f) to mediumView,
    SizeF(270f, 280f) to largeView
)

appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))

נניח שהווידג'ט משתמש בפריסות רספונסיביות שמוגדרות בקטעי הקוד הקודמים. כלומר, הפריסה שצוינה כ-R.layout.widget_weather_forecast_small משמשת מ-180dp ‏ (minResizeWidth) x‏ 110dp ‏ (minResizeHeight) עד 269x279dp (נקודות הסף הבאות – 1). באופן דומה, ‫R.layout.widget_weather_forecast_medium משמש מ-270x110dp עד 270x279dp, ו-R.layout.widget_weather_forecast_large משמש מ-270x280dp עד ‫530dp (maxResizeWidth) x 450dp (maxResizeHeight).

כשהמשתמש משנה את גודל הווידג'ט, המראה שלו משתנה בהתאם לכל גודל בתאים, כמו בדוגמאות הבאות.

דוגמה לווידג'ט מזג האוויר בגודל הקטן ביותר של רשת 3x2. בממשק המשתמש מוצגים שם המיקום (טוקיו), הטמפרטורה (14°) וסמל שמציין מזג אוויר מעונן חלקית.
איור 2. ‫3x2 R.layout.widget_weather_forecast_small.

דוגמה לווידג'ט של מזג האוויר בגודל 'בינוני' של 4x2. שינוי הגודל של הווידג'ט באופן הזה מתבסס על כל ממשק המשתמש מהגודל הקודם של הווידג'ט, ומוסיף את התווית 'מעונן חלקית' ותחזית של הטמפרטורות מ-16:00 עד 19:00.
איור 3. ‫4x2 R.layout.widget_weather_forecast_medium.

דוגמה לווידג'ט של מזג האוויר בגודל 'בינוני' של 5x2. שינוי הגודל של הווידג'ט בדרך הזו יוצר ממשק משתמש זהה לגודל הקודם, רק שהוא נמתח באורך של תא אחד כדי לתפוס יותר מקום אופקי.
איור 4. ‫5x2 R.layout.widget_weather_forecast_medium.

דוגמה לווידג'ט של מזג האוויר בגודל 'גדול' (5x3). שינוי הגודל של הווידג'ט
            בדרך הזו מתבסס על כל רכיבי ממשק המשתמש מהגדלים הקודמים של הווידג'ט,
            ומוסיף תצוגה בתוך הווידג'ט שמכילה תחזית של מזג האוויר
            ביום שלישי וביום רביעי. סמלים שמציינים מזג אוויר שטוף שמש או גשום, וטמפרטורות גבוהות ונמוכות לכל יום.
איור 5. ‫5x3 R.layout.widget_weather_forecast_large.

דוגמה לווידג'ט של מזג האוויר בגודל 'גדול' (5x4). שינוי הגודל של הווידג'ט
            בדרך הזו מתבסס על כל ממשק המשתמש מהגדלים הקודמים של הווידג'ט,
            ומוסיף את ימי חמישי ושישי (ואת הסמלים המתאימים שלהם
            שמציינים את סוג מזג האוויר וגם את הטמפרטורה המקסימלית והמינימלית
            לכל יום).
איור 6. ‫5x4 R.layout.widget_weather_forecast_large.