Google IO 2021上重磅介紹的Android 12,號稱歷代設計變化最大的版本。其全新的Material You設計語言、流暢的動畫特效再到面目一新的小組件,都使人印象深入。本文將聚焦小組件環節,談談它在從新設計以後的各類新特性和適配方法。java
小組件在Android平臺上命名爲AppWidget
,有的時候還被翻譯成小部件、小插件和微件。說的都是一個東西:顯示在Launcher上,能在Logo之外提供更多信息的特別設計。它方便用戶免於打開App便可直接查看信息和進行簡單的交互,在PC上、早前的Symbian上都有相似的設計。android
簡要回顧下移動平臺在小組件設計上的持續探索:git
Windows Phone
的動態磁貼在自由尺寸的Logo上靈活展現信息的設計很是超前,奈何生態構建困難,早已退場iOS 10
才引入小組件,但負一屏限制着它的發展。直到iOS 14
的全面支持才大獲成功,大有後來居上的態勢OriginOS
則將Logo和小組件完美融合,試圖一統磁貼和小組件的概念,很是值得稱讚也許是受到了友商們的持續刺激,Google終於開始從新審視小組件這個元老級功能,並在Android 12
裏進行了從新設計、從新出發。github
下面將結合代碼實戰,帶領你們逐步感覺Android 12裏小組件的各項新特性和對應的適配方法。markdown
事實上即便未作任何適配,在12上直接運行的小組件與11就有明顯不一樣,主要表如今選擇器和展現的效果。app
以Chrome和Youtube Music的小組件爲例:框架
能夠看到12上的一些變化:dom
11上的小組件選擇器不支持搜索並且沒法摺疊,拖拽到桌面上也是初始的直角效果。ide
健康信息愈加重要,手擼一個展現今日步數的小組件,搭配androidplot
框架展現詳細的步數圖表。oop
override fun onUpdate(...) {
for (appWidgetId in appWidgetIds) {
showBarChartToWidget(context, appWidgetManager, appWidgetId)
}
}
private fun showBarChartToWidget(...) {
// Create plot view.
val plot = XYPlot(context, "Pedometers chart")
...
// Set graph shape
plot.setBorderStyle(Plot.BorderStyle.ROUNDED, 12f, 12f)
plot.isDrawingCacheEnabled = true
// Reflect chart's bitmap to widget.
val bmp = plot.drawingCache
val remoteViews = RemoteViews(context.packageName, R.layout.widget_pedometer)
remoteViews.setBitmap(R.id.bar_chart, "setImageBitmap", bmp)
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
}
複製代碼
不用特別適配,直接運行到12上,就能有圓角效果。
但佈局須要聽從以下兩點建議:
事實上,系統預設了以下dimension以設置默認的圓角表現。
system_app_widget_background_radius
: 小組件背景的圓角尺寸,默認16dp,上限28dpsystem_app_widget_inner_radius
: 小組件內部視圖的圓角尺寸 ,默認8dp,上限20dpsystem_app_widget_internal_padding
:內部視圖的padding值,默認16dp看下官方的對於內外圓角尺寸的示意圖。
注意:
固然12之前的系統想要支持圓角設計也很簡單:自定義radius的attribute,應用在shape drawable上,手動將drawable應用到background。具體可參考官方Sample:
給小組件添加暗黑主題支持便可自動適配動態色彩。
<!-- values/themes.xml -->
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.AppWidget" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <item name="colorPrimary">@color/purple_500</item> <item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorOnPrimary">@color/white</item> ... </style>
</resources>
<!-- values-night/themes.xml -->
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.AppWidget" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <item name="colorPrimary">@color/purple_200</item> <item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorOnPrimary">@color/black</item> ... </style>
</resources>
複製代碼
12針對小組件選擇時的預覽界面進行了改進,方便展現更加精準的預覽效果。
以前只能使用previewImage
屬性展現一張預覽圖,功能迭代的過程當中忘記更新它的話,可能致使預覽和實際效果發生誤差。
12新引入了previewLayout
屬性用以配置小組件的實際佈局,使得用戶可以在小組件的選擇器裏看到更加接近實際效果的視圖,而再也不是一層不變的靜態圖片。
這樣一來在保證效果一致的同時免去了額外維護預覽圖的麻煩。
<appwidget-provider <!-- 既存的圖片屬性指定UI提供的設計圖 -->
android:previewImage="@drawable/app_widget_pedometer_preview_2"
<!-- 新的預覽API裏指定實際的佈局 -->
android:previewLayout="@layout/widget_pedometer"
</appwidget-provider>
複製代碼
左邊是步數小組件一開始的設計圖,右邊是最後的實際效果。
若是忘記說服UI從新做圖的話,在11上的預覽圖會和實際效果有較大誤差。而12上不用在意設計圖是否更新,藉助新的API便可直接預覽實際效果,所見即所得。
通常來講previewLayout
屬性最好指定小組件的實際佈局。但若是預覽的測試數據和實際的默認值有衝突的話,能夠指定專用的預覽佈局,只須要確保佈局的一致。
description
屬性則能夠在小組件預覽的下方展現額外的說明,便於用戶更好地瞭解其功能定位。
<appwidget-provider android:description="@string/app_widget_pedometer_description">
</appwidget-provider>
複製代碼
須要提醒的是description
屬性並不是12新增,但12以前的選擇器不支持展現這個說明。
以前的小組件不支持CheckBox
等控件,從12開始全面支持CheckBox
、Switch
和RadioButton
三種狀態控件。
下面是採用這三種控件的簡單效果。
再作個簡單的待辦事項以更好地說明狀態小組件的使用。
// 小組件件佈局裏指定CheckBox控件便可
<LinearLayout ... android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:theme="@style/Theme.AppWidget.AppWidgetContainer">
<include layout="@layout/widget_todo_list_title_region" />
<CheckBox android:id="@+id/checkbox_first" style="@style/Widget.AppWidget.Checkbox" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/todo_list_sample_1" Tools:text="@string/todo_list_tool" />
...
</LinearLayout>
複製代碼
若是將一樣的代碼運行到11上,則會顯示加載失敗。
日誌
AppWidgetHostView: Error inflating AppWidget AppWidgetProviderInfo(UserHandle{0}/ComponentInfo{com.example.splash/com.example.splash.widget.TodoListAppWidget}): android.view.InflateException: Binary XML file line #13 in com.example.splash:layout/widget_todo_list: Binary XML file line #13 in com.example.splash:layout/widget_todo_list: Error inflating class android.widget.CheckBox
文本內容不肯定的話,能夠經過代碼動態地控制文本,同時還能夠監聽用戶的選擇事件。
好比咱們要展現Android開發者現在要學習的三座大山,選中的時候彈出Toast。
private fun updateAppWidget(...) {
val viewId1 = R.id.checkbox_first
val pendingIntent = PendingIntent.getBroadcast(...)
val rv = RemoteViews(context.packageName, R.layout.widget_todo_list)
rv.apply {
// 設置文本
setTextViewText(viewId1, context.resources.getString(R.string.todo_list_android))
...
// 設置CheckBox的默認選中狀態
setCompoundButtonChecked(viewId1, true)
// 監聽相應的CheckBox的選中事件
setOnCheckedChangeResponse(
viewId1,
RemoteViews.RemoteResponse.fromPendingIntent(pendingIntent)
)
}
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
}
override fun onReceive(context: Context?, intent: Intent?) {
...
val checked = intent.extras?.getBoolean(RemoteViews.EXTRA_CHECKED, false) ?: false
val viewId = intent.extras?.getInt(EXTRA_VIEW_ID) ?: -1
Toast.makeText(
context,
"ViewId : $viewId's checked status is now : $checked",
Toast.LENGTH_SHORT
).show()
}
複製代碼
12針對小組件的尺寸配置環節也進行了改進,更加便捷。
在已有的minWidth、minResizeWidth等屬性之外,新增了幾個屬性以更便捷地配置小組件的尺寸。
<appwidget-provider ... android:targetCellWidth="3" android:targetCellHeight="2" android:maxResizeWidth="250dp" android:maxResizeHeight="110dp">
</appwidget-provider>
複製代碼
iOS上添加小組件後尺寸就固定了,不支持調節。而Android 12上小組件在長按後便可靈活調節。
想要支持這個特性只須要給widgetFeatures
屬性指定reconfigurable
值便可。
<appwidget-provider android:widgetFeatures="reconfigurable">
</appwidget-provider>
複製代碼
The reconfigurable flag was introduced in Android 9 (API level 28), but it was not widely supported in launchers until Android 12.
事實上這個屬性早在Android 9的時候就引入了,但官方說從S開始才全面支持。我在11版本的Pixel Launcher上發現已經能夠直接調節尺寸了,不知道官方的意思是否是別的Launcher並不支持。
configure
屬性能夠在小組件展現以前啓動一個配置畫面,供用戶選擇小組件所需的內容、主題和風格等。
若是想讓用戶快速看到效果,即不想展現這個畫面的話,只要在widgetFeatures
裏指定新的configuration_optional
值便可。
<appwidget-provider ... android:configure="com.example.appwidget.activity.WidgetConfigureActivity" android:widgetFeatures="reconfigurable|configuration_optional">
</appwidget-provider>
複製代碼
後面改主意了又想替換配置的話,能夠長按小組件找到配置的入口。
一是小組件右下方的編輯按鈕,二是上方出現的Setup菜單,這在之前的版本上是沒有的。
小組件內容較多的時候,爲了展現的完整每每會給它限定Size,這意味着只有Launcher空間足夠大小組件才能成功放置。當Launcher空間捉急的時候就尷尬了,用戶只能在移除別的小組件和放棄你的小組件之間作個抉擇。
免除這種困擾的最佳作法是在不一樣的Size下采用不一樣的佈局,對展現的內容作出取捨。即Size充足的狀況下提供更多豐富的內容,反之只呈現最基本、最經常使用的信息。
以前是如何作到這一需求呢?除了預設各類尺寸的小組件的通常思路之外,經過onAppWidgetOptionsChanged
回調也能夠控制佈局的變化,但每每很是繁瑣。
而12上藉助新增的RemoteViews(Map<SizeF, RemoteViews> map)
API能夠大大簡化實現過程。在小組件放置的時候就將Size和佈局的映射關係告知系統,當Size變化了AppWidgetManager
將自動響應更新對應的佈局。
好比待辦事項小組件在Size爲3x2的時候額外展現添加按鈕,2x2的時候只展現事項列表的相應式佈局。
代碼的實現也簡單清晰:
private fun updateAppWidgetWithResponsiveLayouts(...) {
...
// 尺寸夠寬的狀況下Button才顯示
val wideView = RemoteViews(rv)
wideView.setViewVisibility(button, View.VISIBLE)
val viewMapping: Map<SizeF, RemoteViews> = mapOf(
SizeF(100f, 100f) to rv,
SizeF(200f, 100f) to wideView
)
// 將Size和RemoteViews佈局的映射關係告知AppWidgetManager
val remoteViews = RemoteViews(viewMapping)
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
}
複製代碼
好處:
現在移動設備的尺寸、形態豐富多樣,尤爲是摺疊屏越發成熟。若是響應式佈局仍不能知足更精細的需求,能夠在Size變化的回調裏,獲取目標Size對佈局進一步的精確把控。
利用AppWidgetManager新增的OPTION_APPWIDGET_SIZES
KEY能夠從AppWidgetManager裏拿到目標Size。
// 監聽目標尺寸
override fun onAppWidgetOptionsChanged(...) {
...
// Get the new sizes.
val sizes = newOptions?.getParcelableArrayList<SizeF>(
AppWidgetManager.OPTION_APPWIDGET_SIZES
)
// Do nothing if sizes is not provided by the launcher.
if (sizes.isNullOrEmpty()) {
return
}
Log.d("Widget", "PedometerAppWidget#onAppWidgetOptionsChanged() size:${sizes}")
// Get exact layout
if (BuildCompat.isAtLeastS()) {
val remoteViews = RemoteViews(sizes.associateWith(::createRemoteViews))
appWidgetManager?.updateAppWidget(appWidgetId, remoteViews)
}
}
複製代碼
以下的日誌顯示Size變化的時候會將目標Size回傳。
Widget : PedometerAppWidget#onAppWidgetOptionsChanged() size:[377.42856x132.0, 214.57143x216.57143]
複製代碼
以後從預設的精細布局裏匹配相應的視圖。
private fun createRemoteViews(size: SizeF): RemoteViews {
val smallView: RemoteViews = ...
val tallView: RemoteViews = ...
val wideView: RemoteViews = ...
...
return when (size) {
SizeF(100f, 100f) -> smallView
SizeF(100f, 200f) -> tallView
SizeF(200f, 100f) -> wideView
...
}
}
複製代碼
注意:實際上Size列表由Launcher提供,若是3rd Launcher沒有適配這一特性的話,回傳的Size可能爲空
RemoteViews做爲小組件視圖的重要管理類,本次OSV也添加了諸多API,以便更加自由地控制視圖的展現。
setColorStateList()
setViewLayoutMargin()
setViewLayoutWidth()
等這些新API能夠助力咱們實不少方便的功能,好比CheckBox選中以後更新文本顏色,思路很簡單:
override fun onReceive(context: Context?, intent: Intent?) {
...
// Get target widget.
val appWidgetManager = AppWidgetManager.getInstance(context)
val thisAppWidget = ComponentName(context!!.packageName, TodoListAppWidget::class.java.name)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
// Update widget color parameters dynamically.
for (appWidgetId in appWidgetIds) {
val remoteViews = RemoteViews(context.packageName, R.layout.widget_todo_list)
remoteViews.setColorStateList(
viewId,
"setTextColor",
getColorStateList(context, checked)
)
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
}
}
private fun getColorStateList(context: Context, checkStatus: Boolean): ColorStateList =
if (checkStatus)
ColorStateList.valueOf(context.getColor(R.color.widget_checked_text_color))
else
ColorStateList.valueOf(context.getColor(R.color.widget_unchecked_text_color))
複製代碼
再好比Chart線圖過小,看不清楚。可讓它在點擊以後放大,再點擊以後恢復原樣。
// 根據記錄的縮放狀態得到預設的寬高
// 經過setViewLayoutWidth和setViewLayoutHeight更新寬高
override fun onReceive(context: Context?, intent: Intent?) {
...
val widthScaleSize = if (scaleOutStatus) 200f else 260f
val heightScaleSize = if (scaleOutStatus) 130f else 160f
// Update widget layout parameters dynamically.
for (appWidgetId in appWidgetIds) {
val remoteViews = RemoteViews(context.packageName, R.layout.widget_pedometer)
remoteViews.setViewLayoutWidth(viewId, widthScaleSize, TypedValue.COMPLEX_UNIT_DIP)
remoteViews.setViewLayoutHeight(viewId, heightScaleSize, TypedValue.COMPLEX_UNIT_DIP)
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
}
}
複製代碼
12版本上點擊Widget啓動App的時候能夠呈現更流暢的過渡效果,適配也很簡單。官方指示只需給小組件的根佈局指定android的 backgoround
id便可。
<LinearLayout ... android:id="@android:id/background">
</LinearLayout>
複製代碼
實際的動做顯示添加這個ID後App啓動沒有什麼變化,箇中緣由須要繼續研究。
12開始對從Broadcast Receiver或Serivce啓動Activity作了更嚴格的限制,但不包括Widget發起的場合。但爲了不視覺上的突兀,這種後臺啓動的狀況下不展現遷移動畫。
小組件裏展現ListView的需求也很常見,提供數據的話須要聲明一個 RemoteViewsService
以返回RemoteViewsFactory,比較繞。
而12裏新增的 setRemoteAdapter(int , RemoteCollectionItems)
API則能夠大大簡化這個綁定過程。
好比製做一個即將到來的事件列表小組件,經過這個API即可以高效注入數據。
private fun updateCountDownList(...) {
...
// 建立用於構建Remote集合數據的Builder
val builder = RemoteViews.RemoteCollectionItems.Builder()
val menuResources = context.resources.obtainTypedArray(R.array.count_down_list_titles)
// 往Builder裏添加各Item對應的RemoteViews
for (index in 0 until menuResources.length()) {
...
builder.addItem(index.toLong(), constructRemoteViews(context, resId))
}
// 構建Remote集合數據
// 並經過setRemoteAdapter直接放入到ListView裏
val collectionItems = builder.setHasStableIds(true).build()
remoteViews.setRemoteAdapter(R.id.count_down_list, collectionItems)
...
}
// 建立ListView各Item對應的RemoteViews
private fun constructRemoteViews(...): RemoteViews {
val remoteViews = RemoteViews(context.packageName, R.layout.item_count_down)
val itemData = context.resources.getStringArray(stringArrayId)
// 遍歷Item數據行設置對應的文本
itemData.forEachIndexed { index, value ->
val viewId = when (index) {
0 -> R.id.item_title
1 -> R.id.item_time
...
}
remoteViews.setTextViewText(viewId, value)
}
return remoteViews
}
複製代碼
若是Item的佈局不固定不止一種,可使用setViewTypeCount
指定佈局類型的數目,告知ListView須要提供的ViewHolder種類。若是不指定也能夠,系統將自動識別佈局的種類,須要系統額外處理而已。
但要注意:若是指定的數目和實際的不一致會引起異常。
IllegalArgumentException: View type count is set to 2, but the collection contains 3 different layout ids
另外,須要補充一下,支持該API的View必須是AdapterView的子類,好比常見的ListView、GridView等。RecyclerView是不支持的,畢竟小組件裏數據量很少,不能使用也不要緊。
簡要羅列一下12針對小組件新增的API,方便你們查閱。
方法 | 做用 |
---|---|
RemoteViews(Map<SizeF, RemoteViews>) | 根據響應式佈局映射表建立目標RemoteViews |
addStableView() | 向RemoteViews動態添加子View,相似ViewGroup#addView() |
setCompoundButtonChecked() | 針對CheckBox或Switch控件更新選中狀態 |
setRadioGroupChecked() | 針對RadioButton控件更新選中狀態 |
setRemoteAdapter(int , RemoteCollectionItems) | 直接將數據填充進小組件的ListView |
setColorStateList() | 動態更新小組件視圖的顏色 |
setViewLayoutMargin() | 動態更新小組件視圖的邊距 |
setViewLayoutWidth()、setViewLayoutHeight() | 動態更新小組件視圖的寬高 |
setOnCheckedChangeResponse() | 監聽CheckBox等三種狀態小組件的狀態變化 |
屬性 | 做用 |
---|---|
description | 配置小組件在選擇器裏的補充描述 |
previewLayout | 配置小組件的預覽佈局 |
reconfigurable | 指定小組件的尺寸支持直接調節 |
configuration_optional | 指定小組件的內容能夠採用默認設計,無需啓動配置畫面 |
targetCellWidth、targetCellHeight | 限定小組件所佔的Launcher單元格 |
maxResizeWidth、maxResizeHeight | 配置小組件所能支持的最大高寬尺寸 |
經過上面的解讀,你們能夠感覺到Google在小組件的從新設計上耗費了諸多努力,它給這個老舊的功能注入不少新玩法和新花樣。
簡要回顧一下Android 12裏小組件的新特性:
如此之多的新特性,在助力小組件高效開發的同時,還能給用戶呈現更加優秀的使用體驗。
跟隨Android 12的腳步,快快嘗試起來,讓現有的小組件從新綻開光彩。