Android 自定义通知的基础使用及UI适配

2,218 阅读6分钟

这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天

自定义通知系列文章包括:

  1. 自定义通知的基础使用、UI适配(展开&折叠)
  2. bug修复,TransactionTooLargeException

本文是第一篇,记录了在实际开发中遇到的各种适配问题,值得一看~

本文主角:

  1. NotificationChannel
  2. NotificationManager(notify方法)
  3. RemoteViews
  4. NotificationManagerCompat.cancel(notifyId)
  5. UI适配(展开&折叠)

创建一条自定义通知

NotificationChannel + RemoteView + Notification 三个主要的类

安卓8之后引入了通知渠道(NotificationChannel),可将多条(notifyId不同)通知绑定到同一个通知渠道下,用户可在设置中手动关闭

通知渠道.png

/**
*id:渠道ID,必须唯一,且不超过40个字符
*name:渠道名,用户在设置中看到的,如上图
*importance:重要程度
*/
public NotificationChannel(String id, CharSequence name, @Importance int importance) {
}

---importance参数的可选值---
//在通知栏可见,不会有提示音,不会横幅弹出
public static final int IMPORTANCE_LOW = 2;
//在通知栏可见,有提示音,不会横幅弹出
public static final int IMPORTANCE_DEFAULT = 3;
//在通知栏可见,有提示音,会横幅弹出
public static final int IMPORTANCE_HIGH = 4;

来创建一个通知渠道

//建议单独写在一个常量类中,防止别人在不了解的情况下随意修改
private const val CHANNEL_ID_DAILY = "channel_id_daily"

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
    val dailyChannel: NotificationChannel = NotificationChannel(
        CHANNEL_ID_DAILY,
        getString(R.string.notify_channel_name_daily),
        NotificationManager.IMPORTANCE_DEFAULT
    ).apply {
     	//设置一些参数,可以参考API文档自行设置
        setShowBadge(false)
        enableLights(true)
        enableVibration(true)
        lightColor = Color.RED
    }
    notificationManager.createNotificationChannel(dailyChannel)
}

image.png

创建一条通知

private const val REQUEST_CODE_DAILY = 0x1000
private const val NOTIFY_ID_DAILY = 0x2000

//通过传入channelId将这条通知和上面定义的通知渠道绑定
val notification = NotificationCompat.Builder(this, CHANNEL_ID_DAILY)
    .setSmallIcon(R.mipmap.ic_launcher)//必须设置,否则会奔溃
    .setLargeIcon(BitmapFactory.decodeResource(this.resources, R.mipmap.ic_launcher))
    .setCustomContentView(remoteViews)    //折叠后通知显示的布局
    .setCustomHeadsUpContentView(remoteViews)//横幅样式显示的布局
    .setCustomBigContentView(remoteViews) //展开后通知显示的布局
    .setContent(remoteViews)              //兼容低版本
    .setColor(ContextCompat.getColor(this, R.color.color_367AF6))//小图标的颜色
    .setAutoCancel(true)                   // 允许点击后清除通知
    .setPriority(NotificationCompat.PRIORITY_MAX)
    .setDefaults(NotificationCompat.DEFAULT_ALL)  // 默认配置,包括通知的提示音,震动效果等
    .setContentIntent(pendingIntent) //一定要设置,点击整个remoteView就可跳转
notificationManager.notify(NOTIFY_ID_DAILY, notification.build())

标准通知是直接调用setContentTitle & setContentText来创建通知的标题和内容, 这里并没有用到,而是多了一个remoteViews,看看怎么创建RemoteViews

val remoteViews = RemoteViews(this.packageName, R.layout.layout_cus_notify)
val funIntent = Intent(this, ArticleActivity::class.java)//演示代码:点击后跳转到ArticleActivity
funIntent.putExtra("key_article_nun", "第一篇")
val pendingIntent: PendingIntent =
    PendingIntent.getActivity(
        this, REQUEST_CODE_DAILY, funIntent,
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            PendingIntent.FLAG_MUTABLE
        } else {
            PendingIntent.FLAG_UPDATE_CURRENT
        }
    )
remoteViews.setTextViewText(R.id.text_info, "欢迎您查看的我的文章")
remoteViews.setOnClickPendingIntent(R.id.btn_fun, pendingIntent) //为按钮设置点击事件

创建后的通知长这样~

image.png

可以看到,RemoteView设置点击的方法并不是setOnClickListener,看看RemoteViews的真面目

public class RemoteViews implements Parcelable, Filter

并没有继承View,是一个序列化的类,那它和View到底有什么关系?这里暂时不展开讨论,会在第三篇中遇到的bug中,详细对RemoteViews进行说明~要注意,RemoteView只支持以下几种布局

  • AdapterViewFlipper
  • FrameLayout
  • GridLayout
  • GridView
  • LinearLayout
  • ListView
  • RelativeLayout
  • StackView
  • ViewFlipper

支持的控件有:

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextClock
  • TextView

API 31之后还支持以下的控件

  • CheckBox
  • RadioButton
  • RadioGroup
  • Switch

没错,不支持ConstraintLayout,IDE也会提醒你的~

image.png

那支持自定义View吗?试试EditText(继承自TextView),报错了

Caused by: android.view.InflateException: 
Binary XML file line #26 in com.test.notification:layout/layout_cus_notify: 
Class not allowed to be inflated android.widget.EditText

IDE也温馨提示了~

image.png

总结:RemoteViews只支持以上的控件,不支持它们的子类和其他View,当然也不支持自定义View。

关闭一条通知

标准通知的做法是,为整个通知设置点击的PendingIntent,通知点击后自动关闭通知

setContentIntent(PendingIntent)
setAutoCancel(true)

在自定义通知里这样是行不通的,这个想法也在Stack Overflow里面得到了证实,那如何关闭呢? 实现思路:给关闭按钮设置单独的PendingIntent,PendingIntent接收的intent是Broadcast,点击关闭按钮后发送特定的action,广播收到特定的action后cancel掉对应的notifyId

NotificationUtils.cancel(notifyId)

算是另辟蹊径了,但也确实没有找到更好的方法,线上跑了一段时间之后,发现ANR增加了,报错是在Broadcast的onReceive方法中,于是把cancel方法放到子线程去执行,发现还是会报ANR,因为我们项目比较特殊,有一个一直在后台运行的Service,于是就把PendingIntent的接收者改为Service,PendingIntent携带的Extra就是notifyId,代码如下,最终,没有再上报相关的ANR,ANR占比下降

//点击关闭按钮的相关代码
val closeIntent = Intent(context, CpuUseService::class.java)
closeIntent.putExtra(UpdateConstant.KEY_CLOSE_NOTIFY_ID, notifyId)
val closePendingIntent: PendingIntent =
    PendingIntent.getService(
        context,
        notifyId,
        closeIntent,
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else PendingIntent.FLAG_UPDATE_CURRENT
)
remoteViews.setOnClickPendingIntent(R.id.btn_close, closePendingIntent)

//Service接收的相关代码
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    super.onStartCommand(intent, flags, startId)
    val bundle = intent?.extras
    val notifyId = bundle?.getInt(UpdateConstant.KEY_CLOSE_NOTIFY_ID)
	lifecycleScope.launch(Dispatchers.IO) {
		repeatOnLifecycle(Lifecycle.State.CREATED) {
			NotificationUtils.cancel(notifyId)
		}
    } 
    return START_STICKY
}

通知的UI适配(背景颜色)

问题:未居中

image.png

改为自适应后如下:

image.png

经过测试,android:background在部分机型上并没有生效,还是显示系统默认的颜色,如上图,使用如下API设置背景颜色有效

RemoteView?.setInt(R.id.ll_root, "setBackgroundColor", ContextCompat.getColor(context, R.color.color_367AF6))

建议:通知的背景应适配系统背景的颜色,这样看上去协调统一,因为不同的手机通知的背景色是不一样的。

悬浮通知

像横幅一样显示在手机的顶部,就叫悬浮通知,部分机型有悬浮通知权限 image.png

setCustomHeadsUpContentView(mRemoteViews) //设置悬浮通知的布局
setCustomContentView(cusSmallRemoteViews) //设置收起后通知的布局 
setCustomBigContentView(cusBigRemoteViews) //设置展开后通知的布局 
setContent(cusBigRemoteViews) //兼容低版本,可根据需求设置

项目需求是:默认以展开样式显示横幅通知,不过,安卓12以上,通知默认都是折叠的,如果直接设置悬浮通知的布局为展开后的ui,会出现只显示部分通知的问题,适配伪代码如下

//创建通知需要传入NotificationChannel对应的channelId
val notification = NotificationCompat.Builder(context, channelId)
    .setCustomContentView(cusSmallRemoteViews) //折叠后通知显示的布局
    .setCustomBigContentView(cusBigRemoteViews)//展开后通知显示的布局
    .setContent(cusSmallRemoteViews) //兼容低版本,可根据需求设置
    ...忽略其他需要设置的api
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
    notification.setCustomHeadsUpContentView(cusBigRemoteViews)//设置悬浮通知的样式
}else{
    //安卓12以上不用设置setCustomHeadsUpContentView
}
notificationManager.notify(notifyId, notification.build())

就是安卓12不用单独设置setCustomHeadsUpContentView,用setCustomContentView和setCustomBigContentView单独设置折叠和展开就可以了,这样的话,在安卓12上,默认就是显示折叠的样式,用户点打开的箭头,就会显示展开的样式。

通知的UI适配(显示区域)

产品说:为什么在安卓12上,我们app显示区域的高度比其他app的要少,看下图,蓝色通知的高度比白色通知的高度少

image.png

原来,安卓12(targetSdk >= 32)自定义通知的显示区域在收紧(折叠时高度上限为64dp,展开时高度上限为 256dp),系统强制显示通知的小图标,包括常驻通知,显示区域为图中阴影部分

image.png

image.png

真相了,我们的app是适配到安卓12的,所以显示区域少,解包看看别人的app,发现适配到安卓11,所以,解决方案是,安卓12和11以下分别用两套布局,这样效果是最完美的,但是通知有展开和折叠两种样式,也就是说,每改动一次通知的UI,要同步修改4个布局,难受...

参考链接

NotificationChannel  |  Android Developers

RemoteViews  |  Android Developers

Android Notification SetAutoCancel not working - Stack Overflow

还没适配 Android 12 的要抓紧了 - 掘金

Android 12 Pending Intent