用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,)//当前图层也是顶层图层绘制黑白Btmapcanva.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.0fCanvas(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 = nullcanva.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 = nullcanva.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 = nullcanva.nativeCanvas.restoreToCount(layerId)}}
}
/ 总结 /
Compose申明式UI必定式未来,当然xml不会遗弃,只是我们需要跳出舒适圈,不找借口,学习动手,我所体会到的Compose在效率和自定义等方面已经带来了很好的体验,相信写Flutter的大伙们极度舒适吧。后面有时间会继续写文章。这些特效都会收集到ComposeUnit中,代码到以后开源共享,希望完成ComposeUnit来和大家见面。最近比较忙写的少可以预览一下。
推荐阅读:
我的新书,《第一行代码 第3版》已出版!
Android aab的打包、调试、安装
Compose中的文本框,你值得拥有!
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注
用Compose来搞个水墨画效果吧相关推荐
- 超详细的Android so库的逆向调试
/ 今日科技快讯 / 2021年上半年,中国移动实现营收4436亿元,同比增长13.8%:股东应占利润为人民币591亿元,增长6%. 中国电信营收2192亿元,同比增长13.1%:净利润为17 ...
- 从 0 到 1 搞一个 Compose Desktop 版本的天气应用(附源码)
大家好,好久不见,今天带大家一起来玩下 Compose Desktop ,带大家从头到尾写一个桌面版的天气应用,并且打好包让别人也可以进行使用,废话不多说,先来看下最终的实现效果吧! 效果是不是挺好? ...
- 从0到1,用Compose搞一个桌面版的天气应用
/ 今日科技快讯 / 近日,一加9周年庆上,OPPO首席产品官.一加创始人刘作虎在活动上宣布OPPO正式开启双品牌时代,OPPO线上就是一加.同时,OPPO为一加开启「护航计划」,未来三年将单 ...
- 海外直播、聊天交友APP的开发及上架GooglePlay体验【Compose版】
前言 Jetpack Compose在2021年7月底的时候正式发布了Release 1.0版本,在8月中旬的时候正好赶上公司海外项目计划重构,于是主动请缨向领导申请下来了此次开发的机会.由于之前一直 ...
- 三、如何搞自定义数据集?
文章目录 前言 一.定义的数据集,未做预处理. 二.定义数据集,并做数据预处理. 1.预处理部分 2.定义数据过程 前言 MNIST数据这个最最基础的数据集已经被走在程序猿道路上的同学们玩坏了,所以今 ...
- Spring Boot 2.0(五):Docker Compose + Spring Boot + Nginx + Mysql 实践
我知道大家这段时间看了我写关于 docker 相关的几篇文章,不疼不痒的,仍然没有感受 docker 的便利,是的,我也是这样认为的,I know your felling . 前期了解概念什么的确实 ...
- 程序员的自我修养之马桶修理工:compose方法的妙用!
我始终自嘲,程序员就像是马桶修理工,但是同样是对于马桶的修理,却有两种不同的方式:第一种,遇到已经坏掉的部件,直接买一个全新的部件,然后换上就行了:第二种,就是能够知道部件到底是坏在哪个地方了,直接对 ...
- docker compose 在单机环境下一键打包运行
docker-compose 1. docker run 启动服务 2. docker-compose启动服务 3. docker-compose 小demo 3. 1 step1 : Setup 3 ...
- Compose 手势事件:防止重复点击,双击,长按,全局触摸隐藏键盘
前言 JetPack Compose (后续简称compose) release版已经出来了三四个月了,虽然没正式版之前也学过几次,但一直没有机会用,在加上api的变更,导致之前学的都忘完了,现在终于 ...
最新文章
- 数字图像处理——第十章 图像分割
- 16 分频 32 分频是啥意思_Verilog中任意分频的实现
- robotframwork的WEB功能测试(一)—切换window窗口
- 基于Spring+SpringMvc实现的足球队管理系统
- qstring删除最后一个字符_Excel去掉最后一个字符两个方法,正反思维,你支持哪一个?...
- 移动端调试利器------微信开源项目
- 计算机组网技术与配置 pdf,教案计算机组网技术.pdf
- ML之FE:数据随机抽样之利用pandas的sample函数对超大样本的数据集进行随机采样,并另存为csv文件
- [0715]Jsoi Test elevator
- [Drupal] How to get the real path of a node, no matter it is a path or a url alias
- IBM将发布以固态硬盘为基础的全企业系统
- redis超时原因排查
- 加载elementor时出现问题_element ui 按需引入出现问题
- WayOs内置智能重启:自动计算UTC时间为本地时间,可以调整为几时重启
- Windows 下安装 SVN 服务器、创建版本库、授权访问
- 读书笔记 -《硅谷之火》《硅谷热》
- 图论及其应用 2012年 期末考试答案总结
- 2020switch电信最快的dns_求教电信宽带switch用哪个dns快
- iOS 全息备份研究
- SVAC-Intra-Prei 代码分析(帧内预测最佳预测角度的选择)