如何使用 RemoteViews 更新通知?

47
我正在创建一个通知,其中包含使用自定义服务(Service)中的RemoteViews。该服务在前台模式下运行,也就是说,只要用户可以看到通知,服务就会保持活动状态。通知被设置为“进行中”,因此用户无法将其滑动关闭。
例如,我想更改布局中的ImageView所显示的位图(bitmap),或者更改TextView中的文本值。远程视图(remote view)中的布局设置为XML布局文件。
我的问题是,一旦通知被创建并对用户可见,如果我调用任何RemoteViews的函数(例如setImageViewResource()来更改在ImageView中显示的Bitmap时,除非我随后调用setImageViewResource(),否则更改不会显示出来。
NotificationManager.notify( id, notification );

或者
Service.startForeground(id,notification);

不过,我对此持怀疑态度。我无法相信要更新已创建的通知中的RemoteViews UI,我必须重新初始化通知。如果我的通知中有Button控件,它会在触摸和释放时自动更新。所以肯定有一种正确的方法来做到这一点,但我不知道怎么做。

这是我在Service实例中创建通知的代码:

this.notiRemoteViews = new MyRemoteViews(this,this.getApplicationContext().getPackageName(),R.layout.activity_noti1);

Notification.Builder notibuilder = new Notification.Builder(this.getApplicationContext());
notibuilder.setContentTitle("Test");
notibuilder.setContentText("test");
notibuilder.setSmallIcon(R.drawable.icon2);
notibuilder.setOngoing(true);

this.manager = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE);
this.noti = notibuilder.build();
this.noti.contentView = this.notiRemoteViews;
this.noti.bigContentView = this.notiRemoteViews;
this.startForeground(NOTIFICATION_ID, this.noti);

一个可以“强制”通知界面变更的功能:

public void updateNotiUI(){
    this.startForeground(NOTIFICATION_ID, this.noti);
}

MyRemoteViews类中,如果需要更改UI,则可以执行以下操作:

this.setImageViewResource(R.id.iconOFF, R.drawable.icon_off2);
this.ptMyService.updateNotiUI();

有没有人能告诉我在通知中更新RemoteViews的UI组件的正确方法是什么?
4个回答

67

这里给你提供一个详细的例子,用 RemoteViews 更新通知:

private static final int NOTIF_ID = 1234;
private NotificationCompat.Builder mBuilder;
private NotificationManager mNotificationManager;
private RemoteViews mRemoteViews;
private Notification mNotification;
...

// call this method to setup notification for the first time
private void setUpNotification(){

    mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

    // we need to build a basic notification first, then update it
    Intent intentNotif = new Intent(this, MainActivity.class);
    intentNotif.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
    PendingIntent pendIntent = PendingIntent.getActivity(this, 0, intentNotif, PendingIntent.FLAG_UPDATE_CURRENT);

    // notification's layout
    mRemoteViews = new RemoteViews(getPackageName(), R.layout.custom_notification_small);
    // notification's icon
    mRemoteViews.setImageViewResource(R.id.notif_icon, R.drawable.ic_launcher);
    // notification's title
    mRemoteViews.setTextViewText(R.id.notif_title, getResources().getString(R.string.app_name));
    // notification's content
    mRemoteViews.setTextViewText(R.id.notif_content, getResources().getString(R.string.content_text));

    mBuilder = new NotificationCompat.Builder(this);

    CharSequence ticker = getResources().getString(R.string.ticker_text);
    int apiVersion = Build.VERSION.SDK_INT;

    if (apiVersion < VERSION_CODES.HONEYCOMB) {
        mNotification = new Notification(R.drawable.ic_launcher, ticker, System.currentTimeMillis());
        mNotification.contentView = mRemoteViews;
        mNotification.contentIntent = pendIntent;

        mNotification.flags |= Notification.FLAG_NO_CLEAR; //Do not clear the notification
        mNotification.defaults |= Notification.DEFAULT_LIGHTS;

        // starting service with notification in foreground mode
        startForeground(NOTIF_ID, mNotification);

    }else if (apiVersion >= VERSION_CODES.HONEYCOMB) {
        mBuilder.setSmallIcon(R.drawable.ic_launcher)
                .setAutoCancel(false)
                .setOngoing(true)
                .setContentIntent(pendIntent)
                .setContent(mRemoteViews)
                .setTicker(ticker);

        // starting service with notification in foreground mode
        startForeground(NOTIF_ID, mBuilder.build());
    }
}

// use this method to update the Notification's UI
private void updateNotification(){

    int api = Build.VERSION.SDK_INT;
    // update the icon
    mRemoteViews.setImageViewResource(R.id.notif_icon, R.drawable.icon_off2);
    // update the title
    mRemoteViews.setTextViewText(R.id.notif_title, getResources().getString(R.string.new_title));
    // update the content
    mRemoteViews.setTextViewText(R.id.notif_content, getResources().getString(R.string.new_content_text));

    // update the notification
    if (api < VERSION_CODES.HONEYCOMB) {
        mNotificationManager.notify(NOTIF_ID, mNotification);
    }else if (api >= VERSION_CODES.HONEYCOMB) {
        mNotificationManager.notify(NOTIF_ID, mBuilder.build());
    }
}

通知的布局,即res/layout/custom_notification_small.xml

<!-- We have to set the height to 64dp, this is the rule of the small notification -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="64dp"
    android:orientation="horizontal"
    android:id="@+id/notif_small"
    android:background="@drawable/notification_background">

    <ImageView
        android:id="@+id/notif_icon"
        android:contentDescription="@string/notif_small_desc"
        android:layout_width="47dp"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_alignParentLeft="true"
        android:src="@drawable/ic_launcher"
        android:layout_marginLeft="7dp"
        android:layout_marginRight="9dp"/>

    <TextView
        android:id="@+id/notif_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/notif_icon"
        android:singleLine="true"
        android:paddingTop="8dp"
        android:textSize="17sp"
        android:textStyle="bold"
        android:textColor="#000000"
        android:text="@string/app_name"/>

    <TextView
        android:id="@+id/notif_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/notif_icon"
        android:paddingBottom="9dp"
        android:layout_alignParentBottom="true"
        android:singleLine="true"
        android:textSize="13sp"
        android:textColor="#575757"
        android:text="Content" />
</RelativeLayout>

希望这个例子对你有很大帮助!

注意:在Honeycomb版本之前,您无法更新自定义的NotificationCompat。因此,我添加了一种替代方式来在Honeycomb版本之前更新它,即首先检查API级别并改用已弃用的Notification


你的答案是正确的,但我们需要在updateNotification()处调用。 - Dilip
@Dilip,请再看一下我的回答。我已经更新了它,这样你就可以更好地理解了。 - Anggrayudi H
startForeground() RemoteView通知更新的唯一可行解决方案。非常棒的答案。 - Gal Rom
@AnggrayudiH 嘿,它给了我这个错误 android.app.RemoteServiceException: Bad notification posted from package com.areel.android: Couldn't expand RemoteViews for: StatusBarNotification(pkg=com.areel.android user=UserHandle{0} id=1234 tag=null key=0|com.areel.android|1234|null|10432: Notification(pri=0 contentView=com.areel.android/0x7f0b0044 vibrate=null sound=null tick defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE)) - Akash Dubey
为什么你没有在 onStartCommand 中设置东西呢?只是好奇,因为我刚开始使用前台服务,并且我找到的所有示例都使用了 @Override onStartCommand。 - therealone
有没有文档建议只使用这种方式,因为其他方式对于图像不起作用,并且文档中没有任何指南。这让我疯了一天,现在终于解决了我的问题。 - Ajay Naredla

16

警告!

更新通知的唯一正确方式是在每次 NotificationManager#notify 之前重新创建 RemoteViews。为什么?由于存在内存泄漏,会导致 TransactionTooLargeException。以下问题已经报告:

对于 RemoteViews 的每个调用(例如 setViewVisibility(...) 等)都会将相应的操作添加到待应用的操作队列中。在 notify 后,远程视图被膨胀并实际应用操作。但是队列不会被清除!

查看在调试此情况时拍摄的屏幕截图。

在此输入图像描述

在这里,我使用来自ViewModel的数据更新音频播放器通知。应用程序在第81行停止,并且您可以看到具有51个操作数组的RemoteViews实例!但是,我只切换了两次音轨并按下了暂停!当然,过一段时间后我必须观察应用程序崩溃并出现TransactionTooLargeException。

浅显的研究证实,没有公共API可以直接或间接地清除操作队列,因此更新通知视图的唯一方法是分别保持其状态并重新创建传递给Notification.Builder的RemoteViews实例,不过这并不会大量超载UI线程。


2
兄弟,你救了我一天!谢谢。 我以为我实现了很酷的优化,可以节省内存。但它带来了更多的伤害。 - Oleksandr Albul
@Max Elkin:这是什么意思?我们应该使用startForeground(ID, notification)而不是NotificationManager.notify(ID, notification)吗? - user1090751

4
你需要调用NotificationManager.notify(id, notification)方法来告诉通知系统你想要更新通知视图。这是文档链接:http://developer.android.com/training/notify-user/managing.html
编写一个返回通知对象的方法。
private Notification getNotification(NotificationCompat.Builder mBuilder) {
    RemoteViews mRemoteViews = new RemoteViews(getPackageName(), R.layout.notification_layout);
    // Update your RemoteViews
    mBuilder.setContent(mRemoteView);
    Notification mNotification = mBuilder.build();
    // set mNotification.bigContentView if you want to
    return mNotification;

}

private void refreshNotification() {
    mNotificationManager.notify(getNotification(mNotificationBuilder),
                        NOTIFICATION_ID);
    // mNotificationBuilder is initialized already
}

另外,请注意,bigContentViewRemoteViews没有完全重绘。如果bigContentView的某些元素的可见性被设置为GONE,并且您想在下一次显示它时,必须明确将其可见性设置为VISIBLE


你们两位的回答终于帮了我,非常感谢。@Anggrayudi先回答所以他应该得到50个声望点数。Froyo的回答也很不错,谢谢。 - Rafael

1
不要存储Notification对象,而是存储Notification.Builder对象。在推送通知之前每次生成新的通知。
NotificationManager.notify( id, notification );

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接