作者:郭霖
转载地址:https://mp.weixin.qq.com/s/brZ6gog_4KYQaatmFtkAZA
说起来,在不知不觉中,我竟然凑成了这沉浸式状态栏三部曲。
其实最开始的时候,我主要是因为工作上的原因想要在Android版的Edge浏览器上实现首页图片沉浸式的功能。那么为了实现这个功能,我提前去做了一些技术调研,并将调研的结果整理成了一篇文章,具体可参阅 再学一遍android:fitsSystemWindows属性 。
做完技术调研之后,接下来就是功能实现了。对于Android版的Edge浏览器而言,首页图片的沉浸式一直是部分网友长久以来的呼声,经过我的各种攻坚和踩坑之后,终于将这个功能完成了。具体可参阅 我为Android版Microsoft Edge所带来的变化 。
实现沉浸式之后的效果如下图所示:
不过,有朋友在评论区提出了这样一个疑问:
确实,这是一个做沉浸式功能时比较容易被忽略的问题。如果背景图片的颜色和状态栏图标的颜色非常接近的话,那么的确会造成状态栏图标看不清楚的情况。
这里我举了一些沉浸式效果做得不太好的案例,具体是什么App我就不提了。
可以看到,这些App虽然实现了沉浸式状态栏的效果,但是由于状态栏上的图标变得难以看清,所以最终效果可能反而不好。
但是,Edge浏览器是不会存在这种问题的。为什么呢?这就是我在上篇文章中说的,在实现沉浸式状态栏时运用了一些小黑科技。那么借助这些小黑科技,我终于可以凑成这沉浸式状态栏三部曲了。
话不多说,下面技术开讲。
其实想要解决上图中的这种由于颜色值接近,导致部分内容看不清的情况,我能想到两种解决方案。一种是从设计层面解决,一种是从技术层面解决。
从设计层面解决相对会比较容易一些,同时应该也是大部分App会采用的方案,那就是在背景图的上方再盖一层阴影。有了这层阴影之后,我们可以让状态栏上的图标始终都是浅色的。即使出现浅色的背景图,由于阴影层的存在,状态栏上的图标依然是可以看得清的。
但如果只是用这个方案解决的话,那么我就不会写本篇文章了。因为这里我们会采用第二种方案,从技术层面解决。
首先从技术层面进行分析,要解决这个问题,无非就是需要将背景图颜色和状态栏图标的颜色区分开。
Android系统其实给了我们API来控制状态栏图标的颜色,但是只能设置成黑、白这两种颜色,而不可以将状态栏图标改成五颜六色的样子。
默认情况下,系统会认为我们拥有的是一个深色的状态栏,那么状态栏上面的图标自然就应该白色的,因为只有这样才能看得清上面的图标。
而调用如下API则可以让系统认为我们拥有的是一个浅色的状态栏:
private fun setLightStatusBar() {
val flags = window.decorView.systemUiVisibility
window.decorView.systemUiVisibility = flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
如此一来,状态栏上面的图标就会变成黑色的,以和浅色的状态栏相互映衬。
如果要动态恢复成默认的深色状态栏,只需要这样设置:
private fun setDarkStatusBar() {
val flags = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
window.decorView.systemUiVisibility = flags xor View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
这就是我们拥有的用于控制状态栏图标颜色的API。
好了,现在有了这个法宝来控制状态栏图标的颜色,那么接下来的问题就是,什么时候应该显示白色的状态栏图标?什么时候应该显示黑色的状态栏图标?
答案是显而易见的,为了能让前景背景的颜色区分更加明显,当然应该是底部是深色背景图的时候显示白色的状态栏图标,底部是浅色背景图的时候显示黑色的状态栏图标。
因此,现在的问题就转移成了,我们如何才能识别一张背景图的指定区域是属于深色还是浅色?
非常幸运,在Android系统上我们是可以做到这一点的,只需要借助Google提供的Palette库即可。
Palette是一个专门用于对图像进行颜色提取和识别的库,功能虽然不能说是非常强大,但是已经完全可以满足我们这里的需求了。
要使用Palette库,首先需要将它引入到项目当中,如下所示:
dependencies {
implementation 'androidx.palette:palette:1.0.0'
}
接下来我们就可以借助Palette来进行一些颜色提取功能了,示例用法如下:
Palette
.from(bitmap)
.setRegion(left, top, right, bottom)
.maximumColorCount(colorCount)
.generate {
}
这是Palette最基础、最常见的用法。
首先,我们传入一个bitmap对象,这样Palette就会对它来进行图像解析。
然后调用setRegion()方法来指定解析这个bitmap对象的哪个区域。比方说我们本篇文章是要解决状态栏图标的问题,那肯定就要去解析手机状态栏那个区域的颜色值,其他区域的颜色值对我们来说没有意义。
接着调用maximumColorCount()方法来告诉Palette一共需要提取多少个颜色特征点。具体的颜色提取算法是由Palette自己控制的,我们无需关心。反正只需要知道,最终提取出来的这些颜色值都是这个bitmap的指定区域里最具代表性的就可以了。
最后调用generate()方法开始解析,Palette会开启异步线程来执行解析操作,并将最终结果回调到Lambda表达式当中。
现在我们已经得到这些提取出的特征点颜色值了,那么接下来,我们又该如何处理它们呢?
需要说明的事,后续的处理逻辑其实并没有一个非常严格的规定。我只说一下我个人的处理方式,大家也完全可以去定义自己的处理逻辑。
先贴一下代码,我再进行解释:
Palette
.from(bitmap)
.maximumColorCount(colorCount)
.setRegion(left, top, right, bottom)
.generate {
it?.let { palette ->
var mostPopularSwatch: Palette.Swatch? = null
for (swatch in palette.swatches) {
if (mostPopularSwatch == null
|| swatch.population > mostPopularSwatch.population) {
mostPopularSwatch = swatch
}
}
mostPopularSwatch?.let { swatch ->
val luminance = ColorUtils.calculateLuminance(swatch.rgb)
// 当luminance小于0.5时,我们认为这是一个深色值.
if (luminance < 0.5) {
setDarkStatusBar()
} else {
setLightStatusBar()
}
}
}
}
由于刚才在maximumColorCount()方法中传入了提取颜色特征点的数量,因此generate()方法的回调当中我们就可以得到多个颜色特征点(Swatch)。
而每个颜色特征点都会有一个权重值,调用getPopulation()方法可以获取,表示该特征点在选定的bitmap区域的重要程度。我选取了权重值最高的那个特征点来作为这个bitmap区域的代表颜色值。
接下来再调用ColorUtils.calculateLuminance()方法来计算选取的这个颜色值的亮度。当亮度低于0.5时,我就认为这是一个深色的颜色值,那么此时将状态栏设置成深色模式,状态栏图标就会自动变成白色。反之就将状态栏设置成浅色模式,此时状态栏图标就会自动变成黑色。
大概流程就是这个样子,我觉得原理还是非常简单的,我甚至都没有给出一个完整的实例,只是贴出了一些代码片段。
至于Palette,终归只是一个比较小众的库,知道或使用过的人可能并不多,所以用上这种小众技术我觉得足以称得上是黑科技了。
那么最后我们就来看一看实际的运行效果吧。
这里我准备了几张不同的背景图,由Palette解析之后,会根据识别出的颜色值动态更改状态栏图标的颜色。
这是深色背景图的效果。
这是浅色背景图的效果。
可以看到,不管在什么背景图下,状态栏图标的颜色都可以做到自动适配,保证图标始终是清晰可见的。
目前这种使用Palette来动态进行颜色识别的方案,我感觉至少是可以保证99%以上的场景都能够正确适配的,但是也存在一些特别极端的场景。
比如说背景图就是一张黑白左右分割的图片,这种情况下Palette会选取哪种颜色来作为代表色其实是不确定的。但不管是选中了黑还是白,都一定会导致状态栏上有一半区域的图标是不可见的。效果如下图所示:
不过对于这种极端情况,我觉得就没必要强求了。甚至我都并不认为这是一个Bug,反而觉得这是一种很酷的效果,你们觉得呢?
好的,本篇文章就到这里。文中我只帖出了所有关键代码的示例,以及最终运行效果的截图。如果你不想自己动手去敲一遍,也可以直接参考我的完整源码:
https://github.com/guolindev/ImmersiveStatusBar