使用贝塞尔曲线实现水波纹加载icon的功能
源码地址
贝塞尔曲线
安卓path提供了以下几种方法处理贝赛尔曲线:
quadTo(float x1, float y1, float x2, float y2)
rQuadTo(float dx1, float dy1, float dx2, float dy2)
cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
前两个方法是用于绘制二阶贝塞尔曲线,后两个是绘制三节贝塞尔曲线,两个一组,区别是rQuadTo和rCubicTo用的是相对距离,相对于起始点的距离(起始点即为调用时path当前的点,可以通过path.moveTo(float x1, float y1)的方法移动到你想要的位置),而另外两个用的则是绝对距离。
关于参数,以quadTo为例,P1(float x1, float y1)构成的点就是贝赛尔曲线的控制点,P2(float x2, float y2)为终点,起点P0就是当前path所在的点;
三阶贝塞尔曲线多一个控制点,所以方法中间多了一组控制点。
我们这里的水波纹可以看作是正弦函数,通过两个二阶贝塞尔曲线可以实现,水波纹的上移则通过改变P0点的y值,水平移动通过改变P0点的x值
定义变量
var waveLength: Float = 0f //水波宽度,即二阶贝塞尔P0-P2的距离
var waveHeight: Float = 0f //波峰控制点的偏移量,即P1的y值与P0的y值的差值
var distance = 0f //横向偏移量
var waveY: Float = 0f //当前P0的y坐标
因为要看上去是一直朝着一个方向移动,所以需要曲线移动的最大边距是正弦函数的x轴,所以distance是一个在0和waveLength之间变化的数
在onSizeChanged根据当前View大小自适应初始化相关参数
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
width = getWidth().toFloat()
height = getHeight().toFloat()
waveY = height //P0从底部开始,所以初始值是View的height
waveHeight = height * 0.12f //取height的12%作为控制点的偏移量
waveLength = width * 2 / 3f //取整个View宽度的2/3作为一个波的宽度
}
这里我们使用取整个View宽度的2/3作为一个波的宽度,因此两个曲线就足够了,但是考虑到我们横向移动的范围有一个曲线的宽度,所以我们需要3个曲线
贝塞尔曲线的path,根据当前loading的高度以及水平偏移量动态计算
wavePath.moveTo(-distance, waveY) //当前帧绘制第一个贝塞尔曲线的P0点
//后面的贝塞尔曲线的起始点是上一个曲线的终点
for (i in 0..2) {
wavePath.rQuadTo(
waveLength / 2, //控制点在半个周期中间
//这里通过(-1.0).pow(i.toDouble())依次生成交替的正负1,控制波峰或者是波谷,
waveHeight * (-1.0).pow(i.toDouble()).toFloat(),
waveLength,
0f
)
}
distance先以waveLength / 50f的间隔增大到一个曲线宽度再以相同的间隔减为0,就达到了曲线一直在横向滚动的效果,看上去是个水波
distanceTemp += waveLength / 50f
val residual = distanceTemp % waveLength
//当横坐标移动到一个周期之后则反向移动
distance =
if ((distanceTemp / waveLength).toInt() and 1 == 1)
waveLength - residual
else residual
waveY -= height / 100f
如果这个时候我们把这个waterPath画出来,我们能得到这样的效果——一条上升的水波浪
利用path裁剪Bitmap
现在水波浪曲线已经有了,我们只需要将曲线与底部围成一个封闭的path,然后再配合我们的Bitmap就能实现水波纹加载图片的动态效果
//围成封闭的Path
wavePath.lineTo(width, height)
wavePath.lineTo(0f, height)
wavePath.close()
创建一个和View等大的Bitmap并绑定到Canvas上,我们知道通过Bitmap创建的Canvas,对这个画布做的所有操作实际上都作用在了这个传进去的Bitmap上,所以我们通过waveCanvas以SRC_IN的模式组合path和Icon(我们想要加载的图标,也是一个Bitmap),之后再拿到处理后的waveBitmap,将其绘制在OnDraw的参数Canvas上,就达到了效果
val waveBitmap = Bitmap.createBitmap(width.toInt(), height.toInt(), Bitmap.Config.ARGB_8888)
val waveCanvas = Canvas(waveBitmap)
waveCanvas.drawPath(wavePath, mPaint)
mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
waveCanvas.drawBitmap(getBitmapFromDrawable(), 0f, 0f, mPaint)
到此waveBitmap已经是一个icon和path,最后draw这个bitmap,大功告成
canvas.drawBitmap(waveBitmap, 0f, 0f, mPaint)
效果
附上源码地址