前言
上一篇文章:仿微信滑動(dòng)按鈕
本文是自定義View實(shí)踐第二篇蔽豺,上一篇實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的滑動(dòng)按鈕筹淫,知道了一些自定義View的基本步驟,本文是使用貝塞爾曲線實(shí)現(xiàn)的一個(gè)加載中控件,接下來(lái)進(jìn)入正文講解庸推。
效果圖
可以看到,WaveLoadingView除了用于loading外浇冰,還可以用于顯示進(jìn)度的場(chǎng)景贬媒。
實(shí)現(xiàn)方式
在效果圖中,波浪是曲線的形式的肘习,所以我們需要想辦法把曲線畫出來(lái)际乘,在數(shù)學(xué)領(lǐng)域中,用于實(shí)現(xiàn)曲線的函數(shù)有很多種漂佩,但在開發(fā)中脖含,比較常用的就是正弦曲線和貝塞爾曲線了罪塔,下面簡(jiǎn)單介紹一下:
1、正弦曲線
正弦曲線是我們非常熟悉的曲線器赞,它的函數(shù)如下:
y = Asin(ωx + φ) + h
A表示振幅垢袱,用于表示曲線的波峰和波谷的距離;
ω表示角速度,用于控制正弦曲線的周期;
φ表示初相港柜,用于控制正弦曲線的左右移動(dòng);
h表示編劇请契,用于控制曲線的上下移動(dòng).
當(dāng)A、ω夏醉、h取一定的值爽锥,φ取不同的值時(shí),就可以讓曲線在水平方向移動(dòng)起來(lái)畔柔,如下:
上面是A = 2氯夷,ω = 0.8, h = 0靶擦, φ不斷變化的正弦曲線腮考。
2、貝塞爾曲線
貝塞爾曲線有一階玄捕、二階踩蔚、... 、n階枚粘,一階的貝塞爾曲線是一條直線馅闽,從第2階開始才是曲線,n階的貝塞爾曲線可以由(n - 1)階貝塞爾曲線推導(dǎo)出來(lái)馍迄,關(guān)于貝塞爾曲線的推導(dǎo)可以閱讀深入理解貝塞爾曲線福也。
這里我使用二階貝塞爾曲線,它的函數(shù)如下:
f(t) = (1- t)^2 * P0 + 2t(1- t)P1 + t^2 * P2 (0<= t <= 1)
P0攀圈、P1暴凑、P2都是已知的點(diǎn),稱為控制點(diǎn), t是一個(gè)變量赘来,范圍為0到1现喳,函數(shù)值隨著t的變化而變化.
下面我們?nèi)0 = (x0, y0) = (-20, 0),P1 = (x1, y1) = (-10, 20)撕捍,P2 = (x2, y2) = (0, 0)拿穴,然后把這3個(gè)點(diǎn)的值代入二階貝塞爾曲線函數(shù)泣洞,形成的曲線如下:
這樣就畫出了一條曲線(那兩條直線是用于輔助的)忧风,接下來(lái)我們繼續(xù)取P3 = (x3, y3) = (10, -20),P4 = (x4, y4) = (20, 0)球凰,然后把P2狮腿、P3腿宰、P4再次代入二階貝塞爾曲線函數(shù),形成的曲線如下:
這樣就有點(diǎn)接近正弦曲線了缘厢,只要我們不斷的取控制點(diǎn)吃度,不斷的代入二階貝塞爾曲線函數(shù),就可以形成一條周期的曲線贴硫,到這里我們也發(fā)現(xiàn)了二階貝塞爾曲線函數(shù)不是一個(gè)周期函數(shù)椿每,所以它不像正向曲線那樣連綿不絕,一個(gè)二階貝塞爾曲線函數(shù)一次只能通過3個(gè)控制點(diǎn)畫出一條曲線英遭。
3间护、如何選擇?
我們也發(fā)現(xiàn)了貝塞爾曲線相對(duì)正弦曲線的實(shí)現(xiàn)有點(diǎn)復(fù)雜挖诸,但是汁尺,在Android中,貝塞爾曲線已經(jīng)有了封裝好的api供我們使用多律,使用起來(lái)非常簡(jiǎn)單痴突,不需要我們?nèi)ビ么a實(shí)現(xiàn)那個(gè)函數(shù),相反正弦曲線就需要我們從零做起狼荞,要用代碼去實(shí)現(xiàn)正弦函數(shù)辽装,還要進(jìn)行大量計(jì)算、范圍檢查等粘秆,所以從使用的復(fù)雜來(lái)看如迟,選用貝塞爾曲線的工作量更小一點(diǎn)。
在Android中攻走,貝塞爾曲線是通過Path來(lái)實(shí)現(xiàn)的殷勘,在Path中,與二階貝塞爾曲線有關(guān)的函數(shù)是:
path.quadTo(x1, y1, x2, y2)//絕對(duì)坐標(biāo)
path.rQuadTo(x1, y1, x2, y2)//相對(duì)坐標(biāo)
再貼一次圖一的貝塞爾曲線:
假設(shè)坐標(biāo)系參考圖一的xy軸昔搂,即x軸向右玲销,y軸向上,原點(diǎn)是(0, 0)摘符, 通過以下代碼就可以畫出上圖的曲線贤斜,如下:
var path = Path()
path.moveTo(-20f, 0f)//(x0, y0) = (-20, 0)
path.quadTo(
-10f, 20f, //(x1, y1) = (-10, 20)
0f, 0f //(x2, y2) = (0, 0)
)
//上面是絕對(duì)坐標(biāo),下面代碼使用相對(duì)坐標(biāo)的方式畫出
var path = Path()
path.moveTo(-20f, 0f)//(x0, y0) = (-20, 0)
path.rQuadTo(
10f, 20f, //(x1, y1)相對(duì)(x0, y0)為(10, 20)
20f, 0f //(x2, y2)相對(duì)(x0, y0)為(20, 0)
)
如果想要畫出圖二的貝塞爾曲線逛裤,只需要在前面曲線的基礎(chǔ)上再加一句quadTo或rQuadTo瘩绒,如下:
var path = Path()
path.moveTo(-20f, 0f)//(x0, y0) = (-20, 0)
path.quadTo(
-10f, 20f, //(x1, y1) = (-10, 20)
0f, 0f //(x2, y2) = (0, 0)
)
path.quadTo(
10f, -20f, //(x3, y3) = (10, -20)
20f, 0f //(x4, y4) = (20, 0)
)
//上面是絕對(duì)坐標(biāo),下面代碼使用相對(duì)坐標(biāo)的方式畫出
var path = Path()
path.moveTo(-20f, 0f)//(x0, y0) = (-20, 0)
path.rQuadTo(
10f, 20f, //(x1, y1)相對(duì)(x0, y0)為(10, 20)
20f, 0f //(x2, y2)相對(duì)(x0, y0)為(20, 0)
)
path.rQuadTo(
10f, -20f, //(x3, y3)相對(duì)(x2, y2)為(10, -20)
20f, 0f //(x4, y4)相對(duì)(x2, y2)為(20, 0)
)
絕對(duì)坐標(biāo)的每個(gè)點(diǎn)都是以坐標(biāo)系的原點(diǎn)為參考带族;而相對(duì)坐標(biāo)是以moveTo方法那個(gè)點(diǎn)為原點(diǎn)作為參考锁荔,如果只調(diào)用了一次moveTo方法,而調(diào)用了多次rQuadTo方法蝙砌,那么從第二次rQuadTo方法開始阳堕,它參考上一次rQuadTo方法的最后一個(gè)坐標(biāo)值跋理,例如上面相對(duì)坐標(biāo)計(jì)算中,第二次rQuadTo方法的(x3, y3)恬总,(x4, y4)是參考(x2, y2)計(jì)算出來(lái)的前普,而不是參考(x0, y0)。
上面是為了講解方便把坐標(biāo)系說(shuō)成x軸向右壹堰,y軸向上拭卿,但是在android中,坐標(biāo)系是x軸向右贱纠,y軸向下记劈,原點(diǎn)是View的左上角,這一點(diǎn)要注意并巍。
實(shí)現(xiàn)步驟
下面開始講主要的實(shí)現(xiàn)步驟:
1目木、測(cè)量控件大小
我使用一個(gè)Shape枚舉表示控件的4種形狀,如下:
enum class Shape{
CIRCLE,//圓形懊渡,默認(rèn)形狀
SQUARE, //正方形
RECT, //矩形
NONE//沒有形狀約束
}
對(duì)于圓形和正方形刽射,控件的測(cè)量寬和高應(yīng)該保持一樣的,而對(duì)于矩形和NONE剃执,控件的測(cè)量寬和高可以不一樣誓禁,如下:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val measureWidth = MeasureSpec.getSize(widthMeasureSpec)
val measureHeight = MeasureSpec.getSize(heightMeasureSpec)
when(shape){
Shape.CIRCLE, Shape.SQUARE -> {//圓形或正方形
val measureSpec = if(measureHeight < measureWidth) heightMeasureSpec else widthMeasureSpec
//傳入的measureSpec一樣
super.onMeasure(measureSpec, measureSpec)
}else -> {//矩形或NONE
//傳入的measureSpec不一樣
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
}
所以如果用戶使用圓形或正方形,但是輸入的寬高不一樣肾档,我就取寬和高的最小值的測(cè)量模式去測(cè)量控件摹恰,這樣就保證了控件的測(cè)量寬高一樣;而用戶如果使用矩形或NONE怒见,就保持原來(lái)的測(cè)量就行了俗慈。
一個(gè)控件有可能經(jīng)過多次measure后才確定測(cè)量寬高,在多次onMeasure()方法調(diào)用后遣耍,接下來(lái)會(huì)調(diào)用onSizeChanged()方法闺阱,且只會(huì)調(diào)用一次,這個(gè)方法調(diào)用后接下來(lái)就會(huì)調(diào)用onLayout()方法確定控件的最終寬高舵变,我在onSizeChanged()里面獲取測(cè)量寬高確定了控件作畫的范圍大小和暫時(shí)的控件大小酣溃,如下:
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
//控件作畫的范圍大小
canvasWidth = measuredWidth
canvasHeight = measuredHeight
//控件大小,暫時(shí)等于canvas大小纪隙,后面在onlayout()中會(huì)改變
viewWidth = canvasWidth
viewHeight = canvasHeight
//...
}
控件作畫的范圍大小和控件大小關(guān)系如下:
綠色框就是控件作畫的范圍大小赊豌,紅色框就是控件大小,也就是說(shuō)每次控件大小確定之后绵咱,我只取中間的部分繪制碘饼,很多人會(huì)有疑問?為什么只取中間的部分繪制,而不在整個(gè)控件范圍繪制派昧?這是因?yàn)?strong>當(dāng)控件的父布局是ConstraintLayout,控件寬或高取match_parent時(shí)拢切,會(huì)出現(xiàn)以下情況:
藍(lán)色框就是手機(jī)屏幕,黑色背景就是控件大小主穗,你還記得我上面在onMeasure()方法講過泻拦,如果控件的形狀是圓形,那么控件的測(cè)量寬高應(yīng)該相等的忽媒,并取最小值為基準(zhǔn)争拐,所以如果控件大小輸入是layout_width = "match_parent" ,layout_height = "200dp" 或 layout_width = "200dp" 晦雨,layout_height = "match_parent"架曹,經(jīng)過測(cè)量后控件大小應(yīng)該是寬 = 高 = 200dp,效果應(yīng)該都是如下圖:
可實(shí)際情況卻不是圖3闹瞧,而是圖1或圖2绑雄,這是因?yàn)?strong>ConstraintLayout布局會(huì)讓子控件的setMeasuredDimension()失效,所以導(dǎo)致 measuredHeight 和 height 不一樣奥邮,寬同理万牺,所以在遇到父布局是ConstraintLayout時(shí),并且控件的寬或高設(shè)置了“match_parent”洽腺,并且你自定義了測(cè)量過程脚粟,就會(huì)導(dǎo)致自定義View過程中測(cè)量出來(lái)大小不等于View最終大小,即getMeasureHeigth()或getMeasureWidth() != getWidth()或getHeigth()蘸朋,為什么ConstraintLayout就會(huì)有這種情況而其他如Linearlayout就沒有珊楼?我也不知道,可能需要大家通過源碼了解了度液,而我的解決辦法就是讓每次作畫的范圍在控件的中心厕宗,就像圖1和圖2一樣,這樣就不會(huì)那么難看堕担。
2已慢、裁剪畫布形狀
怎么把控件弄成圓形、正方形霹购、矩形這些形狀佑惠,如果控件形狀是正方形或矩形,還可以設(shè)置圓角,一個(gè)方法是通過BitmapShader實(shí)現(xiàn)膜楷,使用BitmapShader要經(jīng)過3步:
1旭咽、新建Bitmap;
2赌厅、以1新建的Bitmap創(chuàng)建一個(gè)Canvas穷绵,在Canvas上畫出波浪;
3特愿、最后新建一個(gè)BitmapShader與1的Bitmap關(guān)聯(lián)仲墨,然后設(shè)置給畫筆,用畫筆在onDraw方法傳進(jìn)來(lái)的Canvas上畫一個(gè)形狀出來(lái)揍障,然后這個(gè)形狀就會(huì)含有波浪.
但我沒有使用BitmapShader目养,因?yàn)椴ɡ说囊苿?dòng)需要開啟一個(gè)無(wú)限循環(huán)動(dòng)畫,就會(huì)不斷的調(diào)用onDraw()方法毒嫡,而在onDraw()方法不斷的新建對(duì)象是一個(gè)不推薦的做法癌蚁,雖然Bitmap可以通過recycler()復(fù)用,但是還是避免不了每次都要新建Canvas對(duì)象, 所以為了減少對(duì)象分配兜畸,我使用了Canvas的clipPathAPI來(lái)把畫布裁剪成我想要的形狀匈勋,然后把波浪畫在裁剪后的畫布上,這樣也能實(shí)現(xiàn)與BitmapShader同樣的效果膳叨,如下:
private fun preDrawShapePath(w: Int, h: Int) {
clipPath.reset()
when (shape) {
Shape.CIRCLE -> { //...
//path路徑為圓形
clipPath.addCircle(
shapeCircle.centerX, shapeCircle.centerY,
shapeCircle.circleRadius,
Path.Direction.CCW
)
}
Shape.SQUARE -> {
//...
//path路徑為正方形或圓角正方形
if (shapeCorner == 0f)
clipPath?.addRect(shapeRect, Path.Direction.CCW)
else
clipPath.addRoundRect(
shapeRect,
shapeCorner, shapeCorner,
Path.Direction.CCW
)
}
Shape.RECT -> {
//...
}
}
}
preDrawShapePath()中根據(jù)Shape來(lái)add不同的形狀給Path來(lái)把這些路徑信息預(yù)先保存下來(lái)洽洁,前面已經(jīng)講過每次作畫的范圍都在控件的中心,//...省略的都是居中計(jì)算菲嘴,保存好形狀的Path將在onDraw方法中使用饿自,如下:
override fun onDraw(canvas: Canvas?) {
clipCanvasShape(canvas)
//...
}
private fun clipCanvasShape(canvas: Canvas?) {
//調(diào)用canvas的clipPath方法裁剪畫布
if (shape != Shape.NONE) canvas?.clipPath(clipPath)
//...
}
在onDraw方法中使用canvas.clipPath()方法傳入Path裁剪畫布,這樣以后作畫的范圍都被限定在這個(gè)畫布形狀之內(nèi)龄坪。
3昭雌、畫波浪
使用貝塞爾曲線畫波浪,如下:
private fun preDrawWavePath() {
wavePath.reset()
//波長(zhǎng)等于畫布的寬度
val waveLen = canvasWidth
//波峰
val waveHeight = (waveAmplitude * canvasHeight).toInt()
//波浪的起始y坐標(biāo)
waveStartY = calculateWaveStartYbyProcess()
//把path移到起始位置健田,這里使用了path.moveTo()方法
wavePath.moveTo(-canvasWidth * 2f, waveStartY)
//下面就是畫波浪的過程烛卧,都使用了path.rXX()方法,表示把上一次結(jié)束點(diǎn)的坐標(biāo)作為原點(diǎn)妓局,從而簡(jiǎn)化計(jì)算量
val rang = -canvasWidth * 2..canvasWidth
for (i in rang step waveLen) {
wavePath.rQuadTo(
waveLen / 4f, waveHeight / 2f,
waveLen / 2f, 0f
)
wavePath.rQuadTo(
waveLen / 4f, -waveHeight / 2f,
waveLen / 2f, 0f
)
}
//波浪的深度就是畫布的高度
wavePath.rLineTo(0f, canvasHeight.toFloat())
wavePath.rLineTo(-canvasWidth * 3f, 0f)
//最后使用path.close()把波浪的路徑關(guān)閉总放,使整個(gè)波浪圍起來(lái)
wavePath.close()
}
preDrawWavePath() 中把波浪路徑的信息保存在path中,下面一張圖很好的說(shuō)明波浪的整個(gè)路徑好爬,如下:
我把控件大小充滿了父容器局雄,所以控件的作畫范圍就是綠色框的大小,波浪的波長(zhǎng)就是一個(gè)畫布的寬度即綠色框的寬度存炮,我把波浪的起始點(diǎn)移到屏幕范圍外炬搭,從起始點(diǎn)開始蜈漓,畫了三個(gè)波長(zhǎng),把波浪畫出屏幕的范圍宫盔,從而方便的待會(huì)的波浪的上下移動(dòng)融虽,最后記得使用path.close()把波浪的路徑關(guān)閉,使整個(gè)波浪圍起來(lái)灼芭。
保存好波浪路徑的信息的Path在onDraw方法中使用有额,如下:
override fun onDraw(canvas: Canvas?) {
clipCanvasShape(canvas)
drawWave(canvas)
//...
}
private fun drawWave(canvas: Canvas?) {
wavePaint.style = Paint.Style.FILL_AND_STROKE
wavePaint.color = waveColor
//...
//使用canvas的drawPath()方法把波浪畫在畫布上
canvas?.drawPath(wavePath, wavePaint)
}
使用canvas的drawPath()方法直接把波浪畫在畫布上,這時(shí)在屏幕上顯示的效果如下:
這樣就畫出了一條波浪了姿鸿,第二條波浪呢?可以再用另外一個(gè)Path按照上述preDrawWavePath()方法的流程再畫一條倒源,只要波浪的起始點(diǎn)坐標(biāo)不同就行苛预,但我沒有用這種辦法,我是通過Canvas的translate()方法平移畫布笋熬,利用兩次平移的偏移量不一樣热某,畫出了第二條,如下:
private fun drawWave(canvas: Canvas?) {
wavePaint.style = Paint.Style.FILL_AND_STROKE
//首先保存兩次畫布狀態(tài)胳螟,記為畫布1昔馋、2
canvas?.save()//畫布1
canvas?.save()//畫布2
//記當(dāng)前畫布為畫布3
//調(diào)用canvas的translate()方法水平平移一下畫布3
canvas?.translate(canvasSlowOffsetX, 0)
wavePaint.color = adjustAlpha(waveColor, 0.7f)
//首先在畫布3畫出第一條波浪
canvas?.drawPath(wavePath, wavePaint)
//恢復(fù)保存的畫布2狀態(tài)
canvas?.restore()
//下面是在畫布2上作畫
//調(diào)用canvas的translate()方法水平平移一下畫布2
canvas?.translate(canvasFastOffsetX, 0)
wavePaint.color = waveColor
//然后在畫布2上畫出第二條波浪
canvas?.drawPath(wavePath, wavePaint)
//恢復(fù)保存的畫布1狀態(tài)
canvas?.restore()
//后面都是在畫布1上作畫
}
熟悉Canvas的save()、restore()方法都知道糖耸,每調(diào)用一次save()秘遏,可以理解為畫布的一次入棧(保存),每調(diào)用一次restore()嘉竟,可以理解為畫布的出棧(恢復(fù))邦危,畫布3是默認(rèn)就有的,畫布1舍扰、2是我保存生成的倦蚪,所以上述畫布1,2边苹,3之間是獨(dú)立的陵且,互不影響的,而canvasSlowOffsetX和canvasFastOffsetX兩個(gè)值是不一樣的个束,這樣就造成了畫布2和3平移時(shí)偏移量不一樣慕购,所以用同一個(gè)Path畫在兩個(gè)偏移量不一樣的畫布上就可以形成兩條波浪,效果圖如下:
4茬底、讓波浪動(dòng)起來(lái)
讓波浪移動(dòng)起來(lái)很簡(jiǎn)單脓钾,使用一個(gè)無(wú)限循環(huán)動(dòng)畫,在動(dòng)畫的進(jìn)度回調(diào)中計(jì)算畫布的偏移量桩警,然后調(diào)用invalidate()就行可训,如下:
waveValueAnim.apply {
duration = ANIM_TIME
repeatCount = ValueAnimator.INFINITE//無(wú)限循環(huán)
repeatMode = ValueAnimator.RESTART
addUpdateListener{ animation ->
//...
canvasFastOffsetX = (canvasFastOffsetX + fastWaveOffsetX) % canvasWidth
canvasSlowOffsetX = (canvasSlowOffsetX + slowWaveOffsetX) % canvasWidth
invalidate()
}
}
在適當(dāng)?shù)臅r(shí)機(jī)啟動(dòng)動(dòng)畫昌妹,如下:
override fun onDraw(canvas: Canvas?) {
clipCanvasShape(canvas)
drawWave(canvas)
//...
//啟動(dòng)動(dòng)畫
startLoading()
}
fun startLoading(){
if(!waveValueAnim.isStarted) waveValueAnim.start()
}
到這里整個(gè)控件就完成了。
5握截、優(yōu)化
大家都知道手機(jī)的資源都是非常有限的飞崖,我在做自定義View時(shí),特別是涉及到無(wú)限循環(huán)的動(dòng)畫時(shí)谨胞,要注意優(yōu)化我們的代碼固歪,因?yàn)橐话愕钠聊凰⑿轮芷谑?6ms,這意味著在這16ms內(nèi)你要把有關(guān)動(dòng)畫的所有計(jì)算和流程完成胯努,不然就會(huì)造成掉幀牢裳,從而卡頓,在自定義View時(shí)我想到可以從下面幾點(diǎn)做一些優(yōu)化叶沛,提高效率:
5.1蒲讯、減少對(duì)象的內(nèi)存分配,盡可能做到對(duì)象復(fù)用
每次系統(tǒng)GC的時(shí)候都會(huì)暫停系統(tǒng)ms級(jí)別的時(shí)間灰署,而無(wú)限循環(huán)的動(dòng)畫的邏輯代碼會(huì)在短時(shí)間內(nèi)被循環(huán)往復(fù)的調(diào)用, 這樣如果在邏輯代碼中在堆上創(chuàng)建過多的臨時(shí)變量判帮,會(huì)導(dǎo)致內(nèi)存的使用量在短時(shí)間內(nèi)上升,從而頻繁的引發(fā)系統(tǒng)的GC行為溉箕,這樣無(wú)疑會(huì)拖累動(dòng)畫的效率晦墙,讓動(dòng)畫變得卡頓。
在自定義View涉及到無(wú)限循環(huán)動(dòng)畫時(shí)肴茄,我們不能忽略對(duì)象的內(nèi)存分配晌畅,不要經(jīng)常在onDraw()方法中new對(duì)象:如果這些臨時(shí)變量每次的使用都是固定,完全不需要每次循環(huán)執(zhí)行的時(shí)候重復(fù)創(chuàng)建寡痰,我們可以考慮將它們從臨時(shí)變量轉(zhuǎn)為成員變量踩麦,在動(dòng)畫初始化或View初始化時(shí)將這些成員變量初始化好,需要的時(shí)候直接調(diào)用即可氓癌;對(duì)于不規(guī)則圖形的繪制我們會(huì)需要到Path谓谦,并且對(duì)于越復(fù)雜的 Path,Canvas 在繪制的時(shí)候贪婉,也會(huì)更加的耗時(shí)反粥,因此我們需要做的就是盡量?jī)?yōu)化 Path 的創(chuàng)建過程, 還有Path 類中本身提供reset()和rewind()方法用于復(fù)用Path對(duì)象疲迂, reset()方法是用于對(duì)象的復(fù)位才顿,rewind()方法在對(duì)象的復(fù)位基礎(chǔ)上還可以讓Path對(duì)象不釋放之前已經(jīng)分配的內(nèi)存就,重用之前分配的內(nèi)存尤蒿。
5.2郑气、抽取重復(fù)運(yùn)算,盡可能減少浮點(diǎn)運(yùn)算
在自定義View的時(shí)候不難免遇到大量的運(yùn)算腰池,特別在做無(wú)限循環(huán)動(dòng)畫時(shí)尾组,其邏輯代碼會(huì)在短時(shí)間內(nèi)被循環(huán)往復(fù)的調(diào)用, 這樣如果在邏輯代碼中在做過多的重復(fù)運(yùn)算無(wú)疑會(huì)降低動(dòng)畫的效率忙芒,特別是在做浮點(diǎn)運(yùn)算時(shí),CPU 在處理浮點(diǎn)運(yùn)算時(shí)候讳侨、會(huì)變的特別的慢呵萨,要多個(gè)指令周期才能完成。
因此我們還應(yīng)該努力減少浮點(diǎn)運(yùn)算跨跨,在不考慮精度的情況下潮峦,可以將浮點(diǎn)運(yùn)算轉(zhuǎn)成整型來(lái)運(yùn)算,同時(shí)我們還應(yīng)該把重復(fù)的運(yùn)算從邏輯代碼中抽取出來(lái)勇婴,不用每次都運(yùn)算忱嘹,例如在WaveLoadingView中, 我創(chuàng)建Path的過程的計(jì)算大部分都是在onLayout()中成耕渴,把重復(fù)運(yùn)算的結(jié)果提前用Path保存好拘悦,然后在onDraw()中使用,因?yàn)閛nDraw()在做動(dòng)畫時(shí)會(huì)被頻繁的被調(diào)用萨螺。
5.3窄做、考慮使用SurfaceView
傳統(tǒng)的View的測(cè)量愧驱、布局慰技、繪制都是在UI線程中完成的,而Android 的UI線程除了View的繪制之外组砚,還需要進(jìn)行額外的用戶處理邏輯吻商、輪詢消息事件等,這樣當(dāng)View的繪制和動(dòng)畫比較復(fù)雜糟红,計(jì)算量比較大的情況艾帐,就不再適合使用 View 這種方式來(lái)繪制了。這時(shí)候我們可以考慮使用SurfaceView 盆偿,SurfaceView 能夠在非 UI 線程中進(jìn)行圖形繪制柒爸,釋放了 UI 線程的壓力。當(dāng)然WaveLoadingView也可以使用SurfaceView 來(lái)實(shí)現(xiàn)事扭。
結(jié)語(yǔ)
WaveLoadingView的實(shí)現(xiàn)就講解完畢捎稚,本次自定義View的過程都使用了kotlin進(jìn)行編寫,整體的代碼量的確比java的減少了許多求橄,但語(yǔ)言畢竟只是一個(gè)工具今野,我們主要是學(xué)習(xí)自定義View的實(shí)踐過程,當(dāng)你經(jīng)常動(dòng)手實(shí)踐后罐农,你會(huì)發(fā)現(xiàn)自定義View沒有想象那么難条霜,來(lái)來(lái)去去就那幾個(gè)方法,大部分時(shí)間都是花在實(shí)現(xiàn)的細(xì)節(jié)和運(yùn)算上涵亏,更多實(shí)現(xiàn)請(qǐng)查看文末地址宰睡。