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

欢迎Follow我的GitHub, 关注我的简书, 博客目录.

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-datanameresource. 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!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容