用Compose来搞个水墨画效果吧


/   今日科技快讯   /

近日,阿拉丁研究院正式发布《2021年上半年小程序互联网发展白皮书》。据白皮书数据统计,2021年上半年全网小程序数量超过700万!其中微信小程序DAU超4.1亿,数量超过430万,MAU9亿,日人均使用时长1350秒。

/   作者简介   /

大家周五好,周末好好休息,我们下周见!

本篇文章转载自路很长OoO的博客,文章主要和大家分享了他使用Compose实现水墨画效果的实践体会,相信会对大家有所帮助!

文章地址:

https://juejin.cn/post/6947700226858123271

/   水墨画效果   /

前两天看掘金发现一位前端大佬写的水墨变彩色效果很好看,今天咱们用Compose来实现一下效果。效果如下,效果是前端大佬们做的效果。

/   分析动态效果   /

1. 第一我们可以看到图片【黑色】变【彩色】

2. 第二点击之后不规则区域逐渐放大且显示出底部彩色照片

效果也许比较简单,但要做好提供大家使用可能需要做好几点:

  • 图片显示大小正好适配画布大小

  • 点击任意地方从此处开始放大

  • 自定义不规则放大区域

/   素材寻找   /

我们百度一张觉得喜欢的任意图片,然后用PS打开。图像->调整->黑白,即可得到水墨画效果的黑白图片,导出图片备用。

/   PorterDuffXfermode   /

Android绘制中可以通PorterDuffXfermode将绘制图形的像素与Canvas中对应位置的像素按照一定规则进行混合,形成新的像素值来更新Canvas中最终像素颜色值,这样会创建很多可能性的特效。使用PorterDuffXfermode时,将其作为参数传给Paint.setXfermode(Xfermode xfermode)方法,再用该画笔paint进行绘图时Android就会使用传入的PorterDuffXfermode,如果不想再使用Xfermode,那么可以执行Paint.setXfermode(null)。

PorterDuffXfermode支持以下十几种像素颜色的混合模式,分别为:CLEAR、SRC、DST、SRC_OVER、DST_OVER、SRC_IN、DST_IN、SRC_OUT、DST_OUT、SRC_ATOP、DST_ATOP、XOR、DARKEN、LIGHTEN、MULTIPLY、SCREEN。

从上面我们可以看到PorterDuff.Mode为枚举类,一共有16个枚举值:

1.PorterDuff.Mode.CLEAR

所绘制不会提交到画布上。

2.PorterDuff.Mode.SRC

显示上层绘制图。

3.PorterDuff.Mode.DST

显示下层绘制图片

4.PorterDuff.Mode.SRC_OVER

正常绘制显示,上下层绘制叠盖。

5.PorterDuff.Mode.DST_OVER

上下层都显示。下层居上显示。

6.PorterDuff.Mode.SRC_IN

取两层绘制交集。显示上层。

7.PorterDuff.Mode.DST_IN

取两层绘制交集。显示下层。

8.PorterDuff.Mode.SRC_OUT

取上层绘制非交集部分。

9.PorterDuff.Mode.DST_OUT

取下层绘制非交集部分。

10.PorterDuff.Mode.SRC_ATOP

取下层非交集部分与上层交集部分

11.PorterDuff.Mode.DST_ATOP

取上层非交集部分与下层交集部分

12.PorterDuff.Mode.XOR

异或:去除两图层交集部分

13.PorterDuff.Mode.DARKEN

取两图层全部区域,交集部分颜色加深

14.PorterDuff.Mode.LIGHTEN

取两图层全部,点亮交集部分颜色

15.PorterDuff.Mode.MULTIPLY

取两图层交集部分叠加后颜色

16.PorterDuff.Mode.SCREEN

取两图层全部区域,交集部分变为透明色

将图片适配到Canvas

一个好的自定义,要让开发者使用起来方便,而我们的图片大小不一,那绘制到画布上肯定会大小不一,那我们将图片宽高设置为画布的宽高即可。

android中我们知道图片的压缩分为质量压缩,采样率压缩等。如果熟悉的应该对于下面的方法不会陌生。

//提供了我们很好的工具用来进行长宽来进行对图片的缩放。获取新的Bitmap。
Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,
boolean filter)

我们将两个素材图片扔到资源文件下面,开始将图片绘制到画布上。我们的画布多大,那么图片就应该缩放到我们的画布上。代码如下,我们创建了一个400.dp宽和200.dp高的画布,然后获取和画布大小一致缩放后的Bitmap,绘制到画布上。

@Preview
@Composable
fun InkColorCanvas() {
    val imageBitmap = getBitmap(R.drawable.csmr)
    val imageBitmap_default = getBitmap(R.drawable.hbmr)
    Canvas(
        modifier = Modifier
            .width(400.dp)
            .height(200.dp)
    ) {
        drawIntoCanvas { canva ->
            //彩色图片,获取新的bitmap,宽高和画布宽高一致适配画布
            val multiColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(), false
            )
            //黑白图片
            val blackColorBitmpa = Bitmap.createScaledBitmap(
                    imageBitmap_default.asAndroidBitmap(),
                    size.width.toInt(),
                    size.height.toInt(),
                    false
                )
            //新建画笔    
            val paint = Paint().asFrameworkPaint()
            //绘制图片到画布上
            canva.nativeCanvas.drawBitmap(multiColorBitmpa, 0f, 0f, paint) 
        }
    }
}

同样我们任意设置画布的宽高,应该自动缩放。不用我们开发者担心。即使屏幕旋转之后也会再次测量自动适配。

 //自动填充整个屏幕,旋转屏幕也自动适配。
 Canvas(modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
    )

效果如下:

保存当前画布图层

不管在基本的UI设计软件中如PhotoShop中,或者视频编辑软件Primere等图层是最基本的概念,当然在编程绘制中也有图层的概念。Android绘制中Canvas.saveLayer可以将当前的画布内容作为图层保存到堆栈中,达到图层的概念,创建一个新的Layer到“栈”中,可以使用saveLayer, savaLayerAlpha,从“栈”中推出一个Layer,可以使用restore,restoreToCount。但Layer入栈时,后续的DrawXXX操作都发生在这个 Layer上,而Layer退栈时,就会把本层绘制的图像“绘制”到上层或是Canvas上,在复制Layer到Canvas上时,可以指定Layer的 透明度(Layer),这是在创建Layer时指定的:public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)等,一句话多多动手。

//保存图层
val layerId: Int = canva.nativeCanvas.saveLayer(
    0f,
    0f,
    size.width,
    size.height,
    paint,
)

绘制黑白Bitmap

上个步骤中我们已经将彩色画布保存为图层推入堆栈内部。我们再次绘制黑白图Bitmap作为顶层图层。

@Preview
@Composable
fun InkColorCanvas() {
    val imageBitmap = getBitmap(R.drawable.csmr)
    val imageBitmap_default = getBitmap(R.drawable.hbmr)
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
    ) {
        drawIntoCanvas { canva ->
            val multiColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(), false
            )
            val blackColorBitmpa = Bitmap.createScaledBitmap(
                    imageBitmap_default.asAndroidBitmap(),
                    size.width.toInt(),
                    size.height.toInt(),
                    false
                )
            val paint = Paint().asFrameworkPaint()
            //绘制彩色图片
            canva.nativeCanvas.drawBitmap(multiColorBitmpa, 0f, 0f, paint) 
            //保存图层到堆栈
            val layerId: Int = canva.nativeCanvas.saveLayer(
                0f,
                0f,
                size.width,
                size.height,
                paint,
            )
            //当前图层也是顶层图层绘制黑白Btmap
            canva.nativeCanvas.drawBitmap(blackColorBitmpa, 0f, 0f, paint)

        }
    }
}

下图1-效果图,图2-堆栈图层。

混合模式PorterDuff.Mode.DST_IN

PorterDuff.Mode.DST_IN 取两层绘制交集,显示下层。接下来我们设置画笔混合模式为PorterDuff.Mode.DST_IN且绘制一个中心为屏幕中心,半径为250px的圆。

//PorterDuffXfermode 设置画笔的图形混合模式
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)//画圆
canva.nativeCanvas.drawCircle(size.width / 2, size.height / 2, 250f, paint)            

效果如下:

到这里我想基本搞定了最重要的部分了。

/   动画扩大混合区域   /

那如何让逐渐扩大展示出所有的彩色图片呢?很简单,动画有没有。只要最终半径大于画布的对角线即可,让半径从0变到斜边长的一半以上即可。

勾股定理

斜边=sqrt(size.width.toDouble().pow(2.0) + size.height.toDouble().pow(2)) 

@Preview
@Composable
fun InkColorCanvas() {
    val imageBitmap = getBitmap(R.drawable.csmr)
    val imageBitmap_default = getBitmap(R.drawable.hbmr)
    val animal = Animatable(0.0f)
    var xbLength = 0.0f
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight().pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        val offset = awaitPointerEventScope {
                            awaitFirstDown().position
                        }
                        launch {
                            animal.animateTo(
                                xbLength,
                                animationSpec = spring(stiffness = Spring.DampingRatioLowBouncy)
                            )
                        }

                    }
                }
            }
    ) {
        drawIntoCanvas { canva ->
            val multiColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(), false
            )
            val blackColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap_default.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(),
                false
            )
            val paint = Paint().asFrameworkPaint()
            canva.nativeCanvas.drawBitmap(multiColorBitmpa, 0f, 0f, paint) //绘制图片
            //保存图层
            val layerId: Int = canva.nativeCanvas.saveLayer(
                0f,
                0f,
                size.width,
                size.height,
                paint,
            )
            canva.nativeCanvas.drawBitmap(blackColorBitmpa, 0f, 0f, paint)
            //PorterDuffXfermode 设置画笔的图形混合模式
            paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
            //画圆
            canva.nativeCanvas.drawCircle(
                size.width / 2,
                size.height / 2,
                animal.value,
                paint
            )
            //画布斜边
            xbLength = kotlin.math.sqrt(size.width.toDouble().pow(2.0) + size.height.toDouble().pow(2)).toFloat()
            paint.xfermode = null
            canva.nativeCanvas.restoreToCount(layerId)
        }
    }
}           

/   扩大区域跟随按压   /

可能想要在任何按下地方开始扩大选取。很简单,获取屏幕指针获取屏幕按压坐标即可,设置为选区圆的起始坐标。

  • 通过pointerInput来获取屏幕按下坐标

  • 通过remember { mutableStateOf(Offset(0f,0f)) }记住按下的坐标

@Preview
@Composable
fun InkColorCanvas() {
    val imageBitmap = getBitmap(R.drawable.csmr)
    val imageBitmap_default = getBitmap(R.drawable.hbmr)
    val scrrenOffset = remember { mutableStateOf(Offset(0f,0f)) }

    val animalState = remember { mutableStateOf(false) }

    val animal: Float by animateFloatAsState(
        if (animalState.value) {
            1f
        } else {
            0f
        }, animationSpec = TweenSpec(durationMillis = 4000)
    )
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight().pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        val position=awaitPointerEventScope {
                              awaitFirstDown().position
                        }
                        launch {
                            scrrenOffset.value= Offset(position.x,position.y)
                            animalState.value=!animalState.value
                        }

                    }
                }
            }
    ) {
        drawIntoCanvas { canva ->
            val multiColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(), false
            )
            val blackColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap_default.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(),
                false
            )
            val paint = Paint().asFrameworkPaint()
            canva.nativeCanvas.drawBitmap(multiColorBitmpa, 0f, 0f, paint) //绘制图片
            //保存图层
            val layerId: Int = canva.nativeCanvas.saveLayer(
                0f,
                0f,
                size.width,
                size.height,
                paint,
            )
            canva.nativeCanvas.drawBitmap(blackColorBitmpa, 0f, 0f, paint)
            //PorterDuffXfermode 设置画笔的图形混合模式
            paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
            val xbLength = kotlin.math.sqrt(size.width.toDouble().pow(2.0) + size.height.toDouble().pow(2)).toFloat()*animal
            //画圆
            canva.nativeCanvas.drawCircle(
                scrrenOffset.value.x,
                scrrenOffset.value.y,
                xbLength,
                paint
            )
            //画布斜边
            paint.xfermode = null
            canva.nativeCanvas.restoreToCount(layerId)
        }
    }
}

/   不规则扩大选区   /

上面我们为了方便圆形进行了扩散,因为圆的缩放可以通过半径进行计算。但是其他的形状就不是那么好处理了。当然我们可以粗略或者精细的进行变换区域。这里由于时间问题,我们粗略的进行计算扩大选区的动画。原理当然要清楚了。

如上图我们的路径形状可以是各式各样的。但是执行到最终也需要扩散到所有边缘才可以。所以我们最终变换结果的路径一定要包围所有的画布区域才可以。如下结合理解。

@Preview
@Composable
fun InkColorCanvas() {
    val imageBitmap = getBitmap(R.drawable.csmr)
    val imageBitmap_default = getBitmap(R.drawable.hbmr)
    val scrrenOffset = remember { mutableStateOf(Offset(0f, 0f)) }

    val animalState = remember { mutableStateOf(false) }

    val animal: Float by animateFloatAsState(
        if (animalState.value) {
            1f
        } else {
            0f
        }, animationSpec = TweenSpec(durationMillis = 6000)
    )
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
            .pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        val position = awaitPointerEventScope {
                            awaitFirstDown().position
                        }
                        launch {
                            scrrenOffset.value = Offset(position.x, position.y)
                            animalState.value = !animalState.value
                        }

                    }
                }
            }
    ) {
        drawIntoCanvas { canva ->
            val multiColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(), false
            )
            val blackColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap_default.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(),
                false
            )
            val paint = Paint().asFrameworkPaint()
            canva.nativeCanvas.drawBitmap(multiColorBitmpa, 0f, 0f, paint) //绘制图片
            //保存图层
            val layerId: Int = canva.nativeCanvas.saveLayer(
                0f,
                0f,
                size.width,
                size.height,
                paint,
            )
            canva.nativeCanvas.drawBitmap(blackColorBitmpa, 0f, 0f, paint)
            //PorterDuffXfermode 设置画笔的图形混合模式
            paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
            val xbLength = kotlin.math.sqrt(size.width.toDouble().pow(2.0) + size.height.toDouble().pow(2)).toFloat() * animal
            //画圆
//            canva.nativeCanvas.drawCircle(
//                scrrenOffset.value.x,
//                scrrenOffset.value.y,
//                xbLength,
//                paint
//            )
            val path = Path().asAndroidPath()
            path.moveTo(scrrenOffset.value.x, scrrenOffset.value.y)
            //随便绘制了哥区域。当然了为了好看曲线可以更美。
            if (xbLength>0) {
                path.addOval(
                    RectF(
                        scrrenOffset.value.x - xbLength,
                        scrrenOffset.value.y - xbLength,
                        scrrenOffset.value.x + 100f + xbLength,
                        scrrenOffset.value.y + 130f + xbLength
                    ), android.graphics.Path.Direction.CCW
                )
                path.addCircle(
                    scrrenOffset.value.x, scrrenOffset.value.y, 100f + xbLength,
                    android.graphics.Path.Direction.CCW
                )
                path.addCircle(
                    scrrenOffset.value.x-100, scrrenOffset.value.y-100, 50f + xbLength,
                    android.graphics.Path.Direction.CCW
                )
            }
            path.close()
            canva.nativeCanvas.drawPath(path, paint)
            //画布斜边
            paint.xfermode = null
            canva.nativeCanvas.restoreToCount(layerId)
        }
    }
}

/   总结   /

Compose申明式UI必定式未来,当然xml不会遗弃,只是我们需要跳出舒适圈,不找借口,学习动手,我所体会到的Compose在效率和自定义等方面已经带来了很好的体验,相信写Flutter的大伙们极度舒适吧。后面有时间会继续写文章。这些特效都会收集到ComposeUnit中,代码到以后开源共享,希望完成ComposeUnit来和大家见面。最近比较忙写的少可以预览一下。

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Android aab的打包、调试、安装

Compose中的文本框,你值得拥有!

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值