深入探索通知与插件的实时刷新

本贴最后更新于 2676 天前,其中的信息可能已经物是人非

在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-datanameresourcename是固定的, 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, 而最终继承于BroadcastReceiveronUpdate负责更新显示数据.

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 与定时器的使用方式. 这个实例对于开发插件与通知而言, 已经完全足够, 抛砖引玉.


转自:https://gold.xitu.io/post/5850adc11b69e6006c75ba22

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1083 引用 • 3461 回帖 • 287 关注
  • 源码阁
    10 引用 • 5 回帖
  • 猿码阁
    19 引用 • 14 回帖
  • 郑禄秀
    9 引用 • 8 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...