👍大佬们的点赞就是我写作的动力👍
前四章
Jetpack-Compose基本布局
JetPack-Compose - 自定义绘制
JetPack-Compose - Flutter 动态UI?
JetPack-Compose UI终结篇
一、水墨画效果
前两天看掘金发现一位前端大佬写的水墨变彩色效果很好看,今天咋们用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
取两图层全部区域,交集部分变为透明色
1、将图片适配到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()
)
效果如下:
2、保存当前画布图层
不管在基本的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,
)
3、绘制黑白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-堆栈图层
4、混合模式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来和大家见面。最近比较忙写的少可以预览一下。👍👍👍👍大佬们的点赞就是我的动力
👍👍👍👍👍,鄙人QQ学习裙730772561