使用貝塞爾曲線實(shí)現(xiàn)一個(gè)loading控件

前言

上一篇文章:仿微信滑動(dòng)按鈕

本文是自定義View實(shí)踐第二篇蔽豺,上一篇實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的滑動(dòng)按鈕筹淫,知道了一些自定義View的基本步驟,本文是使用貝塞爾曲線實(shí)現(xiàn)的一個(gè)加載中控件,接下來(lái)進(jìn)入正文講解庸推。

地址:WaveLoadingView

效果圖

可以看到,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)以下情況:

圖1 :控件大械傥:layout_width = "match_parent" , layout_height = "200dp"
圖2:控件大谢匆:layout_width = "200dp" 五慈, layout_height = "match_parent"

藍(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)該都是如下圖:

圖3

可實(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)查看文末地址宰睡。

地址:WaveLoadingView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蒲凶,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子夹厌,更是在濱河造成了極大的恐慌豹爹,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矛纹,死亡現(xiàn)場(chǎng)離奇詭異臂聋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)或南,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門孩等,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人采够,你說(shuō)我怎么就攤上這事肄方。” “怎么了蹬癌?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵权她,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我逝薪,道長(zhǎng)隅要,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任董济,我火速辦了婚禮步清,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘虏肾。我一直安慰自己廓啊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布封豪。 她就那樣靜靜地躺著谴轮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吹埠。 梳的紋絲不亂的頭發(fā)上第步,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音藻雌,去河邊找鬼雌续。 笑死,一個(gè)胖子當(dāng)著我的面吹牛胯杭,可吹牛的內(nèi)容都是我干的驯杜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼做个,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鸽心!你這毒婦竟也來(lái)了滚局?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤顽频,失蹤者是張志新(化名)和其女友劉穎藤肢,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體糯景,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嘁圈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蟀淮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片最住。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖怠惶,靈堂內(nèi)的尸體忽然破棺而出涨缚,到底是詐尸還是另有隱情,我是刑警寧澤策治,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布脓魏,位于F島的核電站,受9級(jí)特大地震影響通惫,放射性物質(zhì)發(fā)生泄漏茂翔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一讽膏、第九天 我趴在偏房一處隱蔽的房頂上張望檩电。 院中可真熱鬧拄丰,春花似錦府树、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至载矿,卻和暖如春垄潮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背闷盔。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工弯洗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逢勾。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓牡整,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親溺拱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子逃贝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348