这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天
自定义通知系列文章包括:
- 自定义通知的基础使用、UI适配(展开&折叠)
- bug修复,TransactionTooLargeException
本文是第一篇,记录了在实际开发中遇到的各种适配问题,值得一看~
本文主角:
- NotificationChannel
- NotificationManager(notify方法)
- RemoteViews
- NotificationManagerCompat.cancel(notifyId)
- UI适配(展开&折叠)
创建一条自定义通知
NotificationChannel + RemoteView + Notification 三个主要的类
安卓8之后引入了通知渠道(NotificationChannel),可将多条(notifyId不同)通知绑定到同一个通知渠道下,用户可在设置中手动关闭
/**
*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)
}
创建一条通知
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) //为按钮设置点击事件
创建后的通知长这样~
可以看到,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也会提醒你的~
那支持自定义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也温馨提示了~
总结: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适配(背景颜色)
问题:未居中
改为自适应后如下:
经过测试,android:background在部分机型上并没有生效,还是显示系统默认的颜色,如上图,使用如下API设置背景颜色有效
RemoteView?.setInt(R.id.ll_root, "setBackgroundColor", ContextCompat.getColor(context, R.color.color_367AF6))
建议:通知的背景应适配系统背景的颜色,这样看上去协调统一,因为不同的手机通知的背景色是不一样的。
悬浮通知
像横幅一样显示在手机的顶部,就叫悬浮通知,部分机型有悬浮通知权限
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的要少,看下图,蓝色通知的高度比白色通知的高度少
原来,安卓12(targetSdk >= 32)自定义通知的显示区域在收紧(折叠时高度上限为64dp,展开时高度上限为 256dp),系统强制显示通知的小图标,包括常驻通知,显示区域为图中阴影部分
真相了,我们的app是适配到安卓12的,所以显示区域少,解包看看别人的app,发现适配到安卓11,所以,解决方案是,安卓12和11以下分别用两套布局,这样效果是最完美的,但是通知有展开和折叠两种样式,也就是说,每改动一次通知的UI,要同步修改4个布局,难受...
参考链接
NotificationChannel | Android Developers
RemoteViews | Android Developers
Android Notification SetAutoCancel not working - Stack Overflow