前言
使用百度貼吧客戶端的時候發(fā)發(fā)現(xiàn)加載的小動畫挺有意思的,于是自己動手寫寫看。想學(xué)習(xí)自定義View以及自定義動畫的小伙伴一定不要錯過哦。?
讀者朋友需要有最基本的canvas繪圖功底,比如畫筆Paint的簡單使用、Path如何畫直線等簡單的操作,不熟悉也沒關(guān)系,下文帶大家擼代碼的時候會簡單的講一下。?
此篇文章用到如下知識點:
1)、自定義View的測量?
2)、自定義View屬性的自定義及使用?
3)、Path繪制貝塞爾曲線?
4)、Canvas的裁剪?
5)、用ValueAnimator控制動畫?
6)、Canvas文字居中
好了,開始正文!
一、準備工作 1、效果圖
2、動畫拆解
直觀的看我們要實現(xiàn)三個方面?
1)、波浪動畫(藍色部分)?
2)、不規(guī)則的文字(白色的半個“貼”字)?
3)、控件顯示部分限制成圓形?
3、技術(shù)分析
1)、波浪動畫?
要實現(xiàn)波浪動畫,首先要繪制出波浪的形狀,其次再讓他動起來。波浪線看起來有點像是正弦或者余弦函數(shù),但是Android的Path并沒有提供繪制正余弦圖形的函數(shù),但是提供了一個功能更強大的曲線——貝塞爾曲線,貝塞爾曲線分為二階、三階及多階,本案例里使用的是二次貝塞爾曲線,如下圖所示,二階貝塞爾曲線需要三個點才可以確定
?
我們來看一下Android里貝塞爾曲線的源碼:
/*?@param?x1?The?x-coordinate?of?the?control?point?on?a?quadratic?curve ?????*?@param?y1?The?y-coordinate?of?the?control?point?on?a?quadratic?curve ?????*?@param?x2?The?x-coordinate?of?the?end??on?a?quadratic?curve ?????*?@param?y2?The?y-coordinate?of?the?end?point?on?a?quadratic?curve ?????*/ ????public?void?quadTo(float?x1,?float?y1,?float?x2,?float?y2)?{ ????????isSimplePath?=?false; ????????native_quadTo(mNativePath,?x1,?y1,?x2,?y2); ????}
由注解可以看出來quadTo(float x1,
float y1, float x2, float y2)
的四個參數(shù)分別是控制點的x,y坐標,結(jié)束點的x,y坐標,少了一個開始點呀!不要著急,開始點是Path路徑的上一次結(jié)束的點,如果你的Path沒有繪制過路徑,那么Path的最后一個點坐標就是(0,0)如果想自己定義起始點位置,就用Path.moveTo(float
x, float y)
即可。?
但是每次都需要指定具體的控制點和結(jié)束點既麻煩又容易出錯,那么就需要rQuadTo(float
dx1, float dy1, float dx2, float dy2)
出馬了,rQuadTo跟quadTo的區(qū)別在于rQuadTo使用的是相對起始點的坐標,而不是具體的坐標點,舉個例子,如下代碼效果等價:
?//使用quadTo ????Path?path=new?Path(); ????path.moveTo(100,100); ????path.quadTo(150,0,200,100); ????//使用rQuadTo ????Path?path=new?Path(); ????path.moveTo(100,100); ????path.rQuadTo(50,-100,100,0)
此時畫筆最后的落點都為(200,100)。?
畫波浪線的技術(shù)難點解決了那么如何讓波浪動起來呢,想動起來肯定需要波浪在水平方向移動,那么我們需要畫一個很長很長的波浪讓他移動,這樣就實現(xiàn)了上下起伏效果,但是這樣需要畫無數(shù)多條貝塞爾曲線,肯定不行,這時就用到萬能的數(shù)學(xué)理論——周期函數(shù)了,如果我們繪制兩個周期的貝塞爾曲線,每次只讓它顯示一個周期,然后等第二周期顯示結(jié)束的時候再從頭開始,這樣就造成了無限周期的假象,如下圖?
初始位置為1,向右前進,當(dāng)走到2位置的時候重置成3的位置,即1原始的位置,如此往復(fù)就成了綿綿不絕的波浪了?
?
做成效果如下:黃色區(qū)域就是要顯示的區(qū)域,藍色豎線是波浪線兩個周期的總長度?
2)、不規(guī)則的文字
我們可以看到圓球里的“貼”字在波浪區(qū)域顯示的是白色,波浪區(qū)域之外顯示的是藍色,Android并不支持給文字部分區(qū)域著色的功能,那么我們只能靠控制顯示區(qū)域讓文字只顯示特定形狀,強大的Canvas正好有畫布裁剪功能,通過裁剪畫布就能控制繪制區(qū)域,畫布的裁剪可以用Canvas.clipPath(Path
path)
實現(xiàn),傳入一個閉合的Path既可以隨心所欲裁剪畫布,裁剪示意圖如下?
?
利用波浪形閉合路徑講畫布裁剪成波浪形,那么在此接下來的Canvas繪制操的內(nèi)容只能在這個波浪形區(qū)域里顯示,這樣就解決了文字的部分區(qū)域顯示問題。那么接下來我們只用在相同位置繪制相同字體、字號不同色的文字即可實現(xiàn)一個文字顯示兩種顏色了(注意:實際操作的時候,被裁剪的文字要蓋在未被裁減的文字的上邊,即先在畫布裁剪之前繪制藍色的“貼”字,然后再裁剪畫布再在裁剪后的畫布上繪制白色的“貼”)
3)、控件顯示部分限制成圓形
經(jīng)過2)的分析,將顯示部分限制在圓形區(qū)域里不是易如反掌嗎,使用一個圓形的Path裁剪畫布即可。感興趣的同學(xué)也可以嘗試BitmapShader或者Xfermode來將顯示區(qū)域變成圓形
好了,最主要的步驟都分析完了,上一張圖更直觀地展示一下繪制流程?
?
圖中可以看出波浪形的閉合Path有兩個作用,一個是負責(zé)裁剪畫布,一個是負責(zé)繪制藍色,其實只用第一個功能即可,此處只是方便分解步驟。
二、代碼實現(xiàn)
既然是自定義控件,那就要有通用性比如下邊的效果:?
?
loading小球需文字和顏色都可以改變,所以我們要給自己的控件添加這兩個屬性。首先在“res/values/”路徑下新建一個attrs.xml文件,在里邊定義如下屬性:
接下來開始自定義View?
復(fù)寫三個構(gòu)造函數(shù),將單參數(shù)和雙參數(shù)的構(gòu)造函數(shù)的super方法都改為this,保證無論調(diào)用哪個構(gòu)造方法都會跳到三個參數(shù)的構(gòu)造方法中,這樣就可以偷懶只用在三個參數(shù)的構(gòu)造方法里初始化各種參數(shù)了
public?class?Wave?extends?View?{ ????public?Wave(Context?context)?{ ????????this(context,null); ????} ????public?Wave(Context?context,?AttributeSet?attrs)?{ ????????this(context,?attrs,0); ????} ????public?Wave(Context?context,?AttributeSet?attrs,?int?defStyleAttr)?{ ????????super(context,?attrs,?defStyleAttr); ????????//初始化參數(shù) ????????init(context,attrs); ????} }
接下來是初始化函數(shù),在此處我們獲取到自定義的顏色及文字參數(shù),并初始化各種畫筆,代碼比較簡單,看注釋內(nèi)容即可
private?void?init(Context?context,?AttributeSet?attrs)?{ ????????//獲取自定義參數(shù)值 ????????TypedArray?array?=??context.obtainStyledAttributes(attrs,?R.styleable.Wave); ????????//自定義顏色和文字 ????????color?=?array.getColor(R.styleable.Wave_color,?Color.rgb(41,?163,?254)); ????????text?=?array.getString(R.styleable.Wave_text); ????????array.recycle(); ????????//圖形及路徑填充畫筆(抗鋸齒、填充、防抖動) ????????mPaint?=?new?Paint(Paint.ANTI_ALIAS_FLAG); ????????mPaint.setStyle(Paint.Style.FILL); ????????mPaint.setColor(color); ????????mPaint.setDither(true); ????????//文字畫筆(抗鋸齒、白色、粗體) ????????textPaint?=?new?Paint(Paint.ANTI_ALIAS_FLAG); ????????textPaint.setColor(Color.WHITE); ????????textPaint.setTypeface(Typeface.DEFAULT_BOLD); ????????//閉合波浪路徑 ????????path?=?new?Path(); ????}
接下來是生成波浪線的方法,示意圖如下:?
?
將Path起點移動到最左邊粉色點處,然后繪制兩個周期的長度的波形(一上一下是一個周期),每個周期在x軸的跨度為此控件的寬度控制點距波形的軸線的絕對高度是整個控件的3/20,當(dāng)然想讓波形波動幅度大的話這個比例可以隨意調(diào)整,接下來就用前邊講到的rQuadTo( )來生成閉合的波浪圖形,其中mWidth為控件的寬度,mHeight為控件的高度
private?Path?getActionPath(float?percent)?{ ????????Path?path?=?new?Path(); ????????int?x?=?-mWidth; ????????//當(dāng)前x點坐標(根據(jù)動畫進度水平推移,一個動畫周期推移的距離為一個周期的波長) ????????x?+=?percent?*?mWidth; ????????//波形的起點 ????????path.moveTo(x,?mHeight?/?2); ????????//控制點的相對寬度 ????????int?quadWidth?=?mWidth?/?4; ????????//控制點的相對高度 ????????int?quadHeight?=?mHeight?/?20?*?3; ????????//第一個周期波形 ????????path.rQuadTo(quadWidth,?quadHeight,?quadWidth?*?2,?0); ????????path.rQuadTo(quadWidth,?-quadHeight,?quadWidth?*?2,?0); ????????//第二個周期波形 ????????path.rQuadTo(quadWidth,?quadHeight,?quadWidth?*?2,?0); ????????path.rQuadTo(quadWidth,?-quadHeight,?quadWidth?*?2,?0); ????????//右側(cè)的直線 ????????path.lineTo(x?+?mWidth?*?2,?mHeight); ????????//下邊的直線 ????????path.lineTo(x,?mHeight); ????????//自動閉合補出左邊的直線 ????????path.close(); ????????return?path; ????}
上邊代碼所表示的閉合路徑如下圖?
接下來就是重頭戲onDraw了
protected?void?onDraw(Canvas?canvas)?{ ????????//底部的字 ????????textPaint.setColor(color); ????????drawCenterText(canvas,?textPaint,?text); ????????//上層的字 ????????textPaint.setColor(Color.WHITE); ????????canvas.save(Canvas.CLIP_SAVE_FLAG); ????????????//裁剪成圓形 ????????????Path?o?=?new?Path(); ????????????o.addCircle(mWidth?/?2,?mHeight?/?2,?mWidth?/?2,?Path.Direction.CCW); ????????????canvas.clipPath(o); ????????????//生成閉合波浪路徑 ????????????path?=?getActionPath(currentPersent); ????????????//畫波浪 ????????????canvas.drawPath(path,?mPaint); ????????????//裁剪文字 ????????????canvas.clipPath(path); ????????????drawCenterText(canvas,?textPaint,?text); ????????canvas.restore(); ????}
這里繪制思路是:在canvas上繪制藍色的文字 ——>將畫布裁剪成圓形 ——>繪制波浪 ——>裁剪畫布成波浪形 ——>繪制文字,這里一定要注意繪制順序,先繪制的在下部,后繪制的在上部。思路簡單只用注意到兩次clip即可。?
細心的朋友一定看到了一個函數(shù)drawCenterText(canvas,
textPaint, text)
沒錯,這個函數(shù)就是講文字繪于控件正中心的方法。有的讀者可能一直在使用Canvas.drawText(
String text, float x, float y, Paint paint)
?這個方法,但是參數(shù)中的(x,y)到底是哪個坐標呢,是文字左上角的點的坐標嗎?不是的,接下來我們用代碼驗證一下這個(x,y)到底在文字的哪個部位
????????canvas.drawText(text,600,200,textPaint); ????????canvas.drawCircle(600,200,3,paint); ????????canvas.translate(600,?200); ????????Rect?bgRect=new?Rect(0,0,1000,400); ????????canvas.drawRect(bgRect,bgPaint); ????????Rect?textBound=new?Rect(); ????????textPaint.getTextBounds(text,0,text.length(),textBound); ????????paint.setColor(Color.RED); ????????canvas.drawRect(textBound,paint); ????????Paint.FontMetrics?metrics=textPaint.getFontMetrics(); ????????paint.setColor(Color.RED); ????????//?ascent?橙色 ????????paint.setColor(Color.rgb(255,126,0)); ????????canvas.drawLine(0,?metrics.ascent,?500,metrics.ascent,?paint); ????????//?descent ????????paint.setColor(Color.rgb(255,0,234)); ????????canvas.drawLine(0,?metrics.descent,?500,?metrics.descent,?paint); ????????//?top ????????paint.setColor(Color.DKGRAY); ????????canvas.drawLine(0,?metrics.top,?500,?metrics.top,?paint); ????????//?bottom ????????paint.setColor(Color.GREEN); ????????canvas.drawLine(0,?metrics.bottom,?500,?metrics.bottom,?paint);
首先是在畫布的(600,200)處畫上文字,為了方便觀察(600,200)在文字的什么部位,我在(600,200)處畫了一個半徑3像素的圓圈。然后平移畫布到(600,200)的地方然后依次畫出了文字的邊框圖以及FontMetrics信息里的top、ascent、descent、bottom信息?
我把運行結(jié)果截圖做了處理,方便大家看?
?
從結(jié)果看(600,200)那個藍色的點并不是在文字的左上角,而是左下角,這個點所在的y坐標即是大家常說的BaseLine的位置,那現(xiàn)在這個函數(shù)Canvas.drawText(
String text, float x, float y, Paint paint)
就可以理解為——將文字的基準點放在(x,y)處,那么這個基準點可以改變嗎?答案是肯定的,可以通過繪制文字的畫筆的setTextAlign(Align
align)
方法設(shè)置為Paint.Align.CENTER或者Paint.Align.RIGHT,如果不設(shè)置的話默認是Paint.Align.LEFT。讀者朋友們有興趣的話可以試試設(shè)置成CENTER之后(600,200)的藍圈圈是不是跑到了文字的中部呢?從上圖我們也可以看出,整個文字是介于FontMetrics.top
和FontMetrics.bottom
之間。?
好了,貼上文字居中的代碼,相信認真看上邊那段話的朋友一定能輕松讀懂
??private?void?drawCenterText(Canvas?canvas,?Paint?textPaint,?String?text)?{ ????????Rect?rect?=?new?Rect(0,?0,?mWidth,?mHeight); ????????textPaint.setTextAlign(Paint.Align.CENTER); ????????Paint.FontMetrics?fontMetrics?=?textPaint.getFontMetrics(); ????????float?top?=?fontMetrics.top; ????????float?bottom?=?fontMetrics.bottom; ????????int?centerY?=?(int)?(rect.centerY()?-?top?/?2?-?bottom?/?2); ????????canvas.drawText(text,?rect.centerX(),?centerY,?textPaint); ????}
分析好上邊的代碼 我們就能繪制出一個靜態(tài)的小球了,動畫既然要動,肯定就像汽車一樣需要一個”引擎”,在上面說到的繪制波浪路徑的函數(shù)中我們忽略了getActionPath(float
percent)
的參數(shù)percent,這個參數(shù)即是當(dāng)前動畫的進度,那么我們?nèi)绾蝸碇圃爝@個進度呢?需要怎樣把這個動畫“引擎”點燃呢。我們可以通過各種手段計時,生成一個計時Thread或者自己寫一個Handler等等,只要能均勻的生成進度即可。?
本文中用到一個巧妙的定時器ValueAnimator
?大家常說的屬性動畫ObjectAnimator就是它的一個子類,使用它來作為動畫的引擎再方便不過了,從字面翻譯”ValueAnimator”那就是“值動畫者”直譯雖然low但是恰恰更好理解,就是讓數(shù)值動起來,從什么值動到什么值呢??ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
?
這句話就是定義一個值從0變化到1的一個animator,我們的percent值就是從0變化到1的中間過程值,那么怎么得到這個過程值呢?——監(jiān)聽器!對!
animator.addUpdateListener(new?ValueAnimator.AnimatorUpdateListener()?{ ????????????@Override ????????????public?void?onAnimationUpdate(ValueAnimator?animation)?{ ????????????????float?percent?=?animation.getAnimatedFraction(); ????????????} ????????});
那么數(shù)值從0變到1需要多久呢?怎么能無限重復(fù)呢?重復(fù)的時候是重頭開始還是反轉(zhuǎn)進行呢?別急下面三句話就是讓動畫無限重復(fù),每次從頭開始,一個周期1000毫秒
?animator.setDuration(1000); ?animator.setRepeatCount(ValueAnimator.INFINITE); ?animator.setRepeatMode(ValueAnimator.RESTART);
好了,引擎設(shè)置好了,發(fā)動?animator.start();
?
上效果?
?
WTF!這是什么鬼,為什么鬼畜地慢幾拍??
打印出來橫坐標看看
07-09?18:18:47.308??E/Jcs:?getActionPath:?-21 07-09?18:18:47.326??E/Jcs:?getActionPath:?-15 07-09?18:18:47.342??E/Jcs:?getActionPath:?-10 07-09?18:18:47.359??E/Jcs:?getActionPath:?-5 07-09?18:18:47.375??E/Jcs:?getActionPath:?-2 07-09?18:18:47.392??E/Jcs:?getActionPath:?0 07-09?18:18:47.409??E/Jcs:?getActionPath:?0
最后幾拍的數(shù)值差好像不太對呀!拍拍腦門突然一想,我的動畫不均勻是忘記設(shè)置一個均勻的插值器了!哎!
animator.setInterpolator(new?LinearInterpolator());
補上一個線性插值器,整個世界都順暢了?
百度Loading小球Github源碼
三、結(jié)語
靜態(tài)配圖是用Illustrator配合ps做的,gif是在www.ezgif.com上生成的