概述
Android上经常看到各种桌面小组件,命名为AppWidget,通常天气,时钟会比较常见小组件。Android12发布后,对小组件做了很多优化。测试了一下新增的组件,在Android12以下会有错误,所以想使用新增的控件,需要做适配。不过由于没有Android12的设备,无法体验优化的效果怎么样。所以本篇没有Android12的新增内容。
如何使用小组件
创建
直接new一个widget即可。然后对小组件的基本信息进行设置。
Class Name : 组件名称
Placement : 组件可以显示的地方,Homescreen屏幕,Keyguard锁屏
Resizable : 组件调整大小
MiniMum Width: 组件宽占多少格
Minimum Heigt: 组件高占多少格
新建后会生成三个文件,以demo为例,会生成TestWidget组件类,test_widget组件布局,test_widget_info组件布局设置。
test_widget_info
先看test_widget_info,这个可以再次修改上面创建时的设置,同时也可以添加一些新的设置。如下:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_widget_description"
android:initialKeyguardLayout="@layout/test_widget"
android:initialLayout="@layout/test_widget"
android:minWidth="250dp"
android:minHeight="180dp"
android:previewImage="@drawable/example_appwidget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen" />
属性介绍:
- minWidth 和 minHeight :指定小部件默认情况下占用的最小空间。
注意:为使小部件能够在设备间移植,小部件的最小大小不得超过 4 x 4 单元格。 - minResizeWidth和minResizeHeight:指定小部件的绝对最小大小。
- 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
<a name="TestWidget"></a>
#### TestWidget
小组件本质上是一个广播,这里需要在清单文件注册。
```kotlin
<receiver
android:name=".appwidget.gv.TestWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="com.leapmotor.testdemo.widget.TEST_WIDGET" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/test_widget_info" />
</receiver>
- 上面在自动创建时就会自动生成,第一个action的作用是所有的窗口小组件都接收android.appwidget.action.APPWIDGET_UPDATE动作的广播,该广播根据设置的android:updatePeriodMillis设定的时间间隔发出,用于定时更新桌面上的所有窗口小组件。
- 第二个action是自己定义的,可以通过在该广播接收器中注册自定义的动作以使窗口小组件接收自定义的广播。
TestWidget类内容,有一个TextView实现修改文字并点击发送广播,同时显示GridView,源码如下:
class TestWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {
// Enter relevant functionality for when the first widget is created
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
override fun onReceive(context: Context?, intent: Intent?) {
super.onReceive(context, intent)
//receiver broadcaster
Log.e(TAG, "onReceive: ${intent?.action} ---- ${intent?.getIntExtra("content", 0)}")
}
val TAG = "MMM"
private val clickAction = "itemClick"
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val widgetText = "Test GridView"
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.test_widget)
views.setTextViewText(R.id.appwidget_text, widgetText)
val broadIntent = Intent(context, MyAppWidgetService::class.java)
broadIntent.action = "Title";
broadIntent.component = ComponentName(context, TestWidget::class.java) //不加无法发送广播
val broadPendingIntent =
PendingIntent.getBroadcast(context, 0, broadIntent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.appwidget_text, broadPendingIntent);
//gridview jjjjj
val intent = Intent(context, MyAppWidgetService::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
views.setRemoteAdapter(R.id.gvTestWidgetList, intent);
views.setEmptyView(R.id.gvTestWidgetList, R.layout.none_data);
val clickIntent = Intent(context, TestWidget::class.java)
clickIntent.action = clickAction
clickIntent.data = Uri.parse(clickIntent.toUri(Intent.URI_INTENT_SCHEME))
val pendingIntentTemplate = PendingIntent.getBroadcast(
context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT
)
views.setPendingIntentTemplate(
R.id.gvTestWidgetList,
pendingIntentTemplate
)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
设置界面文字
首先要获取RemoteViews,然后就可以通过这个类去设置界面。比如对TextView设置文字,示例如下:
val widgetText = "Test GridView"
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.test_widget)
views.setTextViewText(R.id.appwidget_text, widgetText)
发送广播和接收广播
开启一个服务,也是通过发送广播实现的,发送广播后在接收onReceice中去开启服务。
发送
val broadIntent = Intent(context, MyAppWidgetService::class.java)
broadIntent.action = "Title";
broadIntent.component = ComponentName(context, TestWidget::class.java) //不加无法发送广播
val broadPendingIntent =
PendingIntent.getBroadcast(context, 0, broadIntent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.appwidget_text, broadPendingIntent);
接收
override fun onReceive(context: Context?, intent: Intent?) {
super.onReceive(context, intent)
//receiver broadcaster
Log.e(TAG, "onReceive: ${intent?.action} ---- ${intent?.getIntExtra("content", 0)}")
}
跳转Activity
//设置点击跳转界面
Intent intent = new Intent(context, SecondActivity.class);
intent.setAction("key");
PendingIntent pendingIntent = PendingIntent.getActivity(context, 20, intent, PendingIntent.FLAG_CANCEL_CURRENT);
getmRemoteViews(context).setOnClickPendingIntent(R.id.tvNewAppWidgetSmall, pendingIntent);
实现展示GridView和点击
MyRemoteFactory
public class MyRemoteFactory implements RemoteViewsService.RemoteViewsFactory {
private Context context;
private List<Integer> list = new ArrayList<>();
public MyRemoteFactory(Context context, Intent intent) {
this.context = context;
}
@Override
public void onCreate() {
list.add(1);
list.add(2);
list.add(3);
list.add(4);
}
@Override
public void onDataSetChanged() {
//
}
@Override
public void onDestroy() {
list.clear();
}
@Override
public int getCount() {
return list.size();
}
@Override
public RemoteViews getViewAt(int position) {
if (position < 0 || position >= list.size()) return null;
Integer integer = list.get(position);
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.item_second);
remoteViews.setTextViewText(R.id.tvItemSecondNum,"NUM:" + integer);
//设置点击
Intent intent = new Intent();
intent.putExtra("content", integer);
remoteViews.setOnClickFillInIntent(R.id.tvItemSecondNum, intent);
return remoteViews;
}
@Override
public RemoteViews getLoadingView() {
return null;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return true;
}
}
MyAppWidgetService
服务要在清单中注册:public class MyAppWidgetService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new MyRemoteFactory(this.getApplicationContext(), intent);
}
}
<service
android:name=".appwidget.gv.MyAppWidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
在TestWidget的onUpdate中
//设置界面相关
val intent = Intent(context, MyAppWidgetService::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
views.setRemoteAdapter(R.id.gvTestWidgetList, intent);
views.setEmptyView(R.id.gvTestWidgetList, R.layout.none_data);
//设置点击
val clickIntent = Intent(context, TestWidget::class.java)
clickIntent.action = clickAction
clickIntent.data = Uri.parse(clickIntent.toUri(Intent.URI_INTENT_SCHEME))
val pendingIntentTemplate = PendingIntent.getBroadcast(
context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT
)
views.setPendingIntentTemplate(
R.id.gvTestWidgetList,
pendingIntentTemplate
)
所有界面相关设置好以后,要在onUpdate中设置appWidgetManager.updateAppWidget(appWidgetId, views)才会生效。
其他地方更新小组件
在其他界面如何更新小组件,一般可以通过发送广播或者获取到 RemoteViews 以后去更新界面。
通过发送广播通知小组件更新
//这个 action 是在清单文件中注册的
Intent intent = new Intent("https://juejin.cn/post/6968851189190377480#heading-15");
sendBroadcast(intent);
直接更新
// 获取Widget管理器
AppWidgetManager awm = AppWidgetManager
.getInstance(MainActivity.this);
// Widget组件名字
ComponentName compe = new ComponentName(MainActivity.this,
MyAppWidgetProvider.class);
// 创建RemoteViews
RemoteViews rv = new RemoteViews(MainActivity.this
.getPackageName(), R.layout.my_widget_layout);
// 修改RemoteViews中的刷新按钮
// 可以只用RemoteViews.setXXX对应的方法修改RemoteView中的组件。
rv.setTextViewText(R.id.button_refresh, "刷新Refresh");
// 设置字体颜色
rv.setTextColor(R.id.button_refresh, Color.RED);
//更新Widget
awm.updateAppWidget(compe, rv);
参考
- Android12
别羡慕苹果的小部件了,安卓也有
- Android12以下
在Android 窗口小组件(Widget)中显示(StackView,ListView,GridView)集合View
Android桌面小部件开发,及注意事项
Android 开发之 App Widget 详解