使用 Glance 建構 UI

本頁面說明如何使用現有的 Glance 元件,處理大小並透過 Glance 提供彈性的回應式版面配置。

使用BoxColumnRow

Glance 有三個主要的可組合函式版面配置:

  • Box:將元素放在另一個元素上。這會轉譯為 RelativeLayout

  • Column:將元素沿著垂直軸相鄰。會轉譯為垂直方向的 LinearLayout

  • Row:將元素沿著水平軸相鄰。會轉譯為水平方向的 LinearLayout

Glance 支援 Scaffold 物件。將 ColumnRowBox 可組合項放入指定的 Scaffold 物件中。

欄、列和方塊版面配置的圖片。
圖 1 含 Column、Row 和 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 有助於提高效能,並透過清單位置和 Android 12 以上版本更新 appWidget (例如在清單中新增或移除項目時) 提升效能。以下範例說明如何指定 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
        // ...
    }
}

使用這種模式時,請確認以下事項:

  • 中繼資料值的最小和最大尺寸,是根據內容大小正確定義。
  • 在預期大小範圍內,內容的靈活度夠高。

一般來說,只要符合下列任一條件,建議您使用這個模式:

a) AppWidget 有固定大小,或 b) 在調整大小時不會變更內容。

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 調整大小時更流暢。

下表根據 SizeModeAppWidget 可用的大小顯示大小值:

可用大小 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 等同於提供確切的版面配置,每當可用的 AppWidget 大小有所變更,就會要求 GlanceAppWidget 內容,例如當使用者調整主畫面中的 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)

建議您直接提供資源 ID 以縮減最終 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",
)

處理文字

Glance 1.1.0 包含用來設定文字樣式的 API。請使用 TextStyle 類別的 fontSizefontWeightfontFamily 屬性設定文字樣式。

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 屬性提供給 CheckBoxSwitchRadioButton 來自訂顏色:

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 Scaffold 和標題列是同一個示範。

如要進一步瞭解設計細節,請參閱 Figma 上這個設計套件中的元件設計。