自定義控件其實很簡單1/3

尊重原創轉載請註明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵權必究!

炮兵鎮樓

前幾天身子骨出了點小毛病不爽,再加上CSDN抽風也木有更新,現在補上之前漏掉的1/3

上一節結尾的時候我們說到,Paint類中我們還有一個方法沒講

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


Shader類呢也是個灰常灰常簡單的類,它有五個子類,像PathEffect一樣每個子類都實現了一種Shader,Shader在三維軟件中我們稱之爲着色器,其作用嘛就像它的名字一樣是來給圖像着色的或者更通俗的說法是上色!這麼說該懂了吧!再不懂去廁所哭去!這五個Shader裏最異類的是BitmapShader,因爲只有它是允許我們載入一張圖片來給圖像着色,那我們還是先來看看這個怪胎吧

BitmapShader

只有一個含參的構造方法BitmapShader (Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)而其他的四個兄弟姐妹呢都有兩個!它只有一個蛋,又一魂談!那好吧,我們來看看它是什麼個效果,順便呢也學習一下Shader的用法先,來看我們熟悉的代碼:

  1. public class ShaderView extends View {  
  2.     private static final int RECT_SIZE = 400;// 矩形尺寸的一半  
  3.   
  4.     private Paint mPaint;// 畫筆  
  5.   
  6.     private int left, top, right, bottom;// 矩形坐上右下座標  
  7.   
  8.     public ShaderView(Context context, AttributeSet attrs) {  
  9.         super(context, attrs);  
  10.         // 獲取屏幕尺寸數據  
  11.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  12.   
  13.         // 獲取屏幕中點座標  
  14.         int screenX = screenSize[0] / 2;  
  15.         int screenY = screenSize[1] / 2;  
  16.   
  17.         // 計算矩形左上右下座標值  
  18.         left = screenX - RECT_SIZE;  
  19.         top = screenY - RECT_SIZE;  
  20.         right = screenX + RECT_SIZE;  
  21.         bottom = screenY + RECT_SIZE;  
  22.   
  23.         // 實例化畫筆  
  24.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
  25.   
  26.         // 獲取位圖  
  27.         Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a);  
  28.   
  29.         // 設置着色器  
  30.         mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));  
  31.     }  
  32.   
  33.     @Override  
  34.     protected void onDraw(Canvas canvas) {  
  35.         // 繪製矩形  
  36.         canvas.drawRect(left, top, right, bottom, mPaint);  
  37.     }  
  38. }  
如果上面我們沒有設置Shader:
  1. mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));  
那麼我們Draw出來的圖像一定是一個位於屏幕正中黑色的正方形,但是我們設置了Shader後還是一樣的嗎?看看效果:

我靠!這什麼玩意!罪過罪過!真是看不懂!別急,Shader.TileMode裏有三種模式:CLAMP、MIRROR和REPETA,我們看看其他兩種模式是什麼效果呢:

  1. mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR));  

誒?這效果還能接受,我們還能看得出一點效果,說白了就是上下左右的鏡像而已,那再看看REPETA:

  1. 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呢?

  1. mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.MIRROR));  
也就是說我們在X軸方向上採取CLAMP模式而Y軸方向上採取MIRROR模式,那麼這樣肯定是可行的撒:

大家可以看到圖像分爲兩部分左邊呢Y軸鏡像了,而右邊像是被拉伸了一樣怪怪的!其實CLAMP的意思就是邊緣拉伸的意思,比如上圖中左邊Y軸鏡像了,而右邊會緊挨着左邊將圖像邊緣上的第一個像素沿X軸複製!產生一種被拉伸的效果!就像扯蛋,不過這裏扯的不是蛋而是圖像邊緣的第一個像素,就是這麼簡單。但是!作爲一個嚴謹的男人必須要又一個嚴謹的態度!這時我就會想,如果兩種模式互換會怎樣呢?比如:

  1. mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.MIRROR, Shader.TileMode.CLAMP));  
這樣來看,應該是X軸會鏡像而Y軸會拉伸對吧,看看效果:

這…………好像跟我們想象中的不大一樣唉……是我們做錯了嗎?不是的,結合上一個例子大家有沒有注意BitmapShader是先應用了Y軸的模式而X軸是後應用的!所以着色是先在Y軸拉伸瞭然後再沿着X軸重複對吧(陰笑ing……)?!

要善於發現生活規律!我曾經說過:存在必定合理,那麼這麼一個玩意有什麼用處呢?給大家看一個非常有趣的東西:


這玩意是不是感覺跟以前那種小霸王某個遊戲的開場動畫很類似?我們就是利用BitmapShader來實現的,而且實現方法也異常簡單:

  1. public class BrickView extends View {  
  2.     private Paint mFillPaint, mStrokePaint;// 填充和描邊的畫筆  
  3.     private BitmapShader mBitmapShader;// Bitmap着色器  
  4.   
  5.     private float posX, posY;// 觸摸點的XY座標  
  6.   
  7.     public BrickView(Context context, AttributeSet attrs) {  
  8.         super(context, attrs);  
  9.   
  10.         // 初始化畫筆  
  11.         initPaint();  
  12.     }  
  13.   
  14.     /** 
  15.      * 初始化畫筆 
  16.      */  
  17.     private void initPaint() {  
  18.         /* 
  19.          * 實例化描邊畫筆並設置參數 
  20.          */  
  21.         mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
  22.         mStrokePaint.setColor(0xFF000000);  
  23.         mStrokePaint.setStyle(Paint.Style.STROKE);  
  24.         mStrokePaint.setStrokeWidth(5);  
  25.   
  26.         // 實例化填充畫筆  
  27.         mFillPaint = new Paint();  
  28.   
  29.         /* 
  30.          * 生成BitmapShader 
  31.          */  
  32.         Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.brick);  
  33.         mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);  
  34.         mFillPaint.setShader(mBitmapShader);  
  35.     }  
  36.   
  37.     @Override  
  38.     public boolean onTouchEvent(MotionEvent event) {  
  39.         /* 
  40.          * 手指移動時獲取觸摸點座標並刷新視圖 
  41.          */  
  42.         if (event.getAction() == MotionEvent.ACTION_MOVE) {  
  43.             posX = event.getX();  
  44.             posY = event.getY();  
  45.   
  46.             invalidate();  
  47.         }  
  48.         return true;  
  49.     }  
  50.   
  51.     @Override  
  52.     protected void onDraw(Canvas canvas) {  
  53.         // 設置畫筆背景色  
  54.         canvas.drawColor(Color.DKGRAY);  
  55.   
  56.         /* 
  57.          * 繪製圓和描邊 
  58.          */  
  59.         canvas.drawCircle(posX, posY, 300, mFillPaint);  
  60.         canvas.drawCircle(posX, posY, 300, mStrokePaint);  
  61.     }  
  62. }  
我只是單純地加載了一張板磚的貼圖然後呢在BitmapShader中XY軸重複就這麼簡單 = = ,是不是感腳被耍了一樣好無聊~~~~是你不動腦而已……囧!來看看另一個Shader叫做
LinearGradient

線性漸變,顧名思義這錘子玩意就是來畫漸變的,實際上Shader的五個子類中除了上面我們說的那個怪胎,還有個變形金剛ComposeShader外其餘三個都是漸變只是效果不同而已,而這個LinearGradient線性漸變一說大家估計都懂,先來看張效果圖:


是不是秒懂了!恩,說明你頭腦簡單,這個實現也很簡單,具體代碼跟上面的BitmapShader一樣只是把BitmapShader換成了LinearGradient而已:

  1. mPaint.setShader(new LinearGradient(left, top, right, bottom, Color.RED, Color.YELLOW, Shader.TileMode.REPEAT));  
上面我們提到過除了BitmapShader外其他子類都有兩個構造方法,上面我們用到了
  1. 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已經填充了圖形所以不起作用,如果我們改改,把終點座標變一下:
  1. mPaint.setShader(new LinearGradient(left, top, right - RECT_SIZE, bottom - RECT_SIZE, Color.RED, Color.YELLOW, Shader.TileMode.REPEAT));  
此時我們漸變終點座標落在了圖形的終點上,根據我們的REPEAT模式,會呈現一個漸變重複的效果:

僅僅兩種顏色的漸變根本無法滿足我們身體的慾望,太單調乏味!我們是不是可以定義多種顏色漸變呢?答案是必須的,LinearGradient的另一個構造方法

  1. LinearGradient(float x0, float y0, float x1, float y1, int[] colors, float[] positions, Shader.TileMode tile)  
就爲我們實現了這麼一個功能:
  1. mPaint.setShader(new LinearGradient(left, top, right, bottom, new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, new float[] { 00.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可以爲空:

  1. mPaint.setShader(new LinearGradient(left, top, right, bottom, new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, null, Shader.TileMode.MIRROR));  
爲空時各種顏色的漸變將會均分整個漸變區域:

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

效果並不複雜實現也非常簡單:

  1. public class ReflectView extends View {  
  2.     private Bitmap mSrcBitmap, mRefBitmap;// 位圖  
  3.     private Paint mPaint;// 畫筆  
  4.     private PorterDuffXfermode mXfermode;// 混合模式  
  5.   
  6.     private int x, y;// 位圖起點座標  
  7.   
  8.     public ReflectView(Context context, AttributeSet attrs) {  
  9.         super(context, attrs);  
  10.   
  11.         // 初始化資源  
  12.         initRes(context);  
  13.     }  
  14.   
  15.     /* 
  16.      * 初始化資源 
  17.      */  
  18.     private void initRes(Context context) {  
  19.         // 獲取源圖  
  20.         mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.gril);  
  21.   
  22.         // 實例化一個矩陣對象  
  23.         Matrix matrix = new Matrix();  
  24.         matrix.setScale(1F, -1F);  
  25.   
  26.         // 生成倒影圖  
  27.         mRefBitmap = Bitmap.createBitmap(mSrcBitmap, 00, mSrcBitmap.getWidth(), mSrcBitmap.getHeight(), matrix, true);  
  28.   
  29.         int screenW = MeasureUtil.getScreenSize((Activity) context)[0];  
  30.         int screenH = MeasureUtil.getScreenSize((Activity) context)[1];  
  31.   
  32.         x = screenW / 2 - mSrcBitmap.getWidth() / 2;  
  33.         y = screenH / 2 - mSrcBitmap.getHeight() / 2;  
  34.   
  35.         // ………………………………  
  36.         mPaint = new Paint();  
  37.         mPaint.setShader(new LinearGradient(x, y + mSrcBitmap.getHeight(), x, y + mSrcBitmap.getHeight() + mSrcBitmap.getHeight() / 40xAA000000, Color.TRANSPARENT, Shader.TileMode.CLAMP));  
  38.   
  39.         // ………………………………  
  40.         mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);  
  41.     }  
  42.   
  43.     @Override  
  44.     protected void onDraw(Canvas canvas) {  
  45.         canvas.drawColor(Color.BLACK);  
  46.         canvas.drawBitmap(mSrcBitmap, x, y, null);  
  47.   
  48.         int sc = canvas.saveLayer(x, y + mSrcBitmap.getHeight(), x + mRefBitmap.getWidth(), y + mSrcBitmap.getHeight() * 2null, Canvas.ALL_SAVE_FLAG);  
  49.   
  50.         canvas.drawBitmap(mRefBitmap, x, y + mSrcBitmap.getHeight(), null);  
  51.   
  52.         mPaint.setXfermode(mXfermode);  
  53.   
  54.         canvas.drawRect(x, y + mSrcBitmap.getHeight(), x + mRefBitmap.getWidth(), y + mSrcBitmap.getHeight() * 2, mPaint);  
  55.   
  56.         mPaint.setXfermode(null);  
  57.   
  58.         canvas.restoreToCount(sc);  
  59.     }  
  60. }  
SweepGradient
的意思是梯度漸變,也稱之爲掃描式漸變,因爲其效果有點類似雷達的掃描效果,他也有兩個構造方法:

  1. SweepGradient(float cx, float cy, int color0, int color1)  
其實都跟LinearGradient差不多的,簡直沒什麼可說的,直接上效果跳過無聊的講解:

  1. mPaint.setShader(new SweepGradient(screenX, screenY, Color.RED, Color.YELLOW));  

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

RadialGradient

徑向漸變,徑向漸變說的簡單點就是個圓形中心向四周漸變的效果,他也一樣有兩個構造方法……艾瑪!我都要吐了……

  1. RadialGradient (float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)  
簡單,一看就懂,例子勞資都懶得上了,屮!
  1. RadialGradient (float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)  
同上!還是說點有意思的,來個妹子養養眼:

是不是很漂亮?不過哥壓根不放在眼裏,哥女朋友比她更漂亮~~好,我們應用1/6裏學到的知識給她校校色,因爲這張圖片的顏色實在太過鮮豔了不符合小清新的LOMO風格~~~~

  1. public class DreamEffectView extends View {  
  2.     private Paint mBitmapPaint;// 位圖畫筆  
  3.     private Bitmap mBitmap;// 位圖  
  4.     private PorterDuffXfermode mXfermode;// 圖形混合模式  
  5.     private int x, y;// 位圖起點座標  
  6.     private int screenW, screenH;// 屏幕寬高  
  7.   
  8.     public DreamEffectView(Context context, AttributeSet attrs) {  
  9.         super(context, attrs);  
  10.   
  11.         // 初始化資源  
  12.         initRes(context);  
  13.   
  14.         // 初始化畫筆  
  15.         initPaint();  
  16.     }  
  17.   
  18.     /** 
  19.      * 初始化資源 
  20.      *  
  21.      * @param context 
  22.      *            丟你螺母 
  23.      */  
  24.     private void initRes(Context context) {  
  25.         // 獲取位圖  
  26.         mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.gril);  
  27.   
  28.         // 實例化混合模式  
  29.         mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);  
  30.   
  31.         screenW = MeasureUtil.getScreenSize((Activity) context)[0];  
  32.         screenH = MeasureUtil.getScreenSize((Activity) context)[1];  
  33.   
  34.         x = screenW / 2 - mBitmap.getWidth() / 2;  
  35.         y = screenH / 2 - mBitmap.getHeight() / 2;  
  36.     }  
  37.   
  38.     /** 
  39.      * 初始化畫筆 
  40.      */  
  41.     private void initPaint() {  
  42.         // 實例化畫筆  
  43.         mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  44.   
  45.         // 去飽和、提亮、色相矯正  
  46.         mBitmapPaint.setColorFilter(new ColorMatrixColorFilter(new float[] { 0.8587F, 0.2940F, -0.0927F, 06.79F, 0.0821F, 0.9145F, 0.0634F, 06.79F, 0.2019F, 0.1097F, 0.7483F, 06.79F, 00010 }));  
  47.     }  
  48.   
  49.     @Override  
  50.     protected void onDraw(Canvas canvas) {  
  51.         canvas.drawColor(Color.BLACK);  
  52.   
  53.         // 新建圖層  
  54.         int sc = canvas.saveLayer(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), null, Canvas.ALL_SAVE_FLAG);  
  55.   
  56.         // 繪製混合顏色  
  57.         canvas.drawColor(0xcc1c093e);  
  58.   
  59.         // 設置混合模式  
  60.         mBitmapPaint.setXfermode(mXfermode);  
  61.   
  62.         // 繪製位圖  
  63.         canvas.drawBitmap(mBitmap, x, y, mBitmapPaint);  
  64.   
  65.         // 還原混合模式  
  66.         mBitmapPaint.setXfermode(null);  
  67.   
  68.         // 還原畫布  
  69.         canvas.restoreToCount(sc);  
  70.     }  
  71. }  
這樣感覺是不是好很多了呢?


類似的效果在一些相機特效中稱之爲夢幻,我最近在做的一個開源相機項目也有類似的效果。但是這樣的效果好像還湊合,但是整張圖片沒重點,我們可以模擬單反相機的暗角效果,壓暗圖片周圍的顏色亮度提亮中心,讓整張圖片的中心突出來!實現方式有很多種,比如1/4我們提到過的BlurMsakFilter向內模糊就可以得到一個類似的效果,但是BlurMsakFilter計算出來的像素太生硬毫無生氣,這裏我們就使用RadialGradient來模擬一下下贔屓:

  1. public class DreamEffectView extends View {  
  2.     private Paint mBitmapPaint, mShaderPaint;// 位圖畫筆和Shader圖形的畫筆  
  3.     private Bitmap mBitmap;// 位圖  
  4.     private PorterDuffXfermode mXfermode;// 圖形混合模式  
  5.     private int x, y;// 位圖起點座標  
  6.     private int screenW, screenH;// 屏幕寬高  
  7.   
  8.     public DreamEffectView(Context context, AttributeSet attrs) {  
  9.         super(context, attrs);  
  10.   
  11.         // 初始化資源  
  12.         initRes(context);  
  13.   
  14.         // 初始化畫筆  
  15.         initPaint();  
  16.     }  
  17.   
  18.     /** 
  19.      * 初始化資源 
  20.      *  
  21.      * @param context 
  22.      *            丟你螺母 
  23.      */  
  24.     private void initRes(Context context) {  
  25.         // 獲取位圖  
  26.         mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.gril);  
  27.   
  28.         // 實例化混合模式  
  29.         mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);  
  30.   
  31.         screenW = MeasureUtil.getScreenSize((Activity) context)[0];  
  32.         screenH = MeasureUtil.getScreenSize((Activity) context)[1];  
  33.   
  34.         x = screenW / 2 - mBitmap.getWidth() / 2;  
  35.         y = screenH / 2 - mBitmap.getHeight() / 2;  
  36.     }  
  37.   
  38.     /** 
  39.      * 初始化畫筆 
  40.      */  
  41.     private void initPaint() {  
  42.         // 實例化畫筆  
  43.         mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  44.   
  45.         // 去飽和、提亮、色相矯正  
  46.         mBitmapPaint.setColorFilter(new ColorMatrixColorFilter(new float[] { 0.8587F, 0.2940F, -0.0927F, 06.79F, 0.0821F, 0.9145F, 0.0634F, 06.79F, 0.2019F, 0.1097F, 0.7483F, 06.79F, 00010 }));  
  47.   
  48.         // 實例化Shader圖形的畫筆  
  49.         mShaderPaint = new Paint();  
  50.   
  51.         // 設置徑向漸變,漸變中心當然是圖片的中心也是屏幕中心,漸變半徑我們直接拿圖片的高度但是要稍微小一點  
  52.         // 中心顏色爲透明而邊緣顏色爲黑色  
  53.         mShaderPaint.setShader(new RadialGradient(screenW / 2, screenH / 2, mBitmap.getHeight() * 7 / 8, Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP));  
  54.     }  
  55.   
  56.     @Override  
  57.     protected void onDraw(Canvas canvas) {  
  58.         canvas.drawColor(Color.BLACK);  
  59.   
  60.         // 新建圖層  
  61.         int sc = canvas.saveLayer(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), null, Canvas.ALL_SAVE_FLAG);  
  62.   
  63.         // 繪製混合顏色  
  64.         canvas.drawColor(0xcc1c093e);  
  65.   
  66.         // 設置混合模式  
  67.         mBitmapPaint.setXfermode(mXfermode);  
  68.   
  69.         // 繪製位圖  
  70.         canvas.drawBitmap(mBitmap, x, y, mBitmapPaint);  
  71.   
  72.         // 還原混合模式  
  73.         mBitmapPaint.setXfermode(null);  
  74.   
  75.         // 還原畫布  
  76.         canvas.restoreToCount(sc);  
  77.   
  78.         // 繪製一個跟圖片大小一樣的矩形  
  79.         canvas.drawRect(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), mShaderPaint);  
  80.     }  
  81. }  
Look Look~~是不是效果更好了呢?四周的亮度被無情地壓了下去,重點直接呈現在中心

所以……大家一定要活用工具~~~~善於組合思考!說起組合,接下來Shader的最後一個子類簡直就是組合他媽生的

ComposeShader

就是組合Shader的意思,顧名思義就是兩個Shader組合在一起作爲一個新Shader……老掉牙的劇情是吧!同樣,這錘子玩意也有兩個構造方法

  1. ComposeShader (Shader shaderA, Shader shaderB, Xfermode mode)  
  2. ComposeShader (Shader shaderA, Shader shaderB, PorterDuff.Mode mode)  
兩個都差不多的,只不過一個指定了只能用PorterDuff的混合模式而另一個只要是Xfermode下的混合模式都沒問題!上面我們爲米女圖片加暗角的時候只是單純地使用了一下徑向漸變,但是其實實際獲得的效果並不好,因爲我們應用的徑向漸變是個圓形的,但是我們的圖片實際上是個豎向矩形的,直接往上面“蓋”一個徑向漸變實際效果簡而言之應該是這樣的:

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


我們來試試看:

  1. public class DreamEffectView extends View {  
  2.     private Paint mBitmapPaint, mShaderPaint;// 位圖畫筆和Shader圖形的畫筆  
  3.     private Bitmap mBitmap, darkCornerBitmap;// 源圖的Bitmap和我們自己畫的暗角Bitmap  
  4.     private PorterDuffXfermode mXfermode;// 圖形混合模式  
  5.     private int x, y;// 位圖起點座標  
  6.     private int screenW, screenH;// 屏幕寬高  
  7.   
  8.     public DreamEffectView(Context context, AttributeSet attrs) {  
  9.         super(context, attrs);  
  10.   
  11.         // 初始化資源  
  12.         initRes(context);  
  13.   
  14.         // 初始化畫筆  
  15.         initPaint();  
  16.     }  
  17.   
  18.     /** 
  19.      * 初始化資源 
  20.      *  
  21.      * @param context 
  22.      *            丟你螺母 
  23.      */  
  24.     private void initRes(Context context) {  
  25.         // 獲取位圖  
  26.         mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.gril);  
  27.   
  28.         // 實例化混合模式  
  29.         mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);  
  30.   
  31.         screenW = MeasureUtil.getScreenSize((Activity) context)[0];  
  32.         screenH = MeasureUtil.getScreenSize((Activity) context)[1];  
  33.   
  34.         x = screenW / 2 - mBitmap.getWidth() / 2;  
  35.         y = screenH / 2 - mBitmap.getHeight() / 2;  
  36.     }  
  37.   
  38.     /** 
  39.      * 初始化畫筆 
  40.      */  
  41.     private void initPaint() {  
  42.         // 實例化畫筆  
  43.         mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  44.   
  45.         // 去飽和、提亮、色相矯正  
  46.         mBitmapPaint.setColorFilter(new ColorMatrixColorFilter(new float[] { 0.8587F, 0.2940F, -0.0927F, 06.79F, 0.0821F, 0.9145F, 0.0634F, 06.79F, 0.2019F, 0.1097F, 0.7483F, 06.79F, 00010 }));  
  47.   
  48.         // 實例化Shader圖形的畫筆  
  49.         mShaderPaint = new Paint();  
  50.   
  51.         // 根據我們源圖的大小生成暗角Bitmap  
  52.         darkCornerBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);  
  53.   
  54.         // 將該暗角Bitmap注入Canvas  
  55.         Canvas canvas = new Canvas(darkCornerBitmap);  
  56.   
  57.         // 計算徑向漸變半徑  
  58.         float radiu = canvas.getHeight() * (2F / 3F);  
  59.   
  60.         // 實例化徑向漸變  
  61.         RadialGradient radialGradient = new RadialGradient(canvas.getWidth() / 2F, canvas.getHeight() / 2F, radiu, new int[] { 000xAA000000 }, new float[] { 0F, 0.7F, 1.0F }, Shader.TileMode.CLAMP);  
  62.   
  63.         // 實例化一個矩陣  
  64.         Matrix matrix = new Matrix();  
  65.   
  66.         // 設置矩陣的縮放  
  67.         matrix.setScale(canvas.getWidth() / (radiu * 2F), 1.0F);  
  68.   
  69.         // 設置矩陣的預平移  
  70.         matrix.preTranslate(((radiu * 2F) - canvas.getWidth()) / 2F, 0);  
  71.   
  72.         // 將該矩陣注入徑向漸變  
  73.         radialGradient.setLocalMatrix(matrix);  
  74.   
  75.         // 設置畫筆Shader  
  76.         mShaderPaint.setShader(radialGradient);  
  77.   
  78.         // 繪製矩形  
  79.         canvas.drawRect(00, canvas.getWidth(), canvas.getHeight(), mShaderPaint);  
  80.     }  
  81.   
  82.     @Override  
  83.     protected void onDraw(Canvas canvas) {  
  84.         canvas.drawColor(Color.BLACK);  
  85.   
  86.         // 新建圖層  
  87.         int sc = canvas.saveLayer(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), null, Canvas.ALL_SAVE_FLAG);  
  88.   
  89.         // 繪製混合顏色  
  90.         canvas.drawColor(0xcc1c093e);  
  91.   
  92.         // 設置混合模式  
  93.         mBitmapPaint.setXfermode(mXfermode);  
  94.   
  95.         // 繪製位圖  
  96.         canvas.drawBitmap(mBitmap, x, y, mBitmapPaint);  
  97.   
  98.         // 還原混合模式  
  99.         mBitmapPaint.setXfermode(null);  
  100.   
  101.         // 還原畫布  
  102.         canvas.restoreToCount(sc);  
  103.   
  104.         // 繪製我們畫好的徑向漸變圖  
  105.         canvas.drawBitmap(darkCornerBitmap, x, y, null);  
  106.     }  
  107. }  
運行一下可以看到如下結果:

是不是感腳暗角比上面我們做的那個更Perfect?圖片中心大部分區域不受任何干擾只把四角邊緣處的亮度壓了下去,當然這個效果對哥來說也不是很滿意,不過先將就湊合着 = = 。上例中我們使用到了兩個東西,一個是在獨立的Canvas中繪製自己的Bitmap,在哥下一節我們就會詳細講到這裏就先不扯了,而另一個則是我們說的重點:Matrix,其實在上面我們做倒影的時候已經簡單地使用過了Matrix,那麼Matrix究竟是什麼呢?其中文直譯爲矩陣,而我更願意稱之爲矩陣變換或者圖形變換,它和我們1/6中學到的圖形的混合可謂是雙簧,同樣地重要!

在本文的開頭哥給大家挖了一個坑,在講BitmapShader的時候我們在

  1. mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));  
的模式下得到了如下這樣狗吃屎的效果:

不知道大家有沒有質疑過爲什麼?如果沒有,情有可原!但是在我們使用另外兩種模式REPEAT和MIRROR的時候難道大家就沒想過爲什麼我們的位圖會像那樣重複或者鏡像嗎?是必須那樣嗎?那個例子中我們使用的是一個邊長爲800置於屏幕中心的矩形,我們何不嘗試將其鋪滿屏幕看看?

  1. public class ShaderView extends View {  
  2.     private static final int RECT_SIZE = 400;// 矩形尺寸的一半  
  3.   
  4.     private Paint mPaint;// 畫筆  
  5.   
  6.     private int left, top, right, bottom;// 矩形坐上右下座標  
  7.     private int screenX, screenY;  
  8.   
  9.     public ShaderView(Context context, AttributeSet attrs) {  
  10.         super(context, attrs);  
  11.   
  12.         // 獲取屏幕尺寸數據  
  13.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  14.   
  15.         // 獲取屏幕中點座標  
  16.         screenX = screenSize[0] / 2;  
  17.         screenY = screenSize[1] / 2;  
  18.   
  19.         // 計算矩形左上右下座標值  
  20.         left = screenX - RECT_SIZE;  
  21.         top = screenY - RECT_SIZE;  
  22.         right = screenX + RECT_SIZE;  
  23.         bottom = screenY + RECT_SIZE;  
  24.   
  25.         // 實例化畫筆  
  26.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  27.   
  28.         // 獲取位圖  
  29.         Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a);  
  30.   
  31.         // 設置着色器  
  32.         mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));  
  33.         // mPaint.setShader(new LinearGradient(left, top, right - RECT_SIZE, bottom - RECT_SIZE, Color.RED, Color.YELLOW, Shader.TileMode.MIRROR));  
  34.         // mPaint.setShader(new LinearGradient(left, top, right, bottom, new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, null, Shader.TileMode.MIRROR));  
  35.         // mPaint.setShader(new SweepGradient(screenX, screenY, Color.RED, Color.YELLOW));  
  36.         // mPaint.setShader(new SweepGradient(screenX, screenY, new int[] { Color.GREEN, Color.WHITE, Color.GREEN }, null));  
  37.     }  
  38.   
  39.     @Override  
  40.     protected void onDraw(Canvas canvas) {  
  41.         // 繪製矩形  
  42.         // canvas.drawRect(left, top, right, bottom, mPaint);  
  43.         canvas.drawRect(00, screenX * 2, screenY * 2, mPaint);  
  44.     }  
  45. }  
看看效果:

是不是忽然明白了什麼?好了,你可以從坑裏爬出來了…………這樣看是不是懂了?BitmapShader是從畫布的左上方開始着色,回到我們剛纔的問題,這個着色方式必須是這樣的麼?顯然不是!在Shader類中有一對setter和getter方法:setLocalMatrix(Matrix localM)和getLocalMatrix(Matrix localM)我們可以利用它們來設置或獲取Shader的變換矩陣,比如上面的例子我還是繪製成一個邊長爲800的矩形:

  1. public class ShaderView extends View {  
  2.     private static final int RECT_SIZE = 400;// 矩形尺寸的一半  
  3.   
  4.     private Paint mPaint;// 畫筆  
  5.   
  6.     private int left, top, right, bottom;// 矩形坐上右下座標  
  7.     private int screenX, screenY;  
  8.   
  9.     public ShaderView(Context context, AttributeSet attrs) {  
  10.         super(context, attrs);  
  11.   
  12.         // 獲取屏幕尺寸數據  
  13.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  14.   
  15.         // 獲取屏幕中點座標  
  16.         screenX = screenSize[0] / 2;  
  17.         screenY = screenSize[1] / 2;  
  18.   
  19.         // 計算矩形左上右下座標值  
  20.         left = screenX - RECT_SIZE;  
  21.         top = screenY - RECT_SIZE;  
  22.         right = screenX + RECT_SIZE;  
  23.         bottom = screenY + RECT_SIZE;  
  24.   
  25.         // 實例化畫筆  
  26.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  27.   
  28.         // 獲取位圖  
  29.         Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a);  
  30.   
  31.         // 實例化一個Shader  
  32.         BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);  
  33.   
  34.         // 實例一個矩陣對象  
  35.         Matrix matrix = new Matrix();  
  36.   
  37.         // 設置矩陣變換  
  38.         matrix.setTranslate(left, top);  
  39.   
  40.         // 設置Shader的變換矩陣  
  41.         bitmapShader.setLocalMatrix(matrix);  
  42.   
  43.         // 設置着色器  
  44.         mPaint.setShader(bitmapShader);  
  45.         // mPaint.setShader(new LinearGradient(left, top, right - RECT_SIZE, bottom - RECT_SIZE, Color.RED, Color.YELLOW, Shader.TileMode.MIRROR));  
  46.         // mPaint.setShader(new LinearGradient(left, top, right, bottom, new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, null, Shader.TileMode.MIRROR));  
  47.         // mPaint.setShader(new SweepGradient(screenX, screenY, Color.RED, Color.YELLOW));  
  48.         // mPaint.setShader(new SweepGradient(screenX, screenY, new int[] { Color.GREEN, Color.WHITE, Color.GREEN }, null));  
  49.     }  
  50.   
  51.     @Override  
  52.     protected void onDraw(Canvas canvas) {  
  53.         // 繪製矩形  
  54.         canvas.drawRect(left, top, right, bottom, mPaint);  
  55.         // canvas.drawRect(0, 0, screenX * 2, screenY * 2, mPaint);  
  56.     }  
  57. }  
不一樣的是我在給畫筆設置着色器前爲我們的着色器設置了一個變換矩陣,讓我們的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的位置:

  1. public class MatrixView extends View {  
  2.     private static final int RECT_SIZE = 400;// 矩形尺寸的一半  
  3.   
  4.     private Paint mPaint;// 畫筆  
  5.   
  6.     private int left, top, right, bottom;// 矩形坐上右下座標  
  7.     private int screenX, screenY;  
  8.   
  9.     public MatrixView(Context context, AttributeSet attrs) {  
  10.         super(context, attrs);  
  11.   
  12.         // 獲取屏幕尺寸數據  
  13.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  14.   
  15.         // 獲取屏幕中點座標  
  16.         screenX = screenSize[0] / 2;  
  17.         screenY = screenSize[1] / 2;  
  18.   
  19.         // 計算矩形左上右下座標值  
  20.         left = screenX - RECT_SIZE;  
  21.         top = screenY - RECT_SIZE;  
  22.         right = screenX + RECT_SIZE;  
  23.         bottom = screenY + RECT_SIZE;  
  24.   
  25.         // 實例化畫筆  
  26.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  27.   
  28.         // 獲取位圖  
  29.         Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a);  
  30.   
  31.         // 實例化一個Shader  
  32.         BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);  
  33.   
  34.         // 實例一個矩陣對象  
  35.         Matrix matrix = new Matrix();  
  36.   
  37.         // 設置矩陣變換  
  38.         matrix.setTranslate(500500);  
  39.   
  40.         // 設置Shader的變換矩陣  
  41.         bitmapShader.setLocalMatrix(matrix);  
  42.   
  43.         // 設置着色器  
  44.         mPaint.setShader(bitmapShader);  
  45.     }  
  46.   
  47.     @Override  
  48.     protected void onDraw(Canvas canvas) {  
  49.         // 繪製矩形  
  50.         // canvas.drawRect(left, top, right, bottom, mPaint);  
  51.         canvas.drawRect(00, screenX * 2, screenY * 2, mPaint);  
  52.     }  
  53. }  
這段代碼其實跟前面的有點類似,不糾結,我們只是簡單地在Matrix中做了個平移:

效果也很簡單,那我們再來個旋轉5度?

  1. // 設置矩陣變換  
  2. matrix.setTranslate(500500);  
  3. matrix.setRotate(5);  
完事後看看效果尼瑪怎麼是介個樣子?說好的平移呢?被狗吃了?

Why?其實是這樣的,在我們new了一個Matrix對象後,這個Matrix對象中已經就爲我們封裝了一組原始數據:

  1. float[]{  
  2.         100  
  3.         010  
  4.         001  
  5. }  
而我們的setXXX方法執行的操作是把原本Matrix對象中的數據重置,重新設置新的數據,比如:
  1. matrix.setTranslate(500500);  
後數據即變爲:
  1. float[]{  
  2.         10500  
  3.         01500  
  4.         001  
  5. }  
而如果再旋轉了呢?比如我們上面的:
  1. matrix.setTranslate(500500);  
  2. matrix.setRotate(5);  
那旋轉的數據就會直接覆蓋掉我們平移的數據:
  1. float[]{  
  2.         cos, sin, 0  
  3.         sin, cos, 0  
  4.         001  
  5. }  
具體參數值我也就不計算了,從這裏大家也可以看出Android給我們封裝的方法是多麼的體貼到位~~~你只需要setRotate個角度即可壓根就不需要你關心如何去算的對吧?我們來看另外的兩個方法preXXX和postXXX,這裏我把setRotate換成preRotate:
  1. matrix.setTranslate(500500);  
  2. matrix.preRotate(5);  

  1. matrix.setTranslate(500500);  
  2. matrix.postRotate(5);  

好像沒啥區別啊?看不出有什麼特別的對吧?其實呢這是一個誰先誰後的問題,preXXX和postXXX我們之前說過一個是前乘一個是後乘,那麼具體表現是什麼樣的呢?,非常簡單,比如我有如下代碼

  1. matrix.preScale(0.5f, 1);   
  2. matrix.setScale(10.6f);   
  3. matrix.postScale(0.7f, 1);   
  4. matrix.preTranslate(150);  
那麼Matrix的計算過程即爲:translate (15, 0) -> scale (1, 0.6f) -> scale (0.7f, 1),我們說過set會重置數據,所以最開始的
  1. matrix.preScale(0.5f, 1);  
也就GG了

同樣地,對於類似的變換:

  1. matrix.preScale(0.5f, 1);   
  2. matrix.preTranslate(100);  
  3. matrix.postScale(0.7f, 1);    
  4. matrix.postTranslate(150);  
其計算過程爲:translate (10, 0) -> scale (0.5f, 1) -> scale (0.7f, 1) -> translate (15, 0),是不是很簡單呢?你一定不傻逼對吧!
那麼對於上圖的結果真的是一樣的嗎?這裏我教給大家一個方法自己去驗證,Matrix有一個getValues方法可以獲取當前Matrix的變換浮點數組,也就是我們之前說的矩陣:

  1. /* 
  2.  * 新建一個9個單位長度的浮點數組 
  3.  * 因爲我們的Matrix矩陣是9個單位長的對吧 
  4.  */  
  5. float[] fs = new float[9];  
  6.   
  7. // 將從matrix中獲取到的浮點數組裝載進我們的fs裏  
  8. matrix.getValues(fs);  
  9. Log.d("Aige", Arrays.toString(fs));// 輸出看看唄!  
大家覺得好奇的都可以去驗證,這三類方法我就不多說了,Matrix中還有其他很多實用的方法,以後我們用到的時候在講,因爲Matrix太常用了~~~~

現在,大家回過頭去再看看我給妹子圖加暗角的那段代碼,裏面關於Matrix的操作能大致看懂了麼:

  1. // 計算徑向漸變半徑  
  2. float radiu = canvas.getHeight() * (2F / 3F);  
  3.   
  4. // 實例化徑向漸變  
  5. RadialGradient radialGradient = new RadialGradient(canvas.getWidth() / 2F, canvas.getHeight() / 2F, radiu, new int[] { 000xAA000000 }, new float[] { 0F, 0.7F, 1.0F }, Shader.TileMode.CLAMP);  
  6.   
  7. // 實例化一個矩陣  
  8. Matrix matrix = new Matrix();  
  9.   
  10. // 設置矩陣的縮放  
  11. matrix.setScale(canvas.getWidth() / (radiu * 2F), 1.0F);  
  12.   
  13. // 設置矩陣的預平移  
  14. matrix.preTranslate(((radiu * 2F) - canvas.getWidth()) / 2F, 0);  
  15.   
  16. // 將該矩陣注入徑向漸變  
  17. radialGradient.setLocalMatrix(matrix);  
  18.   
  19. // 設置畫筆Shader  
  20. mShaderPaint.setShader(radialGradient);  
是不是灰常滴簡單呢?這裏其實你只要注意我們除了平移所有變換操作都是基於一個原點的即可!找對原點你就成功一大半了!
好了,對Matrix的一個簡單介紹就到這裏,正如我所說,Matrix的應用是相當廣泛的,不僅僅是在我們的Shader,我們的canvas也有setMatrix(matrix)方法來設置矩陣變換,更常見的是在ImageView中對ImageView進行變換,當我們手指在屏幕上劃過一定的距離後根據這段距離來平移我們的控件,根據兩根手指之間拉伸的距離和相對於上一次旋轉的角度來縮放旋轉我們的圖片:


  1. public class MatrixImageView extends ImageView {  
  2.     private static final int MODE_NONE = 0x00123;// 默認的觸摸模式  
  3.     private static final int MODE_DRAG = 0x00321;// 拖拽模式  
  4.     private static final int MODE_ZOOM = 0x00132;// 縮放or旋轉模式  
  5.   
  6.     private int mode;// 當前的觸摸模式  
  7.   
  8.     private float preMove = 1F;// 上一次手指移動的距離  
  9.     private float saveRotate = 0F;// 保存了的角度值  
  10.     private float rotate = 0F;// 旋轉的角度  
  11.   
  12.     private float[] preEventCoor;// 上一次各觸摸點的座標集合  
  13.   
  14.     private PointF start, mid;// 起點、中點對象  
  15.     private Matrix currentMatrix, savedMatrix;// 當前和保存了的Matrix對象  
  16.     private Context mContext;// Fuck……  
  17.   
  18.     public MatrixImageView(Context context, AttributeSet attrs) {  
  19.         super(context, attrs);  
  20.         this.mContext = context;  
  21.   
  22.         // 初始化  
  23.         init();  
  24.     }  
  25.   
  26.     /** 
  27.      * 初始化 
  28.      */  
  29.     private void init() {  
  30.         /* 
  31.          * 實例化對象 
  32.          */  
  33.         currentMatrix = new Matrix();  
  34.         savedMatrix = new Matrix();  
  35.         start = new PointF();  
  36.         mid = new PointF();  
  37.   
  38.         // 模式初始化  
  39.         mode = MODE_NONE;  
  40.   
  41.         /* 
  42.          * 設置圖片資源 
  43.          */  
  44.         Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.mylove);  
  45.         bitmap = Bitmap.createScaledBitmap(bitmap, MeasureUtil.getScreenSize((Activity) mContext)[0], MeasureUtil.getScreenSize((Activity) mContext)[1], true);  
  46.         setImageBitmap(bitmap);  
  47.     }  
  48.   
  49.     @Override  
  50.     public boolean onTouchEvent(MotionEvent event) {  
  51.         switch (event.getAction() & MotionEvent.ACTION_MASK) {  
  52.         case MotionEvent.ACTION_DOWN:// 單點接觸屏幕時  
  53.             savedMatrix.set(currentMatrix);  
  54.             start.set(event.getX(), event.getY());  
  55.             mode = MODE_DRAG;  
  56.             preEventCoor = null;  
  57.             break;  
  58.         case MotionEvent.ACTION_POINTER_DOWN:// 第二個點接觸屏幕時  
  59.             preMove = calSpacing(event);  
  60.             if (preMove > 10F) {  
  61.                 savedMatrix.set(currentMatrix);  
  62.                 calMidPoint(mid, event);  
  63.                 mode = MODE_ZOOM;  
  64.             }  
  65.             preEventCoor = new float[4];  
  66.             preEventCoor[0] = event.getX(0);  
  67.             preEventCoor[1] = event.getX(1);  
  68.             preEventCoor[2] = event.getY(0);  
  69.             preEventCoor[3] = event.getY(1);  
  70.             saveRotate = calRotation(event);  
  71.             break;  
  72.         case MotionEvent.ACTION_UP:// 單點離開屏幕時  
  73.         case MotionEvent.ACTION_POINTER_UP:// 第二個點離開屏幕時  
  74.             mode = MODE_NONE;  
  75.             preEventCoor = null;  
  76.             break;  
  77.         case MotionEvent.ACTION_MOVE:// 觸摸點移動時  
  78.             /* 
  79.              * 單點觸控拖拽平移 
  80.              */  
  81.             if (mode == MODE_DRAG) {  
  82.                 currentMatrix.set(savedMatrix);  
  83.                 float dx = event.getX() - start.x;  
  84.                 float dy = event.getY() - start.y;  
  85.                 currentMatrix.postTranslate(dx, dy);  
  86.             }  
  87.             /* 
  88.              * 兩點觸控拖放旋轉 
  89.              */  
  90.             else if (mode == MODE_ZOOM && event.getPointerCount() == 2) {  
  91.                 float currentMove = calSpacing(event);  
  92.                 currentMatrix.set(savedMatrix);  
  93.                 /* 
  94.                  * 指尖移動距離大於10F縮放 
  95.                  */  
  96.                 if (currentMove > 10F) {  
  97.                     float scale = currentMove / preMove;  
  98.                     currentMatrix.postScale(scale, scale, mid.x, mid.y);  
  99.                 }  
  100.                 /* 
  101.                  * 保持兩點時旋轉 
  102.                  */  
  103.                 if (preEventCoor != null) {  
  104.                     rotate = calRotation(event);  
  105.                     float r = rotate - saveRotate;  
  106.                     currentMatrix.postRotate(r, getMeasuredWidth() / 2, getMeasuredHeight() / 2);  
  107.                 }  
  108.             }  
  109.             break;  
  110.         }  
  111.   
  112.         setImageMatrix(currentMatrix);  
  113.         return true;  
  114.     }  
  115.   
  116.     /** 
  117.      * 計算兩個觸摸點間的距離 
  118.      */  
  119.     private float calSpacing(MotionEvent event) {  
  120.         float x = event.getX(0) - event.getX(1);  
  121.         float y = event.getY(0) - event.getY(1);  
  122.         return (float) Math.sqrt(x * x + y * y);  
  123.     }  
  124.   
  125.     /** 
  126.      * 計算兩個觸摸點的中點座標 
  127.      */  
  128.     private void calMidPoint(PointF point, MotionEvent event) {  
  129.         float x = event.getX(0) + event.getX(1);  
  130.         float y = event.getY(0) + event.getY(1);  
  131.         point.set(x / 2, y / 2);  
  132.     }  
  133.   
  134.     /** 
  135.      * 計算旋轉角度 
  136.      *  
  137.      * @param 事件對象 
  138.      * @return 角度值 
  139.      */  
  140.     private float calRotation(MotionEvent event) {  
  141.         double deltaX = (event.getX(0) - event.getX(1));  
  142.         double deltaY = (event.getY(0) - event.getY(1));  
  143.         double radius = Math.atan2(deltaY, deltaX);  
  144.         return (float) Math.toDegrees(radius);  
  145.     }  
  146. }  
記得在xml中設置我們MatrixImageView的scaleType="matrix":
  1. <com.aigestudio.customviewdemo.views.MatrixImageView  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:scaleType="matrix" />  
雖然我們通過Matrix簡單地實現了對ImageView的變換操作,但是有一些小BUG,比如我們兩指縮放/旋轉圖片後擡起一隻手指,此時應該立即切換回平移模式對吧?但是我們上述的代碼中卻是終止了各種操作,事件機制雖然我們還沒講,但是我們也在這幾節中用到了不少,這個簡單的問題你能解決麼?
我在之前講到大家可以使用Matrix的getValues(float[])方法去驗證自己不確定的東西,同時呢,我們也可以使用Matrix的setValues(float[])方法來直接給Matrix設置一個矩陣數組,like:

  1. setValues(new float[]{  
  2.         1057  
  3.         0178  
  4.         001  
  5. });  
效果跟
  1. matrix.setTranslate(5778);  
是一樣的。上面我們說到Matrix矩陣最後的3個數是用來設置透視變換的,爲什麼最後一個值恆爲1?因爲其表示的是在Z軸向的透視縮放,這三個值都可以被設置,前兩個值跟右手座標系的XY軸有關,大家可以嘗試去發現它們之間的規律,我就不多說了。這裏多扯一點,大家一定要學會如何透過現象看本質,即便看到的本質不一定就是實質,但是離實質已經不遠了,不要一來就去追求什麼底層源碼啊、邏輯什麼的,就像上面的矩陣變換一樣,矩陣的9個數作用其實很多人都說不清,與其聽別人胡扯還不如自己動手試試你說是吧,不然苦逼的只是你自己。
在實際應用中我們極少會使用到Matrix的尾三數做透視變換,更多的是使用Camare攝像機,比如我們使用Camare讓ListView看起來像倒下去一樣:(這裏只做瞭解,已超出我們本系列的範疇)

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:background="#FFFFFF"  
  5.     android:gravity="center"  
  6.     android:orientation="vertical" >  
  7.   
  8.     <com.aigestudio.customviewdemo.views.AnimListView  
  9.         android:id="@+id/main_alv"  
  10.         android:layout_width="match_parent"  
  11.         android:layout_height="match_parent"  
  12.         android:scrollbars="none" />  
  13.   
  14. </LinearLayout>  
自定義ListView重寫onDraw:
  1. public class AnimListView extends ListView {  
  2.     private Camera mCamera;  
  3.     private Matrix mMatrix;  
  4.   
  5.     public AnimListView(Context context, AttributeSet attrs) {  
  6.         super(context, attrs);  
  7.         mCamera = new Camera();  
  8.         mMatrix = new Matrix();  
  9.     }  
  10.   
  11.     @Override  
  12.     protected void onDraw(Canvas canvas) {  
  13.         mCamera.save();  
  14.         mCamera.rotate(3000);  
  15.         mCamera.getMatrix(mMatrix);  
  16.         mMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2);  
  17.         mMatrix.postTranslate(getWidth() / 2, getHeight() / 2);  
  18.         canvas.concat(mMatrix);  
  19.         super.onDraw(canvas);  
  20.         mCamera.restore();  
  21.     }  
  22. }  
在MainActivity中設置數據:
  1. public class MainActivity extends Activity {  
  2.   
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.   
  8.         AnimListView animListView = (AnimListView) findViewById(R.id.main_alv);  
  9.         animListView.setAdapter(new BaseAdapter() {  
  10.   
  11.             @Override  
  12.             public View getView(int position, View convertView, ViewGroup parent) {  
  13.                 convertView = LayoutInflater.from(getApplicationContext()).inflate(R.layout.item, null);  
  14.                 return convertView;  
  15.             }  
  16.   
  17.             @Override  
  18.             public long getItemId(int position) {  
  19.                 return 0;  
  20.             }  
  21.   
  22.             @Override  
  23.             public Object getItem(int position) {  
  24.                 return null;  
  25.             }  
  26.   
  27.             @Override  
  28.             public int getCount() {  
  29.                 return 100;  
  30.             }  
  31.         });  
  32.     }  
  33. }  
效果like below:

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

幾個圖中最難的大概就是那個各種圈圈的了:


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


這樣看總清晰了吧……大家在自定義一個View的時候不要老想着自己是個Coder,你老是這樣想越做不出棒的View,你要把自己看成一個Designer,這個View將會是由你創造的!而不是你敲出了它……本來在這一系列之後我有單獨的番外篇叫如何去設計一個控件,今天在做這個圈圈圖的時候突然有這麼一個想法還是乾脆穿插進來說算了,直接粗暴!

既然我們是一個設計者,那麼我們必然要有這麼一張圖紙去概述我們的View,因爲一般情況下大家都知道美工跟開發者之間是有代溝的,假如,我是說假如美工花了一個很屌的界面,但是你如果直接按着他給你的設計圖照着做你會發現做不出一模一樣的來……這時你就該調整自己多去與美工溝通在不大改設計圖的前提下把難度降到你能力範圍內。同樣的,我們這裏也一樣。假如這個圈圈圖是JB美工給你的效果圖,叫你照着做,我們不可能真的這樣照着做,因爲他給的東西對我們來說無規律可循,而對於開發者來說,有規律的東西往往是最簡單,這時我們就要“設計和代碼相結合”:


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


注:因爲我們還沒講如何測繪控件,所以我們在自定義View的時候強制控件的長寬一致以簡化不必要的口水

還是老套路,先分析一下:控件中心往下是最中心的圓,而其他的六個圓都直接或間接地與其相連,上面的三個是大圓而下面的三個是相對較小的圓,大圓之間的線段是緊挨着的而中心大圓和下面三個小圓之間的線段是有一定距離的,右上方的大圓上方還有一段實體描邊弧,弧上有文字,而每個圓內都可以設置文本,大概就是醬紫,那麼我們從哪作爲插入點呢?當然是中心的那個圓,但是我們知道它的圓心是要往控件中心向下偏移一個半徑的:


代碼如何實現呢?不用我說你也應該知道:

  1. public class MultiCricleView extends View {  
  2.     private static final float STROKE_WIDTH = 1F / 256F, // 描邊寬度佔比  
  3.             LINE_LENGTH = 3F / 32F, // 線段長度佔比  
  4.             CRICLE_LARGER_RADIU = 3F / 32F,// 大圓半徑  
  5.             CRICLE_SMALL_RADIU = 5F / 64F,// 小圓半徑  
  6.             ARC_RADIU = 1F / 8F,// 弧半徑  
  7.             ARC_TEXT_RADIU = 5F / 32F;// 弧圍繞文字半徑  
  8.   
  9.     private Paint strokePaint;// 描邊畫筆  
  10.   
  11.     private int size;// 控件邊長  
  12.   
  13.     private float strokeWidth;// 描邊寬度  
  14.     private float ccX, ccY;// 中心圓圓心座標  
  15.     private float largeCricleRadiu;// 大圓半徑  
  16.   
  17.     public MultiCricleView(Context context, AttributeSet attrs) {  
  18.         super(context, attrs);  
  19.   
  20.         // 初始化畫筆  
  21.         initPaint(context);  
  22.     }  
  23.   
  24.     /** 
  25.      * 初始化畫筆 
  26.      *  
  27.      * @param context 
  28.      *            Fuck 
  29.      */  
  30.     private void initPaint(Context context) {  
  31.         /* 
  32.          * 初始化描邊畫筆 
  33.          */  
  34.         strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
  35.         strokePaint.setStyle(Paint.Style.STROKE);  
  36.         strokePaint.setColor(Color.WHITE);  
  37.         strokePaint.setStrokeCap(Paint.Cap.ROUND);  
  38.     }  
  39.   
  40.     @Override  
  41.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  42.         // 強制長寬一致  
  43.         super.onMeasure(widthMeasureSpec, widthMeasureSpec);  
  44.     }  
  45.   
  46.     @Override  
  47.     protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
  48.         // 獲取控件邊長  
  49.         size = w;  
  50.   
  51.         // 參數計算  
  52.         calculation();  
  53.     }  
  54.   
  55.     /* 
  56.      * 參數計算 
  57.      */  
  58.     private void calculation() {  
  59.         // 計算描邊寬度  
  60.         strokeWidth = STROKE_WIDTH * size;  
  61.   
  62.         // 計算大圓半徑  
  63.         largeCricleRadiu = size * CRICLE_LARGER_RADIU;  
  64.   
  65.         // 計算中心圓圓心座標  
  66.         ccX = size / 2;  
  67.         ccY = size / 2 + size * CRICLE_LARGER_RADIU;  
  68.   
  69.         // 設置參數  
  70.         setPara();  
  71.     }  
  72.   
  73.     /** 
  74.      * 設置參數 
  75.      */  
  76.     private void setPara() {  
  77.         // 設置描邊寬度  
  78.         strokePaint.setStrokeWidth(strokeWidth);  
  79.     }  
  80.   
  81.     @Override  
  82.     protected void onDraw(Canvas canvas) {  
  83.         // 繪製背景  
  84.         canvas.drawColor(0xFFF29B76);  
  85.   
  86.         // 繪製中心圓  
  87.         canvas.drawCircle(ccX, ccY, largeCricleRadiu, strokePaint);  
  88.     }  
  89. }  
大家可以看到我在View重寫了這了一個方法:
  1. @Override  
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.     // 強制長寬一致  
  4.     super.onMeasure(widthMeasureSpec, widthMeasureSpec);  
  5. }  
這個方法就是用來測量控件寬高的,而其從爹那獲取的兩個參數widthMeasureSpec和heightMeasureSpec分別封裝了View的Size和Mode,我們會在1/2講View的繪製流程,這裏只需跟着我的腳步在這光滑的地板上摩擦摩擦即可 = = !
onSizeChanged這個方法我們前面也提過就不多扯了,效果如下:


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


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


這樣的一個變換嗎?之前我們曾多次使用到畫布的圖層,如果我們能這樣畫圖形再以中心圓的圓心座標爲旋轉點向右旋轉畫布豈不是可以很簡單:

  1. public class MultiCricleView extends View {  
  2.     private static final float STROKE_WIDTH = 1F / 256F, // 描邊寬度佔比  
  3.             LINE_LENGTH = 3F / 32F, // 線段長度佔比  
  4.             CRICLE_LARGER_RADIU = 3F / 32F,// 大圓半徑  
  5.             CRICLE_SMALL_RADIU = 5F / 64F,// 小圓半徑  
  6.             ARC_RADIU = 1F / 8F,// 弧半徑  
  7.             ARC_TEXT_RADIU = 5F / 32F;// 弧圍繞文字半徑  
  8.   
  9.     private Paint strokePaint;// 描邊畫筆  
  10.   
  11.     private int size;// 控件邊長  
  12.   
  13.     private float strokeWidth;// 描邊寬度  
  14.     private float ccX, ccY;// 中心圓圓心座標  
  15.     private float largeCricleRadiu;// 大圓半徑  
  16.     private float lineLength;// 線段長度  
  17.   
  18.     public MultiCricleView(Context context, AttributeSet attrs) {  
  19.         super(context, attrs);  
  20.   
  21.         // 初始化畫筆  
  22.         initPaint(context);  
  23.     }  
  24.   
  25.     /** 
  26.      * 初始化畫筆 
  27.      *  
  28.      * @param context 
  29.      *            Fuck 
  30.      */  
  31.     private void initPaint(Context context) {  
  32.         /* 
  33.          * 初始化描邊畫筆 
  34.          */  
  35.         strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
  36.         strokePaint.setStyle(Paint.Style.STROKE);  
  37.         strokePaint.setColor(Color.WHITE);  
  38.         strokePaint.setStrokeCap(Paint.Cap.ROUND);  
  39.     }  
  40.   
  41.     @Override  
  42.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  43.         // 強制長寬一致  
  44.         super.onMeasure(widthMeasureSpec, widthMeasureSpec);  
  45.     }  
  46.   
  47.     @Override  
  48.     protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
  49.         // 獲取控件邊長  
  50.         size = w;  
  51.   
  52.         // 參數計算  
  53.         calculation();  
  54.     }  
  55.   
  56.     /* 
  57.      * 參數計算 
  58.      */  
  59.     private void calculation() {  
  60.         // 計算描邊寬度  
  61.         strokeWidth = STROKE_WIDTH * size;  
  62.   
  63.         // 計算大圓半徑  
  64.         largeCricleRadiu = size * CRICLE_LARGER_RADIU;  
  65.   
  66.         // 計算線段長度  
  67.         lineLength = size * LINE_LENGTH;  
  68.   
  69.         // 計算中心圓圓心座標  
  70.         ccX = size / 2;  
  71.         ccY = size / 2 + size * CRICLE_LARGER_RADIU;  
  72.   
  73.         // 設置參數  
  74.         setPara();  
  75.     }  
  76.   
  77.     /** 
  78.      * 設置參數 
  79.      */  
  80.     private void setPara() {  
  81.         // 設置描邊寬度  
  82.         strokePaint.setStrokeWidth(strokeWidth);  
  83.     }  
  84.   
  85.     @Override  
  86.     protected void onDraw(Canvas canvas) {  
  87.         // 繪製背景  
  88.         canvas.drawColor(0xFFF29B76);  
  89.   
  90.         // 繪製中心圓  
  91.         canvas.drawCircle(ccX, ccY, largeCricleRadiu, strokePaint);  
  92.   
  93.         // 繪製左上方圖形  
  94.         drawTopLeft(canvas);  
  95.     }  
  96.   
  97.     /** 
  98.      * 繪製左上方圖形 
  99.      *  
  100.      * @param canvas 
  101.      */  
  102.     private void drawTopLeft(Canvas canvas) {  
  103.         // 鎖定畫布  
  104.         canvas.save();  
  105.   
  106.         // 平移和旋轉畫布  
  107.         canvas.translate(ccX, ccY);  
  108.         canvas.rotate(-30);  
  109.   
  110.         // 依次畫:線-圈-線-圈  
  111.         canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);  
  112.         canvas.drawCircle(0, -lineLength * 3, largeCricleRadiu, strokePaint);  
  113.         canvas.drawLine(0, -largeCricleRadiu * 40, -lineLength * 5, strokePaint);  
  114.         canvas.drawCircle(0, -lineLength * 6, largeCricleRadiu, strokePaint);  
  115.   
  116.         // 釋放畫布  
  117.         canvas.restore();  
  118.     }  
  119. }  
like below:

保存和釋放畫布就不說了,用過N次了。

  1. canvas.translate(ccX, ccY);  
我們將畫布平移了ccx和xxy個單位其實就是讓畫布的左上端原點遠我們的中心圓圓心重合:

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

這裏有一點非常非常地重要!畫布的平移旋轉同樣也會影響畫布的自身座標!如上圖,我們看到畫布的座標也跟着旋轉了30度!我們正是利用了這一點巧妙地在畫圖而避免繁雜的座標計算!!

同理我們可以繪製出其他三個小圓:

  1. public class MultiCricleView extends View {  
  2.     private static final float STROKE_WIDTH = 1F / 256F, // 描邊寬度佔比  
  3.             SPACE = 1F / 64F,// 大圓小圓線段兩端間隔佔比  
  4.             LINE_LENGTH = 3F / 32F, // 線段長度佔比  
  5.             CRICLE_LARGER_RADIU = 3F / 32F,// 大圓半徑  
  6.             CRICLE_SMALL_RADIU = 5F / 64F,// 小圓半徑  
  7.             ARC_RADIU = 1F / 8F,// 弧半徑  
  8.             ARC_TEXT_RADIU = 5F / 32F;// 弧圍繞文字半徑  
  9.   
  10.     private Paint strokePaint;// 描邊畫筆  
  11.   
  12.     private int size;// 控件邊長  
  13.   
  14.     private float strokeWidth;// 描邊寬度  
  15.     private float ccX, ccY;// 中心圓圓心座標  
  16.     private float largeCricleRadiu, smallCricleRadiu;// 大圓半徑和小圓半徑  
  17.     private float lineLength;// 線段長度  
  18.     private float space;// 大圓小圓線段兩端間隔  
  19.   
  20.     private enum Type {  
  21.         LARGER, SMALL  
  22.     }  
  23.   
  24.     public MultiCricleView(Context context, AttributeSet attrs) {  
  25.         super(context, attrs);  
  26.   
  27.         // 初始化畫筆  
  28.         initPaint(context);  
  29.     }  
  30.   
  31.     /** 
  32.      * 初始化畫筆 
  33.      *  
  34.      * @param context 
  35.      *            Fuck 
  36.      */  
  37.     private void initPaint(Context context) {  
  38.         /* 
  39.          * 初始化描邊畫筆 
  40.          */  
  41.         strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
  42.         strokePaint.setStyle(Paint.Style.STROKE);  
  43.         strokePaint.setColor(Color.WHITE);  
  44.         strokePaint.setStrokeCap(Paint.Cap.ROUND);  
  45.     }  
  46.   
  47.     @Override  
  48.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  49.         // 強制長寬一致  
  50.         super.onMeasure(widthMeasureSpec, widthMeasureSpec);  
  51.     }  
  52.   
  53.     @Override  
  54.     protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
  55.         // 獲取控件邊長  
  56.         size = w;  
  57.   
  58.         // 參數計算  
  59.         calculation();  
  60.     }  
  61.   
  62.     /* 
  63.      * 參數計算 
  64.      */  
  65.     private void calculation() {  
  66.         // 計算描邊寬度  
  67.         strokeWidth = STROKE_WIDTH * size;  
  68.   
  69.         // 計算大圓半徑  
  70.         largeCricleRadiu = size * CRICLE_LARGER_RADIU;  
  71.   
  72.         // 計算小圓半徑  
  73.         smallCricleRadiu = size * CRICLE_SMALL_RADIU;  
  74.   
  75.         // 計算線段長度  
  76.         lineLength = size * LINE_LENGTH;  
  77.   
  78.         // 計算大圓小圓線段兩端間隔  
  79.         space = size * SPACE;  
  80.   
  81.         // 計算中心圓圓心座標  
  82.         ccX = size / 2;  
  83.         ccY = size / 2 + size * CRICLE_LARGER_RADIU;  
  84.   
  85.         // 設置參數  
  86.         setPara();  
  87.     }  
  88.   
  89.     /** 
  90.      * 設置參數 
  91.      */  
  92.     private void setPara() {  
  93.         // 設置描邊寬度  
  94.         strokePaint.setStrokeWidth(strokeWidth);  
  95.     }  
  96.   
  97.     @Override  
  98.     protected void onDraw(Canvas canvas) {  
  99.         // 繪製背景  
  100.         canvas.drawColor(0xFFF29B76);  
  101.   
  102.         // 繪製中心圓  
  103.         canvas.drawCircle(ccX, ccY, largeCricleRadiu, strokePaint);  
  104.   
  105.         // 繪製左上方圖形  
  106.         drawTopLeft(canvas);  
  107.   
  108.         // 繪製右上方圖形  
  109.         drawTopRight(canvas);  
  110.   
  111.         // 繪製左下方圖形  
  112.         drawBottomLeft(canvas);  
  113.   
  114.         // 繪製下方圖形  
  115.         drawBottom(canvas);  
  116.   
  117.         // 繪製右下方圖形  
  118.         drawBottomRight(canvas);  
  119.     }  
  120.   
  121.     /** 
  122.      * 繪製左上方圖形 
  123.      *  
  124.      * @param canvas 
  125.      */  
  126.     private void drawTopLeft(Canvas canvas) {  
  127.         // 鎖定畫布  
  128.         canvas.save();  
  129.   
  130.         // 平移和旋轉畫布  
  131.         canvas.translate(ccX, ccY);  
  132.         canvas.rotate(-30);  
  133.   
  134.         // 依次畫:線-圈-線-圈  
  135.         canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);  
  136.         canvas.drawCircle(0, -lineLength * 3, largeCricleRadiu, strokePaint);  
  137.         canvas.drawLine(0, -largeCricleRadiu * 40, -lineLength * 5, strokePaint);  
  138.         canvas.drawCircle(0, -lineLength * 6, largeCricleRadiu, strokePaint);  
  139.   
  140.         // 釋放畫布  
  141.         canvas.restore();  
  142.     }  
  143.   
  144.     /** 
  145.      * 繪製右上方圖形 
  146.      *  
  147.      * @param canvas 
  148.      */  
  149.     private void drawTopRight(Canvas canvas) {  
  150.         // 鎖定畫布  
  151.         canvas.save();  
  152.   
  153.         // 平移和旋轉畫布  
  154.         canvas.translate(ccX, ccY);  
  155.         canvas.rotate(30);  
  156.   
  157.         // 依次畫:線-圈  
  158.         canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);  
  159.         canvas.drawCircle(0, -lineLength * 3, largeCricleRadiu, strokePaint);  
  160.   
  161.         // 釋放畫布  
  162.         canvas.restore();  
  163.     }  
  164.   
  165.     private void drawBottomLeft(Canvas canvas) {  
  166.         // 鎖定畫布  
  167.         canvas.save();  
  168.   
  169.         // 平移和旋轉畫布  
  170.         canvas.translate(ccX, ccY);  
  171.         canvas.rotate(-100);  
  172.   
  173.         // 依次畫:(間隔)線(間隔)-圈  
  174.         canvas.drawLine(0, -largeCricleRadiu - space, 0, -lineLength * 2 - space, strokePaint);  
  175.         canvas.drawCircle(0, -lineLength * 2 - smallCricleRadiu - space * 2, smallCricleRadiu, strokePaint);  
  176.   
  177.         // 釋放畫布  
  178.         canvas.restore();  
  179.     }  
  180.   
  181.     private void drawBottom(Canvas canvas) {  
  182.         // 鎖定畫布  
  183.         canvas.save();  
  184.   
  185.         // 平移和旋轉畫布  
  186.         canvas.translate(ccX, ccY);  
  187.         canvas.rotate(180);  
  188.   
  189.         // 依次畫:(間隔)線(間隔)-圈  
  190.         canvas.drawLine(0, -largeCricleRadiu - space, 0, -lineLength * 2 - space, strokePaint);  
  191.         canvas.drawCircle(0, -lineLength * 2 - smallCricleRadiu - space * 2, smallCricleRadiu, strokePaint);  
  192.   
  193.         // 釋放畫布  
  194.         canvas.restore();  
  195.     }  
  196.   
  197.     private void drawBottomRight(Canvas canvas) {  
  198.         // 鎖定畫布  
  199.         canvas.save();  
  200.   
  201.         // 平移和旋轉畫布  
  202.         canvas.translate(ccX, ccY);  
  203.         canvas.rotate(100);  
  204.   
  205.         // 依次畫:(間隔)線(間隔)-圈  
  206.         canvas.drawLine(0, -largeCricleRadiu - space, 0, -lineLength * 2 - space, strokePaint);  
  207.         canvas.drawCircle(0, -lineLength * 2 - smallCricleRadiu - space * 2, smallCricleRadiu, strokePaint);  
  208.   
  209.         // 釋放畫布  
  210.         canvas.restore();  
  211.     }  
  212. }  
大家可以看到,每一次繪製我都鎖定了畫布並平移旋轉以調整畫布的原點座標極大程度地方便我們計算。效果如下:

上面的代碼會有巨量的重複代碼,正如上面我說,這個圖形我們是畫死的,在沒講完View的測繪和ViewGroup之前我們不會做任何一個完整的控件,So~~~~同時也是爲了方便大家容易理解這玩意是怎麼畫的,我也就不對重複的方法做進一步封裝了,不過在做項目的時候切忌大量的重複代碼。
我們再給這些圈圈裏加些文字,文字的中點很明顯就是這些圈圈的圓心對吧,1/4中我說過如何把文字畫到中心的?

  1. public class MultiCricleView extends View {  
  2.     private static final float STROKE_WIDTH = 1F / 256F, // 描邊寬度佔比  
  3.             SPACE = 1F / 64F,// 大圓小圓線段兩端間隔佔比  
  4.             LINE_LENGTH = 3F / 32F, // 線段長度佔比  
  5.             CRICLE_LARGER_RADIU = 3F / 32F,// 大圓半徑  
  6.             CRICLE_SMALL_RADIU = 5F / 64F,// 小圓半徑  
  7.             ARC_RADIU = 1F / 8F,// 弧半徑  
  8.             ARC_TEXT_RADIU = 5F / 32F;// 弧圍繞文字半徑  
  9.   
  10.     private Paint strokePaint, textPaint;// 描邊畫筆和文字畫筆  
  11.   
  12.     private int size;// 控件邊長  
  13.   
  14.     private float strokeWidth;// 描邊寬度  
  15.     private float ccX, ccY;// 中心圓圓心座標  
  16.     private float largeCricleRadiu, smallCricleRadiu;// 大圓半徑和小圓半徑  
  17.     private float lineLength;// 線段長度  
  18.     private float space;// 大圓小圓線段兩端間隔  
  19.     private float textOffsetY;// 文本的Y軸偏移值  
  20.   
  21.     public MultiCricleView(Context context, AttributeSet attrs) {  
  22.         super(context, attrs);  
  23.   
  24.         // 初始化畫筆  
  25.         initPaint(context);  
  26.     }  
  27.   
  28.     /** 
  29.      * 初始化畫筆 
  30.      *  
  31.      * @param context 
  32.      *            Fuck 
  33.      */  
  34.     private void initPaint(Context context) {  
  35.         /* 
  36.          * 初始化描邊畫筆 
  37.          */  
  38.         strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
  39.         strokePaint.setStyle(Paint.Style.STROKE);  
  40.         strokePaint.setColor(Color.WHITE);  
  41.         strokePaint.setStrokeCap(Paint.Cap.ROUND);  
  42.   
  43.         /* 
  44.          * 初始化文字畫筆 
  45.          */  
  46.         textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.SUBPIXEL_TEXT_FLAG);  
  47.         textPaint.setColor(Color.WHITE);  
  48.         textPaint.setTextSize(30);  
  49.         textPaint.setTextAlign(Paint.Align.CENTER);  
  50.   
  51.         textOffsetY = (textPaint.descent() + textPaint.ascent()) / 2;  
  52.     }  
  53.   
  54.     @Override  
  55.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  56.         // 強制長寬一致  
  57.         super.onMeasure(widthMeasureSpec, widthMeasureSpec);  
  58.     }  
  59.   
  60.     @Override  
  61.     protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
  62.         // 獲取控件邊長  
  63.         size = w;  
  64.   
  65.         // 參數計算  
  66.         calculation();  
  67.     }  
  68.   
  69.     /* 
  70.      * 參數計算 
  71.      */  
  72.     private void calculation() {  
  73.         // 計算描邊寬度  
  74.         strokeWidth = STROKE_WIDTH * size;  
  75.   
  76.         // 計算大圓半徑  
  77.         largeCricleRadiu = size * CRICLE_LARGER_RADIU;  
  78.   
  79.         // 計算小圓半徑  
  80.         smallCricleRadiu = size * CRICLE_SMALL_RADIU;  
  81.   
  82.         // 計算線段長度  
  83.         lineLength = size * LINE_LENGTH;  
  84.   
  85.         // 計算大圓小圓線段兩端間隔  
  86.         space = size * SPACE;  
  87.   
  88.         // 計算中心圓圓心座標  
  89.         ccX = size / 2;  
  90.         ccY = size / 2 + size * CRICLE_LARGER_RADIU;  
  91.   
  92.         // 設置參數  
  93.         setPara();  
  94.     }  
  95.   
  96.     /** 
  97.      * 設置參數 
  98.      */  
  99.     private void setPara() {  
  100.         // 設置描邊寬度  
  101.         strokePaint.setStrokeWidth(strokeWidth);  
  102.     }  
  103.   
  104.     @Override  
  105.     protected void onDraw(Canvas canvas) {  
  106.         // 繪製背景  
  107.         canvas.drawColor(0xFFF29B76);  
  108.   
  109.         // 繪製中心圓  
  110.         canvas.drawCircle(ccX, ccY, largeCricleRadiu, strokePaint);  
  111.         canvas.drawText("AigeStudio", ccX, ccY - textOffsetY, textPaint);  
  112.   
  113.         // 繪製左上方圖形  
  114.         drawTopLeft(canvas);  
  115.   
  116.         // 繪製右上方圖形  
  117.         drawTopRight(canvas);  
  118.   
  119.         // 繪製左下方圖形  
  120.         drawBottomLeft(canvas);  
  121.   
  122.         // 繪製下方圖形  
  123.         drawBottom(canvas);  
  124.   
  125.         // 繪製右下方圖形  
  126.         drawBottomRight(canvas);  
  127.     }  
  128.   
  129.     /** 
  130.      * 繪製左上方圖形 
  131.      *  
  132.      * @param canvas 
  133.      */  
  134.     private void drawTopLeft(Canvas canvas) {  
  135.         // 鎖定畫布  
  136.         canvas.save();  
  137.   
  138.         // 平移和旋轉畫布  
  139.         canvas.translate(ccX, ccY);  
  140.         canvas.rotate(-30);  
  141.   
  142.         // 依次畫:線-圈-線-圈  
  143.         canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);  
  144.         canvas.drawCircle(0, -lineLength * 3, largeCricleRadiu, strokePaint);  
  145.         canvas.drawText("Apple"0, -lineLength * 3 - textOffsetY, textPaint);  
  146.   
  147.         canvas.drawLine(0, -largeCricleRadiu * 40, -lineLength * 5, strokePaint);  
  148.         canvas.drawCircle(0, -lineLength * 6, largeCricleRadiu, strokePaint);  
  149.         canvas.drawText("Orange"0, -lineLength * 6 - textOffsetY, textPaint);  
  150.   
  151.         // 釋放畫布  
  152.         canvas.restore();  
  153.     }  
  154.   
  155.     /** 
  156.      * 繪製右上方圖形 
  157.      *  
  158.      * @param canvas 
  159.      */  
  160.     private void drawTopRight(Canvas canvas) {  
  161.         float cricleY = -lineLength * 3;  
  162.   
  163.         // 鎖定畫布  
  164.         canvas.save();  
  165.   
  166.         // 平移和旋轉畫布  
  167.         canvas.translate(ccX, ccY);  
  168.         canvas.rotate(30);  
  169.   
  170.         // 依次畫:線-圈  
  171.         canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);  
  172.         canvas.drawCircle(0, cricleY, largeCricleRadiu, strokePaint);  
  173.         canvas.drawText("Tropical"0, cricleY - textOffsetY, textPaint);  
  174.   
  175.         // 釋放畫布  
  176.         canvas.restore();  
  177.     }  
  178.   
  179.     private void drawBottomLeft(Canvas canvas) {  
  180.         float lineYS = -largeCricleRadiu - space, lineYE = -lineLength * 2 - space, cricleY = -lineLength * 2 - smallCricleRadiu - space * 2;  
  181.   
  182.         // 鎖定畫布  
  183.         canvas.save();  
  184.   
  185.         // 平移和旋轉畫布  
  186.         canvas.translate(ccX, ccY);  
  187.         canvas.rotate(-100);  
  188.   
  189.         // 依次畫:(間隔)線(間隔)-圈  
  190.         canvas.drawLine(0, lineYS, 0, lineYE, strokePaint);  
  191.         canvas.drawCircle(0, cricleY, smallCricleRadiu, strokePaint);  
  192.         canvas.drawText("Banana"0, cricleY - textOffsetY, textPaint);  
  193.   
  194.         // 釋放畫布  
  195.         canvas.restore();  
  196.     }  
  197.   
  198.     private void drawBottom(Canvas canvas) {  
  199.         float lineYS = -largeCricleRadiu - space, lineYE = -lineLength * 2 - space, cricleY = -lineLength * 2 - smallCricleRadiu - space * 2;  
  200.   
  201.         // 鎖定畫布  
  202.         canvas.save();  
  203.   
  204.         // 平移和旋轉畫布  
  205.         canvas.translate(ccX, ccY);  
  206.         canvas.rotate(180);  
  207.   
  208.         // 依次畫:(間隔)線(間隔)-圈  
  209.         canvas.drawLine(0, lineYS, 0, lineYE, strokePaint);  
  210.         canvas.drawCircle(0, cricleY, smallCricleRadiu, strokePaint);  
  211.         canvas.drawText("Cucumber"0, cricleY - textOffsetY, textPaint);  
  212.   
  213.         // 釋放畫布  
  214.         canvas.restore();  
  215.     }  
  216.   
  217.     private void drawBottomRight(Canvas canvas) {  
  218.         float lineYS = -largeCricleRadiu - space, lineYE = -lineLength * 2 - space, cricleY = -lineLength * 2 - smallCricleRadiu - space * 2;  
  219.   
  220.         // 鎖定畫布  
  221.         canvas.save();  
  222.   
  223.         // 平移和旋轉畫布  
  224.         canvas.translate(ccX, ccY);  
  225.         canvas.rotate(100);  
  226.   
  227.         // 依次畫:(間隔)線(間隔)-圈  
  228.         canvas.drawLine(0, lineYS, 0, lineYE, strokePaint);  
  229.         canvas.drawCircle(0, cricleY, smallCricleRadiu, strokePaint);  
  230.         canvas.drawText("Vibrators"0, cricleY - textOffsetY, textPaint);  
  231.   
  232.         // 釋放畫布  
  233.         canvas.restore();  
  234.     }  
  235. }  
That's so easy right?

稍微有點難的是右上方的那個半回扇形,扇形的中心應該是與右上方的圓心重合的對吧,那我們在畫右上方圖形的時候一起畫不就是了?

  1. /** 
  2.  * 繪製右上方圖形 
  3.  *  
  4.  * @param canvas 
  5.  */  
  6. private void drawTopRight(Canvas canvas) {  
  7.     float cricleY = -lineLength * 3;  
  8.   
  9.     // 鎖定畫布  
  10.     canvas.save();  
  11.   
  12.     // 平移和旋轉畫布  
  13.     canvas.translate(ccX, ccY);  
  14.     canvas.rotate(30);  
  15.   
  16.     // 依次畫:線-圈  
  17.     canvas.drawLine(0, -largeCricleRadiu, 0, -lineLength * 2, strokePaint);  
  18.     canvas.drawCircle(0, cricleY, largeCricleRadiu, strokePaint);  
  19.     canvas.drawText("Tropical"0, cricleY - textOffsetY, textPaint);  
  20.   
  21.     // 畫弧形  
  22.     drawTopRightArc(canvas, cricleY);  
  23.   
  24.     // 釋放畫布  
  25.     canvas.restore();  
  26. }  
  27.   
  28. /** 
  29.  * 繪製右上角畫弧形 
  30.  *  
  31.  * @param canvas 
  32.  * @param cricleY 
  33.  */  
  34. private void drawTopRightArc(Canvas canvas, float cricleY) {  
  35.     canvas.save();  
  36.       
  37.     canvas.translate(0, cricleY);  
  38.     canvas.rotate(-30);  
  39.   
  40.     float arcRadiu = size * ARC_RADIU;  
  41.   
  42.     RectF oval = new RectF(-arcRadiu, -arcRadiu, arcRadiu, arcRadiu);  
  43.   
  44.     arcPaint.setStyle(Paint.Style.FILL);  
  45.     arcPaint.setColor(0x55EC6941);  
  46.     canvas.drawArc(oval, -22.5F, -135true, arcPaint);  
  47.   
  48.     arcPaint.setStyle(Paint.Style.STROKE);  
  49.     arcPaint.setColor(Color.WHITE);  
  50.     canvas.drawArc(oval, -22.5F, -135false, arcPaint);  
  51.   
  52.     canvas.restore();  
  53. }  
代碼邏輯不復雜,大家很容易能看懂……註釋什麼的我就不寫了……扇形上的文本如果大家理解了畫布的變換很容易實現,首先在上面我們繪製扇形的時候已經將畫布原點與右上圓心重合了對吧,這時我們再把畫布向左旋轉 扇形弧度/2 個度數是不是就可以讓畫布的座標與扇形的右邊重合了呢?那第一個文字的座標就是[0,負的文字弧形半徑],第二個文字座標只需轉畫布過 扇形弧度/4 個弧度以此類推可以畫出五個文字:
  1. /** 
  2.  * 繪製右上角畫弧形 
  3.  *  
  4.  * @param canvas 
  5.  * @param cricleY 
  6.  */  
  7. private void drawTopRightArc(Canvas canvas, float cricleY) {  
  8.     canvas.save();  
  9.   
  10.     canvas.translate(0, cricleY);  
  11.     canvas.rotate(-30);  
  12.   
  13.     float arcRadiu = size * ARC_RADIU;  
  14.     RectF oval = new RectF(-arcRadiu, -arcRadiu, arcRadiu, arcRadiu);  
  15.     arcPaint.setStyle(Paint.Style.FILL);  
  16.     arcPaint.setColor(0x55EC6941);  
  17.     canvas.drawArc(oval, -22.5F, -135true, arcPaint);  
  18.     arcPaint.setStyle(Paint.Style.STROKE);  
  19.     arcPaint.setColor(Color.WHITE);  
  20.     canvas.drawArc(oval, -22.5F, -135false, arcPaint);  
  21.   
  22.     float arcTextRadiu = size * ARC_TEXT_RADIU;  
  23.   
  24.     canvas.save();  
  25.     // 把畫布旋轉到扇形左端的方向  
  26.     canvas.rotate(-135F / 2F);  
  27.   
  28.     /* 
  29.      * 每隔33.75度角畫一次文本 
  30.      */  
  31.     for (float i = 0; i < 5 * 33.75F; i += 33.75F) {  
  32.         canvas.save();  
  33.         canvas.rotate(i);  
  34.   
  35.         canvas.drawText("Aige"0, -arcTextRadiu, textPaint);  
  36.   
  37.         canvas.restore();  
  38.     }  
  39.   
  40.     canvas.restore();  
  41.   
  42.     canvas.restore();  
  43. }  
對吧?看看效果:

好了,正如我所說,這只是一個單純地畫,而且此類奇葩的玩意難以真正做成一個控件去複用除非真的是公事公辦,但是,我們依然可以嘗試把它做成一個獨立的控件,這在我們學寫了如何測繪View和ViewGroup之後對你來說一定是小case。

上面我們的最終效果有一點是不對的,大家發現文字TMD居然都旋轉了 - - ,那有木有方法讓文字保持水平呢?其實答案我已經告訴你。自己去發掘吧!

源碼下載:傳送門

這幾天龍體欠安啊、哎……更新進度會有打亂~~~敬請大家follow~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章