在 Android 12 中構(gòu)建更現(xiàn)代的應(yīng)用 Widget

從 2008 年開始,Widget 就一直是 Android 系統(tǒng)的一個重要組成部分,也是自定義主屏幕的一個重要方面。您可以將 Widget 理解為一個 "一目了然" 的應(yīng)用視圖,讓用戶在無需從主屏幕打開應(yīng)用的前提下,就能對應(yīng)用數(shù)據(jù)和核心功能一覽無余。但是從 Android 推出至今,AppWidget 的 API 基本就沒有什么大的變化,從 2012 年到 2021 年更是只有一個 Android 版本包含了對 AppWidget API 的更新。而隨著 Android 12 的推出,也帶來了 Widget API 一些亟需改進(jìn)的更新。

本文我們就來介紹一下 Android 12 中帶來了哪些關(guān)于 Widget API 的更新,以及有哪些好用的工具可以讓開發(fā)應(yīng)用 Widget 變得更加出色。如果您更喜歡通過視頻了解此內(nèi)容,請 點(diǎn)擊此處 查看。

Widget 工作原理

Widget 運(yùn)行在一個名為 AppWidgetHost 的遠(yuǎn)端進(jìn)程中,比如 Home Screen Launcher,也正因如此,它的運(yùn)行受到了一些限制。我們來看看 Widget 的工作原理。

在前端,應(yīng)用首先注冊 AppWidgetProvider 來定義 Widget 行為,以及注冊 AppWidgetProviderInfo 來定義元數(shù)據(jù)。然后 AndroidManifest 引用這些信息,讓操作系統(tǒng)通過 AndroidManifest 讀取元數(shù)據(jù),例如 Widget 初始的布局和默認(rèn)尺寸,并提供 Widget 的預(yù)覽,緊接著,provider 會使用鏈接賬戶來更新布局并對 Widget 進(jìn)行更新。這里需要注意的是,應(yīng)用于 Widget 的構(gòu)建次數(shù)有限,所以操作系統(tǒng)是通過接收方的廣播事件 (包含了更新信息) 對 Widget 進(jìn)行更新,這也意味著 Widget 是定期接收來自應(yīng)用的信息進(jìn)行更新的。

API

Android 12 的推出帶來了很多關(guān)于 AppWidget API 的更新,本文不會對所有的 API 一一介紹,而是重點(diǎn)介紹幾個對 Widget 構(gòu)建非常有用的 API。

實(shí)現(xiàn)圓角

在 Android 12 中許多關(guān)鍵的界面元素都開始采用圓角設(shè)計,為了使 AppWidget 與其他系統(tǒng)組件樣式之間看起來一致,Android 12 引入了 system_app_widget_background_radiussystem_app_widget_inner_radius 兩個新的系統(tǒng)參數(shù)實(shí)現(xiàn)圓角,前一個參數(shù)是用來設(shè)置 Widget 的圓角半徑,后一個則是設(shè)置 Widget 內(nèi)視圖的圓角半徑。要使用這些參數(shù),只需要定義一個設(shè)置了系統(tǒng)參數(shù) 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>

然后將可繪制對象應(yīng)用于 Widget 的外部容器,這樣做可將系統(tǒng)參數(shù)提供的圓角半徑應(yīng)用于 Widget 背景中。同樣,將內(nèi)部視圖的可繪制對象應(yīng)用于表示 Widget 內(nèi)部容器的布局,如代碼所示:

// res/layout/widget_layout.xml
<LinearLayout
    android:background=”@drawable/app_widget_background”
…>
    <LinearLayout
        android:background=”@drawable/app_widget_inner_view_background”
    …>
    </LinearLayout>
</LinearLayout>
圖左: Widget 圓角;圖右: 內(nèi)視圖圓角

從效果中我們可以看到 Widget 當(dāng)前內(nèi)部容器的圓角半徑要小于外部容器,這就是新參數(shù)的使用方法。

動態(tài)顏色

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

我們添加了動態(tài)顏色 API,您可直接獲取并使用 Pixel 設(shè)備系統(tǒng)上提供的主題背景、顏色等參數(shù),從而讓 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>

您可以看到,當(dāng)設(shè)置了主題屬性之后,Widget 直接從系統(tǒng)壁紙中提取了主色,并將其應(yīng)用于深色和淺色主題背景中。

響應(yīng)式布局

Android 12 引入了新的 API 來實(shí)現(xiàn)響應(yīng)式布局,可以隨著 Widget 的尺寸調(diào)整,自動切換到不同的布局。如下圖所示,用戶可以通過拖動來任意更改 Widget 的尺寸,Widget 也會根據(jù)尺寸的不同而動態(tài)更新所要顯示的內(nèi)容。

[圖片上傳失敗...(image-6d94b1-1641791392338)]

那么如何做到讓 Widget 隨著尺寸的變化而動態(tài)更新顯示內(nèi)容呢,用如下代碼舉例,我們定義了三個不同的參數(shù),分別包含最小支持寬度和高度,以及在此大小范圍內(nèi)對應(yīng)的 RemoteView,系統(tǒng)會自動根據(jù)實(shí)際的尺寸而自動對 Widget 進(jìn)行調(diào)整。

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 置于主屏幕中時默認(rèn)的較大單元格尺寸。在 Android 12 之前,可以使用 minWidgetminHeight 屬性,它們指定了以 dp 為單位的默認(rèn) Widget 尺寸,我們建議同時指定這兩個屬性以保持向后兼容。如果您的 Widget 是可調(diào)整尺寸的,那么還可以使用 Android 12 提供的 minResizeWidth/Height 和 maxResizeWidth/Height 屬性來限制 Widget 的可調(diào)整尺寸范圍。

<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 還改進(jìn)了 Widget 選擇器的使用體驗(yàn),引入了兩個新的屬性,第一個屬性是 description,它對 Widget 選擇器的作用進(jìn)行了描述說明,通過它可以了解 Widget 的作用;另一個是 previewLayout,它指定了 Widget 選擇器中展示的 XML 布局。實(shí)際上在 Android 12 之前可以使用 previewImage 屬性來指定靜態(tài)資源達(dá)到類似效果,但是 previewLayout 相比較來說更加精確和方便。另外,由于這些預(yù)覽都是在運(yùn)行時構(gòu)建的,因此也可以動態(tài)適配設(shè)備的主題。

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

目前已經(jīng)介紹了很多 Android 12 引入的新 API,相信不久之后就會看到越來越多的應(yīng)用采用新 API 構(gòu)建出更現(xiàn)代的 Widget 使用體驗(yàn)。

Glance

要構(gòu)建出色的 Widget,除了需要用到目前更現(xiàn)代的 API 之外,我們還需要更現(xiàn)代、更出色的工具來幫助我們,Glance 就是這么一個出色的工具,它也加入到了 Jetpack 大家庭中。Glance 是由 Compose Runtime 提供支持的 API,通過它就可以使用 Compose 風(fēng)格的語法來創(chuàng)建 AppWidget,這也意味著您可以通過 Glance 以 composable 構(gòu)建界面,并將其轉(zhuǎn)換為遠(yuǎn)端視圖顯示到 Widget 中,同時還能用到前文中提到的 Android 12 的新 API,并盡可能的讓其向后兼容。另外,Glance 還會負(fù)責(zé)一些 Widget 生命周期以及其他一些常見的操作,聽上去是不是覺得非常方便。

△ Glance 結(jié)構(gòu)示意圖

接下來我們介紹如何使用 Glance 構(gòu)建 Widget,首先仍需要像之前一樣聲明 AppWidget,并在 AndroidManifest 中將其鏈接到接收器,當(dāng)然,我們在這里使用了 Glance 提供的 GlanceAppWidgetReceiver 和 GlanceAppWidget,Glance 會為您處理大部分的工作,您只需要覆寫 MyAppWidget 中的 Content 方法,提供 AppWidget 內(nèi)容即可。在定義內(nèi)容時,不再使用 XML 語法,而是使用 Compose 語法,要顯示的內(nèi)容將會被轉(zhuǎn)換為遠(yuǎn)端視圖展示在 AppWidget 中。

class MyAppWidget: GlanceAppWidget() {
    @Composable
    override fun Content() {
        // 在這里創(chuàng)建 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()
}

有一點(diǎn)需要了解,雖然 Glance 使用 Compose Runtime 和 Compose 的語法,但它仍是一個獨(dú)立的框架,由于受到在遠(yuǎn)端進(jìn)行構(gòu)建的限制,您不可能重用在 Jetpack Compose UI 中定義的組件。但如果您已對 Jetpack Compose 非常熟悉,那么 Glance 將非常易于理解。

另外,由于 Glance 使用用戶事件 API 的方式處理交互,我們處理同用戶的交互將變得更加輕松。如果您了解 Widget 的工作原理就會知道 Widget 在不同進(jìn)程上工作,這使得處理簡單的用戶事件也變得困難,因?yàn)椴辉谕贿M(jìn)程就代表您沒有這個 Widget 的所有權(quán),只能通過進(jìn)程回調(diào)來處理各種事件。

Glance 將這些復(fù)雜性抽象了出來,您只需通過向需要的 composable 對象定義 clickable modifier 即可讓其支持處理用戶點(diǎn)擊事件,Glance 會將其中的注入行為全部抽象出來,用戶點(diǎn)擊了 composanle,即可回調(diào)所定義的操作。我們還定義了一些常用的操作,例如,如何啟動 Activity,只要調(diào)用 launchActivity 傳遞 Activity 目標(biāo)類即可。

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

此外,我們還可以提供自定義操作來執(zhí)行一些自定義代碼,例如,我們可能希望每當(dāng)用戶點(diǎn)擊此按鈕時就會更新地理位置并刷新 Widget,如下列代碼所示,Glance 會在背后為您處理一些需要注入的工作,并通過廣播接收器處理此次點(diǎn)擊,最終調(diào)用您定義的操作代碼。但請注意,如果該種操作為網(wǎng)絡(luò)請求或數(shù)據(jù)庫訪問等較為耗時的操作,請使用 WorkManager API。

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

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

SizeMode.Single 是默認(rèn)選項(xiàng),該選項(xiàng)指定了我們在此處定義的 Widget 內(nèi)容不會因?yàn)榭捎贸叽缱兓淖儯@意味著我們在 Widget 元數(shù)據(jù)上定義的最小支持尺寸只會通過 Content 方法被調(diào)用一次,如果 Widget 的可用尺寸發(fā)生更改,例如用戶調(diào)整了 Widget 尺寸,則不會刷新內(nèi)容。如下圖所示,使用了 SizeMode.Single 選項(xiàng)的 Widget,無論其尺寸如何變化,其輸出的尺寸大小永遠(yuǎn)不會得到變化,這是因?yàn)?Content 方法只被調(diào)用了一次,內(nèi)容在尺寸發(fā)生變化時并沒有得到刷新。

class MyAppWidget: GlanceAppWidget() {
    override val sizeMode = SizeMode.Single
 
    @Composable
    override fun Content() {
        val size = LocalSize.current
        //…
    }
}
△ SizeMode.Single 選項(xiàng)示意圖

若在每次尺寸發(fā)生變更都對內(nèi)容進(jìn)行刷新,則可使用 SizeMode.Exact 選項(xiàng)。此選項(xiàng)會在用戶每次調(diào)整 Widget 尺寸時,重新創(chuàng)建 Widget 界面并再次調(diào)用 Content 方法,并同時提供最大可用尺寸以便讓我們能夠在空間足夠的情況下更改界面,比如添加額外按鈕等等。如下圖中,Widget 尺寸發(fā)生變化時,其內(nèi)部的輸出也會隨時發(fā)生變化,這是因?yàn)槊看?Widget 界面都會被重新創(chuàng)建。

class MyAppWidget: GlanceAppWidget() {
    override val sizeMode = SizeMode.Exact
 
    @Composable
    override fun Content() {
        val size = LocalSize.current
        //…
    }
}
△ SizeMode.Exact 選項(xiàng)示意圖

盡管 SizeMode.Exact 選項(xiàng)看似能夠完全滿足需求,但是每次都需要重新創(chuàng)建界面,可能會導(dǎo)致用戶在調(diào)整尺寸時界面的轉(zhuǎn)換因?yàn)橐恍┬阅軉栴}有點(diǎn)不流暢,此時我們就可以通過 SizeMode.Responsive 選項(xiàng)。例如,此處我們將一些尺寸映射到某些特定形狀,每當(dāng)創(chuàng)建或更新 AppWidget 時 Glance 都會調(diào)用每個 Size 定義好的的 Content 方法,每次都將映射到特定尺寸并存儲在內(nèi)存中,系統(tǒng)能夠在用戶調(diào)整 Widget 尺寸時,根據(jù)可用尺寸選擇最合適的尺寸,而無需重新創(chuàng)建界面從而提供更平穩(wěn)的轉(zhuǎn)換和更出色的性能。正如下圖所展示的那樣,當(dāng) Widget 尺寸發(fā)生變更時,只有當(dāng)其尺寸能夠匹配到所預(yù)先定義好的尺寸范圍中,其內(nèi)部輸出才會發(fā)生變化,更應(yīng)該注意的是,此時并沒有重新創(chuàng)建界面。

△ SizeMode.Responsive 選項(xiàng)示意圖

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

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
        //…
    }
}

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

如需了解更多內(nèi)容,歡迎您查閱 Android 開發(fā)者網(wǎng)站: 應(yīng)用 Widget 概覽,我們非常期待您嘗試我們提供的新 API,并期待看到您構(gòu)建出的 Widget 和您的反饋。

歡迎您 點(diǎn)擊這里 向我們提交反饋,或分享您喜歡的內(nèi)容、發(fā)現(xiàn)的問題。您的反饋對我們非常重要,感謝您的支持!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容