[compose]-实现单组件黑白化

192 阅读3分钟
需求分析

最近接到一个需求,在特殊日子要将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作用域的方法 1692260132353.jpg 我发现了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,因为里面的ContentDrawScopeCanvasDrawScope 都继承了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方法。

参考资料