使用貝塞爾曲線實(shí)現(xiàn)水波紋加載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)
前兩個(gè)方法是用于繪制二階貝塞爾曲線,后兩個(gè)是繪制三節(jié)貝塞爾曲線稿存,兩個(gè)一組喳整,區(qū)別是rQuadTo和rCubicTo用的是相對(duì)距離屿脐,相對(duì)于起始點(diǎn)的距離(起始點(diǎn)即為調(diào)用時(shí)path當(dāng)前的點(diǎn),可以通過(guò)path.moveTo(float x1, float y1)的方法移動(dòng)到你想要的位置)苗分,而另外兩個(gè)用的則是絕對(duì)距離。
關(guān)于參數(shù),以quadTo為例锌钮,P1(float x1, float y1)構(gòu)成的點(diǎn)就是貝賽爾曲線的控制點(diǎn),P2(float x2, float y2)為終點(diǎn)引矩,起點(diǎn)P0就是當(dāng)前path所在的點(diǎn)梁丘;
三階貝塞爾曲線多一個(gè)控制點(diǎn)脓魏,所以方法中間多了一組控制點(diǎn)兰吟。
我們這里的水波紋可以看作是正弦函數(shù)茂翔,通過(guò)兩個(gè)二階貝塞爾曲線可以實(shí)現(xiàn)混蔼,水波紋的上移則通過(guò)改變P0點(diǎn)的y值,水平移動(dòng)通過(guò)改變P0點(diǎn)的x值
定義變量
var waveLength: Float = 0f //水波寬度珊燎,即二階貝塞爾P0-P2的距離
var waveHeight: Float = 0f //波峰控制點(diǎn)的偏移量惭嚣,即P1的y值與P0的y值的差值
var distance = 0f //橫向偏移量
var waveY: Float = 0f //當(dāng)前P0的y坐標(biāo)
因?yàn)橐瓷先ナ且恢背粋€(gè)方向移動(dòng),所以需要曲線移動(dòng)的最大邊距是正弦函數(shù)的x軸悔政,所以distance是一個(gè)在0和waveLength之間變化的數(shù)
在onSizeChanged根據(jù)當(dāng)前View大小自適應(yīng)初始化相關(guān)參數(shù)
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從底部開(kāi)始晚吞,所以初始值是View的height
waveHeight = height * 0.12f //取height的12%作為控制點(diǎn)的偏移量
waveLength = width * 2 / 3f //取整個(gè)View寬度的2/3作為一個(gè)波的寬度
}
這里我們使用取整個(gè)View寬度的2/3作為一個(gè)波的寬度,因此兩個(gè)曲線就足夠了,但是考慮到我們橫向移動(dòng)的范圍有一個(gè)曲線的寬度谋国,所以我們需要3個(gè)曲線
貝塞爾曲線的path,根據(jù)當(dāng)前l(fā)oading的高度以及水平偏移量動(dòng)態(tài)計(jì)算
wavePath.moveTo(-distance, waveY) //當(dāng)前幀繪制第一個(gè)貝塞爾曲線的P0點(diǎn)
//后面的貝塞爾曲線的起始點(diǎn)是上一個(gè)曲線的終點(diǎn)
for (i in 0..2) {
wavePath.rQuadTo(
waveLength / 2, //控制點(diǎn)在半個(gè)周期中間
//這里通過(guò)(-1.0).pow(i.toDouble())依次生成交替的正負(fù)1槽地,控制波峰或者是波谷,
waveHeight * (-1.0).pow(i.toDouble()).toFloat(),
waveLength,
0f
)
}
distance先以waveLength / 50f的間隔增大到一個(gè)曲線寬度再以相同的間隔減為0,就達(dá)到了曲線一直在橫向滾動(dòng)的效果捌蚊,看上去是個(gè)水波
distanceTemp += waveLength / 50f
val residual = distanceTemp % waveLength
//當(dāng)橫坐標(biāo)移動(dòng)到一個(gè)周期之后則反向移動(dòng)
distance =
if ((distanceTemp / waveLength).toInt() and 1 == 1)
waveLength - residual
else residual
waveY -= height / 100f
如果這個(gè)時(shí)候我們把這個(gè)waterPath畫出來(lái)集畅,我們能得到這樣的效果——一條上升的水波浪
利用path裁剪Bitmap
現(xiàn)在水波浪曲線已經(jīng)有了,我們只需要將曲線與底部圍成一個(gè)封閉的path缅糟,然后再配合我們的Bitmap就能實(shí)現(xiàn)水波紋加載圖片的動(dòng)態(tài)效果
//圍成封閉的Path
wavePath.lineTo(width, height)
wavePath.lineTo(0f, height)
wavePath.close()
創(chuàng)建一個(gè)和View等大的Bitmap并綁定到Canvas上挺智,我們知道通過(guò)Bitmap創(chuàng)建的Canvas,對(duì)這個(gè)畫布做的所有操作實(shí)際上都作用在了這個(gè)傳進(jìn)去的Bitmap上窗宦,所以我們通過(guò)waveCanvas以SRC_IN的模式組合path和Icon(我們想要加載的圖標(biāo)赦颇,也是一個(gè)Bitmap),之后再拿到處理后的waveBitmap赴涵,將其繪制在OnDraw的參數(shù)Canvas上媒怯,就達(dá)到了效果
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已經(jīng)是一個(gè)icon和path,最后draw這個(gè)bitmap,大功告成
canvas.drawBitmap(waveBitmap, 0f, 0f, mPaint)
效果
icon.gif
附上源碼地址