在 Android 12 中構建更現代的應用 Widget

Android開發者發表於2022-01-13

從 2008 年開始,Widget 就一直是 Android 系統的一個重要組成部分,也是自定義主螢幕的一個重要方面。您可以將 Widget 理解為一個 "一目瞭然" 的應用檢視,讓使用者在無需從主螢幕開啟應用的前提下,就能對應用資料和核心功能一覽無餘。但是從 Android 推出至今,AppWidget 的 API 基本就沒有什麼大的變化,從 2012 年到 2021 年更是隻有一個 Android 版本包含了對 AppWidget API 的更新。而隨著 Android 12 的推出,也帶來了 Widget API 一些亟需改進的更新。

本文我們就來介紹一下 Android 12 中帶來了哪些關於 Widget API 的更新,以及有哪些好用的工具可以讓開發應用 Widget 變得更加出色。如果您更喜歡通過視訊瞭解此內容,請在此處檢視:

https://www.bilibili.com/vide...

△ 在 Android 12 中構建更現代的應用 Widget

Widget 工作原理

Widget 執行在一個名為 AppWidgetHost 的遠端程式中,比如 Home Screen Launcher,也正因如此,它的執行受到了一些限制。我們來看看 Widget 的工作原理。

在前端,應用首先註冊 AppWidgetProvider 來定義 Widget 行為,以及註冊 AppWidgetProviderInfo 來定義後設資料。然後 AndroidManifest 引用這些資訊,讓作業系統通過 AndroidManifest 讀取後設資料,例如 Widget 初始的佈局和預設尺寸,並提供 Widget 的預覽,緊接著,provider 會使用連結賬戶來更新佈局並對 Widget 進行更新。這裡需要注意的是,應用於 Widget 的構建次數有限,所以作業系統是通過接收方的廣播事件 (包含了更新資訊) 對 Widget 進行更新,這也意味著 Widget 是定期接收來自應用的資訊進行更新的。

API

Android 12 的推出帶來了很多關於 AppWidget API 的更新,本文不會對所有的 API 一一介紹,而是重點介紹幾個對 Widget 構建非常有用的 API。

實現圓角

在 Android 12 中許多關鍵的介面元素都開始採用圓角設計,為了使 AppWidget 與其他系統元件樣式之間看起來一致,Android 12 引入了 system_app_widget_background_radiussystem_app_widget_inner_radius 兩個新的系統引數實現圓角,前一個引數是用來設定 Widget 的圓角半徑,後一個則是設定 Widget 內檢視的圓角半徑。要使用這些引數,只需要定義一個設定了系統引數 corner 的可繪製物件即可,如程式碼所示:

// res/drawable/app_widget_background.xml
<shape android:shape="rectangle">
    <corners android:radius="@android:dimen/system_app_widget_background_radius">
    …
</shape>

// res/drawable/app_widget_inner_view_background.xml
<shape android:shape="rectangle">
    <corners android:radius="@android:dimen/system_app_widget_inner_radius">
    …
</shape>

然後將可繪製物件應用於 Widget 的外部容器,這樣做可將系統引數提供的圓角半徑應用於 Widget 背景中。同樣,將內部檢視的可繪製物件應用於表示 Widget 內部容器的佈局,如程式碼所示:

// res/layout/widget_layout.xml
<LinearLayout
    android:background=”@drawable/app_widget_background”
…>
    <LinearLayout
        android:background=”@drawable/app_widget_inner_view_background”
    …>
    </LinearLayout>
</LinearLayout>

圖左:  Widget 圓角;圖右: 內檢視圓角

△ 圖左: Widget 圓角;圖右: 內檢視圓角

從效果中我們可以看到 Widget 當前內部容器的圓角半徑要小於外部容器,這就是新引數的使用方法。

動態顏色

正如我們之前在 Google I/O 大會上宣佈的那樣,從 Android 12 開始,Widget 可以為按鈕、背景及其他元件使用裝置主題顏色,包括淺色主題和深色主題。這樣可使過渡更流暢,而且還能在不同的 Widget 之間保持一致。

我們新增了動態顏色 API,您可直接獲取並使用 Pixel 裝置系統上提供的主題背景、顏色等引數,從而讓 Widget 同主螢幕的樣式保持一致:

// res/layout/widget_layout.xml
<LinearLayout
    android:theme="@android:style/Theme.DeviceDefault.DayNight"
    android:background="?android:attr/colorBackground">
    <ImageView
        android:tint="?android:attr/colorAccent" />
    …
</LinearLayout>

您可以看到,當設定了主題屬性之後,Widget 直接從系統桌布中提取了主色,並將其應用於深色和淺色主題背景中。

響應式佈局

Android 12 引入了新的 API 來實現響應式佈局,可以隨著 Widget 的尺寸調整,自動切換到不同的佈局。如下圖所示,使用者可以通過拖動來任意更改 Widget 的尺寸,Widget 也會根據尺寸的不同而動態更新所要顯示的內容。

那麼如何做到讓 Widget 隨著尺寸的變化而動態更新顯示內容呢,用如下程式碼舉例,我們定義了三個不同的引數,分別包含最小支援寬度和高度,以及在此大小範圍內對應的 RemoteView,系統會自動根據實際的尺寸而自動對 Widget 進行調整。

val viewMapping: Map<SizeF, RemoteViews> = mapof(
    SizeF(180.0f, 110.0f) to RemoteViews(
        context. packageName,
        R.layout.widget_small
    ),
    SizeF (270.0f, 110.0f) to RemoteViews(
        context.packageName,
        R.layout.widget_medium
    ),
    SizeF(270.0f, 280.0f) to RemoteViews(
        context.packageName,
        R.layout.widget_large
    )
)
appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))

Android 12 中還提供了新的 targetCellWidthtargetCellHeight 屬性,這些屬性指定了 Widget 置於主螢幕中時預設的較大單元格尺寸。在 Android 12 之前,可以使用 minWidgetminHeight 屬性,它們指定了以 dp 為單位的預設 Widget 尺寸,我們建議同時指定這兩個屬性以保持向後相容。如果您的 Widget 是可調整尺寸的,那麼還可以使用 Android 12 提供的 minResizeWidget/Height 和 maxResizeWidget/Height 屬性來限制 Widget 的可調整尺寸範圍。

<appwidget-provider
    android:targetCellWidth="3"
    android: targetCellHeight="2"
    android:minWidth="140dp"
    android:minHeight="110dp"
    android:maxResizeWidth="570dp"
    android:maxResizeHeight="450dp"
    android:minResizeWidth="140dp"
    android:minResizeHeight="110dp"
    …>

Widget 選擇器

Android 12 還改進了 Widget 選擇器的使用體驗,引入了兩個新的屬性,第一個屬性是 description,它對 Widget 選擇器的作用進行了描述說明,通過它可以瞭解 Widget 的作用;另一個是 previewLayout,它指定了 Widget 選擇器中展示的 XML 佈局。實際上在 Android 12 之前可以使用 previewImage 屬性來指定靜態資源達到類似效果,但是 previewLayout 相比較來說更加精確和方便。另外,由於這些預覽都是在執行時構建的,因此也可以動態適配裝置的主題。

<appwidget-provider
    android:description=
        "@string/app_widget_weather_description"
    android:previewLayout=
        "@layout/widget_weather_forecast_small"
…
/>

△ description 屬性

△ description 屬性

△ previewLayout 屬性

△ previewLayout 屬性

目前已經介紹了很多 Android 12 引入的新 API,相信不久之後就會看到越來越多的應用採用新 API 構建出更現代的 Widget 使用體驗。

Glance

要構建出色的 Widget,除了需要用到目前更現代的 API 之外,我們還需要更現代、更出色的工具來幫助我們,Glance 就是這麼一個出色的工具,它也加入到了 Jetpack 大家庭中。Glance 是由 Compose Runtime 提供支援的 API,通過它就可以使用 Compose 風格的語法來建立 AppWidget,這也意味著您可以通過 Glance 以 composable 構建介面,並將其轉換為遠端檢視顯示到 Widget 中,同時還能用到前文中提到的 Android 12 的新 API,並儘可能的讓其向後相容。另外,Glance 還會負責一些 Widget 生命週期以及其他一些常見的操作,聽上去是不是覺得非常方便。

△ Glance 結構示意圖

△ Glance 結構示意圖

接下來我們介紹如何使用 Glance 構建 Widget,首先仍需要像之前一樣宣告 AppWidget,並在 AndroidManifest 中將其連結到接收器,當然,我們在這裡使用了 Glance 提供的 GlanceAppWidgetReceiver 和 GlanceAppWidget,Glance 會為您處理大部分的工作,您只需要覆寫 MyAppWidget 中的 Content 方法,提供 AppWidget 內容即可。在定義內容時,不再使用 XML 語法,而是使用 Compose 語法,要顯示的內容將會被轉換為遠端檢視展示在 AppWidget 中。

class MyAppWidget: GlanceAppWidget() {
    @Composable
    override fun Content() {
        // 在這裡建立 AppWidget
        Column(
            modifier = Modifier.expandHeight().expandWidth(),
            verticalAlignment = Alignment.Top,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(text = “Where to”, modifier = Modifier.padding(12.dp))
            userDestinations()
        }
    }
}
 
class MyAppWidgetReceiver: GlanceAppWidgetReceiver() {
    // 告知 MyAppWidgetReceiver 該使用哪個 GlanceAppWidget
    override val glanceAppWidget: GlanceAppWidget = MyAppWidget()
}

有一點需要了解,雖然 Glance 使用 Compose Runtime 和 Compose 的語法,但它仍是一個獨立的框架,由於受到在遠端進行構建的限制,您不可能重用在 Jetpack Compose UI 中定義的元件。但如果您已對 Jetpack Compose 非常熟悉,那麼 Glance 將非常易於理解。

另外,由於 Glance 使用使用者事件 API 的方式處理互動,我們處理同使用者的互動將變得更加輕鬆。如果您瞭解 Widget 的工作原理就會知道 Widget 在不同程式上工作,這使得處理簡單的使用者事件也變得困難,因為不在同一程式就代表您沒有這個 Widget 的所有權,只能通過程式回撥來處理各種事件。

Glance 將這些複雜性抽象了出來,您只需通過向需要的 composable 物件定義 clickable modifier 即可讓其支援處理使用者點選事件,Glance 會將其中的注入行為全部抽象出來,使用者點選了 composanle,即可回撥所定義的操作。我們還定義了一些常用的操作,例如,如何啟動 Activity,只要呼叫 launchActivity 傳遞 Activity 目標類即可。

Button(
    text = “Home”,
    modifier = Modifier.clickable(launchActivity<NavigationActivity>)
)

此外,我們還可以提供自定義操作來執行一些自定義程式碼,例如,我們可能希望每當使用者點選此按鈕時就會更新地理位置並重新整理 Widget,如下列程式碼所示,Glance 會在背後為您處理一些需要注入的工作,並通過廣播接收器處理此次點選,最終呼叫您定義的操作程式碼。但請注意,如果該種操作為網路請求或資料庫訪問等較為耗時的操作,請使用 WorkManager API。

Button(
    text = “My Location”,
    modifier = Modifier.clickable(customAction<UpdateLocationAction>)
)

在前文中我們也提到,您可以使用可調整尺寸的 Widget,但是處理不同的響應式佈局也並非易事,Glance 就試圖通過定義三種不同的 SizeMode 選項從而讓這種工作變得稍微輕鬆一些。

SizeMode.Single 是預設選項,該選項指定了我們在此處定義的 Widget 內容不會因為可用尺寸變化而改變,這意味著我們在 Widget 後設資料上定義的最小支援尺寸只會通過 Content 方法被呼叫一次,如果 Widget 的可用尺寸發生更改,例如使用者調整了 Widget 尺寸,則不會重新整理內容。如下圖所示,使用了 SizeMode.Single 選項的 Widget,無論其尺寸如何變化,其輸出的尺寸大小永遠不會得到變化,這是因為 Content 方法只被呼叫了一次,內容在尺寸發生變化時並沒有得到重新整理。

class MyAppWidget: GlanceAppWidget() {
    override val sizeMode = SizeMode.Single
 
    @Composable
    override fun Content() {
        val size = LocalSize.current
        //…
    }
}

△ SizeMode.Single 選項示意圖

△ SizeMode.Single 選項示意圖

若在每次尺寸發生變更都對內容進行重新整理,則可使用 SizeMode.Exact 選項。此選項會在使用者每次調整 Widget 尺寸時,重新建立 Widget 介面並再次呼叫 Content 方法,並同時提供最大可用尺寸以便讓我們能夠在空間足夠的情況下更改介面,比如新增額外按鈕等等。如下圖中,Widget 尺寸發生變化時,其內部的輸出也會隨時發生變化,這是因為每次 Widget 介面都會被重新建立。

class MyAppWidget: GlanceAppWidget() {
    override val sizeMode = SizeMode.Exact
 
    @Composable
    override fun Content() {
        val size = LocalSize.current
        //…
    }
}

△ SizeMode.Exact 選項示意圖

△ SizeMode.Exact 選項示意圖

儘管 SizeMode.Exact 選項看似能夠完全滿足需求,但是每次都需要重新建立介面,可能會導致使用者在調整尺寸時介面的轉換因為一些效能問題有點不流暢,此時我們就可以通過 SizeMode.Responsive 選項。例如,此處我們將一些尺寸對映到某些特定形狀,每當建立或更新 AppWidget 時 Glance 都會呼叫每個 Size 定義好的的 Content 方法,每次都將對映到特定尺寸並儲存在記憶體中,系統能夠在使用者調整 Widget 尺寸時,根據可用尺寸選擇最合適的尺寸,而無需重新建立介面從而提供更平穩的轉換和更出色的效能。正如下圖所展示的那樣,當 Widget 尺寸發生變更時,只有當其尺寸能夠匹配到所預先定義好的尺寸範圍中,其內部輸出才會發生變化,更應該注意的是,此時並沒有重新建立介面。

△ SizeMode.Responsive 選項示意圖

△ SizeMode.Responsive 選項示意圖

同樣,我們還可以在 Content() 方法中定義更加多元化的樣式,讓 Widget 在不同的尺寸下展示更獨特的內容。

class MyAppWidget: GlanceAppWidget() {
    companion object {
        private val SMALL_SQUARE = DpSize (100.dp, 160. dp)
        private val HORIZONTAL_RECTANGLE = DpSize (250.dp, 100.dp)
        private val BIG_SQUARE = DpSize (250.dp, 250.dp)
    }
 
    override val sizeMode = SizeMode.Responsive(
        SMALL_SQUARE, HORIZONTAL_RECTANGLE, BIG_SQUARE
    )
 
    @Composable
    override fun Content() {
        val size = LocalSize.current
        //…
    }
}

除了以上提到的內容外,還有例如對 Widget 狀態管理的支援,和即開即用的 Material You 主題背景等更多內容,等待著您的探索。

如需瞭解更多內容,歡迎您查閱 Android 開發者網站: 應用 Widget 概覽,我們非常期待您嘗試我們提供的新 API,並期待看到您構建出的 Widget 和您的反饋。

歡迎您 點選這裡 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支援!

相關文章