使用 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 有助於改善效能,並透過清單和 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
        // ...
    }
}

使用此模式時,請確認:

  • 系統會根據內容大小,正確定義大小下限和上限的中繼資料值
  • 內容在預期大小範圍內具有足夠的彈性。

一般來說,您應該在下列情況下使用此模式:

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。內容包含額外按鈕,但不包含頂端和底部的文字。
  • 在第三次呼叫中,size 會評估為 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。這可能會導致效能問題,並在內容複雜時導致 UI 跳躍。
  • 可用的大小可能因啟動器的實作方式而異。舉例來說,如果啟動器未提供大小清單,系統會使用最小可能的大小。
  • 在 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) }
        )
    }
}

您也可以為 CheckBoxSwitchRadioButton 提供 colors 屬性,自訂顏色:

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 中查看這個設計套件中的元件設計。

如要進一步瞭解標準版面配置,請參閱「標準小工具版面配置」。