畫布,在我們android的View或者自定義View中佔有舉足輕重的地位,在android中它的意義也是字面的意思,我們可以通過畫筆繪製幾何圖形,文本,路徑或者位圖。我們可以將Canvas的API主要分爲三類,一類是繪製相關的,例如drawText,drawLine...,一類是變換相關的,canvas.translate、canvas.rotate....,還有一類是狀態保存和恢複相關的如canvas.save、canvas.restore....下面開始分別講解。
繪製相關
和畫布繪製相關的也就是我們在Canvas類中定義的類似drawXXXX以draw開頭的方法:
上圖截取了一部分Canvas中的drawXXX的方法。
繪製文本drawText
我們平時使用TextView的時候都是調用setText方法將我們需要顯示的文本內容顯示在TextView上面,其實最終也是調用Canvas的drawText方法。先看下面的方法,特地將text的初始座標放在(0,15)這個位置上,看一下效果:
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.GREEN);
mPaint.setTextSize(30);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.RED);
canvas.drawText("測試DrawText的功能的Demo",0,15,mPaint);
}
效果就是綠色的文本只顯示了一部分,這是爲什麼呢。其實在android中當我們使用canvas.drawText的時候,設置的座標的位置其實是設置的文字的左下角那邊的位置。爲什麼這麼說呢?大家小學的時候,都用過那種作業本,漢語拼音,四線三格,我們都是以第三條線位標準寫拼音的,Android的標準也是差不多的。看下圖:
與字體相關的在上圖標註的幾個基準線:
baseline:字符基準線(其實就是我們上面drawText標註的(0,15)這個15的值)
ascent:字符最高點到baseLine的推薦距離。
top:字符最高點到baseline之間的距離。
descent:字符最低點到baseline之間的推薦距離
bottom:字符最低點到baseline之間的最大距離
leading:行間距,即前一行的descent到當前行的ascent之間的距離。
與繪製文本相關的還有FontMetrics,可以獲取上面我們標記解釋的幾個值:
public static class FontMetrics {
/**
* The maximum distance above the baseline for the tallest glyph in
* the font at a given text size.
*/
public float top;
/**
* The recommended distance above the baseline for singled spaced text.
*/
public float ascent;
/**
* The recommended distance below the baseline for singled spaced text.
*/
public float descent;
/**
* The maximum distance below the baseline for the lowest glyph in
* the font at a given text size.
*/
public float bottom;
/**
* The recommended additional space to add between lines of text.
*/
public float leading;
}
我們可以通過 下面方法來獲得,並讀取上面的屬性:
Paint.FontMetrics metrics = mPaint.getFontMetrics();
文字的測量
paint中提供給了測量兩種測量文字的方法,但是有一些細小的差別:分別位measureText()和getTextBounds()方法,他們都屬於Paint的方法,兩個函數的區別在於:
如果你用代碼分別使用getTextBounds() 和 measureText() 來測量文字的寬度,你會發現 measureText() 測出來的寬度總是要比getTextBounds()d的 大一點點。這是因爲這兩個方法其實測量的是兩個不一樣的東西。
getTextBounds(): 它測量的是文字的顯示範圍(關鍵詞:顯示)。就是能夠包裹住這段文字的嘴角的矩形,就是這段文字的 bounds。
measureText() : 它測量的是文字繪製時所佔用的寬度。繪製文字的時候,往往需要佔用比他的實際顯示寬度更多一點的寬度,以此來讓文字和文字之間保留一些間距,不會顯得過於擁擠,也就是設置我們不同行文字之間的間距,導致了 measureText() 比getTextBounds()測量出的寬度要大一些
drawRect,drawCicle,drawBitmap,drawArc...等等都是比較簡單的,直接使用API就ok了。
Path
path即路徑,可以用來繪製直線,曲線,以及這些直線或曲線構成的集合圖形,還可用於根據路徑繪製文字。常用的API有移動,連線,閉合,添加圖形等....
private void init() {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(6);
mPaint.setColor(Color.RED);
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.moveTo(100,70); //路徑起始點移動到(100,70)的位置
mPath.lineTo(140,800); //從(100,70)向(140,800)畫一條線
mPath.lineTo(250,600); //從(140,800)向(250,600)畫一條線
mPath.close(); //將線路閉合
canvas.drawPath(mPath,mPaint);
}
上圖的效果是這樣的,其實我們可以將mPath.lineTo(140,800)修改爲mPath.rLineTo(40,730),rLineTo的意思其中r就是“相對位置”的意思,就是相對於path的上一個所畫路徑的結束點的相對位置也即是140-100 = 40,800-70=730;上圖我們看到紅色的圖像是閉合的,有人可以以爲使是調用mPath.close的結果,其實上例中如果價格mPath.close去掉,也是這樣的結果,主要問題出在Paint的style上面,上例中將style設置爲FILL,如果修改爲STROKE的話
上面2圖體現的結果不一樣,圖1未封閉,圖2成爲一個閉合的圖像:
private void init() {
mPaint = new Paint();
//修改畫筆的樣式 爲Style
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(6);
mPaint.setColor(Color.RED);
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.moveTo(100,70);
mPath.lineTo(140,800);
mPath.lineTo(250,600);
//圖一需要去掉下面一句代碼
mPath.close();
canvas.drawPath(mPath,mPaint);
}
上圖中設置畫筆都爲STROKE,左圖1註釋掉mPath.close(), 右圖2加上mPath.close()方法,結果就不一樣了。path.close() 方法的意思就是,將路徑閉合,如果路徑本來就是閉合的就不用管,如果不是閉合的就將首尾兩點連線閉合。
Path.addArc
添加一個弧形。最終調用的都是含有6個參數的addArc方法,left,top表示的是弧形所在矩形的左上方的位置座標,right,bottom表示弧形所在矩形的右下方的位置座標,startAngle就是弧形開始的位置,sweepAngel表示弧形掃過的角度:
public void addArc(RectF oval, float startAngle, float sweepAngle) {
addArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle);
}
public void addArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle) {
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.addArc(200,200,400,400,-225,225);
mPath.close();
canvas.drawPath(mPath,mPaint);
}
Path.addRect
addRect添加一個矩形,addRect方法中的參數除了矩形的left,top,right,bottom的座標外,還有一個參數表示繪製的順序:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.addRect(500,500,900,900,Path.Direction.CW);
canvas.drawPath(mPath,mPaint);
}
Path.Direction.CW 順時針方向繪製
Path.direction.CCW 逆時針方向繪製
其他的mPath.addCircle,mPath.addOval 都需要這個參數
Path追加圖形
例如下面一段代碼:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.addArc(200,200,400,400,-225,225);
mPath.arcTo(400,200,600,400,-180,225,false);
canvas.drawPath(mPath,mPaint);
}
上例中首先調mPath.addArc畫一個弧形,然後調用mPath.arcTo在原來的圖像上面追加一個弧形,類似前面的mPath.lineTo方法,在arcTo方法中最後一個參數爲false,
boolean forceMoveTo
也就是forceMoveTo = false; 這個參數的意思是繪製的時候,是否移動起點的位置。當forceMoveTo = true,表示繪製的時候將起點移動到所要繪製的圖像的起點位置,當forceMoveTo = false的時候,意思就是不移動起點,但是必須繪製一條從前一個圖像的終點 指向 即將繪製的圖像的起點位置的直線:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.moveTo(0,0);
mPath.lineTo(100,100);
mPath.arcTo(400,200,600,400,0,270,true);
canvas.drawPath(mPath,mPaint);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.lineTo(100,100);
mPath.arcTo(400,200,600,400,0,270,false);
canvas.drawPath(mPath,mPaint);
}
左圖爲 forceMoveTo = true 的效果,右圖爲 forceMove = false的效果。
Path.addPath
還可以在一條路徑上面,通過addPath將另外一條路徑添加到當前的Path中
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//添加一個路徑
mPath.moveTo(100, 70);
mPath.lineTo(140, 180);
mPath.lineTo(250, 330);
mPath.lineTo(400, 630);
mPath.lineTo(100, 830);
Path newPath = new Path();
newPath.moveTo(100, 1000);
newPath.lineTo(600, 1300);
newPath.lineTo(400, 1700);
mPath.addPath(newPath);
canvas.drawPath(mPath,mPaint);
}
Path添加多階貝塞爾曲線
貝塞爾曲線是用一系列點來控制曲線狀態的,將這些點分爲兩類,一類是數據點,一類是控制點。
在很多繪圖或者動畫效果中需要用到貝塞爾曲線,在API中爲我們提供了一階,二階,三階貝塞爾曲線的API。其實一節貝塞爾曲線就是一條直線用lineTo就可以繪製,二階貝塞爾曲線用quadTo或者rQuadTo(相對位置)方法繪製,三階貝塞爾曲線由cubicTo 或者rCubicTo方法進行繪製,例如下面是繪製二階貝塞爾曲線的一般使用方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//畫二階貝塞爾曲線
mPath.moveTo(300, 500);
mPath.quadTo(500, 100, 800, 500);
//參數表示相對位置,等同於上面一行代碼
// mPath.rQuadTo(200, -400, 500, 0);
canvas.drawPath(mPath,mPaint);
}
- 一階曲線原理
一階曲線是沒有控制點的,僅有兩個數據點(A 和 B),最終效果一個線段。
一階公式如下:
- 二階曲線原理
二階曲線由兩個數據點(A 和 C),一個控制點(B)來描述曲線狀態,大致如下:
連接DE,取點F,使得: ,這樣獲取到的點F就是貝塞爾曲線上的一個點,動態圖如下:
二階公式如下:
二階貝塞爾曲線公式推倒方法如下:
三階貝塞爾曲線去下:
公式如下:
高階貝塞爾曲線的通用公式如下,可以認爲是一個遞歸的操作過程
變換相關
Canvas中有許多與變換相關的API,可以幫助我們事先許多意想不到的效果。
canvas.translate
canvas.translate使畫布平移,先看下面的代碼,畫兩個矩形,一個紅色矩形座標爲(50,50),(400,400) 改變畫筆顏色之後,畫綠色矩形,座標還爲(50,50),(400,400),看效果:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(50,50,400,400,mPaint);
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(50,50,400,400,mPaint);
}
看上面的圖像,綠色矩形在紅色矩形的下方,再看下方代碼和效果基本上可以看出平移的一些套路了
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(50,50,400,400,mPaint);
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(50,50,400,400,mPaint);
canvas.translate(-50,-50);
mPaint.setColor(Color.BLUE);
canvas.drawRect(50,50,400,400,mPaint);
}
畫紅色矩形的座標爲(50,50),(400,400),但是canvas.translate(100,100)之後畫綠色矩形,我們看出同樣是(50,50),(400,400)的座標,但是綠色矩形和紅色矩形沒有重合,歸結原因是畫布平移之後,Canvas座標原點平移到(100,100),然後再畫綠色的矩形,實際上畫的綠色的矩形"相對於"原來紅色矩形的座標系應該爲(100+50,100+50),(100+400,100+400)。藍色矩形同理。可以得出一個結論,畫筆平移方法translate(x,y)方法傳遞x,y的值爲 正數 畫布座標原點向下平移,傳遞的值爲負數,畫布座標原點想上平移。
canvas.scale
canvas.scale可以對畫布進行縮放,有2個重載的方法:
public void scale(float sx, float sy) {
if (sx == 1.0f && sy == 1.0f) return;
nScale(mNativeCanvasWrapper, sx, sy);
}
/**
* Preconcat the current matrix with the specified scale.
*
* @param sx The amount to scale in X
* @param sy The amount to scale in Y
* @param px The x-coord for the pivot point (unchanged by the scale)
* @param py The y-coord for the pivot point (unchanged by the scale)
*/
public final void scale(float sx, float sy, float px, float py) {
if (sx == 1.0f && sy == 1.0f) return;
translate(px, py);
scale(sx, sy);
translate(-px, -py);
}
先看第一個函數,傳入float sx,float sy是縮放比率:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,700,700,mPaint);
canvas.scale(0.5f,0.5f);
mPaint.setColor(Color.GREEN);
canvas.drawRect(200,200,700,700,mPaint);
}
我們看到調用scale(0.5f,0.5f)之後,同樣座標的紅色矩形和綠色矩形,爲啥綠色矩形會出現那樣的位置呢。我個人對這個的理解是:其實我們畫同樣大小的紅色和綠色大小的矩形,但是顯示出的大小結果不一樣了,就像我們查看百度地圖或者高德地圖一樣,當我們縮小地圖的時候,我們看到的範圍其實更大了,其實是我們顯示地圖的比率變了而已。類比上面的效果,當我們畫(200,200),(700,700)的紅色矩形,我們的座標系,可以理解爲一個像素點爲1cm,那麼紅色矩形就是寬高爲500cm的矩形,但是當我們將畫布縮小之後,我們一個單位像素點就變成了1/0.5 = 2cm,所以我們的綠色圖形的寬高就爲原來的一半,起始座標相對於原來的畫布變爲(100,100)。所以按照這個理解,我們可以推測出canvas.scale(2,2)的效果爲綠的圖形放大了。起始座標爲(400,400).
第二個方法 public final void scale(float sx, float sy, float px, float py) 傳遞的是四個參數:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,700,700,mPaint);
canvas.scale(0.5f,0.5f,200,200);
mPaint.setColor(Color.GREEN);
canvas.drawRect(200,200,700,700,mPaint);
}
看到最後的效果是這樣的。起始上面的代碼的四個參數的scale方法等同於下面的代碼
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,700,700,mPaint);
canvas.translate(200,200);
canvas.scale(0.5f,0.5f);
canvas.translate(200,200);
mPaint.setColor(Color.GREEN);
canvas.drawRect(200,200,700,700,mPaint);
}
那麼 public final void scale(float sx, float sy, float px, float py) 的意思就是畫布先平移translate(px,py)然後,scale(0.5f,0.5f),最後 translate(-px,-py).
canvas.rotate
canvas.rotate(float degress),傳遞的參數爲度數:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//旋轉操作
canvas.drawRect(0,0,300,300,mPaint);
canvas.rotate(45);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,300,300,mPaint);
}
看到上面的綠色矩形旋轉了45°,有一部分旋轉到屏幕之外了。。當canvas.rotate(degree)傳入的 度數爲正數的時候是繞順時針旋轉,當傳入的度數爲負數的時候,繞逆時針旋轉,默認是繞畫布的座標原點旋轉,上例就是繞(0,0)順時針旋轉45°。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//旋轉操作
canvas.drawRect(200,200,700,700,mPaint);
canvas.rotate(-45,450,450);
mPaint.setColor(Color.GREEN);
canvas.drawRect(200,200,700,700,mPaint);
}
上例中canvas.rotate(-45,450,450) 後面兩個參數是旋轉的中心450,450,畫布繞(450,450)逆時針旋轉45度。
canvas.skew(float sx,float sy)
畫布的錯切(傾斜),sx,sy值,分別表示將畫布在x方向和y方向上傾斜角度的tan 值。
當sx=1時,即將畫布在x方向上旋轉45度,其實就是x軸保持方向不變,y軸逆時針旋轉45度。
當sy=1時,即將畫布在y方向上旋轉45度,其實就是y軸保持方向不變,x軸順時針旋轉45度。
當sx、sy都改變時,兩者都進行相應的移動。
例如下面的代碼:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//傾斜 錯切操作
canvas.drawRect(0,0,200,200,mPaint);
canvas.skew(1,0);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,200,200,mPaint);
}
紅色矩形爲錯切之前的矩形,將畫布向x方向傾斜45°之後,然後再繪製綠色矩形,可能不是很好理解,看下面一幅圖:
紅色線爲原先的x軸和y軸,當調用canvas.skew(1,0)之後,向x方向錯切45°,其實就是將y軸以x爲軸線旋轉45°,然後再畫矩形.如果上面圖中不畫紅色矩形直接錯切之後的效果:一切只是視角不一樣而已。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//傾斜 錯切操作
canvas.skew(1,0);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,200,200,mPaint);
}
canvas.clipXXX
canvas.clipXXX系列方法可以對畫布進行裁剪,使裁剪之後額圖形只能在裁剪的區域內繪製,否則 “無效”。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GRAY); //繪製灰色背景
canvas.drawRect(0,0,500,500,mPaint); //畫一個矩形區域爲(0,0) (500,500)
canvas.clipRect(new Rect(0,0,200,200)); //裁剪(0,0) (200,200)的區域
mPaint.setColor(Color.GREEN); //畫筆設置爲綠色
canvas.drawRect(0,0,200,200,mPaint); //繪製(0,0) (200,200)的綠色矩形
canvas.drawRect(250,250,400,400,mPaint); //繪製(250,250) (400,400)的綠色矩形
}
效果圖爲:
看到(250,250),(400,400)的綠色矩形並沒有顯示出來,是因爲我們裁剪的大小區域爲(0,0),(200,200) 而我們的(250,250),(400,400)的矩形區域不在上面的矩形區域中,所以並沒有繪製出來。
canvas還有其他的裁剪方法,例如canvas.clipPath,和clipRect類似,只不過clipPath能夠裁剪的圖形多一些。clipOutRect反向裁剪。也就是說裁剪之後繪製的圖像,只能顯示在裁剪區域之外,在裁剪區域之內的不能顯示出來。
canvas.setMatrix
setMatrix其實可以做上面的平移 旋轉,縮放 等等操作。使用Matrix進行變換的一般操作步驟爲:
- 創建
Matrix
對象;- 調用
Matrix
的pre/postTranslate/Rotate/Scale/Skew()
方法來設置幾何變換;- 使用
Canvas.setMatrix(matrix)
或Canvas.concat(matrix)
來把幾何變換應用到Canvas
。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//matrix變換
canvas.drawRect(0,0,400,400,mPaint);
Matrix matrix = new Matrix();
matrix.setTranslate(200,200);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,400,400,mPaint);
}
Canvas.setMatrix(matrix)
:用Matrix
直接替換Canvas
當前的變換矩陣,即拋棄Canvas
當前的變換,改用Matrix
的變換。Canvas.concat(matrix)
:用Canvas
當前的變換矩陣和Matrix
相乘,即基於Canvas
當前的變換,疊加上Matrix
中的變換。- 使用matrix時候 有的時候需要調用matrix.reset()清除之前的變換。
狀態保存和恢復
Canvas調用translate,scale,rotate,skew,matrix等的變換之後,後續的操作都是基於變換之後的Canvas的,都會受到影響,對後續的操作很不方便,Canvas提供了save,saveLayer,saveLayerAlpha,restore,restorToCount等方法來保存和恢復狀態。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,500,500,mPaint);
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,300,300,mPaint);
canvas.drawRect(200,200,500,500,mPaint);
}
兩個矩形的大小雖然一致,但是明顯畫布平移之後,座標零點變換位置了,變爲原來畫布的(100,100)位置,如果我們想回到原來的座標零點,需要在調用translate(-100,-100)才能將綠色矩形調整至原來的座標零點。並且後面的(200,200),(500,500)的綠色矩形也是在畫布平移之後的畫布的基礎上繪製的。Android中Canvas提供一些保存狀態的API,讓後續的繪製可以不受這些變換的影響。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,500,500,mPaint);
canvas.save();
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,300,300,mPaint);
mPaint.setColor(Color.BLUE);
canvas.restore();
canvas.drawRect(0,0,300,300,mPaint);
}
上述代碼中調用了canvas.store() 和 canvas.restore()方法,store的意思就是保存當前的畫布狀態,restore就是恢復之前的畫布狀態,這裏也就是恢復到tanslate(100,100)之前的畫布狀態,然後再繪製藍色的矩形,就是在座標零點了。
Android中一般情況下store和restore是成對出現的,意思爲保存當前狀態,退回到上一步的畫布狀態。例如下面多次成對出現這個兩個方法(restore方法總是退回到最近的store方法調用之前的畫布的狀態)。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,500,500,mPaint);
canvas.save();
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,300,300,mPaint);
canvas.save();
canvas.translate(100,100);
canvas.drawLine(0,0,500,500,mPaint);
canvas.restore();
canvas.restore();
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,300,300,mPaint);
}
先畫紅色矩形,調用store,然後平移畫布繪製綠色矩形,然後再調用store然後再平移畫布,繪製綠色直線,最後兩次調用restore,回到畫布最開始的狀態,在座標零點繪製藍色矩形。有點類似一個 “入棧出棧” 的過程 。還有一種restoreToCount的方法可以一步到位。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,500,500,mPaint);
int saveState = canvas.save();
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,300,300,mPaint);
canvas.translate(100,100);
canvas.drawLine(0,0,500,500,mPaint);
canvas.restoreToCount(saveState);
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,300,300,mPaint);
}
調用 int saveState = canvas.save() ; 保存當前的狀態,然後調用canvas.restoreToCount(saveState) 直接回退到那個狀態。出棧操作就是直接回退到指定的棧的那一層。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//創建離屏繪製圖層
int layerId = canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint);
// 在圖層之間進行繪製
//最後將創建的圖層繪製到canvas中
canvas.restoreToCount(layerId);
}
下面看一個引導頁的效果實現:
多看幾遍,我們可以將效果圖分解爲幾個階段:
1.6個小球繞中心 旋轉的階段
2.6個小球 以球所在的中心 先擴大 後收縮的階段。
3.類似水波紋擴散的階段。
自定義View的成員變量
private Paint mPaint; //畫小圓的畫筆
private Paint mHolePaint; //最後白色的波浪的畫筆
private int colors [] = null;
private int mCenterX,mCenterY;
private float mCircleRadius = 18; //旋轉的小球的半徑
private float mCurrentAngle = 0; //小球旋轉的時候的初始角度,做動畫使用
private float mDiatance;
private SplashState mState; //當前的狀態
private ValueAnimator mValueAnimator;
private float mRotateRadius = 100; //旋轉大圓的半徑
//當前大圓的半徑
private float mCurrentRotateRadius = mRotateRadius;
//擴散圓的半徑
private float mCurrentHoleRadius = 0F;
定義上面分析的集中狀態:
//抽象內部類 描述上面3種狀態
private abstract class SplashState{
abstract void drawState(Canvas canvas);
}
//小球的旋轉狀態
private class RotateState extends SplashState{
.....
}
//小球的擴散聚合狀態
private class MerginState extends SplashState{
.....
}
//擴散 水波紋狀態
private class SpreadState extends SplashState{
.....
}
自定義View中最重要的onDraw方法,但是看起來卻那麼簡單:第一個狀態也就是旋轉狀態
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mState == null){
mState = new RotateState();
}
mState.drawState(canvas);
}
看旋轉狀態的RotateState的實現
private class RotateState extends SplashState{
public RotateState(){
mValueAnimator = ValueAnimator.ofFloat(0f, (float) (Math.PI*2));
// mValueAnimator.setRepeatCount(1);
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.setDuration(1500);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentAngle = (float) animation.getAnimatedValue();
invalidate();
}
});
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mState = new MerginState();
}
});
mValueAnimator.start();
}
@Override
void drawState(Canvas canvas) {
drawBackground(canvas);
drawCicle(canvas);
}
}
定義屬性動畫,使小球旋轉起來 看drawCicle方法:
private void drawCicle(Canvas canvas){
int length = colors.length;
float angle = (float)((2*Math.PI)/length);
for(int i=0;i<length;i++){
float sweepAngle = i*angle+mCurrentAngle; //mCurrentAngle爲屬性動畫中獲取的值
//逆時針旋轉
// float cx = (float)(Math.sin(sweepAngle)*mCurrentRotateRadius+mCenterX);
// float cy = (float)(Math.cos(sweepAngle)*mCurrentRotateRadius+mCenterY);
//順時針旋轉
float cx = (float)(Math.cos(sweepAngle)*mCurrentRotateRadius+mCenterX);
float cy = (float)(Math.sin(sweepAngle)*mCurrentRotateRadius+mCenterY);
mPaint.setColor(colors[i]);
canvas.drawCircle(cx,cy,mCircleRadius,mPaint);
}
}
drawBackground是在3個狀態中都要繪製的:
private void drawBackground(Canvas canvas){
if(mCurrentHoleRadius>0){ //擴散半徑>0的時候 就是最好一個狀態
float strokeWidth = mDiatance - mCurrentHoleRadius; //線寬在減小
float radius = strokeWidth / 2 + mCurrentHoleRadius; //半徑一直在增大
mHolePaint.setStrokeWidth(strokeWidth);
canvas.drawCircle(mCenterX,mCenterY, radius, mHolePaint);
}else{
canvas.drawColor(Color.WHITE); //否則 只繪製白色的背景
}
}
上面的float radius = strokeWidth / 2 + mCurrentHoleRadius; 需要加上線寬的一半 ,否則白色圓擴散的半徑就接不上了,這裏爲啥,可以看之前的AndroidUi之Paint 在最開始講解了線寬 對繪製圓的影響。
注意:在MerginState的時候,執行屬性動畫時候,調用了
//反轉執行動畫...
mValueAnimator.reverse();
public MerginState(){
mValueAnimator = ValueAnimator.ofFloat(0,mRotateRadius);
mValueAnimator.setDuration(1000);
mValueAnimator.setInterpolator(new OvershootInterpolator(5));
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentRotateRadius = (float) animation.getAnimatedValue();
invalidate();
}
});
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mState = new SpreadState();
}
});
//反轉執行動畫...
mValueAnimator.reverse();
}
本來原來的屬性動畫,是旋轉半徑從0-mRotateRadius 並且攔截器爲 OvershootInterpolator。現在調用reverse效果之後,從最大半徑mRotateRadius的位置到0。
Demo中還有2個自定義View的效果,一個是爆炸效果,一個是類似QQ消息氣泡效果,可以自行查看。