做桌面组件已经是两年多前的事情,今日稍微做个记录。
需求
- 三种大小的组件:2 * 2,4 * 2,4 * 4
- 可以刷新数据
- 点击跳到App内落地页
实现
1.声明Widget的属性
在res/xml目录下创建xml文件,里面包含宽高、刷新时间等属性,所以这里定义3种xml
2 * 2
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/desk_widget_2_2"
android:minHeight="110dp"
android:minWidth="155dp"
android:previewImage="@drawable/desk_widget_icon_2_2"
android:updatePeriodMillis="72720000"
android:resizeMode="none"
android:widgetCategory="home_screen">
</appwidget-provider>
4 * 2
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/desk_widget_4_2"
android:minHeight="110dp"
android:minWidth="375dp"
android:previewImage="@drawable/desk_widget_icon_4_2"
android:updatePeriodMillis="72720000"
android:resizeMode="none"
android:widgetCategory="home_screen">
</appwidget-provider>
4 * 4
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/desk_widget_4_4"
android:minHeight="280dp"
android:minWidth="375dp"
android:previewImage="@drawable/desk_widget_icon_4_4"
android:updatePeriodMillis="72720000"
android:resizeMode="none"
android:widgetCategory="home_screen">
</appwidget-provider>
三种xml定义基本一样,对各个属性进行一下说明:
- android:minWidth: 最小宽度
android:minHeight:最小高度
android:updatePeriodMillis: 更新widget的时间间隔(ms)
android:previewImage: 预览图片,拖动小部件到桌面时有个预览图
android:initialLayout: 加载到桌面时对应的布局文件
android:resizeMode:拉伸的方向
android:widgetCategory: 是否可以显示在主屏幕 (home_screen) 或锁定屏幕 (keyguard) 上,5.0及以上只有home_screen有效
在设置宽高时遇到一些问题,宽高其实会直接决定卡片在手机屏幕上占的格子格子数 * 70 - 30 = 长度
。所以2 * 2的卡片理论上宽高都是110dp,实际使用时发现如果手机一行有4个icon那么显示是正常的,如果手机一行有5个icon,那个两个格子的宽度是小于我们需要的宽度的。
在实际使用中都是设置的实际宽高,这样可能组件在某些手机上占用了过大的距离,但是能保证显示完整。
2.创建布局文件
略
3.管理Widget状态
系统提供了AppWidgetProvider这个类,我们来看看它的源码:
public void onReceive(Context context, Intent intent) {
// Protect against rogue update broadcasts (not really a security issue,
// just filter bad broacasts out so subclasses are less likely to crash).
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (appWidgetIds != null && appWidgetIds.length > 0) {
this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
}
}
} else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
this.onDeleted(context, new int[] { appWidgetId });
}
} else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
&& extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
appWidgetId, widgetExtras);
}
} else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.onEnabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (oldIds != null && oldIds.length > 0) {
this.onRestored(context, oldIds, newIds);
this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
}
}
}
}
这里面就包含了Widget的几个生命周期:onEnabled、onAppWidgetOptionsChanged、onDeleted、onDisabled、onRestored、onUpdate,可见生命周期的控制是onReceive函数中实现的。
下面可以看一下我们使用AppWidgetProvider做的一些事情
3.1 定义一个基类BaseWidgetProvider
- 声明Action
public static final String APP_STATE_BACK = "android.appwidget.action.APP_STATE_BACK";
- 持有AppWidgetManager,需要注意的是AppWidgetManager是一个单例
AppWidgetManager.getInstance(context);
- 持有所有组件的id,可以通过AppWidgetManager去获取。这里也有一个需要注意的点,就是同一种组件是可以被重复添加的,所以一个provider会对应多个组件。
int[] ids = appWidgetManager.getAppWidgetIds(new ComponentName(context, this.getClass()));
- onReceive中要做的事情
action状态对应的行为
网络状态变化(可用)/App切到后台/用户登录:刷新组件
用户退出登录:重置组件
其他:交给实现类处理 - 更新Widget
将子类加载好的remoteViews设置给对应的组件
mAppWidgetmanager.updateAppWidget(appWidgetId, remoteViews);
3.2 实现类逻辑
- BaseWidgetProvider分发的action
基本是点击跳转行为,跳转的url从onReceive中的intent中获取,intent中的url是如何塞进去后面会提到 - 初始化Widget
创建RemoteViews,并且给View设值,方法比较特殊
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), getViewLayout());
remoteViews.setViewVisibility(@IdRes int viewId, @View.Visibility int visibility)
remoteViews.setTextViewText
remoteViews.setImageViewBitmap
- 添加点击事件
这个过程需要使用RemoteViews和PackageManager
添加打开App的行为
PackageManager packageManager = context.getPackageManager();
intent = packageManager.getLaunchIntentForPackage(context.getPackageName());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
remoteViews.setOnClickPendingIntent(id, pendingIntent);
添加跳转到指定页面的行为
Intent intent = new Intent(action);
intent.setData(Uri.parse(url));
intent.setClass(context, cls);
PendingIntent pendingIntent =
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE|PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(viewId, pendingIntent);
5.AndroidManifest注册桌面组件
我们定义了3种组件,这里对应也是注册3个receiver。至于为什么是receiver,因为AppWidgetProvider的父类是BroadcastReceiver。
<!-- 桌面Widget -->
<receiver android:name=".desk.DeskWidgetMinProvider"
android:exported="true">
<intent-filter>
<!-- 接收的action -->
<!-- APPWIDGET_UPDATE是必须要有的 -->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APP_STATE_BACK" />
<action android:name="android.appwidget.action.NET_CONNECT_AVAILABLE" />
<action android:name="android.appwidget.action.USER_STATE_ENTER" />
<action android:name="android.appwidget.action.USER_STATE_EXIT" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/desk_widget_2_2" />
</receiver>
<receiver android:name=".desk.DeskWidgetMediumProvider"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APP_STATE_BACK" />
<action android:name="android.appwidget.action.NET_CONNECT_AVAILABLE" />
<action android:name="android.appwidget.action.USER_STATE_ENTER" />
<action android:name="android.appwidget.action.USER_STATE_EXIT" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/desk_widget_4_2" />
</receiver>
<receiver android:name=".desk.DeskWidgetMaxProvider"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APP_STATE_BACK" />
<action android:name="android.appwidget.action.NET_CONNECT_AVAILABLE" />
<action android:name="android.appwidget.action.USER_STATE_ENTER" />
<action android:name="android.appwidget.action.USER_STATE_EXIT" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/desk_widget_4_4" />
</receiver>
其他点
在画
参考文档
https://www.jianshu.com/p/1eec51bf74be
https://developer.android.google.cn/guide/topics/appwidgets/overview?hl=zh-cn
网友评论