概述

Android上经常看到各种桌面小组件,命名为AppWidget,通常天气,时钟会比较常见小组件。Android12发布后,对小组件做了很多优化。测试了一下新增的组件,在Android12以下会有错误,所以想使用新增的控件,需要做适配。不过由于没有Android12的设备,无法体验优化的效果怎么样。所以本篇没有Android12的新增内容。

如何使用小组件

创建

直接new一个widget即可。然后对小组件的基本信息进行设置。

  1. Class Name : 组件名称
  2. Placement : 组件可以显示的地方,Homescreen屏幕,Keyguard锁屏
  3. Resizable : 组件调整大小
  4. MiniMum Width: 组件宽占多少格
  5. Minimum Heigt: 组件高占多少格

新建后会生成三个文件,以demo为例,会生成TestWidget组件类,test_widget组件布局,test_widget_info组件布局设置。

test_widget_info

先看test_widget_info,这个可以再次修改上面创建时的设置,同时也可以添加一些新的设置。如下:

  1. <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:description="@string/app_widget_description"
  3. android:initialKeyguardLayout="@layout/test_widget"
  4. android:initialLayout="@layout/test_widget"
  5. android:minWidth="250dp"
  6. android:minHeight="180dp"
  7. android:previewImage="@drawable/example_appwidget_preview"
  8. android:resizeMode="horizontal|vertical"
  9. android:updatePeriodMillis="86400000"
  10. android:widgetCategory="home_screen" />

属性介绍:

  • minWidthminHeight :指定小部件默认情况下占用的最小空间。
    注意:为使小部件能够在设备间移植,小部件的最小大小不得超过 4 x 4 单元格。
  • minResizeWidthminResizeHeight:指定小部件的绝对最小大小。
  • updatePeriodMillis:定义小部件框架通过调用 onUpdate() 回调方法来从 AppWidgetProvider 请求更新的频率应该是多大。
  • initialLayout:指向用于定义小部件布局的布局资源。
  • configure:定义要在用户添加小部件时启动以便用户配置小部件属性的 Activity。
  • previewImage:指定预览来描绘小部件经过配置后是什么样子的,用户在选择小部件时会看到该预览。
  • autoAdvanceViewId :指定应由小部件的托管应用自动跳转的小部件子视图的视图 ID。
  • resizeMode :指定可以按什么规则来调整微件的大小,可选值为“horizontal|vertical”,一般默认设置横竖都可以进行调整。
  • minResizeHeight :指定可将微件大小调整到的最小高度。
  • minResizeWidth:指定可将微件大小调整到的最小宽度。
  • widgetCategory:声明小部件是否可以显示在主屏幕 (home_screen) 或锁定屏幕 (keyguard) 上。只有低于 5.0 的 Android 版本才支持锁定屏幕微件。对于 Android 5.0 及更高版本,只有 home_screen 有效,所以现在将这个值写为home_screen即可。

    布局 test_widget

    这个是界面相关,这里写了一个简单的文字和GridView。支持的控件有限,主要是比较常用的几个,Android12新增了CheckBox、Swith、RadioButton,在12以下使用会有问题。可用控件如下:

  • FrameLayout、LinearLayout、RelativeLayout、GridLayout

  • TextView、Button、ImageView、ProgressBar、ImageButton、TextClock、ListView、GridView、StackView、Chronometer、AnalogClock、ViewFlipper、AdapterViewFlipper ```kotlin

  1. <a name="TestWidget"></a>
  2. #### TestWidget
  3. 小组件本质上是一个广播,这里需要在清单文件注册。
  4. ```kotlin
  5. <receiver
  6. android:name=".appwidget.gv.TestWidget"
  7. android:exported="true">
  8. <intent-filter>
  9. <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  10. <action android:name="com.leapmotor.testdemo.widget.TEST_WIDGET" />
  11. </intent-filter>
  12. <meta-data
  13. android:name="android.appwidget.provider"
  14. android:resource="@xml/test_widget_info" />
  15. </receiver>
  • 上面在自动创建时就会自动生成,第一个action的作用是所有的窗口小组件都接收android.appwidget.action.APPWIDGET_UPDATE动作的广播,该广播根据设置的android:updatePeriodMillis设定的时间间隔发出,用于定时更新桌面上的所有窗口小组件。
  • 第二个action是自己定义的,可以通过在该广播接收器中注册自定义的动作以使窗口小组件接收自定义的广播。

TestWidget类内容,有一个TextView实现修改文字并点击发送广播,同时显示GridView,源码如下:

  1. class TestWidget : AppWidgetProvider() {
  2. override fun onUpdate(
  3. context: Context,
  4. appWidgetManager: AppWidgetManager,
  5. appWidgetIds: IntArray
  6. ) {
  7. // There may be multiple widgets active, so update all of them
  8. for (appWidgetId in appWidgetIds) {
  9. updateAppWidget(context, appWidgetManager, appWidgetId)
  10. }
  11. }
  12. override fun onEnabled(context: Context) {
  13. // Enter relevant functionality for when the first widget is created
  14. }
  15. override fun onDisabled(context: Context) {
  16. // Enter relevant functionality for when the last widget is disabled
  17. }
  18. override fun onReceive(context: Context?, intent: Intent?) {
  19. super.onReceive(context, intent)
  20. //receiver broadcaster
  21. Log.e(TAG, "onReceive: ${intent?.action} ---- ${intent?.getIntExtra("content", 0)}")
  22. }
  23. val TAG = "MMM"
  24. private val clickAction = "itemClick"
  25. internal fun updateAppWidget(
  26. context: Context,
  27. appWidgetManager: AppWidgetManager,
  28. appWidgetId: Int
  29. ) {
  30. val widgetText = "Test GridView"
  31. // Construct the RemoteViews object
  32. val views = RemoteViews(context.packageName, R.layout.test_widget)
  33. views.setTextViewText(R.id.appwidget_text, widgetText)
  34. val broadIntent = Intent(context, MyAppWidgetService::class.java)
  35. broadIntent.action = "Title";
  36. broadIntent.component = ComponentName(context, TestWidget::class.java) //不加无法发送广播
  37. val broadPendingIntent =
  38. PendingIntent.getBroadcast(context, 0, broadIntent, PendingIntent.FLAG_UPDATE_CURRENT)
  39. views.setOnClickPendingIntent(R.id.appwidget_text, broadPendingIntent);
  40. //gridview jjjjj
  41. val intent = Intent(context, MyAppWidgetService::class.java)
  42. intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
  43. views.setRemoteAdapter(R.id.gvTestWidgetList, intent);
  44. views.setEmptyView(R.id.gvTestWidgetList, R.layout.none_data);
  45. val clickIntent = Intent(context, TestWidget::class.java)
  46. clickIntent.action = clickAction
  47. clickIntent.data = Uri.parse(clickIntent.toUri(Intent.URI_INTENT_SCHEME))
  48. val pendingIntentTemplate = PendingIntent.getBroadcast(
  49. context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT
  50. )
  51. views.setPendingIntentTemplate(
  52. R.id.gvTestWidgetList,
  53. pendingIntentTemplate
  54. )
  55. // Instruct the widget manager to update the widget
  56. appWidgetManager.updateAppWidget(appWidgetId, views)
  57. }
  58. }

设置界面文字

首先要获取RemoteViews,然后就可以通过这个类去设置界面。比如对TextView设置文字,示例如下:

  1. val widgetText = "Test GridView"
  2. // Construct the RemoteViews object
  3. val views = RemoteViews(context.packageName, R.layout.test_widget)
  4. views.setTextViewText(R.id.appwidget_text, widgetText)

发送广播和接收广播

开启一个服务,也是通过发送广播实现的,发送广播后在接收onReceice中去开启服务。

  • 发送

    1. val broadIntent = Intent(context, MyAppWidgetService::class.java)
    2. broadIntent.action = "Title";
    3. broadIntent.component = ComponentName(context, TestWidget::class.java) //不加无法发送广播
    4. val broadPendingIntent =
    5. PendingIntent.getBroadcast(context, 0, broadIntent, PendingIntent.FLAG_UPDATE_CURRENT)
    6. views.setOnClickPendingIntent(R.id.appwidget_text, broadPendingIntent);
  • 接收

    1. override fun onReceive(context: Context?, intent: Intent?) {
    2. super.onReceive(context, intent)
    3. //receiver broadcaster
    4. Log.e(TAG, "onReceive: ${intent?.action} ---- ${intent?.getIntExtra("content", 0)}")
    5. }

    跳转Activity
    1. //设置点击跳转界面
    2. Intent intent = new Intent(context, SecondActivity.class);
    3. intent.setAction("key");
    4. PendingIntent pendingIntent = PendingIntent.getActivity(context, 20, intent, PendingIntent.FLAG_CANCEL_CURRENT);
    5. getmRemoteViews(context).setOnClickPendingIntent(R.id.tvNewAppWidgetSmall, pendingIntent);

    实现展示GridView和点击
  • MyRemoteFactory

    1. public class MyRemoteFactory implements RemoteViewsService.RemoteViewsFactory {
    2. private Context context;
    3. private List<Integer> list = new ArrayList<>();
    4. public MyRemoteFactory(Context context, Intent intent) {
    5. this.context = context;
    6. }
    7. @Override
    8. public void onCreate() {
    9. list.add(1);
    10. list.add(2);
    11. list.add(3);
    12. list.add(4);
    13. }
    14. @Override
    15. public void onDataSetChanged() {
    16. //
    17. }
    18. @Override
    19. public void onDestroy() {
    20. list.clear();
    21. }
    22. @Override
    23. public int getCount() {
    24. return list.size();
    25. }
    26. @Override
    27. public RemoteViews getViewAt(int position) {
    28. if (position < 0 || position >= list.size()) return null;
    29. Integer integer = list.get(position);
    30. RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.item_second);
    31. remoteViews.setTextViewText(R.id.tvItemSecondNum,"NUM:" + integer);
    32. //设置点击
    33. Intent intent = new Intent();
    34. intent.putExtra("content", integer);
    35. remoteViews.setOnClickFillInIntent(R.id.tvItemSecondNum, intent);
    36. return remoteViews;
    37. }
    38. @Override
    39. public RemoteViews getLoadingView() {
    40. return null;
    41. }
    42. @Override
    43. public int getViewTypeCount() {
    44. return 1;
    45. }
    46. @Override
    47. public long getItemId(int position) {
    48. return position;
    49. }
    50. @Override
    51. public boolean hasStableIds() {
    52. return true;
    53. }
    54. }
  • MyAppWidgetService
    服务要在清单中注册:

    1. public class MyAppWidgetService extends RemoteViewsService {
    2. @Override
    3. public RemoteViewsFactory onGetViewFactory(Intent intent) {
    4. return new MyRemoteFactory(this.getApplicationContext(), intent);
    5. }
    6. }
  1. <service
  2. android:name=".appwidget.gv.MyAppWidgetService"
  3. android:exported="false"
  4. android:permission="android.permission.BIND_REMOTEVIEWS" />
  • 在TestWidget的onUpdate中

    1. //设置界面相关
    2. val intent = Intent(context, MyAppWidgetService::class.java)
    3. intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
    4. views.setRemoteAdapter(R.id.gvTestWidgetList, intent);
    5. views.setEmptyView(R.id.gvTestWidgetList, R.layout.none_data);
    6. //设置点击
    7. val clickIntent = Intent(context, TestWidget::class.java)
    8. clickIntent.action = clickAction
    9. clickIntent.data = Uri.parse(clickIntent.toUri(Intent.URI_INTENT_SCHEME))
    10. val pendingIntentTemplate = PendingIntent.getBroadcast(
    11. context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT
    12. )
    13. views.setPendingIntentTemplate(
    14. R.id.gvTestWidgetList,
    15. pendingIntentTemplate
    16. )
  • 所有界面相关设置好以后,要在onUpdate中设置appWidgetManager.updateAppWidget(appWidgetId, views)才会生效。

    其他地方更新小组件

    在其他界面如何更新小组件,一般可以通过发送广播或者获取到 RemoteViews 以后去更新界面。

  1. 通过发送广播通知小组件更新

    1. //这个 action 是在清单文件中注册的
    2. Intent intent = new Intent("https://juejin.cn/post/6968851189190377480#heading-15");
    3. sendBroadcast(intent);
  2. 直接更新

    1. // 获取Widget管理器
    2. AppWidgetManager awm = AppWidgetManager
    3. .getInstance(MainActivity.this);
    4. // Widget组件名字
    5. ComponentName compe = new ComponentName(MainActivity.this,
    6. MyAppWidgetProvider.class);
    7. // 创建RemoteViews
    8. RemoteViews rv = new RemoteViews(MainActivity.this
    9. .getPackageName(), R.layout.my_widget_layout);
    10. // 修改RemoteViews中的刷新按钮
    11. // 可以只用RemoteViews.setXXX对应的方法修改RemoteView中的组件。
    12. rv.setTextViewText(R.id.button_refresh, "刷新Refresh");
    13. // 设置字体颜色
    14. rv.setTextColor(R.id.button_refresh, Color.RED);
    15. //更新Widget
    16. awm.updateAppWidget(compe, rv);

    参考

Android 12上焕然一新的小组件:美观、便捷和实用

  • Android12以下

在Android 窗口小组件(Widget)中显示(StackView,ListView,GridView)集合View
Android桌面小部件开发,及注意事项
Android 开发之 App Widget 详解