Android 12上面目一新的小組件:美觀、便捷和實用

Google IO 2021上重磅介紹的Android 12,號稱歷代設計變化最大的版本。其全新的Material You設計語言、流暢的動畫特效再到面目一新的小組件,都使人印象深入。本文將聚焦小組件環節,談談它在從新設計以後的各類新特性和適配方法。java

小組件在Android平臺上命名爲AppWidget,有的時候還被翻譯成小部件、小插件和微件。說的都是一個東西:顯示在Launcher上,能在Logo之外提供更多信息的特別設計。它方便用戶免於打開App便可直接查看信息和進行簡單的交互,在PC上、早前的Symbian上都有相似的設計。android

前言

簡要回顧下移動平臺在小組件設計上的持續探索:git

  • 早期的Android版本缺少美觀,小組件更是常年未改。彷佛除了天氣、時鐘等經常使用小組件之外鮮少使用,逐漸被人遺忘
  • Windows Phone的動態磁貼在自由尺寸的Logo上靈活展現信息的設計很是超前,奈何生態構建困難,早已退場
  • Apple向來穩重(保守),直到iOS 10才引入小組件,但負一屏限制着它的發展。直到iOS 14的全面支持才大獲成功,大有後來居上的態勢
  • VIVO緊隨其後重磅推出的OriginOS則將Logo和小組件完美融合,試圖一統磁貼和小組件的概念,很是值得稱讚

也許是受到了友商們的持續刺激,Google終於開始從新審視小組件這個元老級功能,並在Android 12裏進行了從新設計、從新出發。github

12-widget

下面將結合代碼實戰,帶領你們逐步感覺Android 12裏小組件的各項新特性和對應的適配方法。markdown

1. 選擇和展現的統一變化

事實上即便未作任何適配,在12上直接運行的小組件與11就有明顯不一樣,主要表如今選擇器和展現的效果。app

以Chrome和Youtube Music的小組件爲例:框架

12-widget-picker

能夠看到12上的一些變化:dom

  • 選擇器
  • 頂部懸浮搜索框,能夠更加快速地找到目標小組件
  • 小組件按照App自動摺疊,避免無關的小組件佔用屏幕空間
  • App標題還對包含的小組件數目進行了提示
  • 拖拽到桌面上以後小組件默認擁有圓角設計

11上的小組件選擇器不支持搜索並且沒法摺疊,拖拽到桌面上也是初始的直角效果。ide

11-widget-picker

2. 美觀的圓角設計

健康信息愈加重要,手擼一個展現今日步數的小組件,搭配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上,就能有圓角效果。

12-widget

但佈局須要聽從以下兩點建議:

  • 四周的邊角不要放置內容,防止被切掉
  • 背景不要採用透明的、空的視圖或佈局,避免系統沒法探測邊界去進行裁切

事實上,系統預設了以下dimension以設置默認的圓角表現。

  • system_app_widget_background_radius: 小組件背景的圓角尺寸,默認16dp,上限28dp
  • system_app_widget_inner_radius: 小組件內部視圖的圓角尺寸 ,默認8dp,上限20dp
  • system_app_widget_internal_padding:內部視圖的padding值,默認16dp

看下官方的對於內外圓角尺寸的示意圖。

12-widget

注意:

  1. 這些dimension能夠被ROM廠商或3rd Launcher修改,不必定能保證一致性的尺寸
  2. 官方沒有說明小組件的內部視圖如何才能應用上內部圓角尺寸,DEMO確實也沒有適配上,不知道是ROM的問題仍是App的問題,有待後續的進一步研究

固然12之前的系統想要支持圓角設計也很簡單:自定義radius的attribute,應用在shape drawable上,手動將drawable應用到background。具體可參考官方Sample:

github.com/android/use…

3. 動態的色彩效果

給小組件添加暗黑主題支持便可自動適配動態色彩。

<!-- 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-widget

4. 改進的小組件預覽

12針對小組件選擇時的預覽界面進行了改進,方便展現更加精準的預覽效果。

4.1 動態預覽

以前只能使用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屬性最好指定小組件的實際佈局。但若是預覽的測試數據和實際的默認值有衝突的話,能夠指定專用的預覽佈局,只須要確保佈局的一致。

4.2 添加預覽說明

description屬性則能夠在小組件預覽的下方展現額外的說明,便於用戶更好地瞭解其功能定位。

<appwidget-provider android:description="@string/app_widget_pedometer_description">
</appwidget-provider>
複製代碼
12-widget

須要提醒的是description屬性並不是12新增,但12以前的選擇器不支持展現這個說明。

5. 支持新的交互控件

以前的小組件不支持CheckBox等控件,從12開始全面支持CheckBoxSwitchRadioButton三種狀態控件。

下面是採用這三種控件的簡單效果。

12-widget

再作個簡單的待辦事項以更好地說明狀態小組件的使用。

// 小組件件佈局裏指定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>
複製代碼
12-widget

若是將一樣的代碼運行到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

12之前的小部件不支持展現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-widget

6. 便捷地配置尺寸

12針對小組件的尺寸配置環節也進行了改進,更加便捷。

6.1 精確的尺寸

在已有的minWidth、minResizeWidth等屬性之外,新增了幾個屬性以更便捷地配置小組件的尺寸。

  • targetCellWidth和targetCellHeight:佔據Launcher上Cell的寬高格數,用以替代minWidth和minHeight。事實上Launcher是以Cell的單位來展現小組件的,因此直接指定Cell數顯然更合理
  • maxResizeWidth和maxResizeHeight: 配置Launcher上容許配置的最大尺寸,彌補minResizeWidth和minResizeHeight的不足
<appwidget-provider ... android:targetCellWidth="3" android:targetCellHeight="2" android:maxResizeWidth="250dp" android:maxResizeHeight="110dp">
</appwidget-provider>
複製代碼
CheckBox精準佈局預覽

6.2 靈活調節尺寸

iOS上添加小組件後尺寸就固定了,不支持調節。而Android 12上小組件在長按後便可靈活調節。

CheckBox長按效果

想要支持這個特性只須要給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並不支持。

6.3 採用默認配置

configure屬性能夠在小組件展現以前啓動一個配置畫面,供用戶選擇小組件所需的內容、主題和風格等。

若是想讓用戶快速看到效果,即不想展現這個畫面的話,只要在widgetFeatures裏指定新的configuration_optional值便可。

<appwidget-provider ... android:configure="com.example.appwidget.activity.WidgetConfigureActivity" android:widgetFeatures="reconfigurable|configuration_optional">
</appwidget-provider>
複製代碼

後面改主意了又想替換配置的話,能夠長按小組件找到配置的入口。

一是小組件右下方的編輯按鈕,二是上方出現的Setup菜單,這在之前的版本上是沒有的。

12-widget

7. 高效地控制佈局

小組件內容較多的時候,爲了展現的完整每每會給它限定Size,這意味着只有Launcher空間足夠大小組件才能成功放置。當Launcher空間捉急的時候就尷尬了,用戶只能在移除別的小組件和放棄你的小組件之間作個抉擇。

免除這種困擾的最佳作法是在不一樣的Size下采用不一樣的佈局,對展現的內容作出取捨。即Size充足的狀況下提供更多豐富的內容,反之只呈現最基本、最經常使用的信息。

7.1 響應式佈局

以前是如何作到這一需求呢?除了預設各類尺寸的小組件的通常思路之外,經過onAppWidgetOptionsChanged回調也能夠控制佈局的變化,但每每很是繁瑣。

而12上藉助新增的RemoteViews(Map<SizeF, RemoteViews> map)API能夠大大簡化實現過程。在小組件放置的時候就將Size和佈局的映射關係告知系統,當Size變化了AppWidgetManager將自動響應更新對應的佈局。

好比待辦事項小組件在Size爲3x2的時候額外展現添加按鈕,2x2的時候只展現事項列表的相應式佈局。

12-widget

代碼的實現也簡單清晰:

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)
}
複製代碼

好處:

  • 免於同一功能提供一堆尺寸小組件的繁瑣,減輕選擇器的負擔
  • 實現簡單,自動響應

7.2 精確佈局

現在移動設備的尺寸、形態豐富多樣,尤爲是摺疊屏越發成熟。若是響應式佈局仍不能知足更精細的需求,能夠在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可能爲空

8. 自由地更新視圖

RemoteViews做爲小組件視圖的重要管理類,本次OSV也添加了諸多API,以便更加自由地控制視圖的展現。

  • 更改顏色的setColorStateList()
  • 更改邊距的setViewLayoutMargin()
  • 更改寬高的setViewLayoutWidth()

這些新API能夠助力咱們實不少方便的功能,好比CheckBox選中以後更新文本顏色,思路很簡單:

  1. 監聽小組件的點擊事件並傳遞目標視圖
  2. 根據CheckBox的狀態得到預設的文本顏色
  3. 使用setColorStateList()更新
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))
複製代碼
12-widget

再好比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

9. 流暢的啓動效果

12版本上點擊Widget啓動App的時候能夠呈現更流暢的過渡效果,適配也很簡單。官方指示只需給小組件的根佈局指定android的 backgoround id便可。

<LinearLayout ... android:id="@android:id/background">
</LinearLayout>
複製代碼

實際的動做顯示添加這個ID後App啓動沒有什麼變化,箇中緣由須要繼續研究。

12開始對從Broadcast Receiver或Serivce啓動Activity作了更嚴格的限制,但不包括Widget發起的場合。但爲了不視覺上的突兀,這種後臺啓動的狀況下不展現遷移動畫。

10. 簡化的數據綁定

小組件裏展現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
}
複製代碼
CheckBox精準佈局預覽

若是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是不支持的,畢竟小組件裏數據量很少,不能使用也不要緊。

11. 新增API總結

簡要羅列一下12針對小組件新增的API,方便你們查閱。

RemoteViews類

方法 做用
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等三種狀態小組件的狀態變化

XML屬性

屬性 做用
description 配置小組件在選擇器裏的補充描述
previewLayout 配置小組件的預覽佈局
reconfigurable 指定小組件的尺寸支持直接調節
configuration_optional 指定小組件的內容能夠採用默認設計,無需啓動配置畫面
targetCellWidth、targetCellHeight 限定小組件所佔的Launcher單元格
maxResizeWidth、maxResizeHeight 配置小組件所能支持的最大高寬尺寸

結語

經過上面的解讀,你們能夠感覺到Google在小組件的從新設計上耗費了諸多努力,它給這個老舊的功能注入不少新玩法和新花樣。

簡要回顧一下Android 12裏小組件的新特性:

  • 更便捷的小組件選擇器
  • 更美觀的圓角邊框設計
  • 更靈活的小組件預覽
  • 更完整的控件支持
  • 更方便的尺寸調節
  • 更精準的佈局控制
  • 更自由的視圖更新
  • 更簡便的列表數據綁定

如此之多的新特性,在助力小組件高效開發的同時,還能給用戶呈現更加優秀的使用體驗。

跟隨Android 12的腳步,快快嘗試起來,讓現有的小組件從新綻開光彩。

未決事項

  1. 小組件內部視圖的圓角尺寸如何適配?
  2. 小組件啓動App的流暢過渡效果如何實現,是什麼效果?

本文DEMO

NewAppWidget

參考文檔

官方宣傳

官方介紹文檔

官方Sample

推薦閱讀

Android 12上全新的應用啓動畫面,還不適配一下?

全面覆盤Android開發者容易忽視的Backup功能。

爲何推薦使用CameraX?

相關文章
相關標籤/搜索