【问题标题】:How to update Notification with RemoteViews?如何使用 RemoteViews 更新通知?
【发布时间】:2014-05-12 10:41:47
【问题描述】:

我正在使用自定义Service 中的RemoteViews 创建一个通知,该通知在前台模式下与通知一起运行(也就是说,只要通知对用户可见,服务就会保持活动状态)。通知设置为进行中,因此用户无法将其关闭。

我想更改例如ImageView 中显示的位图,包含在远程视图的布局中或更改TextView 中的文本值。远程视图中的布局使用 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);

以及“强制”UI 更改为通知的功能:

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 组件的正确方法是什么?

【问题讨论】:

    标签: android android-notifications android-remoteview


    【解决方案1】:

    这里有一个详细示例,您可以使用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>
    

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

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

    【讨论】:

    • 你的答案是正确的,但是我们需要调用 updateNotification()
    • @Dilip,再次查看我的答案。我已经更新了,所以你可以了解更多。
    • startForeground() RemoteView 通知更新的唯一有效解决方案。很棒的答案。
    • @AnggrayudiH 嘿,它给了我这个错误 android.app.RemoteServiceException:从包 com.areel.android 发布的错误通知:无法扩展 RemoteViews:StatusBarNotification(pkg=com.areel.android 用户=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 标志=0x62 颜色=0x00000000 vis=PRIVATE))
    • 你为什么不在 onStartCommand 中设置这些东西?只是好奇,因为我刚刚开始使用前台服务,我发现的所有示例都使用 @Override onStartCommand
    【解决方案2】:

    警告!

    更新通知的唯一正确方法是在每个 NotificationManager#notify 之前重新创建 RemoteView。为什么?存在导致 TransactionTooLargeException 的内存泄漏,正如这些问题中所报告的那样:

    对 RemoteViews 的每次调用,例如 setViewVisibility(...) 等都会将相应的操作添加到要应用的操作队列中。在通知远程视图后,实际应用了操作。但是队列没有清空!

    看看这个案例在调试过程中截取的截图。

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

    研究证实没有公共 API 可以直接或间接清除操作队列,因此更新通知视图的唯一方法是单独保存其状态并重新创建传递给 Notification.Builder 的 RemoteViews 实例,无论如何这不会使 UI 线程过载很多

    【讨论】:

    • 伙计,你救了我一天!谢谢你。我以为我已经实现了很酷的优化,可以节省内存。但它带来了更多的伤害。
    • @Max Elkin:这是什么意思?我们应该做 startForeground(ID, notification);而不是 NotificationManager.notify(ID, notification)?
    【解决方案3】:

    您必须致电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 H 首先回答,所以他应该得到 50 分的声誉。 Froyo 的回答非常好,谢谢。
    【解决方案4】:

    不要存储Notification 对象,而是Notification.Builder 对象。每次推送之前都会产生新的通知

    NotificationManager.notify( id, notification );
    

    【讨论】:

      猜你喜欢
      • 2015-07-07
      • 2016-10-27
      • 2014-02-05
      • 1970-01-01
      • 1970-01-01
      • 2013-08-09
      • 1970-01-01
      • 2011-05-05
      相关资源
      最近更新 更多