上一節結尾的時候我們說到,Paint類中我們還有一個方法沒講
`setShader(Shader shader) `
這個方法呢其實也沒有什么特別的,那么為什么我們要把它單獨分離出來講那么異類呢?難道它賄賂了我嗎?顯然不是的,哥視金錢如糞土(我的要求很低,只需要一克反物質即可)!怎么可能做出如此下三濫的事情!之所以要把這貨單獨拿出來是為了引出Android在圖形變換中非常重要的一個類!這個類是什么呢?我也先不說,咱還是先來看看Shader:

Shader類呢也是個灰常灰常簡單的類,它有五個子類,像PathEffect一樣每個子類都實現了一種Shader,Shader在三維軟件中我們稱之為著色器,其作用嘛就像它的名字一樣是來給圖像著色的或者更通俗的說法是上色!這么說該懂了吧!再不懂去廁所哭去!這五個Shader里最異類的是BitmapShader,因為只有它是允許我們載入一張圖片來給圖像著色,那我們還是先來看看這個怪胎吧
BitmapShader
只有一個含參的構造方法BitmapShader (Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)而其他的四個兄弟姐妹呢都有兩個!它只有一個蛋,又一魂談!那好吧,我們來看看它是什么個效果,順便呢也學習一下Shader的用法先,來看我們熟悉的代碼:
~~~
public class ShaderView extends View {
private static final int RECT_SIZE = 400;// 矩形尺寸的一半
private Paint mPaint;// 畫筆
private int left, top, right, bottom;// 矩形坐上右下坐標
public ShaderView(Context context, AttributeSet attrs) {
super(context, attrs);
// 獲取屏幕尺寸數據
int[] screenSize = MeasureUtil.getScreenSize((Activity) context);
// 獲取屏幕中點坐標
int screenX = screenSize[0] / 2;
int screenY = screenSize[1] / 2;
// 計算矩形左上右下坐標值
left = screenX - RECT_SIZE;
top = screenY - RECT_SIZE;
right = screenX + RECT_SIZE;
bottom = screenY + RECT_SIZE;
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
// 獲取位圖
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a);
// 設置著色器
mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制矩形
canvas.drawRect(left, top, right, bottom, mPaint);
}
}
~~~
如果上面我們沒有設置Shader:
`mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); `
那么我們Draw出來的圖像一定是一個位于屏幕正中黑色的正方形,但是我們設置了Shader后還是一樣的嗎?看看效果:

我靠!這什么玩意!罪過罪過!真是看不懂!別急,Shader.TileMode里有三種模式:CLAMP、MIRROR和REPETA,我們看看其他兩種模式是什么效果呢:
`mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR)); ` 
誒?這效果還能接受,我們還能看得出一點效果,說白了就是上下左右的鏡像而已,那再看看REPETA:
`mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)); `

這個就更簡單了,明顯的一個重復效果,而REPEAT也就是重復的意思,同理MIRROR也就是鏡像的意思,這個很好理解吧。那第一個CLAMP模式究竟特么的是什么東西呢?看效果根本看不出來,我們不妨換個思維,BitmapShader (Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)的第一個參數是位圖這個很顯然,而后兩個參數則分別表示XY方向上的著色模式,既然可以分開設置,那么我們是不是可以這樣設置一個Shader呢?
`mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.MIRROR)); `
也就是說我們在X軸方向上采取CLAMP模式而Y軸方向上采取MIRROR模式,那么這樣肯定是可行的撒:

大家可以看到圖像分為兩部分左邊呢Y軸鏡像了,而右邊像是被拉伸了一樣怪怪的!其實CLAMP的意思就是邊緣拉伸的意思,比如上圖中左邊Y軸鏡像了,而右邊會緊挨著左邊將圖像邊緣上的第一個像素沿X軸復制!產生一種被拉伸的效果!就像扯蛋,不過這里扯的不是蛋而是圖像邊緣的第一個像素,就是這么簡單。但是!作為一個嚴謹的男人必須要又一個嚴謹的態度!這時我就會想,如果兩種模式互換會怎樣呢?比如:
`mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.MIRROR, Shader.TileMode.CLAMP));`
這樣來看,應該是X軸會鏡像而Y軸會拉伸對吧,看看效果:

這…………好像跟我們想象中的不大一樣唉……是我們做錯了嗎?不是的,結合上一個例子大家有沒有注意BitmapShader是先應用了Y軸的模式而X軸是后應用的!所以著色是先在Y軸拉伸了然后再沿著X軸重復對吧(陰笑ing……)?!
要善于發現生活規律!我曾經說過:存在必定合理,那么這么一個玩意有什么用處呢?給大家看一個非常有趣的東西:

這玩意是不是感覺跟以前那種小霸王某個游戲的開場動畫很類似?我們就是利用BitmapShader來實現的,而且實現方法也異常簡單:
~~~
public class BrickView extends View {
private Paint mFillPaint, mStrokePaint;// 填充和描邊的畫筆
private BitmapShader mBitmapShader;// Bitmap著色器
private float posX, posY;// 觸摸點的XY坐標
public BrickView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化畫筆
initPaint();
}
/**
* 初始化畫筆
*/
private void initPaint() {
/*
* 實例化描邊畫筆并設置參數
*/
mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mStrokePaint.setColor(0xFF000000);
mStrokePaint.setStyle(Paint.Style.STROKE);
mStrokePaint.setStrokeWidth(5);
// 實例化填充畫筆
mFillPaint = new Paint();
/*
* 生成BitmapShader
*/
Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.brick);
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
mFillPaint.setShader(mBitmapShader);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
/*
* 手指移動時獲取觸摸點坐標并刷新視圖
*/
if (event.getAction() == MotionEvent.ACTION_MOVE) {
posX = event.getX();
posY = event.getY();
invalidate();
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
// 設置畫筆背景色
canvas.drawColor(Color.DKGRAY);
/*
* 繪制圓和描邊
*/
canvas.drawCircle(posX, posY, 300, mFillPaint);
canvas.drawCircle(posX, posY, 300, mStrokePaint);
}
}
~~~
我只是單純地加載了一張板磚的貼圖然后呢在BitmapShader中XY軸重復就這么簡單 = = ,是不是感腳被耍了一樣好無聊~~~~是你不動腦而已……囧!來看看另一個Shader叫做
LinearGradient
線性漸變,顧名思義這錘子玩意就是來畫漸變的,實際上Shader的五個子類中除了上面我們說的那個怪胎,還有個變形金剛ComposeShader外其余三個都是漸變只是效果不同而已,而這個LinearGradient線性漸變一說大家估計都懂,先來看張效果圖:

是不是秒懂了!恩,說明你頭腦簡單,這個實現也很簡單,具體代碼跟上面的BitmapShader一樣只是把BitmapShader換成了LinearGradient而已:
`mPaint.setShader(new LinearGradient(left, top, right, bottom, Color.RED, Color.YELLOW, Shader.TileMode.REPEAT)); `
上面我們提到過除了BitmapShader外其他子類都有兩個構造方法,上面我們用到了
`LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, Shader.TileMode tile) `
這是LinearGradient最簡單的一個構造方法,參數雖多其實很好理解x0和y0表示漸變的起點坐標而x1和y1則表示漸變的終點坐標,這兩點都是相對于屏幕坐標系而言的,而color0和color1則表示起點的顏色和終點的顏色,這些即便是213也能懂 - - ……Shader.TileMode上面我們給的是REPEAT重復但是并沒有任何效果,這時因為我們漸變的起點和終點都落在了圖形的兩端,整個漸變shader已經填充了圖形所以不起作用,如果我們改改,把終點坐標變一下:
`mPaint.setShader(new LinearGradient(left, top, right - RECT_SIZE, bottom - RECT_SIZE, Color.RED, Color.YELLOW, Shader.TileMode.REPEAT)); `
此時我們漸變終點坐標落在了圖形的終點上,根據我們的REPEAT模式,會呈現一個漸變重復的效果:

僅僅兩種顏色的漸變根本無法滿足我們身體的欲望,太單調乏味!我們是不是可以定義多種顏色漸變呢?答案是必須的,LinearGradient的另一個構造方法
`LinearGradient(float x0, float y0, float x1, float y1, int[] colors, float[] positions, Shader.TileMode tile) `
就為我們實現了這么一個功能:
`mPaint.setShader(new LinearGradient(left, top, right, bottom, new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, new float[] { 0, 0.1F, 0.5F, 0.7F, 0.8F }, Shader.TileMode.MIRROR)); `

前面四個參數也是定義坐標的不扯了colors是一個int型數組,我們用來定義所有漸變的顏色,positions表示的是漸變的相對區域,其取值只有0到1,上面的代碼中我們定義了一個[0, 0.1F, 0.5F, 0.7F, 0.8F],意思就是紅色到黃色的漸變起點坐標在整個漸變區域(left, top, right, bottom定義了漸變的區域)的起點,而終點則在漸變區域長度 * 10%的地方,而黃色到綠色呢則從漸變區域10%開始到50%的地方以此類推,positions可以為空:
`mPaint.setShader(new LinearGradient(left, top, right, bottom, new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, null, Shader.TileMode.MIRROR)); `
為空時各種顏色的漸變將會均分整個漸變區域:

實際應用中線性漸變也是一個非常好玩的東西,比如哦我們呢可以通過它和混合模式一起制作我們常見的圖片倒影效果:

效果并不復雜實現也非常簡單:
~~~
public class ReflectView extends View {
private Bitmap mSrcBitmap, mRefBitmap;// 位圖
private Paint mPaint;// 畫筆
private PorterDuffXfermode mXfermode;// 混合模式
private int x, y;// 位圖起點坐標
public ReflectView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化資源
initRes(context);
}
/*
* 初始化資源
*/
private void initRes(Context context) {
// 獲取源圖
mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.gril);
// 實例化一個矩陣對象
Matrix matrix = new Matrix();
matrix.setScale(1F, -1F);
// 生成倒影圖
mRefBitmap = Bitmap.createBitmap(mSrcBitmap, 0, 0, mSrcBitmap.getWidth(), mSrcBitmap.getHeight(), matrix, true);
int screenW = MeasureUtil.getScreenSize((Activity) context)[0];
int screenH = MeasureUtil.getScreenSize((Activity) context)[1];
x = screenW / 2 - mSrcBitmap.getWidth() / 2;
y = screenH / 2 - mSrcBitmap.getHeight() / 2;
// ………………………………
mPaint = new Paint();
mPaint.setShader(new LinearGradient(x, y + mSrcBitmap.getHeight(), x, y + mSrcBitmap.getHeight() + mSrcBitmap.getHeight() / 4, 0xAA000000, Color.TRANSPARENT, Shader.TileMode.CLAMP));
// ………………………………
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
canvas.drawBitmap(mSrcBitmap, x, y, null);
int sc = canvas.saveLayer(x, y + mSrcBitmap.getHeight(), x + mRefBitmap.getWidth(), y + mSrcBitmap.getHeight() * 2, null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(mRefBitmap, x, y + mSrcBitmap.getHeight(), null);
mPaint.setXfermode(mXfermode);
canvas.drawRect(x, y + mSrcBitmap.getHeight(), x + mRefBitmap.getWidth(), y + mSrcBitmap.getHeight() * 2, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(sc);
}
}
~~~
**SweepGradient**
的意思是梯度漸變,也稱之為掃描式漸變,因為其效果有點類似雷達的掃描效果,他也有兩個構造方法:
`SweepGradient(float cx, float cy, int color0, int color1) `
其實都跟LinearGradient差不多的,簡直沒什么可說的,直接上效果跳過無聊的講解:
`mPaint.setShader(new SweepGradient(screenX, screenY, Color.RED, Color.YELLOW)); `

`SweepGradient(float cx, float cy, int[] colors, float[] positions) `
類似,不重復浪費口水:
`mPaint.setShader(new SweepGradient(screenX, screenY, new int[] { Color.GREEN, Color.WHITE, Color.GREEN }, null)); `

**RadialGradient**
徑向漸變,徑向漸變說的簡單點就是個圓形中心向四周漸變的效果,他也一樣有兩個構造方法……艾瑪!我都要吐了……
`RadialGradient (float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode) `
簡單,一看就懂,例子勞資都懶得上了,屮!
`RadialGradient (float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode) `
同上!還是說點有意思的,來個妹子養養眼:

是不是很漂亮?不過哥壓根不放在眼里,哥女朋友比她更漂亮~~好,我們應用1/6里學到的知識給她校校色,因為這張圖片的顏色實在太過鮮艷了不符合小清新的LOMO風格~~~~
~~~
public class DreamEffectView extends View {
private Paint mBitmapPaint;// 位圖畫筆
private Bitmap mBitmap;// 位圖
private PorterDuffXfermode mXfermode;// 圖形混合模式
private int x, y;// 位圖起點坐標
private int screenW, screenH;// 屏幕寬高
public DreamEffectView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化資源
initRes(context);
// 初始化畫筆
initPaint();
}
/**
* 初始化資源
*
* @param context
* 丟你螺母
*/
private void initRes(Context context) {
// 獲取位圖
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.gril);
// 實例化混合模式
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);
screenW = MeasureUtil.getScreenSize((Activity) context)[0];
screenH = MeasureUtil.getScreenSize((Activity) context)[1];
x = screenW / 2 - mBitmap.getWidth() / 2;
y = screenH / 2 - mBitmap.getHeight() / 2;
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 去飽和、提亮、色相矯正
mBitmapPaint.setColorFilter(new ColorMatrixColorFilter(new float[] { 0.8587F, 0.2940F, -0.0927F, 0, 6.79F, 0.0821F, 0.9145F, 0.0634F, 0, 6.79F, 0.2019F, 0.1097F, 0.7483F, 0, 6.79F, 0, 0, 0, 1, 0 }));
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
// 新建圖層
int sc = canvas.saveLayer(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), null, Canvas.ALL_SAVE_FLAG);
// 繪制混合顏色
canvas.drawColor(0xcc1c093e);
// 設置混合模式
mBitmapPaint.setXfermode(mXfermode);
// 繪制位圖
canvas.drawBitmap(mBitmap, x, y, mBitmapPaint);
// 還原混合模式
mBitmapPaint.setXfermode(null);
// 還原畫布
canvas.restoreToCount(sc);
}
}
~~~
這樣感覺是不是好很多了呢?

類似的效果在一些相機特效中稱之為夢幻,我最近在做的一個開源相機項目也有類似的效果。但是這樣的效果好像還湊合,但是整張圖片沒重點,我們可以模擬單反相機的暗角效果,壓暗圖片周圍的顏色亮度提亮中心,讓整張圖片的中心突出來!實現方式有很多種,比如1/4我們提到過的BlurMsakFilter向內模糊就可以得到一個類似的效果,但是BlurMsakFilter計算出來的像素太生硬毫無生氣,這里我們就使用RadialGradient來模擬一下下赑屃:
~~~
public class DreamEffectView extends View {
private Paint mBitmapPaint, mShaderPaint;// 位圖畫筆和Shader圖形的畫筆
private Bitmap mBitmap;// 位圖
private PorterDuffXfermode mXfermode;// 圖形混合模式
private int x, y;// 位圖起點坐標
private int screenW, screenH;// 屏幕寬高
public DreamEffectView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化資源
initRes(context);
// 初始化畫筆
initPaint();
}
/**
* 初始化資源
*
* @param context
* 丟你螺母
*/
private void initRes(Context context) {
// 獲取位圖
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.gril);
// 實例化混合模式
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);
screenW = MeasureUtil.getScreenSize((Activity) context)[0];
screenH = MeasureUtil.getScreenSize((Activity) context)[1];
x = screenW / 2 - mBitmap.getWidth() / 2;
y = screenH / 2 - mBitmap.getHeight() / 2;
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 去飽和、提亮、色相矯正
mBitmapPaint.setColorFilter(new ColorMatrixColorFilter(new float[] { 0.8587F, 0.2940F, -0.0927F, 0, 6.79F, 0.0821F, 0.9145F, 0.0634F, 0, 6.79F, 0.2019F, 0.1097F, 0.7483F, 0, 6.79F, 0, 0, 0, 1, 0 }));
// 實例化Shader圖形的畫筆
mShaderPaint = new Paint();
// 設置徑向漸變,漸變中心當然是圖片的中心也是屏幕中心,漸變半徑我們直接拿圖片的高度但是要稍微小一點
// 中心顏色為透明而邊緣顏色為黑色
mShaderPaint.setShader(new RadialGradient(screenW / 2, screenH / 2, mBitmap.getHeight() * 7 / 8, Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP));
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
// 新建圖層
int sc = canvas.saveLayer(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), null, Canvas.ALL_SAVE_FLAG);
// 繪制混合顏色
canvas.drawColor(0xcc1c093e);
// 設置混合模式
mBitmapPaint.setXfermode(mXfermode);
// 繪制位圖
canvas.drawBitmap(mBitmap, x, y, mBitmapPaint);
// 還原混合模式
mBitmapPaint.setXfermode(null);
// 還原畫布
canvas.restoreToCount(sc);
// 繪制一個跟圖片大小一樣的矩形
canvas.drawRect(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), mShaderPaint);
}
}
~~~
Look Look~~是不是效果更好了呢?四周的亮度被無情地壓了下去,重點直接呈現在中心

所以……大家一定要活用工具~~~~善于組合思考!說起組合,接下來Shader的最后一個子類簡直就是組合他媽生的
ComposeShader
就是組合Shader的意思,顧名思義就是兩個Shader組合在一起作為一個新Shader……老掉牙的劇情是吧!同樣,這錘子玩意也有兩個構造方法
~~~
ComposeShader (Shader shaderA, Shader shaderB, Xfermode mode)
ComposeShader (Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
~~~
兩個都差不多的,只不過一個指定了只能用PorterDuff的混合模式而另一個只要是Xfermode下的混合模式都沒問題!上面我們為米女圖片加暗角的時候只是單純地使用了一下徑向漸變,但是其實實際獲得的效果并不好,因為我們應用的徑向漸變是個圓形的,但是我們的圖片實際上是個豎向矩形的,直接往上面“蓋”一個徑向漸變實際效果簡而言之應該是這樣的:

可見漸變的坡度太平緩了,不符合我們的Style,能不能改一下讓它拉伸下變成一個豎向的橢圓形呢?比如下圖這樣的:

我們來試試看:
~~~
public class DreamEffectView extends View {
private Paint mBitmapPaint, mShaderPaint;// 位圖畫筆和Shader圖形的畫筆
private Bitmap mBitmap, darkCornerBitmap;// 源圖的Bitmap和我們自己畫的暗角Bitmap
private PorterDuffXfermode mXfermode;// 圖形混合模式
private int x, y;// 位圖起點坐標
private int screenW, screenH;// 屏幕寬高
public DreamEffectView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化資源
initRes(context);
// 初始化畫筆
initPaint();
}
/**
* 初始化資源
*
* @param context
* 丟你螺母
*/
private void initRes(Context context) {
// 獲取位圖
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.gril);
// 實例化混合模式
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);
screenW = MeasureUtil.getScreenSize((Activity) context)[0];
screenH = MeasureUtil.getScreenSize((Activity) context)[1];
x = screenW / 2 - mBitmap.getWidth() / 2;
y = screenH / 2 - mBitmap.getHeight() / 2;
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 去飽和、提亮、色相矯正
mBitmapPaint.setColorFilter(new ColorMatrixColorFilter(new float[] { 0.8587F, 0.2940F, -0.0927F, 0, 6.79F, 0.0821F, 0.9145F, 0.0634F, 0, 6.79F, 0.2019F, 0.1097F, 0.7483F, 0, 6.79F, 0, 0, 0, 1, 0 }));
// 實例化Shader圖形的畫筆
mShaderPaint = new Paint();
// 根據我們源圖的大小生成暗角Bitmap
darkCornerBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
// 將該暗角Bitmap注入Canvas
Canvas canvas = new Canvas(darkCornerBitmap);
// 計算徑向漸變半徑
float radiu = canvas.getHeight() * (2F / 3F);
// 實例化徑向漸變
RadialGradient radialGradient = new RadialGradient(canvas.getWidth() / 2F, canvas.getHeight() / 2F, radiu, new int[] { 0, 0, 0xAA000000 }, new float[] { 0F, 0.7F, 1.0F }, Shader.TileMode.CLAMP);
// 實例化一個矩陣
Matrix matrix = new Matrix();
// 設置矩陣的縮放
matrix.setScale(canvas.getWidth() / (radiu * 2F), 1.0F);
// 設置矩陣的預平移
matrix.preTranslate(((radiu * 2F) - canvas.getWidth()) / 2F, 0);
// 將該矩陣注入徑向漸變
radialGradient.setLocalMatrix(matrix);
// 設置畫筆Shader
mShaderPaint.setShader(radialGradient);
// 繪制矩形
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mShaderPaint);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
// 新建圖層
int sc = canvas.saveLayer(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), null, Canvas.ALL_SAVE_FLAG);
// 繪制混合顏色
canvas.drawColor(0xcc1c093e);
// 設置混合模式
mBitmapPaint.setXfermode(mXfermode);
// 繪制位圖
canvas.drawBitmap(mBitmap, x, y, mBitmapPaint);
// 還原混合模式
mBitmapPaint.setXfermode(null);
// 還原畫布
canvas.restoreToCount(sc);
// 繪制我們畫好的徑向漸變圖
canvas.drawBitmap(darkCornerBitmap, x, y, null);
}
}
~~~
運行一下可以看到如下結果:

是不是感腳暗角比上面我們做的那個更Perfect?圖片中心大部分區域不受任何干擾只把四角邊緣處的亮度壓了下去,當然這個效果對哥來說也不是很滿意,不過先將就湊合著 = = 。上例中我們使用到了兩個東西,一個是在獨立的Canvas中繪制自己的Bitmap,在哥下一節我們就會詳細講到這里就先不扯了,而另一個則是我們說的重點:Matrix,其實在上面我們做倒影的時候已經簡單地使用過了Matrix,那么Matrix究竟是什么呢?其中文直譯為矩陣,而我更愿意稱之為矩陣變換或者圖形變換,它和我們1/6中學到的圖形的混合可謂是雙簧,同樣地重要!
在本文的開頭哥給大家挖了一個坑,在講BitmapShader的時候我們在
`mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); `
的模式下得到了如下這樣狗吃屎的效果:

不知道大家有沒有質疑過為什么?如果沒有,情有可原!但是在我們使用另外兩種模式REPEAT和MIRROR的時候難道大家就沒想過為什么我們的位圖會像那樣重復或者鏡像嗎?是必須那樣嗎?那個例子中我們使用的是一個邊長為800置于屏幕中心的矩形,我們何不嘗試將其鋪滿屏幕看看?
~~~
public class ShaderView extends View {
private static final int RECT_SIZE = 400;// 矩形尺寸的一半
private Paint mPaint;// 畫筆
private int left, top, right, bottom;// 矩形坐上右下坐標
private int screenX, screenY;
public ShaderView(Context context, AttributeSet attrs) {
super(context, attrs);
// 獲取屏幕尺寸數據
int[] screenSize = MeasureUtil.getScreenSize((Activity) context);
// 獲取屏幕中點坐標
screenX = screenSize[0] / 2;
screenY = screenSize[1] / 2;
// 計算矩形左上右下坐標值
left = screenX - RECT_SIZE;
top = screenY - RECT_SIZE;
right = screenX + RECT_SIZE;
bottom = screenY + RECT_SIZE;
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 獲取位圖
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a);
// 設置著色器
mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
// mPaint.setShader(new LinearGradient(left, top, right - RECT_SIZE, bottom - RECT_SIZE, Color.RED, Color.YELLOW, Shader.TileMode.MIRROR));
// mPaint.setShader(new LinearGradient(left, top, right, bottom, new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, null, Shader.TileMode.MIRROR));
// mPaint.setShader(new SweepGradient(screenX, screenY, Color.RED, Color.YELLOW));
// mPaint.setShader(new SweepGradient(screenX, screenY, new int[] { Color.GREEN, Color.WHITE, Color.GREEN }, null));
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制矩形
// canvas.drawRect(left, top, right, bottom, mPaint);
canvas.drawRect(0, 0, screenX * 2, screenY * 2, mPaint);
}
}
~~~
看看效果:

是不是忽然明白了什么?好了,你可以從坑里爬出來了…………這樣看是不是懂了?BitmapShader是從畫布的左上方開始著色,回到我們剛才的問題,這個著色方式必須是這樣的么?顯然不是!在Shader類中有一對setter和getter方法:setLocalMatrix(Matrix localM)和getLocalMatrix(Matrix localM)我們可以利用它們來設置或獲取Shader的變換矩陣,比如上面的例子我還是繪制成一個邊長為800的矩形:
~~~
public class ShaderView extends View {
private static final int RECT_SIZE = 400;// 矩形尺寸的一半
private Paint mPaint;// 畫筆
private int left, top, right, bottom;// 矩形坐上右下坐標
private int screenX, screenY;
public ShaderView(Context context, AttributeSet attrs) {
super(context, attrs);
// 獲取屏幕尺寸數據
int[] screenSize = MeasureUtil.getScreenSize((Activity) context);
// 獲取屏幕中點坐標
screenX = screenSize[0] / 2;
screenY = screenSize[1] / 2;
// 計算矩形左上右下坐標值
left = screenX - RECT_SIZE;
top = screenY - RECT_SIZE;
right = screenX + RECT_SIZE;
bottom = screenY + RECT_SIZE;
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 獲取位圖
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a);
// 實例化一個Shader
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 實例一個矩陣對象
Matrix matrix = new Matrix();
// 設置矩陣變換
matrix.setTranslate(left, top);
// 設置Shader的變換矩陣
bitmapShader.setLocalMatrix(matrix);
// 設置著色器
mPaint.setShader(bitmapShader);
// mPaint.setShader(new LinearGradient(left, top, right - RECT_SIZE, bottom - RECT_SIZE, Color.RED, Color.YELLOW, Shader.TileMode.MIRROR));
// mPaint.setShader(new LinearGradient(left, top, right, bottom, new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, null, Shader.TileMode.MIRROR));
// mPaint.setShader(new SweepGradient(screenX, screenY, Color.RED, Color.YELLOW));
// mPaint.setShader(new SweepGradient(screenX, screenY, new int[] { Color.GREEN, Color.WHITE, Color.GREEN }, null));
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制矩形
canvas.drawRect(left, top, right, bottom, mPaint);
// canvas.drawRect(0, 0, screenX * 2, screenY * 2, mPaint);
}
}
~~~
不一樣的是我在給畫筆設置著色器前為我們的著色器設置了一個變換矩陣,讓我們的Shader依據自身的坐標→平移left個單位↓平移top個單位,也就是說原本shader的原點應該是畫布(注意不是屏幕!這里只是剛好畫布更屏幕重合了而已!切記!)的左上方[0,0]的位置,通過變換移至了[left,top]的位置,如果沒問題,Shader此時應該是剛好是從我們矩形的左上方開始著色:

Is anyone has question yet?是不是感覺有點兒懂Matrix了?Really?
其實說了半天我們依然沒進入到Matrix的本質,在1/6中我們學習了一個和它比較類似的玩意叫做ColorMatrix大家還記得否?我在講ColorMatrix的時候說過其是一個4x5的顏色矩陣,而同樣,我們的Matrix也是一個矩陣,只不過不是4*5而是3*3的位置坐標矩陣:

變換變換,既然說到變換那么必定涉及最基本的旋轉啊、縮放啊、平移之類,而在Matrix中除了該三者還多了一種:錯切,什么叫錯切呢?所謂錯切數學中也稱之為剪切變換,原理呢就是將圖形上所有點的X/Y坐標保持不變而按比例平移Y/X坐標,并且平移的大小和某點到X/Y軸的垂直距離成正比,變換前后圖形的面積不變。其實對于Matrix可以這樣說:圖形的變換實質上就是圖形上點的變換,而我們的Matrix的計算也正是基于此,比如點P(x0,y0)經過上面的矩陣變換后會去到P(x,y)的位置:

注:除了平移外,縮放、旋轉、錯切、透視都是需要一個中心點作為參照的,如果沒有平移,默認為圖形的[0,0]點,平移只需要指定移動的距離即可,平移操作會改變中心點的位置!非常重要!記牢了!
PS:唉……說實話矩陣的計算原理真心不想寫……不過算是個小總結吧,愛看看不愛看直接跳過即可……
有一點需要注意的是,矩陣的乘法運算是不符合交換律的,因此矩陣B*A和A*B是兩個截然不同的結果,前者表示A右乘B,是列變換;后者表示A左乘B,是行變換。如果有心的童鞋會發現Matrix類中會有三大類方法:setXXX、preXXX和postXXX,而preXXX和postXXX就是分別表示矩陣的左右乘,也有前后乘的說法,對于不懂矩陣的人來說都一樣 = = ……但是要注意一點!!!大家在理解Matrix的時候要把它想象成一個容器,什么容器呢?存放我們變換信息的容器,Matrix的所有方法都是針對其自身的!!!!當我們把所有的變換操作做完后再“一次性地”把它注入我們想要的地方,比如上面我們為shader注入了一個Matrix。還有一點要注意,一定要注意:ColorMatrix和Matrix在應用給其他對象時都是左乘的,而其自身內部是可以左右乘的!千萬別搞混了!UnderStand?一定要理解這一點,不然我只能哭暈在廁所了壓根沒法講下去你也不會聽的懂……
上圖的公式中,GHI都表示的是透視參數,一般情況下我們不會去處理,三維的透視我更樂意使用Camare,所以很多時候G和H的值都為0而I的值恒為1,至于為什么如果有時間待會會說。
所有的Matrix變換中最好理解的其實是縮放變換,因為縮放的本質其實就是圖形上所有的點X/Y軸沿著中心點放大一定的倍數,比如:

這么一個矩陣變換實質就是x = x0 * a、y = y0 * b,難度系數:0

X/Y軸分別放大a\b倍
相對來說平移稍難但是也好理解:

同理x = x0 + a、y = y0 + b,難度系數:0

X/Y軸分別平移!¥¥%#%……%……#¥%¥%
旋轉就很復雜了……分為兩種:一種是直接繞默認中點[0,0]旋轉,另一種是指定中點,也就是將中點[0,0]平移后在旋轉:
直接繞[0,0]順時針轉:

唉、這個先看圖吧:

根據三角函數的關系我們可以得出p(x,y)的坐標:

同樣根據三角函數的關系我們也可以得出p(x0,y0)的坐標:

上述兩公式結合我們則可以得出簡化后的p(x,y)的坐標:

這是什么公式呢?是不是就是上面矩陣的乘積呢?囧……
繞點p(a,b)順時針轉:
其實繞某個點旋轉沒有想象中的那么復雜,相對于繞中心點來說就多了兩步:先將坐標原點移到我們的p(a,b)處然后執行旋轉最后再把坐標圓點移回去:

對了……開頭忘說了……矩陣的乘法是從右邊開始的,額,其實也只有上面這算式才有多個矩陣相乘 = = 冏,也就是說最右邊的兩個會先乘,大家看看最右邊的兩個乘積是什么……是不是就是我們把原點移動到P(a,b)后[x0,y0]的新坐標啊?然后繼續往左乘,旋轉一定得角度這跟上面[0,0]旋轉是一樣的,最后往左乘把坐標還原

不扯了,你聽得懂甚好!不懂沒關系!真的沒關系!哥不騙你!哥從不騙妹子……
哎……錯切我也先不說了,大家聽了這么多槽點是不是頭都大了?麻痹的做個變換還這么麻煩勞資TM還不如不做!是的,這樣去做變換真心太麻煩!要是開發**Android**這么麻煩的話特么誰還玩?所以這些復雜繁瑣的撲街玩意Android早就為我們封裝好了……壓根就不需要我們去管那么多!上面我們曾提到的setXXX、preXXX和postXXX方法就是Android為我們封裝好的針對不同運算的“檔位”,那么怎么用呢?非常非常簡單,你壓根可以現在忘記上面我們說到的各種計算原理,比如,我這里還是拿BitmapShader來說吧,我們想要移動Shader的位置:
~~~
public class MatrixView extends View {
private static final int RECT_SIZE = 400;// 矩形尺寸的一半
private Paint mPaint;// 畫筆
private int left, top, right, bottom;// 矩形坐上右下坐標
private int screenX, screenY;
public MatrixView(Context context, AttributeSet attrs) {
super(context, attrs);
// 獲取屏幕尺寸數據
int[] screenSize = MeasureUtil.getScreenSize((Activity) context);
// 獲取屏幕中點坐標
screenX = screenSize[0] / 2;
screenY = screenSize[1] / 2;
// 計算矩形左上右下坐標值
left = screenX - RECT_SIZE;
top = screenY - RECT_SIZE;
right = screenX + RECT_SIZE;
bottom = screenY + RECT_SIZE;
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 獲取位圖
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a);
// 實例化一個Shader
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 實例一個矩陣對象
Matrix matrix = new Matrix();
// 設置矩陣變換
matrix.setTranslate(500, 500);
// 設置Shader的變換矩陣
bitmapShader.setLocalMatrix(matrix);
// 設置著色器
mPaint.setShader(bitmapShader);
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制矩形
// canvas.drawRect(left, top, right, bottom, mPaint);
canvas.drawRect(0, 0, screenX * 2, screenY * 2, mPaint);
}
}
~~~
這段代碼其實跟前面的有點類似,不糾結,我們只是簡單地在Matrix中做了個平移:

效果也很簡單,那我們再來個旋轉5度?
~~~
// 設置矩陣變換
matrix.setTranslate(500, 500);
matrix.setRotate(5);
~~~
完事后看看效果尼瑪怎么是介個樣子?說好的平移呢?被狗吃了?

Why?其實是這樣的,在我們new了一個Matrix對象后,這個Matrix對象中已經就為我們封裝了一組原始數據:
~~~
float[]{
1, 0, 0
0, 1, 0
0, 0, 1
}
~~~
而我們的setXXX方法執行的操作是把原本Matrix對象中的數據重置,重新設置新的數據,比如:
`matrix.setTranslate(500, 500); `
后數據即變為:
~~~
float[]{
1, 0, 500
0, 1, 500
0, 0, 1
}
~~~
而如果再旋轉了呢?比如我們上面的:
~~~
matrix.setTranslate(500, 500);
matrix.setRotate(5);
~~~
那旋轉的數據就會直接覆蓋掉我們平移的數據:
~~~
float[]{
cos, sin, 0
sin, cos, 0
0, 0, 1
}
~~~
具體參數值我也就不計算了,從這里大家也可以看出Android給我們封裝的方法是多么的體貼到位~~~你只需要setRotate個角度即可壓根就不需要你關心如何去算的對吧?我們來看另外的兩個方法preXXX和postXXX,這里我把setRotate換成preRotate:
~~~
matrix.setTranslate(500, 500);
matrix.preRotate(5);
~~~
和
~~~
matrix.setTranslate(500, 500);
matrix.postRotate(5);
~~~

好像沒啥區別啊?看不出有什么特別的對吧?其實呢這是一個誰先誰后的問題,preXXX和postXXX我們之前說過一個是前乘一個是后乘,那么具體表現是什么樣的呢?,非常簡單,比如我有如下代碼
~~~
matrix.preScale(0.5f, 1);
matrix.setScale(1, 0.6f);
matrix.postScale(0.7f, 1);
matrix.preTranslate(15, 0);
~~~
那么Matrix的計算過程即為:translate (15, 0) -> scale (1, 0.6f) -> scale (0.7f, 1),我們說過set會重置數據,所以最開始的
`matrix.preScale(0.5f, 1); `
也就GG了
同樣地,對于類似的變換:
~~~
matrix.preScale(0.5f, 1);
matrix.preTranslate(10, 0);
matrix.postScale(0.7f, 1);
matrix.postTranslate(15, 0);
~~~
其計算過程為:translate (10, 0) -> scale (0.5f, 1) -> scale (0.7f, 1) -> translate (15, 0),是不是很簡單呢?你一定不傻逼對吧!
那么對于上圖的結果真的是一樣的嗎?這里我教給大家一個方法自己去驗證,Matrix有一個getValues方法可以獲取當前Matrix的變換浮點數組,也就是我們之前說的矩陣:
~~~
/*
* 新建一個9個單位長度的浮點數組
* 因為我們的Matrix矩陣是9個單位長的對吧
*/
float[] fs = new float[9];
// 將從matrix中獲取到的浮點數組裝載進我們的fs里
matrix.getValues(fs);
Log.d("Aige", Arrays.toString(fs));// 輸出看看唄!
~~~
大家覺得好奇的都可以去驗證,這三類方法我就不多說了,Matrix中還有其他很多實用的方法,以后我們用到的時候在講,因為Matrix太常用了~~~~
現在,大家回過頭去再看看我給妹子圖加暗角的那段代碼,里面關于Matrix的操作能大致看懂了么:
~~~
// 計算徑向漸變半徑
float radiu = canvas.getHeight() * (2F / 3F);
// 實例化徑向漸變
RadialGradient radialGradient = new RadialGradient(canvas.getWidth() / 2F, canvas.getHeight() / 2F, radiu, new int[] { 0, 0, 0xAA000000 }, new float[] { 0F, 0.7F, 1.0F }, Shader.TileMode.CLAMP);
// 實例化一個矩陣
Matrix matrix = new Matrix();
// 設置矩陣的縮放
matrix.setScale(canvas.getWidth() / (radiu * 2F), 1.0F);
// 設置矩陣的預平移
matrix.preTranslate(((radiu * 2F) - canvas.getWidth()) / 2F, 0);
// 將該矩陣注入徑向漸變
radialGradient.setLocalMatrix(matrix);
// 設置畫筆Shader
mShaderPaint.setShader(radialGradient);
~~~
是不是灰常滴簡單呢?這里其實你只要注意我們除了平移所有變換操作都是基于一個原點的即可!找對原點你就成功一大半了!
好了,對Matrix的一個簡單介紹就到這里,正如我所說,Matrix的應用是相當廣泛的,不僅僅是在我們的Shader,我們的canvas也有setMatrix(matrix)方法來設置矩陣變換,更常見的是在ImageView中對ImageView進行變換,當我們手指在屏幕上劃過一定的距離后根據這段距離來平移我們的控件,根據兩根手指之間拉伸的距離和相對于上一次旋轉的角度來縮放旋轉我們的圖片:

~~~
public class MatrixImageView extends ImageView {
private static final int MODE_NONE = 0x00123;// 默認的觸摸模式
private static final int MODE_DRAG = 0x00321;// 拖拽模式
private static final int MODE_ZOOM = 0x00132;// 縮放or旋轉模式
private int mode;// 當前的觸摸模式
private float preMove = 1F;// 上一次手指移動的距離
private float saveRotate = 0F;// 保存了的角度值
private float rotate = 0F;// 旋轉的角度
private float[] preEventCoor;// 上一次各觸摸點的坐標集合
private PointF start, mid;// 起點、中點對象
private Matrix currentMatrix, savedMatrix;// 當前和保存了的Matrix對象
private Context mContext;// Fuck……
public MatrixImageView(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
// 初始化
init();
}
/**
* 初始化
*/
private void init() {
/*
* 實例化對象
*/
currentMatrix = new Matrix();
savedMatrix = new Matrix();
start = new PointF();
mid = new PointF();
// 模式初始化
mode = MODE_NONE;
/*
* 設置圖片資源
*/
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.mylove);
bitmap = Bitmap.createScaledBitmap(bitmap, MeasureUtil.getScreenSize((Activity) mContext)[0], MeasureUtil.getScreenSize((Activity) mContext)[1], true);
setImageBitmap(bitmap);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:// 單點接觸屏幕時
savedMatrix.set(currentMatrix);
start.set(event.getX(), event.getY());
mode = MODE_DRAG;
preEventCoor = null;
break;
case MotionEvent.ACTION_POINTER_DOWN:// 第二個點接觸屏幕時
preMove = calSpacing(event);
if (preMove > 10F) {
savedMatrix.set(currentMatrix);
calMidPoint(mid, event);
mode = MODE_ZOOM;
}
preEventCoor = new float[4];
preEventCoor[0] = event.getX(0);
preEventCoor[1] = event.getX(1);
preEventCoor[2] = event.getY(0);
preEventCoor[3] = event.getY(1);
saveRotate = calRotation(event);
break;
case MotionEvent.ACTION_UP:// 單點離開屏幕時
case MotionEvent.ACTION_POINTER_UP:// 第二個點離開屏幕時
mode = MODE_NONE;
preEventCoor = null;
break;
case MotionEvent.ACTION_MOVE:// 觸摸點移動時
/*
* 單點觸控拖拽平移
*/
if (mode == MODE_DRAG) {
currentMatrix.set(savedMatrix);
float dx = event.getX() - start.x;
float dy = event.getY() - start.y;
currentMatrix.postTranslate(dx, dy);
}
/*
* 兩點觸控拖放旋轉
*/
else if (mode == MODE_ZOOM && event.getPointerCount() == 2) {
float currentMove = calSpacing(event);
currentMatrix.set(savedMatrix);
/*
* 指尖移動距離大于10F縮放
*/
if (currentMove > 10F) {
float scale = currentMove / preMove;
currentMatrix.postScale(scale, scale, mid.x, mid.y);
}
/*
* 保持兩點時旋轉
*/
if (preEventCoor != null) {
rotate = calRotation(event);
float r = rotate - saveRotate;
currentMatrix.postRotate(r, getMeasuredWidth() / 2, getMeasuredHeight() / 2);
}
}
break;
}
setImageMatrix(currentMatrix);
return true;
}
/**
* 計算兩個觸摸點間的距離
*/
private float calSpacing(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);
}
/**
* 計算兩個觸摸點的中點坐標
*/
private void calMidPoint(PointF point, MotionEvent event) {
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
}
/**
* 計算旋轉角度
*
* @param 事件對象
* @return 角度值
*/
private float calRotation(MotionEvent event) {
double deltaX = (event.getX(0) - event.getX(1));
double deltaY = (event.getY(0) - event.getY(1));
double radius = Math.atan2(deltaY, deltaX);
return (float) Math.toDegrees(radius);
}
}
~~~
記得在xml中設置我們MatrixImageView的scaleType="matrix":
~~~
<com.aigestudio.customviewdemo.views.MatrixImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="matrix" />
~~~
雖然我們通過Matrix簡單地實現了對ImageView的變換操作,但是有一些小BUG,比如我們兩指縮放/旋轉圖片后抬起一只手指,此時應該立即切換回平移模式對吧?但是我們上述的代碼中卻是終止了各種操作,事件機制雖然我們還沒講,但是我們也在這幾節中用到了不少,這個簡單的問題你能解決么?
我在之前講到大家可以使用Matrix的getValues(float[])方法去驗證自己不確定的東西,同時呢,我們也可以使用Matrix的setValues(float[])方法來直接給Matrix設置一個矩陣數組,like:
~~~
setValues(new float[]{
1, 0, 57
0, 1, 78
0, 0, 1
});
~~~
效果跟
`matrix.setTranslate(57, 78); `
是一樣的。上面我們說到Matrix矩陣最后的3個數是用來設置透視變換的,為什么最后一個值恒為1?因為其表示的是在Z軸向的透視縮放,這三個值都可以被設置,前兩個值跟右手坐標系的XY軸有關,大家可以嘗試去發現它們之間的規律,我就不多說了。這里多扯一點,大家一定要學會如何透過現象看本質,即便看到的本質不一定就是實質,但是離實質已經不遠了,不要一來就去追求什么底層源碼啊、邏輯什么的,就像上面的矩陣變換一樣,矩陣的9個數作用其實很多人都說不清,與其聽別人胡扯還不如自己動手試試你說是吧,不然苦逼的只是你自己。
在實際應用中我們極少會使用到Matrix的尾三數做透視變換,更多的是使用Camare攝像機,比如我們使用Camare讓ListView看起來像倒下去一樣:(這里只做了解,已超出我們本系列的范疇)
~~~
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
android:gravity="center"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.AnimListView
android:id="@+id/main_alv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none" />
</LinearLayout>
~~~
自定義ListView重寫onDraw:
~~~
public class AnimListView extends ListView {
private Camera mCamera;
private Matrix mMatrix;
public AnimListView(Context context, AttributeSet attrs) {
super(context, attrs);
mCamera = new Camera();
mMatrix = new Matrix();
}
@Override
protected void onDraw(Canvas canvas) {
mCamera.save();
mCamera.rotate(30, 0, 0);
mCamera.getMatrix(mMatrix);
mMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2);
mMatrix.postTranslate(getWidth() / 2, getHeight() / 2);
canvas.concat(mMatrix);
super.onDraw(canvas);
mCamera.restore();
}
}
~~~
在MainActivity中設置數據:
~~~
public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AnimListView animListView = (AnimListView) findViewById(R.id.main_alv);
animListView.setAdapter(new BaseAdapter() {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
convertView = LayoutInflater.from(getApplicationContext()).inflate(R.layout.item, null);
return convertView;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public int getCount() {
return 100;
}
});
}
}
~~~
效果like below:

好了,Paint所有的方法已經Over了~~~大家是不是覺得終于解放的趕腳呢?別急……還有個Canvas……光學會了如何用筆而不知道畫什么有何用呢?不過別急,Canvas我們下一節再細講,這一節我們算是對Paint來個總結,注意不是了斷哦!了斷可不行,上一節末尾我留了幾張圖說要在這一節給大家說怎么在View中畫,這一節呢我們來實現它,注意哦,不要指望在這里你會得到一個完美的控件拿去用……我們還沒學怎么去測繪View也還沒有講到ViewGroup,So~~~~在這我們只是單純地先畫。
幾個圖中最難的大概就是那個各種圈圈的了:

其他的三個圖表千篇一律……會畫上面那玩意幾個圖標簡直就是小case,這個圈圈圖也是我在群里搜刮的,看不出來是吧,沒事,我臨摹了一個差不多的:

這樣看總清晰了吧……大家在自定義一個View的時候不要老想著自己是個Coder,你老是這樣想越做不出棒的View,你要把自己看成一個Designer,這個View將會是由你創造的!而不是你敲出了它……本來在這一系列之后我有單獨的番外篇叫如何去設計一個控件,今天在做這個圈圈圖的時候突然有這么一個想法還是干脆穿插進來說算了,直接粗暴!
既然我們是一個設計者,那么我們必然要有這么一張圖紙去概述我們的View,因為一般情況下大家都知道美工跟開發者之間是有代溝的,假如,我是說假如美工花了一個很屌的界面,但是你如果直接按著他給你的設計圖照著做你會發現做不出一模一樣的來……這時你就該調整自己多去與美工溝通在不大改設計圖的前提下把難度降到你能力范圍內。同樣的,我們這里也一樣。假如這個圈圈圖是JB美工給你的效果圖,叫你照著做,我們不可能真的這樣照著做,因為他給的東西對我們來說無規律可循,而對于開發者來說,有規律的東西往往是最簡單,這時我們就要“設計和代碼相結合”:

大家看到我在剛才那張臨摹圖上做了一些改動,從這張草圖上來看改動其實并不大,只是把一些位置啊、大小啊什么的做了一些調整,要記住一點,我們的控件要做到在任何屏幕設備上都能完美使用!而不是像布局、資源圖之類的還要做好幾套,那就是扯蛋!所以我們這的所有尺寸都是以控件的邊長S作為參考依據的:

注:因為我們還沒講如何測繪控件,所以我們在自定義View的時候強制控件的長寬一致以簡化不必要的口水
還是老套路,先分析一下:控件中心往下是最中心的圓,而其他的六個圓都直接或間接地與其相連,上面的三個是大圓而下面的三個是相對較小的圓,大圓之間的線段是緊挨著的而中心大圓和下面三個小圓之間的線段是有一定距離的,右上方的大圓上方還有一段實體描邊弧,弧上有文字,而每個圓內都可以設置文本,大概就是醬紫,那么我們從哪作為插入點呢?當然是中心的那個圓,但是我們知道它的圓心是要往控件中心向下偏移一個半徑的:

代碼如何實現呢?不用我說你也應該知道:
~~~
public class MultiCricleView extends View {
private static final float STROKE_WIDTH = 1F / 256F, // 描邊寬度占比
LINE_LENGTH = 3F / 32F, // 線段長度占比
CRICLE_LARGER_RADIU = 3F / 32F,// 大圓半徑
CRICLE_SMALL_RADIU = 5F / 64F,// 小圓半徑
ARC_RADIU = 1F / 8F,// 弧半徑
ARC_TEXT_RADIU = 5F / 32F;// 弧圍繞文字半徑
private Paint strokePaint;// 描邊畫筆
private int size;// 控件邊長
private float strokeWidth;// 描邊寬度
private float ccX, ccY;// 中心圓圓心坐標
private float largeCricleRadiu;// 大圓半徑
public MultiCricleView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化畫筆
initPaint(context);
}
/**
* 初始化畫筆
*
* @param context
* Fuck
*/
private void initPaint(Context context) {
/*
* 初始化描邊畫筆
*/
strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setColor(Color.WHITE);
strokePaint.setStrokeCap(Paint.Cap.ROUND);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 強制長寬一致
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// 獲取控件邊長
size = w;
// 參數計算
calculation();
}
/*
* 參數計算
*/
private void calculation() {
// 計算描邊寬度
strokeWidth = STROKE_WIDTH * size;
// 計算大圓半徑
largeCricleRadiu = size * CRICLE_LARGER_RADIU;
// 計算中心圓圓心坐標
ccX = size / 2;
ccY = size / 2 + size * CRICLE_LARGER_RADIU;
// 設置參數
setPara();
}
/**
* 設置參數
*/
private void setPara() {
// 設置描邊寬度
strokePaint.setStrokeWidth(strokeWidth);
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制背景
canvas.drawColor(0xFFF29B76);
// 繪制中心圓
canvas.drawCircle(ccX, ccY, largeCricleRadiu, strokePaint);
}
}
~~~
大家可以看到我在View重寫了這了一個方法:
~~~
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 強制長寬一致
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
~~~
這個方法就是用來測量控件寬高的,而其從爹那獲取的兩個參數widthMeasureSpec和heightMeasureSpec分別封裝了View的Size和Mode,我們會在1/2講View的繪制流程,這里只需跟著我的腳步在這光滑的地板上摩擦摩擦即可 = = !
onSizeChanged這個方法我們前面也提過就不多扯了,效果如下:

然后下一步該如何做呢?畫哪個呢?再從左上開始吧……,好我們計算坐標:

沒錯對吧,綠色的點就是我們要計算的坐標。但是這樣去算太TM復雜了!毫無違和感!我們細心觀察,左上邊那一節不就是等同于:

這樣的一個變換嗎?之前我們曾多次使用到畫布的圖層,如果我們能這樣畫圖形再以中心圓的圓心坐標為旋轉點向右旋轉畫布豈不是可以很簡單:
~~~
public class MultiCricleView extends View {
private static final float STROKE_WIDTH = 1F / 256F, // 描邊寬度占比
LINE_LENGTH = 3F / 32F, // 線段長度占比
CRICLE_LARGER_RADIU = 3F / 32F,// 大圓半徑
CRICLE_SMALL_RADIU = 5F / 64F,// 小圓半徑
ARC_RADIU = 1F / 8F,// 弧半徑
ARC_TEXT_RADIU = 5F / 32F;// 弧圍繞文字半徑
private Paint strokePaint;// 描邊畫筆
private int size;// 控件邊長
private float strokeWidth;// 描邊寬度
private float ccX, ccY;// 中心圓圓心坐標
private float largeCricleRadiu;// 大圓半徑
private float lineLength;// 線段長度
public MultiCricleView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化畫筆
initPaint(context);
}
/**
* 初始化畫筆
*
* @param context
* Fuck
*/
private void initPaint(Context context) {
/*
* 初始化描邊畫筆
*/
strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setColor(Color.WHITE);
strokePaint.setStrokeCap(Paint.Cap.ROUND);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 強制長寬一致
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// 獲取控件邊長
size = w;
// 參數計算
calculation();
}
/*
* 參數計算
*/
private void calculation() {
// 計算描邊寬度
strokeWidth = STROKE_WIDTH * size;
// 計算大圓半徑
largeCricleRadiu = size * CRICLE_LARGER_RADIU;
// 計算線段長度
lineLength = size * LINE_LENGTH;
// 計算中心圓圓心坐標
ccX = size / 2;
ccY = size / 2 + size * CRICLE_LARGER_RADIU;
// 設置參數
setPara();
}
/**
* 設置參數
*/
private void setPara() {
// 設置描邊寬度
strokePaint.setStrokeWidth(strokeWidth);
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制背景
canvas.drawColor(0xFFF29B76);
// 繪制中心圓
canvas.drawCircle(ccX, ccY, largeCricleRadiu, strokePaint);
// 繪制左上方圖形
drawTopLeft(canvas);
}
/**
* 繪制左上方圖形
*
* @param canvas
*/
private void drawTopLeft(Canvas canvas) {
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(-30);
// 依次畫:線-圈-線-圈
canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);
canvas.drawCircle(0, -lineLength * 3, largeCricleRadiu, strokePaint);
canvas.drawLine(0, -largeCricleRadiu * 4, 0, -lineLength * 5, strokePaint);
canvas.drawCircle(0, -lineLength * 6, largeCricleRadiu, strokePaint);
// 釋放畫布
canvas.restore();
}
}
~~~
like below:

保存和釋放畫布就不說了,用過N次了。
`canvas.translate(ccX, ccY); `
我們將畫布平移了ccx和xxy個單位其實就是讓畫布的左上端原點遠我們的中心圓圓心重合:

`canvas.rotate(-30); `
向左旋轉畫布30度:

這里有一點非常非常地重要!畫布的平移旋轉同樣也會影響畫布的自身坐標!如上圖,我們看到畫布的坐標也跟著旋轉了30度!我們正是利用了這一點巧妙地在畫圖而避免繁雜的坐標計算!!
同理我們可以繪制出其他三個小圓:
~~~
public class MultiCricleView extends View {
private static final float STROKE_WIDTH = 1F / 256F, // 描邊寬度占比
SPACE = 1F / 64F,// 大圓小圓線段兩端間隔占比
LINE_LENGTH = 3F / 32F, // 線段長度占比
CRICLE_LARGER_RADIU = 3F / 32F,// 大圓半徑
CRICLE_SMALL_RADIU = 5F / 64F,// 小圓半徑
ARC_RADIU = 1F / 8F,// 弧半徑
ARC_TEXT_RADIU = 5F / 32F;// 弧圍繞文字半徑
private Paint strokePaint;// 描邊畫筆
private int size;// 控件邊長
private float strokeWidth;// 描邊寬度
private float ccX, ccY;// 中心圓圓心坐標
private float largeCricleRadiu, smallCricleRadiu;// 大圓半徑和小圓半徑
private float lineLength;// 線段長度
private float space;// 大圓小圓線段兩端間隔
private enum Type {
LARGER, SMALL
}
public MultiCricleView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化畫筆
initPaint(context);
}
/**
* 初始化畫筆
*
* @param context
* Fuck
*/
private void initPaint(Context context) {
/*
* 初始化描邊畫筆
*/
strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setColor(Color.WHITE);
strokePaint.setStrokeCap(Paint.Cap.ROUND);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 強制長寬一致
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// 獲取控件邊長
size = w;
// 參數計算
calculation();
}
/*
* 參數計算
*/
private void calculation() {
// 計算描邊寬度
strokeWidth = STROKE_WIDTH * size;
// 計算大圓半徑
largeCricleRadiu = size * CRICLE_LARGER_RADIU;
// 計算小圓半徑
smallCricleRadiu = size * CRICLE_SMALL_RADIU;
// 計算線段長度
lineLength = size * LINE_LENGTH;
// 計算大圓小圓線段兩端間隔
space = size * SPACE;
// 計算中心圓圓心坐標
ccX = size / 2;
ccY = size / 2 + size * CRICLE_LARGER_RADIU;
// 設置參數
setPara();
}
/**
* 設置參數
*/
private void setPara() {
// 設置描邊寬度
strokePaint.setStrokeWidth(strokeWidth);
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制背景
canvas.drawColor(0xFFF29B76);
// 繪制中心圓
canvas.drawCircle(ccX, ccY, largeCricleRadiu, strokePaint);
// 繪制左上方圖形
drawTopLeft(canvas);
// 繪制右上方圖形
drawTopRight(canvas);
// 繪制左下方圖形
drawBottomLeft(canvas);
// 繪制下方圖形
drawBottom(canvas);
// 繪制右下方圖形
drawBottomRight(canvas);
}
/**
* 繪制左上方圖形
*
* @param canvas
*/
private void drawTopLeft(Canvas canvas) {
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(-30);
// 依次畫:線-圈-線-圈
canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);
canvas.drawCircle(0, -lineLength * 3, largeCricleRadiu, strokePaint);
canvas.drawLine(0, -largeCricleRadiu * 4, 0, -lineLength * 5, strokePaint);
canvas.drawCircle(0, -lineLength * 6, largeCricleRadiu, strokePaint);
// 釋放畫布
canvas.restore();
}
/**
* 繪制右上方圖形
*
* @param canvas
*/
private void drawTopRight(Canvas canvas) {
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(30);
// 依次畫:線-圈
canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);
canvas.drawCircle(0, -lineLength * 3, largeCricleRadiu, strokePaint);
// 釋放畫布
canvas.restore();
}
private void drawBottomLeft(Canvas canvas) {
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(-100);
// 依次畫:(間隔)線(間隔)-圈
canvas.drawLine(0, -largeCricleRadiu - space, 0, -lineLength * 2 - space, strokePaint);
canvas.drawCircle(0, -lineLength * 2 - smallCricleRadiu - space * 2, smallCricleRadiu, strokePaint);
// 釋放畫布
canvas.restore();
}
private void drawBottom(Canvas canvas) {
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(180);
// 依次畫:(間隔)線(間隔)-圈
canvas.drawLine(0, -largeCricleRadiu - space, 0, -lineLength * 2 - space, strokePaint);
canvas.drawCircle(0, -lineLength * 2 - smallCricleRadiu - space * 2, smallCricleRadiu, strokePaint);
// 釋放畫布
canvas.restore();
}
private void drawBottomRight(Canvas canvas) {
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(100);
// 依次畫:(間隔)線(間隔)-圈
canvas.drawLine(0, -largeCricleRadiu - space, 0, -lineLength * 2 - space, strokePaint);
canvas.drawCircle(0, -lineLength * 2 - smallCricleRadiu - space * 2, smallCricleRadiu, strokePaint);
// 釋放畫布
canvas.restore();
}
}
~~~
大家可以看到,每一次繪制我都鎖定了畫布并平移旋轉以調整畫布的原點坐標極大程度地方便我們計算。效果如下:

上面的代碼會有巨量的重復代碼,正如上面我說,這個圖形我們是畫死的,在沒講完View的測繪和ViewGroup之前我們不會做任何一個完整的控件,So~~~~同時也是為了方便大家容易理解這玩意是怎么畫的,我也就不對重復的方法做進一步封裝了,不過在做項目的時候切忌大量的重復代碼。
我們再給這些圈圈里加些文字,文字的中點很明顯就是這些圈圈的圓心對吧,1/4中我說過如何把文字畫到中心的?
~~~
public class MultiCricleView extends View {
private static final float STROKE_WIDTH = 1F / 256F, // 描邊寬度占比
SPACE = 1F / 64F,// 大圓小圓線段兩端間隔占比
LINE_LENGTH = 3F / 32F, // 線段長度占比
CRICLE_LARGER_RADIU = 3F / 32F,// 大圓半徑
CRICLE_SMALL_RADIU = 5F / 64F,// 小圓半徑
ARC_RADIU = 1F / 8F,// 弧半徑
ARC_TEXT_RADIU = 5F / 32F;// 弧圍繞文字半徑
private Paint strokePaint, textPaint;// 描邊畫筆和文字畫筆
private int size;// 控件邊長
private float strokeWidth;// 描邊寬度
private float ccX, ccY;// 中心圓圓心坐標
private float largeCricleRadiu, smallCricleRadiu;// 大圓半徑和小圓半徑
private float lineLength;// 線段長度
private float space;// 大圓小圓線段兩端間隔
private float textOffsetY;// 文本的Y軸偏移值
public MultiCricleView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化畫筆
initPaint(context);
}
/**
* 初始化畫筆
*
* @param context
* Fuck
*/
private void initPaint(Context context) {
/*
* 初始化描邊畫筆
*/
strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setColor(Color.WHITE);
strokePaint.setStrokeCap(Paint.Cap.ROUND);
/*
* 初始化文字畫筆
*/
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(30);
textPaint.setTextAlign(Paint.Align.CENTER);
textOffsetY = (textPaint.descent() + textPaint.ascent()) / 2;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 強制長寬一致
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// 獲取控件邊長
size = w;
// 參數計算
calculation();
}
/*
* 參數計算
*/
private void calculation() {
// 計算描邊寬度
strokeWidth = STROKE_WIDTH * size;
// 計算大圓半徑
largeCricleRadiu = size * CRICLE_LARGER_RADIU;
// 計算小圓半徑
smallCricleRadiu = size * CRICLE_SMALL_RADIU;
// 計算線段長度
lineLength = size * LINE_LENGTH;
// 計算大圓小圓線段兩端間隔
space = size * SPACE;
// 計算中心圓圓心坐標
ccX = size / 2;
ccY = size / 2 + size * CRICLE_LARGER_RADIU;
// 設置參數
setPara();
}
/**
* 設置參數
*/
private void setPara() {
// 設置描邊寬度
strokePaint.setStrokeWidth(strokeWidth);
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制背景
canvas.drawColor(0xFFF29B76);
// 繪制中心圓
canvas.drawCircle(ccX, ccY, largeCricleRadiu, strokePaint);
canvas.drawText("AigeStudio", ccX, ccY - textOffsetY, textPaint);
// 繪制左上方圖形
drawTopLeft(canvas);
// 繪制右上方圖形
drawTopRight(canvas);
// 繪制左下方圖形
drawBottomLeft(canvas);
// 繪制下方圖形
drawBottom(canvas);
// 繪制右下方圖形
drawBottomRight(canvas);
}
/**
* 繪制左上方圖形
*
* @param canvas
*/
private void drawTopLeft(Canvas canvas) {
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(-30);
// 依次畫:線-圈-線-圈
canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);
canvas.drawCircle(0, -lineLength * 3, largeCricleRadiu, strokePaint);
canvas.drawText("Apple", 0, -lineLength * 3 - textOffsetY, textPaint);
canvas.drawLine(0, -largeCricleRadiu * 4, 0, -lineLength * 5, strokePaint);
canvas.drawCircle(0, -lineLength * 6, largeCricleRadiu, strokePaint);
canvas.drawText("Orange", 0, -lineLength * 6 - textOffsetY, textPaint);
// 釋放畫布
canvas.restore();
}
/**
* 繪制右上方圖形
*
* @param canvas
*/
private void drawTopRight(Canvas canvas) {
float cricleY = -lineLength * 3;
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(30);
// 依次畫:線-圈
canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);
canvas.drawCircle(0, cricleY, largeCricleRadiu, strokePaint);
canvas.drawText("Tropical", 0, cricleY - textOffsetY, textPaint);
// 釋放畫布
canvas.restore();
}
private void drawBottomLeft(Canvas canvas) {
float lineYS = -largeCricleRadiu - space, lineYE = -lineLength * 2 - space, cricleY = -lineLength * 2 - smallCricleRadiu - space * 2;
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(-100);
// 依次畫:(間隔)線(間隔)-圈
canvas.drawLine(0, lineYS, 0, lineYE, strokePaint);
canvas.drawCircle(0, cricleY, smallCricleRadiu, strokePaint);
canvas.drawText("Banana", 0, cricleY - textOffsetY, textPaint);
// 釋放畫布
canvas.restore();
}
private void drawBottom(Canvas canvas) {
float lineYS = -largeCricleRadiu - space, lineYE = -lineLength * 2 - space, cricleY = -lineLength * 2 - smallCricleRadiu - space * 2;
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(180);
// 依次畫:(間隔)線(間隔)-圈
canvas.drawLine(0, lineYS, 0, lineYE, strokePaint);
canvas.drawCircle(0, cricleY, smallCricleRadiu, strokePaint);
canvas.drawText("Cucumber", 0, cricleY - textOffsetY, textPaint);
// 釋放畫布
canvas.restore();
}
private void drawBottomRight(Canvas canvas) {
float lineYS = -largeCricleRadiu - space, lineYE = -lineLength * 2 - space, cricleY = -lineLength * 2 - smallCricleRadiu - space * 2;
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(100);
// 依次畫:(間隔)線(間隔)-圈
canvas.drawLine(0, lineYS, 0, lineYE, strokePaint);
canvas.drawCircle(0, cricleY, smallCricleRadiu, strokePaint);
canvas.drawText("Vibrators", 0, cricleY - textOffsetY, textPaint);
// 釋放畫布
canvas.restore();
}
}
~~~
That's so easy right?

稍微有點難的是右上方的那個半回扇形,扇形的中心應該是與右上方的圓心重合的對吧,那我們在畫右上方圖形的時候一起畫不就是了?
~~~
/**
* 繪制右上方圖形
*
* @param canvas
*/
private void drawTopRight(Canvas canvas) {
float cricleY = -lineLength * 3;
// 鎖定畫布
canvas.save();
// 平移和旋轉畫布
canvas.translate(ccX, ccY);
canvas.rotate(30);
// 依次畫:線-圈
canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);
canvas.drawCircle(0, cricleY, largeCricleRadiu, strokePaint);
canvas.drawText("Tropical", 0, cricleY - textOffsetY, textPaint);
// 畫弧形
drawTopRightArc(canvas, cricleY);
// 釋放畫布
canvas.restore();
}
/**
* 繪制右上角畫弧形
*
* @param canvas
* @param cricleY
*/
private void drawTopRightArc(Canvas canvas, float cricleY) {
canvas.save();
canvas.translate(0, cricleY);
canvas.rotate(-30);
float arcRadiu = size * ARC_RADIU;
RectF oval = new RectF(-arcRadiu, -arcRadiu, arcRadiu, arcRadiu);
arcPaint.setStyle(Paint.Style.FILL);
arcPaint.setColor(0x55EC6941);
canvas.drawArc(oval, -22.5F, -135, true, arcPaint);
arcPaint.setStyle(Paint.Style.STROKE);
arcPaint.setColor(Color.WHITE);
canvas.drawArc(oval, -22.5F, -135, false, arcPaint);
canvas.restore();
}
~~~
代碼邏輯不復雜,大家很容易能看懂……注釋什么的我就不寫了……扇形上的文本如果大家理解了畫布的變換很容易實現,首先在上面我們繪制扇形的時候已經將畫布原點與右上圓心重合了對吧,這時我們再把畫布向左旋轉 扇形弧度/2 個度數是不是就可以讓畫布的坐標與扇形的右邊重合了呢?那第一個文字的坐標就是[0,負的文字弧形半徑],第二個文字坐標只需轉畫布過 扇形弧度/4 個弧度以此類推可以畫出五個文字:
~~~
/**
* 繪制右上角畫弧形
*
* @param canvas
* @param cricleY
*/
private void drawTopRightArc(Canvas canvas, float cricleY) {
canvas.save();
canvas.translate(0, cricleY);
canvas.rotate(-30);
float arcRadiu = size * ARC_RADIU;
RectF oval = new RectF(-arcRadiu, -arcRadiu, arcRadiu, arcRadiu);
arcPaint.setStyle(Paint.Style.FILL);
arcPaint.setColor(0x55EC6941);
canvas.drawArc(oval, -22.5F, -135, true, arcPaint);
arcPaint.setStyle(Paint.Style.STROKE);
arcPaint.setColor(Color.WHITE);
canvas.drawArc(oval, -22.5F, -135, false, arcPaint);
float arcTextRadiu = size * ARC_TEXT_RADIU;
canvas.save();
// 把畫布旋轉到扇形左端的方向
canvas.rotate(-135F / 2F);
/*
* 每隔33.75度角畫一次文本
*/
for (float i = 0; i < 5 * 33.75F; i += 33.75F) {
canvas.save();
canvas.rotate(i);
canvas.drawText("Aige", 0, -arcTextRadiu, textPaint);
canvas.restore();
}
canvas.restore();
canvas.restore();
}
~~~
對吧?看看效果:

好了,正如我所說,這只是一個單純地畫,而且此類奇葩的玩意難以真正做成一個控件去復用除非真的是公事公辦,但是,我們依然可以嘗試把它做成一個獨立的控件,這在我們學寫了如何測繪View和ViewGroup之后對你來說一定是小case。
上面我們的最終效果有一點是不對的,大家發現文字TMD居然都旋轉了 - - ,那有木有方法讓文字保持水平呢?其實答案我已經告訴你。自己去發掘吧!
源碼下載:[傳送門](http://download.csdn.net/detail/aigestudio/8234751)