今天是端午节,祝大家节日快乐
这篇文章着实酝酿了许久,一直懒得写。网上关于通知栏样式适配的文章很多,但还不够完美,这也是我写这篇文章的动力所在。
温故而知新
Android通知有两种,默认通知与自定义通知。默认通知简单调用系统接口就能实现,如下:
发送默认通知 默认通知效果自定义通知就稍微麻烦一些,需要定义一个layout文件,使用RemoteViews加载它并设置一些点击事件,再设置到builder,如下:
自定义通知代码 自定义通知效果这个通知很简单,就是两行文本加上一个按钮,按钮具有单独的点击事件,点击后跳转到AnotherActivity。
注意:smallIcon对于自定义通知和默认通知都是必须的,否则通知显示不出来。道理很简单,smallIcon需要在状态栏上显示,不设置怎么行。在5.0及以上,smallIcon必须符合Material Design风格,即白色内容,透明背景。不然系统会使用默认的图片替换。具体可参考Android通知栏微技巧,那些你所没关注过的小细节 标签: android通知通知栏微技巧。后面我会有一篇更详细的文章来介绍这个。contentIntent对于2.3及以下的系统是必须的,否则发送通知时会抛异常。道理也很简单,Android 2.3及以下系统不支持给自定义通知上的元素绑定单独的点击事件,因此必须设置整个通知的点击事件。
为什么要进行样式适配?
默认通知不存在样式适配的问题,因为默认通知的布局、颜色、背景什么的都是系统的,系统总会正确的显示默认通知。但自定义通知就不一样了,自定义通知的布局完全由我们自己掌控,我们可以为元素设置任何背景、颜色。那么,问题来了。Android通知栏的背景各种各样,不同的ROM有不同的背景,白色、黑色、透明等。不同的Android版本通知栏背景也不一样,一旦我们为自定义通知上的元素设置了特定背景或颜色,就肯定会带来兼容性问题(主要是文本啦)。这样的应用一大把,贴个图大家就明白了:
未适配的自定义通知怎么适配?
适配的方式大概有两种,一种简单粗暴:为自定义通知设置固定的背景(上图中的360卫士就这么干的),比如黑色。那么内容自然就是白色或近似白色。这样,在所有的手机上都能正常显示,不会出现在黑色背景通知栏上显示良好,到了白色背景通知栏上就几乎啥也看不见。使用这种方案的应用太多了。我个人很不推崇这种方式,这样会使得自定义通知在将近一半的手机上显示得很突兀,和系统的通知栏不够沉浸,影响整体美观。另一种方案就稍微合理一些:通过读取系统的通知栏样式文件,获取到title和content的颜色,进而将这个颜色设置到自定义通知上。读取通知栏样式文件本身有兼容性问题,不同Android版本的样式文件有变,具体可参考这篇博客 通知栏设置系统字体颜色 ,这种方式也不是在所有手机上生效,实际测试发现,还是有小部分机型没法读取或是读取到的是错误的。拿到title和content的颜色后,还可以通过算法(后面细说)判断这个颜色是近似白色还是近似黑色,进而能判断出通知栏的背景是近似黑色还是近似白色,这样就能根据不同的通知栏背景加载不同的自定义通知布局。进而做到良好的适配。
更好的适配
现在切入主题,谈谈如何来更好的适配自定义通知。有过锁屏开发经验的人应该知道,如果你的应用有读取系统通知栏的权限,那么每当应用程序发出一个通知,你的应用都会收到对应的notification对象,这个时候,我们一般会执行以下操作:
获取并展示app通知调用addView之后,应用程序的通知就会显示在我们的应用里。显然,上面的代码并没有对apply返回的notificationItemLayout做任何其他操作,但确实这个View显示出来时就是样式良好的,可见,notificationItemLayout本身就是带有样式的,即便是默认通知。那么方案来了!我们先构造一个默认通知:
获取通知栏title的颜色通知并不发送出去,只是用来获取通知栏title的颜色,如果你还想获取content的颜色,抱歉,不能通过查找android.R.id.text来获取,这个字段是访问不到的。可通过反射获取,更好的办法是先预先设置一个content,然后遍历viewGoup根据content内容找到对应的TextView再获取颜色。
拿到颜色后,可根据算法判断这个颜色是近似白色还是近似黑色,我们使用黑色作为基准色,使用方差来计算这个颜色是否近似黑色:
比较两个颜色是否近似baseColor传入Color.BLACK,color传入刚刚获取到的title的颜色,根据我实测,阈值为180.0较为合理。上述方法返回true,即表示title的颜色近似黑色,也就是说通知栏背景近似白色。
额,经验丰富的同学应该已经洞察到第二段代码存在的兼容性问题了:根据android.R.id.title去找到title对应的TextView是不靠谱的,因为有些ROM厂商会把id改掉,导致找到的title为空。
同时还有另外一个问题:使用上述方法,Activity不能继承自AppCompatActivity(实测5.0以下机型可以,5.0及以上机型不行),大致的原因是默认通知布局文件中的ImageView(largeIcon和smallIcon)被替换成了AppCompatImageView,而在5.0及以上系统中,AppCompatImageView的setBackgroundResource(int)未被标记为RemotableViewMethod,导致apply时抛异常。
为了解决这两个问题,我们改进getNotificationColor方法:
改进后的方法在getNotificationColorInternal中,设置一个默认的title文本,如果根据id找不到title,则遍历notificationRoot根据设置的title文本找到title:
兼容厂商改id在getNotificationColorCompat中,我们先构造一个默认通知,获取到默认通知的布局文件id,并将布局加载到notificationRoot,此时,如果根据id找不到title,显然设置默认title的办法已经失效了。如何从notificationRoot中找到title是个问题。我的解决办法是:反正都已经拿到notificationRoot了,不如就遍历它,先找到其中的所有TextView,取字体最大的TextView作为title(这是合理的,因为默认通知中最多也就4个TextView,分别是title、content、info、when,title肯定是字体最大,最显眼的),并返回其颜色:
兼容AppCompatActivity实际测试
拿到了通知栏背景的颜色后,我们就可以加载不同样式的布局,达到适配的目的。代码如下:
适配代码效果:
Android 4.4黑色背景的通知栏 坚果手机白色版白色通知栏好了,我的第一篇技术博客到此为止,大家假期玩得Happy!
网友评论
android.widget.RemoteViews$ActionException: view: android.widget.TextView doesn't have method: 给你第二个(int)
at android.widget.RemoteViews.getMethod(RemoteViews.java:1023)
at android.widget.RemoteViews.-wrap5(RemoteViews.java)
at android.widget.RemoteViews$ReflectionAction.apply(RemoteViews.java:1583)
at android.widget.RemoteViews.performApply(RemoteViews.java:4071)
at android.widget.RemoteViews.apply(RemoteViews.java:3808)
at com.android.systemui.statusbar.BaseStatusBar.inflateViews(BaseStatusBar.java:2382)
at com.android.systemui.statusbar.BaseStatusBar.createNotificationViews(BaseStatusBar.java:3195)
楼主为什么我的直接就奔溃了呢
at com.renyu.itooth.common.NotiManager.getNotificationColor(NotiManager.java:210)
at com.renyu.itooth.common.NotiManager.isDarkNotificationTheme(NotiManager.java:197)
这个错误,我看了说是 int layoutId = notification.contentView.getLayoutId()为空,你知道是怎么破吗?
你的这一句在魅族note3 5.1系统上为null呢,具体原因,现在没空,明天看看
NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this);
Intent splash = new Intent(this, MainActivity.class); // 自定义打开的界面
splash.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent contentIntent = PendingIntent.getActivity(this, requestCode, splash, PendingIntent.FLAG_UPDATE_CURRENT);
mBuilder.setContentTitle("WonderBuy")//设置通知栏标题
.setContentText("床前明月光,疑是地上霜。举头望明月,低头思故乡。--李白")
.setContentIntent(contentIntent)
.setTicker("订单详情")
.setWhen(System.currentTimeMillis())
.setPriority(Notification.PRIORITY_DEFAULT)
.setAutoCancel(true)
.setOngoing(false)
.setDefaults(Notification.DEFAULT_ALL)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher))
.setSmallIcon(R.drawable.ic_launcher)
.setContentInfo("订单详情");
mNotificationManager.notify(requestCode, mBuilder.build());