פיתוח ממשק משתמש באמצעות 'בקצרה'

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

שימוש ב-Box, ב-Column וב-Row

ב-Glance יש שלושה פריסות עיקריות שניתן לשלב:

  • Box: הצבת רכיבים זה על גבי זה. הוא מתורגם ל-RelativeLayout.

  • Column: הצבת רכיבים זה אחרי זה בציר האנכי. הוא מתורגם ל-LinearLayout בכיוון אנכי.

  • Row: הצבת רכיבים זה אחרי זה בציר האופקי. הוא מתורגם ל-LinearLayout בכיוון אופקי.

ב-Glance יש תמיכה באובייקטים מסוג Scaffold. מניחים את הרכיבים הניתנים להרכבה של Column,‏ Row ו-Box בתוך אובייקט Scaffold נתון.

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

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

בדוגמה הבאה מוסבר איך ליצור Row שמחלק את הצאצאים שלו באופן שווה אופקית, כפי שמוצג באיור 1:

Row(modifier = GlanceModifier.fillMaxWidth().padding(16.dp)) {
    val modifier = GlanceModifier.defaultWeight()
    Text("first", modifier)
    Text("second", modifier)
    Text("third", modifier)
}

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

שימוש בפריסות שניתן לגלול בהן

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

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

אפשר לציין את מספר הפריטים:

// Remember to import Glance Composables
// import androidx.glance.appwidget.layout.LazyColumn

LazyColumn {
    items(10) { index: Int ->
        Text(
            text = "Item $index",
            modifier = GlanceModifier.fillMaxWidth()
        )
    }
}

יש לספק פריטים בודדים:

LazyColumn {
    item {
        Text("First Item")
    }
    item {
        Text("Second Item")
    }
}

מספקים רשימה או מערך של פריטים:

LazyColumn {
    items(peopleNameList) { name ->
        Text(name)
    }
}

אפשר גם להשתמש בשילוב של הדוגמאות הקודמות:

LazyColumn {
    item {
        Text("Names:")
    }
    items(peopleNameList) { name ->
        Text(name)
    }

    // or in case you need the index:
    itemsIndexed(peopleNameList) { index, person ->
        Text("$person at index $index")
    }
}

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

items(items = peopleList, key = { person -> person.id }) { person ->
    Text(person.name)
}

הגדרה של SizeMode

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

SizeMode.Single

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

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Single

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the minimum size or resizable
        // size defined in the App Widget metadata
        val size = LocalSize.current
        // ...
    }
}

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

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

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

א) ל-AppWidget יש גודל קבוע, או ב) התוכן שלו לא משתנה כשמשנים את הגודל.

SizeMode.Responsive

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

לדוגמה, ביעד AppWidget אפשר להגדיר שלוש מידות ואת התוכן שלהן:

class MyAppWidget : GlanceAppWidget() {

    companion object {
        private val SMALL_SQUARE = DpSize(100.dp, 100.dp)
        private val HORIZONTAL_RECTANGLE = DpSize(250.dp, 100.dp)
        private val BIG_SQUARE = DpSize(250.dp, 250.dp)
    }

    override val sizeMode = SizeMode.Responsive(
        setOf(
            SMALL_SQUARE,
            HORIZONTAL_RECTANGLE,
            BIG_SQUARE
        )
    )

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be one of the sizes defined above.
        val size = LocalSize.current
        Column {
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            }
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width >= HORIZONTAL_RECTANGLE.width) {
                    Button("School")
                }
            }
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "provided by X")
            }
        }
    }
}

בדוגמה הקודמת, השיטה provideContent נקראת שלוש פעמים וממופה לגודל שהוגדר.

  • בקריאה הראשונה, הערך של הגודל הוא 100x100. התוכן לא כולל את הלחצן הנוסף או את הטקסטים העליון והתחתון.
  • בקריאה השנייה, הערך של size הוא 250x100. התוכן כולל את הלחצן הנוסף, אבל לא את הטקסטים העליון והתחתון.
  • בקריאה השלישית, הערך של הגודל הוא 250x250. התוכן כולל את הלחצן הנוסף ואת שני הטקסטים.

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

בטבלה הבאה מוצג הערך של הגודל, בהתאם לגודל הזמין של SizeMode ושל AppWidget:

גודל זמין 105 x 110 203 x 112 72 x 72 203 x 150
SizeMode.Single 110 x 110 110 x 110 110 x 110 110 x 110
SizeMode.Exact 105 x 110 203 x 112 72 x 72 203 x 150
SizeMode.Responsive 80 x 100 80 x 100 80 x 100 150 x 120
* הערכים המדויקים הם לצורכי הדגמה בלבד.

SizeMode.Exact

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

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

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Exact

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the size of the AppWidget
        val size = LocalSize.current
        Column {
            Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width > 250.dp) {
                    Button("School")
                }
            }
        }
    }
}

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

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

באופן כללי, כדאי להשתמש במצב הזה אם אי אפשר להשתמש ב-SizeMode.Responsive (כלומר, לא ניתן להשתמש בקבוצה קטנה של פריסות רספונסיביות).

גישה למשאבים

אפשר להשתמש ב-LocalContext.current כדי לגשת לכל משאב של Android, כפי שמתואר בדוגמה הבאה:

LocalContext.current.getString(R.string.glance_title)

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

רכיבים ותכונות מורכבות ושיטות מקבלים משאבים באמצעות 'ספק', כמו ImageProvider, או באמצעות שיטת עומס יתר כמו GlanceModifier.background(R.color.blue). לדוגמה:

Column(
    modifier = GlanceModifier.background(R.color.default_widget_background)
) { /**...*/ }

Image(
    provider = ImageProvider(R.drawable.ic_logo),
    contentDescription = "My image",
)

טיפול בטקסט

גרסה 1.1.0 של Glance כוללת ממשק API להגדרת סגנונות הטקסט. מגדירים סגנונות טקסט באמצעות המאפיינים fontSize,‏ fontWeight או fontFamily של הכיתה TextStyle.

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

Text(
    style = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 18.sp,
        fontFamily = FontFamily.Monospace
    ),
    text = "Example Text"
)

הוספת לחצנים מורכבים

לחצנים מורכבים הושקעו ב-Android 12. ב-Glance יש תמיכה בתאימות לאחור בלחצנים מורכבים מהסוגים הבאים:

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

var isApplesChecked by remember { mutableStateOf(false) }
var isEnabledSwitched by remember { mutableStateOf(false) }
var isRadioChecked by remember { mutableStateOf(0) }

CheckBox(
    checked = isApplesChecked,
    onCheckedChange = { isApplesChecked = !isApplesChecked },
    text = "Apples"
)

Switch(
    checked = isEnabledSwitched,
    onCheckedChange = { isEnabledSwitched = !isEnabledSwitched },
    text = "Enabled"
)

RadioButton(
    checked = isRadioChecked == 1,
    onClick = { isRadioChecked = 1 },
    text = "Checked"
)

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

class MyAppWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val myRepository = MyRepository.getInstance()

        provideContent {
            val scope = rememberCoroutineScope()

            val saveApple: (Boolean) -> Unit =
                { scope.launch { myRepository.saveApple(it) } }
            MyContent(saveApple)
        }
    }

    @Composable
    private fun MyContent(saveApple: (Boolean) -> Unit) {

        var isAppleChecked by remember { mutableStateOf(false) }

        Button(
            text = "Save",
            onClick = { saveApple(isAppleChecked) }
        )
    }
}

אפשר גם לספק את המאפיין colors ל-CheckBox, ל-Switch ול-RadioButton כדי להתאים אישית את הצבעים שלהם:

CheckBox(
    // ...
    colors = CheckboxDefaults.colors(
        checkedColor = ColorProvider(day = colorAccentDay, night = colorAccentNight),
        uncheckedColor = ColorProvider(day = Color.DarkGray, night = Color.LightGray)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked }
)

Switch(
    // ...
    colors = SwitchDefaults.colors(
        checkedThumbColor = ColorProvider(day = Color.Red, night = Color.Cyan),
        uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Magenta),
        checkedTrackColor = ColorProvider(day = Color.Blue, night = Color.Yellow),
        uncheckedTrackColor = ColorProvider(day = Color.Magenta, night = Color.Green)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked },
    text = "Enabled"
)

RadioButton(
    // ...
    colors = RadioButtonDefaults.colors(
        checkedColor = ColorProvider(day = Color.Cyan, night = Color.Yellow),
        uncheckedColor = ColorProvider(day = Color.Red, night = Color.Blue)
    ),

)

רכיבים נוספים

גרסה 1.1.0 של Glance כוללת את השקת הרכיבים הנוספים שמפורטים בטבלה הבאה:

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

מידע נוסף על פרטי העיצוב זמין בערכת העיצוב הזו ב-Figma.

מידע נוסף על פריסות קנוניות זמין במאמר פריסות קנוניות של ווידג'טים.