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

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

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

במסגרת 'בקצרה' יש שלוש פריסות קומפוזביליות עיקריות:

  • Box: הצבת רכיבים מעל פריט אחר. מתורגמת לRelativeLayout.

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

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

התכונה 'בקצרה' תומכת באובייקטים Scaffold. מיקום של Column, Row Box תכנים קומפוזביליים בתוך אובייקט Scaffold נתון.

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

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

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

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

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

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

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

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. התוכן לא לכלול את הלחצן הנוסף או את הטקסטים העליונים והתחתונים.
  • בקריאה השנייה, הגודל מוערך ל-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 כוללת ממשק API להגדרת סגנונות הטקסט. הגדרת סגנונות טקסט באמצעות fontSize, fontWeight או fontFamily של המחלקה TextStyle.

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

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

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

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

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

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' כוללת הפצה של רכיבים נוספים, כפי שמתואר הטבלה הבאה:

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

לקבלת מידע נוסף על מפרטי עיצוב, עיינו בעיצובים של הרכיבים ערכת עיצוב ב-Figma.