/ 今日科技快讯 /
近日,阿拉丁研究院正式发布《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来和大家见面。最近比较忙写的少可以预览一下。
推荐阅读:
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注