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

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

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

ל-Glance יש שלוש פריסות קומפוזביליות עיקריות:

  • ‫Box: הצבת רכיבים אחד מעל השני. התרגום הוא RelativeLayout.

  • ‫Column: הצבת רכיבים אחד אחרי השני בציר האנכי. המשמעות היא LinearLayout בכיוון אנכי.

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

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

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

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

בדוגמה הבאה אפשר לראות איך ליצור 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,
    itemId = { person -> person.id.hashCode().toLong() }) { person ->
    Text(person.name)
}

גלילה מהירה

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

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


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

  • מעדכנים את התלות של Glance לגרסה 1.3.0-alpha02 ואילך.
  • מגדירים את compileSdk ל-37 ומעלה, כי גלילה מהירה נתמכת במכשירים עם Android מגרסה 17 ומעלה.
  • הגדרת LazyColumn באמצעות VerticalScrollMode. אם המכשיר תומך בגלילה מהירה, משתמשים ב-SnapScrollMatchHeight. אחרת, צריך להשתמש ב-Normal.

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

@Composable
fun SnapScrollLayout() {
    val height = LocalSize.current.height
    val items = listOf(
        ColorItem(Color.Red, "Red"),
        ColorItem(Color.Yellow, "Yellow"),
        ColorItem(Color.Blue, "Blue")
    )

    val scrollMode = if (Build.VERSION.SDK_INT >= 37) {
        VerticalScrollMode.SnapScrollMatchHeight(height)
    } else {
        VerticalScrollMode.Normal
    }

    LazyColumn(
        verticalScrollMode = scrollMode
    ) {
        items(items) { item ->
            ColorCard(item, height)
        }
    }
}

@Composable
private fun ColorCard(item: ColorItem, height: Dp) {
    Box(
        modifier = GlanceModifier
            .background(item.color)
            .fillMaxWidth()
            .height(height),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = item.name,
            modifier = GlanceModifier.background(Color.White)
        )
    }
}

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

גישה למשאבים

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

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

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

רכיבים ושיטות של Composable מקבלים משאבים באמצעות "ספק", כמו 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",
)

טקסט של הכינוי

‫Glance 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. Glance תומך בתאימות לדורות קודמים בסוגים הבאים של כפתורים מורכבים:

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

var isApplesChecked by remember { mutableStateOf(false) }
var isEnabledSwitched by remember { mutableStateOf(false) }
var isRadioChecked by remember { mutableIntStateOf(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"
)

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

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)
    ),

    )

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

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

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

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

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