深入探索通知与插件的实时刷新
Widget
本文的合集已经编著成书,高级Android开发强化实战,欢迎各位读友的建议和指导。在京东即可购买:https://item.jd.com/12385680.html
Android在Android中, 除了本应用的视图外, 还允许操作远程视图(RemoteView). 其中包含两类实例, 一类是通知(Notification), 一类是插件(Widget), 这些都是附着于系统中, 通过广播更新页面. 系统为了避免频繁更新, 规定最低频率, 如果需要饶过这个机制, 则必须使用定时器(Alarm). 本文连接定时器与远程视图, 达到实时更新的目的, 两者都会涉及PendingIntent的使用. 本文包含源码.
本文源码的GitHub下载地址
插件
插件(Widget)实现两种功能, 一种是更新当前时间, 一种是更新启动状态, 需要注册两个IntentFilter. 使用Alarm定时器更新时间, 通过按钮控制启动状态.
注册
AppWidget(插件)在本质上是Receiver, 注册在AndroidManifest中也是, 不同的是定义特定的intent-filter
, 即APPWIDGET_UPDATE
.
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
并指定meta-data
的name
与resource
. name
是固定的, resource
指明所使用的资源信息.
<receiver android:name=".TimerAppWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<action android:name="org.wangchenlong.timerappwidget.action.CHANGE_STATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/timer_widget_provider"/>
</receiver>
AppWidget的描述信息.
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/app_widget_timer"
android:minHeight="52dp"
android:minWidth="260dp"
android:previewImage="@drawable/preview_widget"
android:resizeMode="horizontal|vertical"/>
initialLayout: Widget的布局(Layout)文件.
minWidth & minHeight: 定义Widget的最小宽度和高度, 当数值不是桌面cell的整数倍时, 宽高会被增至最接近的cell大小.
previewImage: 当用户选择添加Widget时的预览图片. 如果未定义, 则展示应用的登录图标.
resizeMode: 在水平和竖直方向是否允许调整大小, 值可选: horizontal(水平方向), vertical(竖直方向), none(不允许调整).
定义
AppWidget继承于AppWidgetProvider
, 而最终继承于BroadcastReceiver
. onUpdate
负责更新显示数据.
public class TimerAppWidget extends AppWidgetProvider {
// 每次更新都会创建新的实例, 只能使用静态变量
private static boolean sIsUpdate = false; // 是否启动更新时间
private static int sImageIndex = 0;
private static long sUpdateImageLastTime = 0L; // 上次更新图片的时间
@Override public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
if (intent.getAction().equals(CHANGE_STATE)) {
sIsUpdate = !sIsUpdate;
if (sIsUpdate) {
startUpdate(context); // 开始更新
} else {
stopUpdate(context); // 结束更新
}
}
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.app_widget_timer);
ComponentName widget = new ComponentName(context, TimerAppWidget.class);
Date date = new Date();
// 设置文字
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.ENGLISH);
rv.setTextViewText(R.id.widget_tv_text, sdf.format(date)); // 设置文字
// 更换图片
long seconds = TimeUnit.MILLISECONDS.toSeconds(date.getTime());
long interval = seconds - sUpdateImageLastTime; // 每隔5秒更换图片与图片文字
if (sUpdateImageLastTime == 0 || interval % 5 == 0) {
sImageIndex++;
rv.setImageViewResource(R.id.widget_tv_image, mAvatars[sImageIndex % IMAGE_COUNT]); // 设置图片
rv.setTextViewText(R.id.widget_tv_image_text, context.getString(mNames[sImageIndex % IMAGE_COUNT]));
sUpdateImageLastTime = seconds;
}
// 点击头像跳转主页
Intent mainIntent = new Intent(context, MainActivity.class);
PendingIntent mainPi = PendingIntent.getActivity(context, 0, mainIntent, FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.widget_tv_image, mainPi); // 设置点击事件
// 开启或关闭时间的控制
Intent changeIntent = new Intent(CHANGE_STATE);
PendingIntent changePi = PendingIntent.getBroadcast(context, 0, changeIntent, FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.widget_b_control, changePi);
// 更新插件
appWidgetManager.updateAppWidget(widget, rv);
}
}
获取当前的远程视图(RemoteViews
)和组件信息(ComponentName
), 最终用于更新插件, 即updateAppWidget
.
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.app_widget_timer);
ComponentName widget = new ComponentName(context, TimerAppWidget.class);
// ...
appWidgetManager.updateAppWidget(widget, rv);
在RemoteViews
中设置显示文字(TextView
)与图片(ImageView
)的控件, 或者点击事件. 注意, 事件触发使用PendingIntent
广播.
rv.setTextViewText(R.id.widget_tv_text, sdf.format(date)); // 设置文字
rv.setImageViewResource(R.id.widget_tv_image, mAvatars[(int) interval % 4]); // 设置图片
Intent mainIntent = new Intent(context, MainActivity.class);
PendingIntent mainPi = PendingIntent.getBroadcast(context, 0, mainIntent, FLAG_CANCEL_CURRENT);
rv.setOnClickPendingIntent(R.id.widget_tv_image, mainPi); // 设置点击事件
更新
更新的延迟消息(PendingIntent
), 获取组件与布局, 系统管理器更新插件, 获取插件ID组; 使用系统默认的插件活动ACTION_APPWIDGET_UPDATE
与参数EXTRA_APPWIDGET_IDS
设置延迟消息, 供定时器(AlarmManager
)使用.
private PendingIntent getUpdateIntent(Context context, boolean isStart) {
// 获取当前的组件
ComponentName widget = new ComponentName(context, TimerAppWidget.class);
// 获取布局, 并设置关闭显示
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.app_widget_timer);
if (isStart) {
rv.setTextViewText(R.id.widget_b_control, context.getString(R.string.stop));
} else {
rv.setTextViewText(R.id.widget_b_control, context.getString(R.string.start));
}
// 获取系统的AppWidgetManager
AppWidgetManager awm = AppWidgetManager.getInstance(context);
// 更新页面组件
awm.updateAppWidget(widget, rv);
int appWidgetIds[] = awm.getAppWidgetIds(widget);
Intent alertIntent = new Intent(context, TimerAppWidget.class);
alertIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); // 设置更新活动
alertIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds); // 设置当前插件的ID
return PendingIntent.getBroadcast(context, 0, alertIntent,
FLAG_CANCEL_CURRENT); // 取消前一个更新
}
启动或关闭定时器, 每秒更新一次.
public void startUpdate(Context context) {
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
// 设置更新
am.setRepeating(AlarmManager.RTC, System.currentTimeMillis(),
1000, getUpdateIntent(context, true));
}
public void stopUpdate(Context context) {
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
// 取消更新
am.cancel(getUpdateIntent(context, false));
}
通知
通知实现与插件类似, 使用相同的控制广播, 保持同步.
定义
通知栏使用自定义布局, 设置头像跳转事件与状态修改事件, 发送相应的PendingIntent广播, 注意状态使用FLAG_UPDATE_CURRENT, 更新当前重复的Intent. 当与Widget的PendingIntent重复时, 进行相应的替换.
public static NotificationCompat.Builder getNotification(Context context) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
builder.setSmallIcon(R.drawable.avatar_jessica);
builder.setWhen(System.currentTimeMillis());
builder.setOngoing(true); // 始终存在
// 开启或关闭时间的控制
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.notification_timer);
// 点击更新状态按钮
Intent changeIntent = new Intent(CHANGE_STATE);
PendingIntent changePi = PendingIntent.getBroadcast(context, 0, changeIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
// 点击头像跳转主页
Intent mainIntent = new Intent(context, MainActivity.class);
PendingIntent mainPi = PendingIntent.getActivity(context, 0, mainIntent, FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.widget_tv_image, mainPi); // 设置头像
rv.setOnClickPendingIntent(R.id.widget_b_control, changePi); // 设置更新
builder.setCustomContentView(rv);
return builder;
}
更新
收到通知后, 实时更新当前时间, 每隔5秒循环更新图片, 由于远程视图, 每次都会创建实例, 因此参数需要使用静态变量.
private void updateData(Context context) {
NotificationCompat.Builder builder = getNotification(context);
// 获取布局, 并设置关闭显示
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.notification_timer);
Date date = new Date();
// 设置文字
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.ENGLISH);
rv.setTextViewText(R.id.widget_tv_text, sdf.format(date)); // 设置文字
// 更换图片
long seconds = TimeUnit.MILLISECONDS.toSeconds(date.getTime());
long interval = seconds - sUpdateImageLastTime; // 每隔5秒更换图片与图片文字
if (sUpdateImageLastTime == 0 || interval % 5 == 0) {
sImageIndex++;
rv.setImageViewResource(R.id.widget_tv_image, mAvatars[sImageIndex % IMAGE_COUNT]); // 设置图片
rv.setTextViewText(R.id.widget_tv_image_text, context.getString(mNames[sImageIndex % IMAGE_COUNT]));
sUpdateImageLastTime = seconds;
}
builder.setCustomContentView(rv);
Notification notification = builder.build();
NotificationManager manager = (NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(sId, notification);
}
动画
至此, 定时更新的插件与通知已经完成, 注意PendingIntent与定时器的使用方式. 这个实例对于开发插件与通知而言, 已经完全足够, 抛砖引玉.
OK, that's all! Enjoy it!