需求分析
最近接到一个需求,在特殊日子要将app做黑白化处理。
传统View,网上有很多参考资料,通过自定义View来降低局部饱和度,也可以通过监听每一个activity,拿到整个页面的DecorView从而达到全局降低饱和度,无论是哪种方案,都是通过ColorMatrix
降低当前View的饱和度,具体就不细说了,参考APP黑白化实现。
最初方案就是监听Activity,拿到页面的DecorView,本以为这样就结束了,PM看完效果后,决定要实现首页黑白化,因为首页布局可配置的,并且要根据模块配置黑白。
可我们的UI是Compose实现的,整个Compose Node 依附在一个Activity里,页面切换都是通过导航。
方案1、修改首页 拆到 Activity 里面。在指定区域降低饱和度。这种方法将整个项目结构都拆了,所以也没去尝试。
方案2、调研Compose组件是否直接支持,通过Compose来实现。
Compose 提供的方案
方案 1 : Image组件,可以设置一个ColorMatrix,从而降低饱和度,
问题:首页的组件不仅有Image,还有其他组件,所以最好的方案就是在Box,Row这样的父组件上设置。
val matrix = ColorMatrix()
matrix.setToSaturation(0F)
Image(
painter = painterResource(id = R.mipmap.ic_launcher),
contentDescription = "",
colorFilter = ColorFilter.colorMatrix(matrix)
)
方案 2 : 除了Image组件,其他组件都不支持colorFilter参数,所以需要通过一个Canvas组件,在上面画一个Rect, 并指定 BlendMode.Saturation,把需要黑白化的布局传入到SaturationBox里面,即可实现。
问题:本以为开开心心完工,测试的时候发现,有一个手机显示白屏,看了下注释,This BlendMode can only be used on Android API level 29 and above
心一下就凉了,上网查了查,还没有什么解决方案,只能参考上面的逻辑,自己写一个方法了。
@Composable
fun SaturationBox(
modifier: Modifier = Modifier,
lowSaturation: Boolean = true,
content: @Composable BoxScope.() -> Unit
) {
Box(modifier = modifier) {
// 正常布局
content()
// 黑白化
if (lowSaturation) {
Canvas(modifier = Modifier.matchParentSize()) {
drawRect(
color = Color.White,
blendMode = BlendMode.Saturation
)
}
}
}
}
最终方案
不论是View 还是Compose,解决思路都是通过降低布局的饱和度。
既然Image组件可以,参考Image组件代码,发现Image 会将ColorMatrix
传入PainterModifierNode
节点,在布局变更的时候,通过CanvasDrawScope.draw
方法进行刷新,重绘(具体逻辑就不细说了,层层点击就能看到),所以我们可以通过Modify找找看,有没有提供CanvasDrawScope
作用域的方法
我发现了drawWithContent方法,看起来可行性极高
Column(
modifier = Modifier
.statusBarsPadding()
.drawWithContent {
//降低饱和度
val saturationMatrix = ColorMatrix().apply { setToSaturation(0f) }
val saturationFilter = ColorFilter.colorMatrix(saturationMatrix)
val paint = Paint().apply {
colorFilter = saturationFilter
}
//在画布上绘制内容
drawIntoCanvas { canvas->
//绘制一个图层
canvas.saveLayer(Rect(0f, 0f, size.width, size.height), paint)
//原始内容
drawContent()
//合并图层
canvas.restore()
}
},
) {
Image(
painter = painterResource(id = R.mipmap.ic_launcher),
contentDescription = "",
)
Text(text = "这是一张图片!!!", color = Color.Red)
}
虽然现在效果对了,但如果要做到每个模块是可控的,这一大堆代码,得做下优化。。第一种方案,通过扩展函数,把方法封装到一个Modifier里,具体就不写了, 使用的时候Modifier.saturation(),第二种方案就是我发现Compose 提供一个Modifier.Element
叫 DrawModifier
,因为里面的ContentDrawScope
和CanvasDrawScope
都继承了DrawScope
接口,所以可以通过的自定义Modifier 去完成操作。
class SaturationModifier : DrawModifier {
override fun ContentDrawScope.draw() {
val saturationMatrix = ColorMatrix().apply { setToSaturation(0f) }
val saturationFilter = ColorFilter.colorMatrix(saturationMatrix)
val paint = Paint().apply {
colorFilter = saturationFilter
}
drawIntoCanvas { canvas->
canvas.saveLayer(Rect(0f, 0f, size.width, size.height), paint)
drawContent()
canvas.restore()
}
}
}
fun Modifier.saturation() = this.then(SaturationModifier())
//使用
Column(
modifier = Modifier.statusBarsPadding().saturation()
) {
Image(
painter = painterResource(id = R.mipmap.ic_launcher),
contentDescription = "",
)
Text(text = "这是一张图片!!!", color = Color.Red)
}
总结
至此,完成了该功能的实现,如果同学们在其他机型上使用有问题的话,请及时反馈。
通过这个需求,大概学习到,组件的参数,包括Modifier, 用来控制的组件的样式,类似于View里面的onDraw方法,而父组件的Sopce,控制组件的摆放位置,类似于View 的 onLayout方法。