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

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

炮兵鎮樓

上一節我們粗略地講了下如何去實現我們的View並概述了View形成動畫的基本原理,這一節我們緊跟上一節的步伐來深挖如何去繪製更復雜的View!

通過上一節的學習我們瞭解到什麼是畫布Canvas什麼是畫筆Paint,並且學習瞭如何去設置畫筆的屬性如何在畫布上畫一個圓,然而,畫筆的屬性並非僅僅就設置個顏色、大小那麼簡單而畫布呢肯定也不單單只是能畫一個圓那麼無趣,工慾善其事必先利其器,既然想畫好圖那必須學會畫筆和畫布的使用,那麼今天我們就來看看Android的畫筆跟我們現實中的畫筆有什麼不同呢?

如上節所說我們可以通過Paint中大量的setter方法來爲畫筆設置屬性:


這些屬性大多我們都可以見名知意,很好理解,即便如此,哥還是帶大家過一遍逐個剖析其用法,其中會不定穿插各種繪圖類比如Canvas、Xfermode、ColorFilter等等的用法。

set(Paint src)

顧名思義爲當前畫筆設置一個畫筆,說白了就是把另一個畫筆的屬性設置Copy給我們的畫筆,不累贅了

setARGB(int a, int r, int g, int b)

不扯了,別跟我說不懂

setAlpha(int a)

同上

setAntiAlias(boolean aa)

這個上一節我們用到了,打開抗鋸齒,不過我要說明一點,抗鋸齒是依賴於算法的,算法決定抗鋸齒的效率,在我們繪製棱角分明的圖像時,比如一個矩形、一張位圖,我們不需要打開抗鋸齒。

setColor(int color)

不扯

setColorFilter(ColorFilter filter)

設置顏色過濾,什麼意思呢?就像拿個篩子把顏色“濾”一遍獲取我們想要的色彩結果,感覺像是扯蛋白說一樣是不是?沒事我們慢慢說你一定會懂,這個方法需要我們傳入一個ColorFilter參數同樣也會返回一個ColorFilter實例,那麼ColorFilter類是什麼呢?追蹤源碼進去你會發現其裏面很簡單幾乎沒有:

  1. public class ColorFilter {  
  2.     int native_instance;  
  3.   
  4.     /** 
  5.      * @hide 
  6.      */  
  7.     public int nativeColorFilter;  
  8.   
  9.     protected void finalize() throws Throwable {  
  10.         try {  
  11.             super.finalize();  
  12.         } finally {  
  13.             finalizer(native_instance, nativeColorFilter);  
  14.         }  
  15.     }  
  16.   
  17.     private static native void finalizer(int native_instance, int nativeColorFilter);  
  18. }  
壓根沒有和圖像處理相關的方法對吧,那麼說明該類必定是個父類或者說其必有一定的子類去實現一些方法,查看API文檔發現果然有三個子類:

ColorMatrixColorFilter、LightingColorFilter和PorterDuffColorFilter,也就是說我們在setColorFilter(ColorFilter filter)的時候可以直接傳入這三個子類對象作爲參數,那麼這三個子類又是什麼東西呢?首先我們來看看

ColorMatrixColorFilter

中文直譯爲色彩矩陣顏色過濾器,要明白這玩意你得先了解什麼是色彩矩陣。在Android中圖片是以RGBA像素點的形式加載到內存中的,修改這些像素信息需要一個叫做ColorMatrix類的支持,其定義了一個4x5的float[]類型的矩陣:

  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         10000,  
  3.         01000,  
  4.         00100,  
  5.         00010,  
  6. });  
其中,第一行表示的R(紅色)的向量,第二行表示的G(綠色)的向量,第三行表示的B(藍色)的向量,最後一行表示A(透明度)的向量,這一順序必須要正確不能混淆!這個矩陣不同的位置表示的RGBA值,其範圍在0.0F至2.0F之間,1爲保持原圖的RGB值。每一行的第五列數字表示偏移值,何爲偏移值?顧名思義當我們想讓顏色更傾向於紅色的時候就增大R向量中的偏移值,想讓顏色更傾向於藍色的時候就增大B向量中的偏移值,這是最最樸素的理解,但是事實上色彩偏移的概念是基於白平衡來理解的,什麼是白平衡呢?說得簡單點就是白色是什麼顏色!如果大家是個單反愛好者或者會些PS就會很容易理解這個概念,在單反的設置參數中有個色彩偏移,其定義的就是白平衡的色彩偏移值,就是當你去拍一張照片的時候白色是什麼顏色的,在正常情況下白色是(255, 255, 255, 255)但是現實世界中我們是無法找到這樣的純白物體的,所以在我們用單反拍照之前就會拿一個我們認爲是白色的物體讓相機記錄這個物體的顏色作爲白色,然後拍攝時整張照片的顏色都會依據這個定義的白色來偏移!而這個我們定義的“白色”(比如:255, 253, 251, 247)和純白(255, 255, 255, 255)之間的偏移值(0, 2, 4, 8)我們稱之爲白平衡的色彩偏移。如果你不理解不要緊,自定義控件系列完結後緊接着就是設計色彩基礎~~~~在這你就像我開頭說的那樣樸素地理解下就好。

那麼說了這麼多,這玩意到底有啥用呢?我們來做個test!還是接着昨天那個圓環,不過我們今天把它改成繪製一個圓並且去掉線程動畫的效果因爲我們不需要:

  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Context mContext;// 上下文環境引用  
  4.   
  5.     public CustomView(Context context) {  
  6.         this(context, null);  
  7.     }  
  8.   
  9.     public CustomView(Context context, AttributeSet attrs) {  
  10.         super(context, attrs);  
  11.         mContext = context;  
  12.   
  13.         // 初始化畫筆  
  14.         initPaint();  
  15.     }  
  16.   
  17.     /** 
  18.      * 初始化畫筆 
  19.      */  
  20.     private void initPaint() {  
  21.         // 實例化畫筆並打開抗鋸齒  
  22.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  23.   
  24.         /* 
  25.          * 設置畫筆樣式爲描邊,圓環嘛……當然不能填充不然就麼意思了 
  26.          *  
  27.          * 畫筆樣式分三種:  
  28.          * 1.Paint.Style.STROKE:描邊  
  29.          * 2.Paint.Style.FILL_AND_STROKE:描邊並填充 
  30.          * 3.Paint.Style.FILL:填充 
  31.          */  
  32.         mPaint.setStyle(Paint.Style.FILL);  
  33.   
  34.         // 設置畫筆顏色爲自定義顏色  
  35.         mPaint.setColor(Color.argb(255255128103));  
  36.   
  37.         /* 
  38.          * 設置描邊的粗細,單位:像素px 注意:當setStrokeWidth(0)的時候描邊寬度並不爲0而是隻佔一個像素 
  39.          */  
  40.         mPaint.setStrokeWidth(10);  
  41.     }  
  42.   
  43.     @Override  
  44.     protected void onDraw(Canvas canvas) {  
  45.         super.onDraw(canvas);  
  46.   
  47.         // 繪製圓形  
  48.         canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2200, mPaint);  
  49.     }  
  50. }  
運行下是一個橙紅色的圓~~是不是有點蘿蔔頭國旗幟的感腳?

下面我們爲Paint設置一個色彩矩陣:

  1. // 生成色彩矩陣  
  2. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  3.         10000,  
  4.         01000,  
  5.         00100,  
  6.         00010,  
  7. });  
  8. mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));  
再次運行發現沒變化啊!!!草!!!是不是感覺被我坑了?如果你真的那麼認爲我只能說你壓根就沒認真看上面的文字,我說過什麼?值爲1時表示什麼?表示不改變原色彩的值!!這時我們改變色彩矩陣:
  1. // 生成色彩矩陣  
  2. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  3.         0.5F, 0000,  
  4.         00.5F, 000,  
  5.         000.5F, 00,  
  6.         00010,  
  7. });  
  8. mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));  
再次運行:

是不是明顯不一樣了?顏色變深了便淳厚了!我們通過色彩矩陣與原色彩的計算得出的色彩就是這樣的。那它們是如何計算的呢?其實說白了就是矩陣之間的運算乘積:


矩陣ColorMatrix的一行乘以矩陣MyColor的一列作爲矩陣Result的一行,這裏MyColor的RGBA值我們需要轉換爲[0, 1]。那麼我們依據此公式來計算下我們得到的RGBA值是否跟我們計算得出來的圓的RGBA值一樣:


我們計算得出最後的RGBA值應該爲:0.5, 0.25, 0.2, 1;有興趣的童鞋可以去PS之類的繪圖軟件裏試試看正不正確對不對~~~這裏就不演示了!看完這裏有朋友又會說了,這玩意有毛線用啊!改個顏色還這麼複雜!勞資直接setColor多爽!!沒錯,你這樣想是對的,因爲畢竟我們只是一個顏色,可是如果是一張圖片呢????一張圖片可有還幾十萬色彩呢!!!你麻痹你跟我說setColor?那麼我們換張圖片來試試唄!看看是什麼樣的效果:

  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Context mContext;// 上下文環境引用  
  4.     private Bitmap bitmap;// 位圖  
  5.       
  6.     private int x,y;// 位圖繪製時左上角的起點座標  
  7.   
  8.     public CustomView(Context context) {  
  9.         this(context, null);  
  10.     }  
  11.   
  12.     public CustomView(Context context, AttributeSet attrs) {  
  13.         super(context, attrs);  
  14.         mContext = context;  
  15.   
  16.         // 初始化畫筆  
  17.         initPaint();  
  18.           
  19.         //初始化資源  
  20.         initRes(context);  
  21.     }  
  22.   
  23.     /** 
  24.      * 初始化畫筆 
  25.      */  
  26.     private void initPaint() {  
  27.         // 實例化畫筆  
  28.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  29.     }  
  30.       
  31.     /** 
  32.      * 初始化資源 
  33.      */  
  34.     private void initRes(Context context) {  
  35.         // 獲取位圖  
  36.         bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);  
  37.           
  38.         /* 
  39.          * 計算位圖繪製時左上角的座標使其位於屏幕中心 
  40.          * 屏幕座標x軸向左偏移位圖一半的寬度 
  41.          * 屏幕座標y軸向上偏移位圖一半的高度 
  42.          */  
  43.         x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;  
  44.         y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;  
  45.     }  
  46.   
  47.     @Override  
  48.     protected void onDraw(Canvas canvas) {  
  49.         super.onDraw(canvas);  
  50.   
  51.         // 繪製位圖  
  52.         canvas.drawBitmap(bitmap, x, y, mPaint);  
  53.     }  
  54. }  
如代碼所示我們清除了所有的畫筆屬性設置因爲沒必要,從資源獲取一個Bitmap繪製在畫布上:

一張灰常漂亮的風景圖,好!現在我們來爲我們的畫筆添加一個顏色過濾:

  1. // 生成色彩矩陣  
  2. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  3.         0.5F, 0000,  
  4.         00.5F, 000,  
  5.         000.5F, 00,  
  6.         00010,  
  7. });  
  8. mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));  
大家看到還是剛纔那個色彩矩陣,運行下看看什麼效果呢:

變暗了對吧!沒意思,我們來點更刺激的,改下ColorMatrix矩陣:

  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         0.33F, 0.59F, 0.11F, 00,  
  3.         0.33F, 0.59F, 0.11F, 00,  
  4.         0.33F, 0.59F, 0.11F, 00,  
  5.         00010,  
  6. });  

噢!變灰了!還是沒意思!繼續改:

  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         -10011,  
  3.         0, -1011,  
  4.         00, -111,  
  5.         00010,  
  6. });  

喲呵!!是不是有點類似PS裏反相的效果?我們常看到的圖片都是RGB的,顛覆一下思維,看看BGR的試試:

  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         00100,  
  3.         01000,  
  4.         10000,  
  5.         00010,  
  6. });  

這樣紅色的變成了藍色而藍色的就變成了紅色,繼續改:

  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         0.393F, 0.769F, 0.189F, 00,  
  3.         0.349F, 0.686F, 0.168F, 00,  
  4.         0.272F, 0.534F, 0.131F, 00,  
  5.         00010,  
  6. });  

是不是有點類似於老舊照片的感腳?繼續:

  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         1.5F, 1.5F, 1.5F, 0, -1,  
  3.         1.5F, 1.5F, 1.5F, 0, -1,  
  4.         1.5F, 1.5F, 1.5F, 0, -1,  
  5.         00010,  
  6. });  

類似去色後高對比度的效果,繼續:

  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         1.438F, -0.122F, -0.016F, 0, -0.03F,  
  3.         -0.062F, 1.378F, -0.016F, 00.05F,  
  4.         -0.062F, -0.122F, 1.483F, 0, -0.02F,  
  5.         00010,  
  6. });  

飽和度對比度加強,好了不演示了……累死我了!截圖粘貼上傳!!

這些各種各樣的圖像效果在哪見過?PS?對的!還有各種拍照軟件拍攝後的特效處理!大致原理都是這麼來的!有人會問愛哥你傻逼麼!這麼多參數怎麼玩!誰記得!而且TMD用參數調顏色?我映像中都是直接在各種繪圖軟件(比如PS)裏拖進度條的!這怎麼玩!淡定!如我所說很多時候你壓根不需要了解太多原理,只需站在巨人的丁丁上即可,所以稍安勿躁!再下一個系列教程“設計色彩”中愛哥教你玩轉色彩並且讓設計和開發無縫結合!

ColorMatrixColorFilter和ColorMatrix就是這麼個東西,ColorMatrix類裏面也提供了一些實在的方法,比如setSaturation(float sat)設置飽和度,而且ColorMatrix每個方法都用了陣列的計算,如果大家感興趣可以自己去深挖來看不過我是真心不推薦的~~~

下面我們來看看ColorFilter的另一個子類

LightingColorFilter

顧名思義光照顏色過濾,這肯定是跟光照是有關的了~~該類有且只有一個構造方法:

  1. LightingColorFilter (int mul, int add)  
這個方法非常非常地簡單!mul全稱是colorMultiply意爲色彩倍增,而add全稱是colorAdd意爲色彩添加,這兩個值都是16進制的色彩值0xAARRGGBB。這個方法使用也是非常的簡單。還是拿上面那張圖片來說吧,比如我們想要去掉綠色:
  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Context mContext;// 上下文環境引用  
  4.     private Bitmap bitmap;// 位圖  
  5.   
  6.     private int x, y;// 位圖繪製時左上角的起點座標  
  7.   
  8.     public CustomView(Context context) {  
  9.         this(context, null);  
  10.     }  
  11.   
  12.     public CustomView(Context context, AttributeSet attrs) {  
  13.         super(context, attrs);  
  14.         mContext = context;  
  15.   
  16.         // 初始化畫筆  
  17.         initPaint();  
  18.   
  19.         // 初始化資源  
  20.         initRes(context);  
  21.     }  
  22.   
  23.     /** 
  24.      * 初始化畫筆 
  25.      */  
  26.     private void initPaint() {  
  27.         // 實例化畫筆  
  28.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  29.   
  30.         // 設置顏色過濾  
  31.         mPaint.setColorFilter(new LightingColorFilter(0xFFFF00FF0x00000000));  
  32.     }  
  33.   
  34.     /** 
  35.      * 初始化資源 
  36.      */  
  37.     private void initRes(Context context) {  
  38.         // 獲取位圖  
  39.         bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);  
  40.   
  41.         /* 
  42.          * 計算位圖繪製時左上角的座標使其位於屏幕中心 
  43.          * 屏幕座標x軸向左偏移位圖一半的寬度 
  44.          * 屏幕座標y軸向上偏移位圖一半的高度 
  45.          */  
  46.         x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;  
  47.         y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;  
  48.     }  
  49.   
  50.     @Override  
  51.     protected void onDraw(Canvas canvas) {  
  52.         super.onDraw(canvas);  
  53.   
  54.         // 繪製位圖  
  55.         canvas.drawBitmap(bitmap, x, y, mPaint);  
  56.     }  
  57. }  
運行後你會發現綠色確實是沒了但是原來偏綠的部分現在居然成了紅色,爲毛!敬請關注下一系列設計色彩文章!!哈哈哈!!當LightingColorFilter(0xFFFFFFFF, 0x00000000)的時候原圖是不會有任何改變的,如果我們想增加紅色的值,那麼LightingColorFilter(0xFFFFFFFF, 0x00XX0000)就好,其中XX取值爲00至FF。那麼這個方法有什麼存在的意義呢?存在必定合理,這個方法存在一定是有它可用之處的,前些天有個盆友在羣裏問點擊一個圖片如何直接改變它的顏色而不是爲他多準備另一張點擊效果的圖片,這種情況下該方法就派上用場了!如下圖一個灰色的星星,我們點擊後讓它變成黃色

代碼如下,註釋很清楚我就不再多說了:

  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Context mContext;// 上下文環境引用  
  4.     private Bitmap bitmap;// 位圖  
  5.   
  6.     private int x, y;// 位圖繪製時左上角的起點座標  
  7.     private boolean isClick;// 用來標識控件是否被點擊過  
  8.   
  9.     public CustomView(Context context) {  
  10.         this(context, null);  
  11.     }  
  12.   
  13.     public CustomView(Context context, AttributeSet attrs) {  
  14.         super(context, attrs);  
  15.         mContext = context;  
  16.   
  17.         // 初始化畫筆  
  18.         initPaint();  
  19.   
  20.         // 初始化資源  
  21.         initRes(context);  
  22.   
  23.         setOnClickListener(new OnClickListener() {  
  24.             @Override  
  25.             public void onClick(View v) {  
  26.                 /* 
  27.                  * 判斷控件是否被點擊過 
  28.                  */  
  29.                 if (isClick) {  
  30.                     // 如果已經被點擊了則點擊時設置顏色過濾爲空還原本色  
  31.                     mPaint.setColorFilter(null);  
  32.                     isClick = false;  
  33.                 } else {  
  34.                     // 如果未被點擊則點擊時設置顏色過濾後爲黃色  
  35.                     mPaint.setColorFilter(new LightingColorFilter(0xFFFFFFFF0X00FFFF00));  
  36.                     isClick = true;  
  37.                 }  
  38.   
  39.                 // 記得重繪  
  40.                 invalidate();  
  41.             }  
  42.         });  
  43.     }  
  44.   
  45.     /** 
  46.      * 初始化畫筆 
  47.      */  
  48.     private void initPaint() {  
  49.         // 實例化畫筆  
  50.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  51.     }  
  52.   
  53.     /** 
  54.      * 初始化資源 
  55.      */  
  56.     private void initRes(Context context) {  
  57.         // 獲取位圖  
  58.         bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a2);  
  59.   
  60.         /* 
  61.          * 計算位圖繪製時左上角的座標使其位於屏幕中心 
  62.          * 屏幕座標x軸向左偏移位圖一半的寬度 
  63.          * 屏幕座標y軸向上偏移位圖一半的高度 
  64.          */  
  65.         x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;  
  66.         y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;  
  67.     }  
  68.   
  69.     @Override  
  70.     protected void onDraw(Canvas canvas) {  
  71.         super.onDraw(canvas);  
  72.   
  73.         // 繪製位圖  
  74.         canvas.drawBitmap(bitmap, x, y, mPaint);  
  75.     }  
  76. }  
運行後點擊星星即可變成黃色再點擊變回灰色,當我們不想要顏色過濾的效果時,setColorFilter(null)並重繪視圖即可!那麼爲什麼要叫光照顏色過濾呢?原因很簡單,因爲它所呈現的效果就像有色光照在物體上染色一樣~~~哎,不說這方法了,看下一個也是最後一個ColorFilter的子類。
PorterDuffColorFilter

PorterDuffColorFilter跟LightingColorFilter一樣,只有一個構造方法:

  1. PorterDuffColorFilter(int color, PorterDuff.Mode mode)  
這個構造方法也接受兩個值,一個是16進製表示的顏色值這個很好理解,而另一個是PorterDuff內部類Mode中的一個常量值,這個值表示混合模式。那麼什麼是混合模式呢?混合混合必定是有兩種東西混才行,第一種就是我們設置的color值而第二種當然就是我們畫布上的元素了!,比如這裏我們把Color的值設爲紅色,而模式設爲PorterDuff.Mode.DARKEN變暗:
  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Context mContext;// 上下文環境引用  
  4.     private Bitmap bitmap;// 位圖  
  5.   
  6.     private int x, y;// 位圖繪製時左上角的起點座標  
  7.   
  8.     public CustomView(Context context) {  
  9.         this(context, null);  
  10.     }  
  11.   
  12.     public CustomView(Context context, AttributeSet attrs) {  
  13.         super(context, attrs);  
  14.         mContext = context;  
  15.   
  16.         // 初始化畫筆  
  17.         initPaint();  
  18.   
  19.         // 初始化資源  
  20.         initRes(context);  
  21.     }  
  22.   
  23.     /** 
  24.      * 初始化畫筆 
  25.      */  
  26.     private void initPaint() {  
  27.         // 實例化畫筆  
  28.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  29.   
  30.         // 設置顏色過濾  
  31.         mPaint.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.DARKEN));  
  32.     }  
  33.   
  34.     /** 
  35.      * 初始化資源 
  36.      */  
  37.     private void initRes(Context context) {  
  38.         // 獲取位圖  
  39.         bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);  
  40.   
  41.         /* 
  42.          * 計算位圖繪製時左上角的座標使其位於屏幕中心 
  43.          * 屏幕座標x軸向左偏移位圖一半的寬度 
  44.          * 屏幕座標y軸向上偏移位圖一半的高度 
  45.          */  
  46.         x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;  
  47.         y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;  
  48.     }  
  49.   
  50.     @Override  
  51.     protected void onDraw(Canvas canvas) {  
  52.         super.onDraw(canvas);  
  53.   
  54.         // 繪製位圖  
  55.         canvas.drawBitmap(bitmap, x, y, mPaint);  
  56.     }  
  57. }  
我們嘗試在畫布上Draw剛纔的那張圖片看看:

變暗了……也變紅了……這就是PorterDuff.Mode.DARKEN模式給我們的效果,當然PorterDuff.Mode還有其他很多的混合模式,大家可以嘗試,但是這裏要注意一點,PorterDuff.Mode中的模式不僅僅是應用於圖像色彩混合,還應用於圖形混合,比如PorterDuff.Mode.DST_OUT就表示裁剪混合圖,如果我們在PorterDuffColorFilter中強行設置這些圖形混合的模式將不會看到任何對應的效果,關於圖形混合我們將在下面詳解。

爲了提升大家的學習興趣也算是擴展,我跟大家說說PS中的圖層混合模式,在PS中圖層的混合模式跟我們PorterDuff.Mode提供的其實是差不多的但是更霸氣強大,同樣我們還是使用上面那張圖來說明:


如圖所示,Layer 1我們填充了一層紅色而Layer 0是我們的圖片,這時我們選擇混合模式爲Darken(默認爲Normal)是不是連名字都跟Android的一樣:


效果也必須是一樣的:


PS的圖層混合模式比Android更多更廣泛,但兩者同名混合模式所產生的效果是一樣的,也基於同樣的算法原理這裏就不多說了。

下面我們來看另一個跟setColorFilter有幾分相似的方法。

setXfermode(Xfermode xfermode)

Xfermode國外有大神稱之爲過渡模式,這種翻譯比較貼切但恐怕不易理解,大家也可以直接稱之爲圖像混合模式,因爲所謂的“過渡”其實就是圖像混合的一種,這個方法跟我們上面講到的setColorFilter蠻相似的,首先它與set一樣沒有公開的實現的方法:

  1. public class Xfermode {  
  2.   
  3.     protected void finalize() throws Throwable {  
  4.         try {  
  5.             finalizer(native_instance);  
  6.         } finally {  
  7.             super.finalize();  
  8.         }  
  9.     }  
  10.   
  11.     private static native void finalizer(int native_instance);  
  12.   
  13.     int native_instance;  
  14. }  
同理可得其必然有一定的子類去實現一些方法供我們使用,查看API文檔發現其果然有三個子類:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode,這三個子類實現的功能要比setColorFilter的三個子類複雜得多,主要是是涉及到圖像處理的一些知識可能對大家來說會比較難以理解,不過我會盡量以通俗的方式闡述它們的作用,那好先來看看我們的第一個子類

AvoidXfermode

首先我要告訴大家的是這個API因爲不支持硬件加速在API 16已經過時了(大家可以在HardwareAccel查看那些方法不支持硬件加速)……如果想在高於API 16的機子上測試這玩意,必須現在應用或手機設置中關閉硬件加速,在應用中我們可以通過在AndroidManifest.xml文件中設置application節點下的android:hardwareAccelerated屬性爲false來關閉硬件加速:

  1. <application  
  2.     android:allowBackup="true"  
  3.     android:hardwareAccelerated="false"  
  4.     android:icon="@drawable/ic_launcher"  
  5.     android:label="@string/app_name"  
  6.     android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" >  
  7.     <activity  
  8.         android:name="com.aigestudio.customviewdemo.activities.MainActivity"  
  9.         android:label="@string/app_name" >  
  10.         <intent-filter>  
  11.             <action android:name="android.intent.action.MAIN" />  
  12.   
  13.             <category android:name="android.intent.category.LAUNCHER" />  
  14.         </intent-filter>  
  15.     </activity>  
  16. </application>  

AvoidXfermode只有一個含參的構造方法AvoidXfermode(int opColor, int tolerance, AvoidXfermode.Mode mode),其具體實現和ColorFilter一樣都被封裝在C/C++內,它怎麼實現我們不管我們只要知道這玩意怎麼用就行對吧。AvoidXfermode有三個參數,第一個opColor表示一個16進制的可以帶透明通道的顏色值例如0x12345678,第二個參數tolerance表示容差值,那麼什麼是容差呢?你可以理解爲一個可以標識“精確”或“模糊”的東西,待會我們細講,最後一個參數表示AvoidXfermode的具體模式,其可選值只有兩個:AvoidXfermode.Mode.AVOID或者AvoidXfermode.Mode.TARGET,兩者的意思也非常簡單,我們先來看

AvoidXfermode.Mode.TARGET

在該模式下Android會判斷畫布上的顏色是否會有跟opColor不一樣的顏色,比如我opColor是紅色,那麼在TARGET模式下就會去判斷我們的畫布上是否有存在紅色的地方,如果有,則把該區域“染”上一層我們畫筆定義的顏色,否則不“染”色,而tolerance容差值則表示畫布上的像素和我們定義的紅色之間的差別該是多少的時候纔去“染”的,比如當前畫布有一個像素的色值是(200, 20, 13),而我們的紅色值爲(255, 0, 0),當tolerance容差值爲255時,即便(200, 20, 13)並不等於紅色值也會被“染”色,容差值越大“染”色範圍越廣反之則反,空說無憑我們來看看具體的實現和效果:

  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Context mContext;// 上下文環境引用  
  4.     private Bitmap bitmap;// 位圖  
  5.     private AvoidXfermode avoidXfermode;// AV模式  
  6.   
  7.     private int x, y, w, h;// 位圖繪製時左上角的起點座標  
  8.   
  9.     public CustomView(Context context) {  
  10.         this(context, null);  
  11.     }  
  12.   
  13.     public CustomView(Context context, AttributeSet attrs) {  
  14.         super(context, attrs);  
  15.         mContext = context;  
  16.   
  17.         // 初始化畫筆  
  18.         initPaint();  
  19.   
  20.         // 初始化資源  
  21.         initRes(context);  
  22.     }  
  23.   
  24.     /** 
  25.      * 初始化畫筆 
  26.      */  
  27.     private void initPaint() {  
  28.         // 實例化畫筆  
  29.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  30.   
  31.         /* 
  32.          * 當畫布中有跟0XFFFFFFFF色不一樣的地方時候才“染”色 
  33.          */  
  34.         avoidXfermode = new AvoidXfermode(0XFFFFFFFF0, AvoidXfermode.Mode.TARGET);  
  35.     }  
  36.   
  37.     /** 
  38.      * 初始化資源 
  39.      */  
  40.     private void initRes(Context context) {  
  41.         // 獲取位圖  
  42.         bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);  
  43.   
  44.         /* 
  45.          * 計算位圖繪製時左上角的座標使其位於屏幕中心 
  46.          * 屏幕座標x軸向左偏移位圖一半的寬度 
  47.          * 屏幕座標y軸向上偏移位圖一半的高度 
  48.          */  
  49.         x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;  
  50.         y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;  
  51.         w = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 + bitmap.getWidth() / 2;  
  52.         h = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 + bitmap.getHeight() / 2;  
  53.     }  
  54.   
  55.     @Override  
  56.     protected void onDraw(Canvas canvas) {  
  57.         super.onDraw(canvas);  
  58.   
  59.         // 先繪製位圖  
  60.         canvas.drawBitmap(bitmap, x, y, mPaint);  
  61.   
  62.         // “染”什麼色是由我們自己決定的  
  63.         mPaint.setARGB(25521153243);  
  64.   
  65.         // 設置AV模式  
  66.         mPaint.setXfermode(avoidXfermode);  
  67.   
  68.         // 畫一個位圖大小一樣的矩形  
  69.         canvas.drawRect(x, y, w, h, mPaint);  
  70.     }  
  71. }  
在高於API 16的測試機上會得到一個矩形的色塊(API 16+的都類似,改ROM和關閉了硬件加速的除外):

我們再用低於API 16(或高於API 16但關閉了硬件加速)的測試機運行就會得到另一個不同的效果:


大家可以看到,在我們的模式爲TARGET容差值爲0的時候此時只有當圖片中像色顏色值爲0XFFFFFFFF的地方纔會被染色,而其他地方不會有改變

AvoidXfermode(0XFFFFFFFF, 0, AvoidXfermode.Mode.TARGET):


而當容差值爲255的時候只要是跟0XFFFFFFFF有點接近的地方都會被染色

而另外一種模式

AvoidXfermode.Mode.AVOID

則與TARGET恰恰相反,TARGET是我們指定的顏色是否與畫布的顏色一樣,而AVOID是我們指定的顏色是否與畫布不一樣,其他的都與TARGET類似

AvoidXfermode(0XFFFFFFFF, 0, AvoidXfermode.Mode.AVOID):


當模式爲AVOID容差值爲0時,只有當圖片中像素顏色值與0XFFFFFFFF完全不一樣的地方纔會被染色

AvoidXfermode(0XFFFFFFFF, 255, AvoidXfermode.Mode.AVOID):


當容差值爲255時,只要與0XFFFFFFFF稍微有點不一樣的地方就會被染色

那麼這玩意究竟有什麼用呢?比如說當我們只想在白色的區域畫點東西或者想把白色區域的地方替換爲另一張圖片的時候就可以採取這種方式!

Xfermode的第二個子類

PixelXorXfermode

與AvoidXfermode一樣也在API 16過時了,該類也提供了一個含參的構造方法PixelXorXfermode(int opColor),該類的計算實現很簡單,從官方給出的計算公式來看就是:op ^ src ^ dst,像素色值的按位異或運算,如果大家感興趣,可以自己用一個純色去嘗試,並自己計算異或運算的值是否與得出的顏色值一樣,這裏我就不講了,Because it was deprecated and useless。

Xfermode的最後一個子類也是惟一一個沒有過時且沿用至今的子類

PorterDuffXfermode

該類同樣有且只有一個含參的構造方法PorterDuffXfermode(PorterDuff.Mode mode),這個PorterDuff.Mode大家看後是否會有些面熟,它跟上面我們講ColorFilter時候用到的PorterDuff.Mode是一樣的!麻雀雖小五臟俱全,雖說構造方法的簽名列表裏只有一個PorterDuff.Mode的參數,但是它可以實現很多酷斃的圖形效果!!而PorterDuffXfermode就是圖形混合模式的意思,其概念最早來自於SIGGRAPH的Tomas Proter和Tom Duff,混合圖形的概念極大地推動了圖形圖像學的發展,延伸到計算機圖形圖像學像Adobe和AutoDesk公司著名的多款設計軟件都可以說一定程度上受到影響,而我們PorterDuffXfermode的名字也來源於這倆人的人名組合PorterDuff,那PorterDuffXfermode能做些什麼呢?我們先來看一張API DEMO裏的圖片:


這張圖片從一定程度上形象地說明了圖形混合的作用,兩個圖形一圓一方通過一定的計算產生不同的組合效果,在API中Android爲我們提供了18種(比上圖多了兩種ADD和OVERLAY)模式:


來定義不同的混合效果,這18種模式Android還爲我們提供了它們的計算方式比如LIGHTEN的計算方式爲[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)],其中Sa全稱爲Source alpha表示源圖的Alpha通道;Sc全稱爲Source color表示源圖的顏色;Da全稱爲Destination alpha表示目標圖的Alpha通道;Dc全稱爲Destination color表示目標圖的顏色,細心的朋友會發現“[……]”裏分爲兩部分,其中“,”前的部分爲“Sa + Da - Sa*Da”這一部分的值代表計算後的Alpha通道而“,”後的部分爲“Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)”這一部分的值代表計算後的顏色值,圖形混合後的圖片依靠這個矢量來計算ARGB的值,如果大家感興趣可以查看維基百科中對Alpha合成的解釋:http://en.wikipedia.org/wiki/Alpha_compositing。作爲一個猿,我們不需要知道複雜的圖形學計算但是一定要知道這些模式會爲我們提供怎樣的效果,當大家看到上面API DEMO給出的效果時一定會覺得PorterDuffXfermode其實就是簡單的圖形交併集計算,比如重疊的部分刪掉或者疊加等等,事實上呢!PorterDuffXfermode的計算絕非是根據於此!上面我們也說了PorterDuffXfermode的計算是要根據具體的Alpha值和RGB值的,既然如此,我們就來看一個比API DEMO稍微複雜的例子來更有力地說明PorterDuffXfermode是如何工作而我們又能用它做些什麼,在這個例子中我將用到兩個帶有Alpha通道的漸變圖形Bitmap:


我們將在不同的模式下混合這兩個Bitmap來看看這兩個漸變色的顏色值在不同的混合模式下究竟發生了什麼?先看看我們的測試代碼:

  1. @TargetApi(Build.VERSION_CODES.HONEYCOMB)  
  2. public class PorterDuffView extends View {  
  3.     /* 
  4.      * PorterDuff模式常量 
  5.      * 可以在此更改不同的模式測試 
  6.      */  
  7.     private static final PorterDuff.Mode MODE = PorterDuff.Mode.ADD;  
  8.   
  9.     private static final int RECT_SIZE_SMALL = 400;// 左右上方示例漸變正方形的尺寸大小  
  10.     private static final int RECT_SIZE_BIG = 800;// 中間測試漸變正方形的尺寸大小  
  11.   
  12.     private Paint mPaint;// 畫筆  
  13.   
  14.     private PorterDuffBO porterDuffBO;// PorterDuffView類的業務對象  
  15.     private PorterDuffXfermode porterDuffXfermode;// 圖形混合模式  
  16.   
  17.     private int screenW, screenH;// 屏幕尺寸  
  18.     private int s_l, s_t;// 左上方正方形的原點座標  
  19.     private int d_l, d_t;// 右上方正方形的原點座標  
  20.     private int rectX, rectY;// 中間正方形的原點座標  
  21.   
  22.     public PorterDuffView(Context context, AttributeSet attrs) {  
  23.         super(context, attrs);  
  24.   
  25.         // 實例化畫筆並設置抗鋸齒  
  26.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  27.   
  28.         // 實例化業務對象  
  29.         porterDuffBO = new PorterDuffBO();  
  30.   
  31.         // 實例化混合模式  
  32.         porterDuffXfermode = new PorterDuffXfermode(MODE);  
  33.   
  34.         // 計算座標  
  35.         calu(context);  
  36.     }  
  37.   
  38.     /** 
  39.      * 計算座標 
  40.      *  
  41.      * @param context 
  42.      *            上下文環境引用 
  43.      */  
  44.     private void calu(Context context) {  
  45.         // 獲取包含屏幕尺寸的數組  
  46.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  47.   
  48.         // 獲取屏幕尺寸  
  49.         screenW = screenSize[0];  
  50.         screenH = screenSize[1];  
  51.   
  52.         // 計算左上方正方形原點座標  
  53.         s_l = 0;  
  54.         s_t = 0;  
  55.   
  56.         // 計算右上方正方形原點座標  
  57.         d_l = screenW - RECT_SIZE_SMALL;  
  58.         d_t = 0;  
  59.   
  60.         // 計算中間方正方形原點座標  
  61.         rectX = screenW / 2 - RECT_SIZE_BIG / 2;  
  62.         rectY = RECT_SIZE_SMALL + (screenH - RECT_SIZE_SMALL) / 2 - RECT_SIZE_BIG / 2;  
  63.     }  
  64.   
  65.     @Override  
  66.     protected void onDraw(Canvas canvas) {  
  67.         super.onDraw(canvas);  
  68.         // 設置畫布顏色爲黑色以便我們更好地觀察  
  69.         canvas.drawColor(Color.BLACK);  
  70.   
  71.         // 設置業務對象尺寸值計算生成左右上方的漸變方形  
  72.         porterDuffBO.setSize(RECT_SIZE_SMALL);  
  73.   
  74.         /* 
  75.          * 畫出左右上方兩個正方形 
  76.          * 其中左邊的的爲src右邊的爲dis 
  77.          */  
  78.         canvas.drawBitmap(porterDuffBO.initSrcBitmap(), s_l, s_t, mPaint);  
  79.         canvas.drawBitmap(porterDuffBO.initDisBitmap(), d_l, d_t, mPaint);  
  80.   
  81.         /* 
  82.          * 將繪製操作保存到新的圖層(更官方的說法應該是離屏緩存)我們將在1/3中學習到Canvas的全部用法這裏就先follow me 
  83.          */  
  84.         int sc = canvas.saveLayer(00, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);  
  85.   
  86.         // 重新設置業務對象尺寸值計算生成中間的漸變方形  
  87.         porterDuffBO.setSize(RECT_SIZE_BIG);  
  88.   
  89.         // 先繪製dis目標圖  
  90.         canvas.drawBitmap(porterDuffBO.initDisBitmap(), rectX, rectY, mPaint);  
  91.   
  92.         // 設置混合模式  
  93.         mPaint.setXfermode(porterDuffXfermode);  
  94.   
  95.         // 再繪製src源圖  
  96.         canvas.drawBitmap(porterDuffBO.initSrcBitmap(), rectX, rectY, mPaint);  
  97.   
  98.         // 還原混合模式  
  99.         mPaint.setXfermode(null);  
  100.   
  101.         // 還原畫布  
  102.         canvas.restoreToCount(sc);  
  103.     }  
  104. }  
代碼中我們使用到了View的離屏緩衝,也通俗地稱之爲層,這個概念很簡單,我們在繪圖的時候新建一個“層”,所有的繪製操作都在該層上而不影響該層以外的圖像,比如代碼中我們在繪製了畫布顏色和左右上方兩個方形後就新建了一個圖層來繪製中間的大正方形,這個方形和左右上方的方形是在兩個不同的層上的:

注:圖中所顯示色彩效果與我們的代碼不同,上圖只爲演示圖層概念

當我們繪製完成後要通過restore將所有緩衝(層)中的繪製操作還原到畫布以結束繪製,具體關於畫布的知識在自定義控件其實很簡單1/3,這裏就不多說了,下面我們看具體各種模式的計算效果

PS:Src爲源圖像,意爲將要繪製的圖像;Dis爲目標圖像,意爲我們將要把源圖像繪製到的圖像……是不是感腳很拗口 = = !Fuck……意會意會~~

PorterDuff.Mode.ADD

計算方式:Saturate(S + D);Chinese:飽和相加


從計算方式和顯示的結果我們可以看到,ADD模式簡單來說就是對圖像飽和度進行相加,這個模式在應用中不常用,我唯一一次使用它是通過代碼控制RGB通道的融合生成圖片。

PorterDuff.Mode.CLEAR

計算方式:[0, 0];Chinese:清除

清除圖像,很好理解不扯了。

PorterDuff.Mode.DARKEN

計算方式:[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)];Chinese:變暗


這個模式計算方式目測很複雜,其實效果很好理解,兩個圖像混合,較深的顏色總是會覆蓋較淺的顏色,如果兩者深淺相同則混合,如圖,黃色覆蓋了紅色而藍色和青色因爲是跟透明混合所以不變。細心的朋友會發現青色和黃色之間有一層類似橙色的過渡色,這就是混合的結果。在實際的測試中源圖和目標圖的DARKEN混合偶爾會有相反的結果比如紅色覆蓋了黃色,這源於Android對顏色值“深淺”的定義,我暫時沒有在官方查到有關資料不知道是否與圖形圖像學一致。DARKEN模式的應用在圖像色彩方面比較廣泛我們可以利用其特性來獲得不同的成像效果,這點與之前介紹的ColorFilter有點類似。

PorterDuff.Mode.DST

計算方式:[Da, Dc];Chinese:只繪製目標圖像


如Chinese所說,很好理解。

PorterDuff.Mode.DST_ATOP

計算方式:[Sa, Sa * Dc + Sc * (1 - Da)];Chinese:在源圖像和目標圖像相交的地方繪製目標圖像而在不相交的地方繪製源圖像


PorterDuff.Mode.DST_IN

計算方式:[Sa * Da, Sa * Dc];Chinese:只在源圖像和目標圖像相交的地方繪製目標圖像


最常見的應用就是蒙板繪製,利用源圖作爲蒙板“摳出”目標圖上的圖像,這裏我講一個很簡單的例子,如果大家用過PS就很容易理解,我這裏有兩張圖:


一張是一個很漂亮的手繪古典美女:


而另一張是一張只有黑色和透明通道的遮罩圖:


我們把這張美女圖畫在我們的屏幕上:

  1. public class DisInView extends View {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Bitmap bitmapDis;// 位圖  
  4.   
  5.     private int x, y;// 位圖繪製時左上角的起點座標  
  6.     private int screenW, screenH;// 屏幕尺寸  
  7.   
  8.     public DisInView(Context context) {  
  9.         this(context, null);  
  10.     }  
  11.   
  12.     public DisInView(Context context, AttributeSet attrs) {  
  13.         super(context, attrs);  
  14.   
  15.         // 初始化畫筆  
  16.         initPaint();  
  17.   
  18.         // 初始化資源  
  19.         initRes(context);  
  20.     }  
  21.   
  22.     /** 
  23.      * 初始化畫筆 
  24.      */  
  25.     private void initPaint() {  
  26.         // 實例化畫筆  
  27.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  28.     }  
  29.   
  30.     /** 
  31.      * 初始化資源 
  32.      */  
  33.     private void initRes(Context context) {  
  34.         // 獲取位圖  
  35.         bitmapDis = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3);  
  36.   
  37.         // 獲取包含屏幕尺寸的數組  
  38.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  39.   
  40.         // 獲取屏幕尺寸  
  41.         screenW = screenSize[0];  
  42.         screenH = screenSize[1];  
  43.   
  44.         /* 
  45.          * 計算位圖繪製時左上角的座標使其位於屏幕中心 
  46.          * 屏幕座標x軸向左偏移位圖一半的寬度 
  47.          * 屏幕座標y軸向上偏移位圖一半的高度 
  48.          */  
  49.         x = screenW / 2 - bitmapDis.getWidth() / 2;  
  50.         y = screenH / 2 - bitmapDis.getHeight() / 2;  
  51.   
  52.     }  
  53.   
  54.     @Override  
  55.     protected void onDraw(Canvas canvas) {  
  56.         super.onDraw(canvas);  
  57.   
  58.         // 繪製美女圖  
  59.         canvas.drawBitmap(bitmapDis, x, y, mPaint);  
  60.     }  
  61. }  
運行後如下:

美女腦袋上有個文字標識巨噁心而且因爲圖片畫質問題美圖周圍還有一片淡黃色的不好看,那我們就通過剛纔那個黑色的透明通道圖把美女“摳”出來:

  1. public class DisInView extends View {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Bitmap bitmapDis, bitmapSrc;// 位圖  
  4.     private PorterDuffXfermode porterDuffXfermode;// 圖形混合模式  
  5.   
  6.     private int x, y;// 位圖繪製時左上角的起點座標  
  7.     private int screenW, screenH;// 屏幕尺寸  
  8.   
  9.     public DisInView(Context context) {  
  10.         this(context, null);  
  11.     }  
  12.   
  13.     public DisInView(Context context, AttributeSet attrs) {  
  14.         super(context, attrs);  
  15.   
  16.         // 實例化混合模式  
  17.         porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);  
  18.   
  19.         // 初始化畫筆  
  20.         initPaint();  
  21.   
  22.         // 初始化資源  
  23.         initRes(context);  
  24.     }  
  25.   
  26.     /** 
  27.      * 初始化畫筆 
  28.      */  
  29.     private void initPaint() {  
  30.         // 實例化畫筆  
  31.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  32.     }  
  33.   
  34.     /** 
  35.      * 初始化資源 
  36.      */  
  37.     private void initRes(Context context) {  
  38.         // 獲取位圖  
  39.         bitmapDis = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3);  
  40.         bitmapSrc = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3_mask);  
  41.   
  42.         // 獲取包含屏幕尺寸的數組  
  43.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  44.   
  45.         // 獲取屏幕尺寸  
  46.         screenW = screenSize[0];  
  47.         screenH = screenSize[1];  
  48.   
  49.         /* 
  50.          * 計算位圖繪製時左上角的座標使其位於屏幕中心 
  51.          * 屏幕座標x軸向左偏移位圖一半的寬度 
  52.          * 屏幕座標y軸向上偏移位圖一半的高度 
  53.          */  
  54.         x = screenW / 2 - bitmapDis.getWidth() / 2;  
  55.         y = screenH / 2 - bitmapDis.getHeight() / 2;  
  56.   
  57.     }  
  58.   
  59.     @Override  
  60.     protected void onDraw(Canvas canvas) {  
  61.         super.onDraw(canvas);  
  62.         canvas.drawColor(Color.WHITE);  
  63.   
  64.         /* 
  65.          * 將繪製操作保存到新的圖層(更官方的說法應該是離屏緩存)我們將在1/3中學習到Canvas的全部用法這裏就先follow me 
  66.          */  
  67.         int sc = canvas.saveLayer(00, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);  
  68.   
  69.         // 先繪製dis目標圖  
  70.         canvas.drawBitmap(bitmapDis, x, y, mPaint);  
  71.   
  72.         // 設置混合模式  
  73.         mPaint.setXfermode(porterDuffXfermode);  
  74.   
  75.         // 再繪製src源圖  
  76.         canvas.drawBitmap(bitmapSrc, x, y, mPaint);  
  77.   
  78.         // 還原混合模式  
  79.         mPaint.setXfermode(null);  
  80.   
  81.         // 還原畫布  
  82.         canvas.restoreToCount(sc);  
  83.     }  
  84. }  
看!只剩米女了~~~~:

當然該混合模式的用法絕不止這麼簡單,這只是闡述了一個原理,更棒的用法就看你怎麼用了~~~~

PorterDuff.Mode.DST_OUT

計算方式:[Da * (1 - Sa), Dc * (1 - Sa)];Chinese:只在源圖像和目標圖像不相交的地方繪製目標圖像

上面那個例子呢我們把米女摳了出來,而這次我們將從一個色塊中把米女的輪廓挖出來~~~~啦啦啦:

  1. public class DisOutView extends View {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Bitmap bitmapSrc;// 位圖  
  4.     private PorterDuffXfermode porterDuffXfermode;// 圖形混合模式  
  5.   
  6.     private int x, y;// 位圖繪製時左上角的起點座標  
  7.     private int screenW, screenH;// 屏幕尺寸  
  8.   
  9.     public DisOutView(Context context) {  
  10.         this(context, null);  
  11.     }  
  12.   
  13.     public DisOutView(Context context, AttributeSet attrs) {  
  14.         super(context, attrs);  
  15.   
  16.         // 實例化混合模式  
  17.         porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);  
  18.   
  19.         // 初始化畫筆  
  20.         initPaint();  
  21.   
  22.         // 初始化資源  
  23.         initRes(context);  
  24.     }  
  25.   
  26.     /** 
  27.      * 初始化畫筆 
  28.      */  
  29.     private void initPaint() {  
  30.         // 實例化畫筆  
  31.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  32.     }  
  33.   
  34.     /** 
  35.      * 初始化資源 
  36.      */  
  37.     private void initRes(Context context) {  
  38.         // 獲取位圖  
  39.         bitmapSrc = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3_mask);  
  40.   
  41.         // 獲取包含屏幕尺寸的數組  
  42.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  43.   
  44.         // 獲取屏幕尺寸  
  45.         screenW = screenSize[0];  
  46.         screenH = screenSize[1];  
  47.   
  48.         /* 
  49.          * 計算位圖繪製時左上角的座標使其位於屏幕中心 
  50.          * 屏幕座標x軸向左偏移位圖一半的寬度 
  51.          * 屏幕座標y軸向上偏移位圖一半的高度 
  52.          */  
  53.         x = screenW / 2 - bitmapSrc.getWidth() / 2;  
  54.         y = screenH / 2 - bitmapSrc.getHeight() / 2;  
  55.   
  56.     }  
  57.   
  58.     @Override  
  59.     protected void onDraw(Canvas canvas) {  
  60.         super.onDraw(canvas);  
  61.         canvas.drawColor(Color.WHITE);  
  62.   
  63.         /* 
  64.          * 將繪製操作保存到新的圖層(更官方的說法應該是離屏緩存)我們將在1/3中學習到Canvas的全部用法這裏就先follow me 
  65.          */  
  66.         int sc = canvas.saveLayer(00, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);  
  67.   
  68.         // 先繪製一層顏色  
  69.         canvas.drawColor(0xFF8f66DA);  
  70.   
  71.         // 設置混合模式  
  72.         mPaint.setXfermode(porterDuffXfermode);  
  73.   
  74.         // 再繪製src源圖  
  75.         canvas.drawBitmap(bitmapSrc, x, y, mPaint);  
  76.   
  77.         // 還原混合模式  
  78.         mPaint.setXfermode(null);  
  79.   
  80.         // 還原畫布  
  81.         canvas.restoreToCount(sc);  
  82.     }  
  83. }  
看看美女那動人的輪廓~~~~~麼麼噠:

PorterDuff.Mode.DST_OVER

計算方式:[Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc];Chinese:在源圖像的上方繪製目標圖像


這個就不說啦,就是兩個圖片誰在上誰在下的意思

PorterDuff.Mode.LIGHTEN

計算方式:[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)];Chinese:變亮


與DARKEN相反,不多說了

PorterDuff.Mode.MULTIPLY

計算方式:[Sa * Da, Sc * Dc];Chinese:正片疊底


該模式通俗的計算方式很簡單,源圖像素顏色值乘以目標圖像素顏色值除以255即得混合後圖像像素的顏色值,該模式在設計領域應用廣泛,因爲其特性黑色與任何顏色混合都會得黑色,在手繪的上色、三維動畫的UV貼圖繪製都有應用,具體效果大家自己嘗試我就不說了

PorterDuff.Mode.OVERLAY

計算方式:未給出;Chinese:疊加


這個模式沒有在官方的API DEMO中給出,谷歌也沒有給出其計算方式,在實際效果中其對亮色和暗色不起作用,也就是說黑白色無效,它會將源色與目標色混合產生一種中間色,這種中間色生成的規律也很簡單,如果源色比目標色暗,那麼讓目標色的顏色倍增否則顏色遞減。

PorterDuff.Mode.SCREEN

計算方式:[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc];Chinese:濾色


計算方式我不解釋了,濾色產生的效果我認爲是Android提供的幾個色彩混合模式中最好的,它可以讓圖像焦媃幻化,有一種色調均和的感覺:

  1. public class ScreenView extends View {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Bitmap bitmapSrc;// 位圖  
  4.     private PorterDuffXfermode porterDuffXfermode;// 圖形混合模式  
  5.   
  6.     private int x, y;// 位圖繪製時左上角的起點座標  
  7.     private int screenW, screenH;// 屏幕尺寸  
  8.   
  9.     public ScreenView(Context context) {  
  10.         this(context, null);  
  11.     }  
  12.   
  13.     public ScreenView(Context context, AttributeSet attrs) {  
  14.         super(context, attrs);  
  15.   
  16.         // 實例化混合模式  
  17.         porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);  
  18.   
  19.         // 初始化畫筆  
  20.         initPaint();  
  21.   
  22.         // 初始化資源  
  23.         initRes(context);  
  24.     }  
  25.   
  26.     /** 
  27.      * 初始化畫筆 
  28.      */  
  29.     private void initPaint() {  
  30.         // 實例化畫筆  
  31.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  32.     }  
  33.   
  34.     /** 
  35.      * 初始化資源 
  36.      */  
  37.     private void initRes(Context context) {  
  38.         // 獲取位圖  
  39.         bitmapSrc = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3);  
  40.   
  41.         // 獲取包含屏幕尺寸的數組  
  42.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  43.   
  44.         // 獲取屏幕尺寸  
  45.         screenW = screenSize[0];  
  46.         screenH = screenSize[1];  
  47.   
  48.         /* 
  49.          * 計算位圖繪製時左上角的座標使其位於屏幕中心 
  50.          * 屏幕座標x軸向左偏移位圖一半的寬度 
  51.          * 屏幕座標y軸向上偏移位圖一半的高度 
  52.          */  
  53.         x = screenW / 2 - bitmapSrc.getWidth() / 2;  
  54.         y = screenH / 2 - bitmapSrc.getHeight() / 2;  
  55.   
  56.     }  
  57.   
  58.     @Override  
  59.     protected void onDraw(Canvas canvas) {  
  60.         super.onDraw(canvas);  
  61.         canvas.drawColor(Color.WHITE);  
  62.   
  63.         /* 
  64.          * 將繪製操作保存到新的圖層(更官方的說法應該是離屏緩存)我們將在1/3中學習到Canvas的全部用法這裏就先follow me 
  65.          */  
  66.         int sc = canvas.saveLayer(00, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);  
  67.   
  68.         // 先繪製一層帶透明度的顏色  
  69.         canvas.drawColor(0xcc1c093e);  
  70.   
  71.         // 設置混合模式  
  72.         mPaint.setXfermode(porterDuffXfermode);  
  73.   
  74.         // 再繪製src源圖  
  75.         canvas.drawBitmap(bitmapSrc, x, y, mPaint);  
  76.   
  77.         // 還原混合模式  
  78.         mPaint.setXfermode(null);  
  79.   
  80.         // 還原畫布  
  81.         canvas.restoreToCount(sc);  
  82.     }  
  83. }  
它比原圖多一層藍紫色調給人感覺更古典~~

PorterDuff.Mode.SRC

計算方式:[Sa, Sc];Chinese:顯示源圖

只繪製源圖,SRC類的模式跟DIS的其實差不多就不多說了,大家多動手自己試試,我已經寫不動了快………………………………………………………………………………………………………………

PorterDuff.Mode.SRC_ATOP

計算方式:[Da, Sc * Da + (1 - Sa) * Dc];Chinese:在源圖像和目標圖像相交的地方繪製源圖像,在不相交的地方繪製目標圖像

PorterDuff.Mode.SRC_IN

計算方式:[Sa * Da, Sc * Da];Chinese:只在源圖像和目標圖像相交的地方繪製源圖像


PorterDuff.Mode.SRC_OUT

計算方式:[Sa * (1 - Da), Sc * (1 - Da)];Chinese:只在源圖像和目標圖像不相交的地方繪製源圖像


PorterDuff.Mode.SRC_OVER

計算方式:[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc];Chinese:在目標圖像的頂部繪製源圖像


PorterDuff.Mode.XOR

計算方式:[Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc];Chinese:在源圖像和目標圖像重疊之外的任何地方繪製他們,而在不重疊的地方不繪製任何內容


XOR我們在將PixelXorXfermode的時候提到過,不瞭解的話上去看看~~~實在寫不動了

那麼這些混合模式究竟有什麼用?能夠給我們帶來什麼好處呢?假設我們要畫一個鬧鐘,如下:


注:該鬧鐘圖標爲已投入運行項目文件並已有商標,請大家不要以任何盈利手段爲目的盜用,當然做練習是沒問題的

構思一下怎麼做,我們需要畫一個圓圈做鐘體,兩個Path(Path爲路徑,我們將會在1/3詳細學習到,這裏就先follow me)作爲指針,問題是兩個鈴鐺~~~~如果我們不會混合模式,一定會想怎麼計算座標啊去繪製曲線然後閉合然後填充啊之類………………實際上有必要嗎?這個鬧鈴不就是一個小圓再用一個大圓去遮罩嗎:


問題是不是一下子就變簡單了?如果等你去計算怎麼畫路徑怎麼閉合曲線填充顏色還有多屏幕的匹配………………哥已經死了又活過來又死了…………在學完1/2的View尺寸計算和佈局後我會教大家如何做類似的View並匹配在所有的屏幕上~~~~這裏就先緩一緩。大家一定要有這樣的思維,當你想要去畫一個View的時候一定要想想看這個View的圖形是不是可以通過基本的幾何圖形混合來生成,如果可以,那麼恭喜你,使用PorterDuffXfermode的混合模式你可以事半功倍!

PorterDuffXfermode的另一個比較常見的應用就是橡皮檫效果,我們可以通過手指不斷地觸摸屏幕繪製Path,再以Path作遮罩遮掉填充的色塊顯示下層的圖像:

  1. public class EraserView extends View {  
  2.     private static final int MIN_MOVE_DIS = 5;// 最小的移動距離:如果我們手指在屏幕上的移動距離小於此值則不會繪製  
  3.   
  4.     private Bitmap fgBitmap, bgBitmap;// 前景橡皮擦的Bitmap和背景我們底圖的Bitmap  
  5.     private Canvas mCanvas;// 繪製橡皮擦路徑的畫布  
  6.     private Paint mPaint;// 橡皮檫路徑畫筆  
  7.     private Path mPath;// 橡皮擦繪製路徑  
  8.   
  9.     private int screenW, screenH;// 屏幕寬高  
  10.     private float preX, preY;// 記錄上一個觸摸事件的位置座標  
  11.   
  12.     public EraserView(Context context, AttributeSet set) {  
  13.         super(context, set);  
  14.   
  15.         // 計算參數  
  16.         cal(context);  
  17.   
  18.         // 初始化對象  
  19.         init(context);  
  20.     }  
  21.   
  22.     /** 
  23.      * 計算參數 
  24.      *  
  25.      * @param context 
  26.      *            上下文環境引用 
  27.      */  
  28.     private void cal(Context context) {  
  29.         // 獲取屏幕尺寸數組  
  30.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  31.   
  32.         // 獲取屏幕寬高  
  33.         screenW = screenSize[0];  
  34.         screenH = screenSize[1];  
  35.     }  
  36.   
  37.     /** 
  38.      * 初始化對象 
  39.      */  
  40.     private void init(Context context) {  
  41.         // 實例化路徑對象  
  42.         mPath = new Path();  
  43.   
  44.         // 實例化畫筆並開啓其抗鋸齒和抗抖動  
  45.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
  46.   
  47.         // 設置畫筆透明度爲0是關鍵!我們要讓繪製的路徑是透明的,然後讓該路徑與前景的底色混合“摳”出繪製路徑  
  48.         mPaint.setARGB(12825500);  
  49.   
  50.         // 設置混合模式爲DST_IN  
  51.         mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));  
  52.   
  53.         // 設置畫筆風格爲描邊  
  54.         mPaint.setStyle(Paint.Style.STROKE);  
  55.   
  56.         // 設置路徑結合處樣式  
  57.         mPaint.setStrokeJoin(Paint.Join.ROUND);  
  58.   
  59.         // 設置筆觸類型  
  60.         mPaint.setStrokeCap(Paint.Cap.ROUND);  
  61.   
  62.         // 設置描邊寬度  
  63.         mPaint.setStrokeWidth(50);  
  64.   
  65.         // 生成前景圖Bitmap  
  66.         fgBitmap = Bitmap.createBitmap(screenW, screenH, Config.ARGB_8888);  
  67.   
  68.         // 將其注入畫布  
  69.         mCanvas = new Canvas(fgBitmap);  
  70.   
  71.         // 繪製畫布背景爲中性灰  
  72.         mCanvas.drawColor(0xFF808080);  
  73.   
  74.         // 獲取背景底圖Bitmap  
  75.         bgBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a4);  
  76.   
  77.         // 縮放背景底圖Bitmap至屏幕大小  
  78.         bgBitmap = Bitmap.createScaledBitmap(bgBitmap, screenW, screenH, true);  
  79.     }  
  80.   
  81.     @Override  
  82.     protected void onDraw(Canvas canvas) {  
  83.         // 繪製背景  
  84.         canvas.drawBitmap(bgBitmap, 00null);  
  85.   
  86.         // 繪製前景  
  87.         canvas.drawBitmap(fgBitmap, 00null);  
  88.   
  89.         /* 
  90.          * 這裏要注意canvas和mCanvas是兩個不同的畫布對象 
  91.          * 當我們在屏幕上移動手指繪製路徑時會把路徑通過mCanvas繪製到fgBitmap上 
  92.          * 每當我們手指移動一次均會將路徑mPath作爲目標圖像繪製到mCanvas上,而在上面我們先在mCanvas上繪製了中性灰色 
  93.          * 兩者會因爲DST_IN模式的計算只顯示中性灰,但是因爲mPath的透明,計算生成的混合圖像也會是透明的 
  94.          * 所以我們會得到“橡皮擦”的效果 
  95.          */  
  96.         mCanvas.drawPath(mPath, mPaint);  
  97.     }  
  98.   
  99.     /** 
  100.      * View的事件將會在7/12詳解 
  101.      */  
  102.     @Override  
  103.     public boolean onTouchEvent(MotionEvent event) {  
  104.         /* 
  105.          * 獲取當前事件位置座標 
  106.          */  
  107.         float x = event.getX();  
  108.         float y = event.getY();  
  109.   
  110.         switch (event.getAction()) {  
  111.         case MotionEvent.ACTION_DOWN:// 手指接觸屏幕重置路徑  
  112.             mPath.reset();  
  113.             mPath.moveTo(x, y);  
  114.             preX = x;  
  115.             preY = y;  
  116.             break;  
  117.         case MotionEvent.ACTION_MOVE:// 手指移動時連接路徑  
  118.             float dx = Math.abs(x - preX);  
  119.             float dy = Math.abs(y - preY);  
  120.             if (dx >= MIN_MOVE_DIS || dy >= MIN_MOVE_DIS) {  
  121.                 mPath.quadTo(preX, preY, (x + preX) / 2, (y + preY) / 2);  
  122.                 preX = x;  
  123.                 preY = y;  
  124.             }  
  125.             break;  
  126.         }  
  127.   
  128.         // 重繪視圖  
  129.         invalidate();  
  130.         return true;  
  131.     }  
  132. }  

運行效果如下:

啊啊啊啊啊啊啊啊啊啊啊啊~~~~~PorterDuffXfermode算是暫告一段落,大家一定要學會PorterDuffXfermode各個混合模式的使用,這是android圖形繪製的重點之一!

再次強調:PorterDuffXfermode是重點~~~~一定要學會如何靈活使用

源碼地址:傳送門

溫馨提示:自定義控件其實很簡單系列文章每週一、週四更新一篇~

下集精彩預告:你知道Shader是什麼嗎?Xfermode和Colorfilter給我們帶來了炫酷的圖像混合效果,然而僅僅是這樣肯定是體現不出Android在圖形圖像處理上的逼格。鎖定本臺敬請關注:自定義控件其實很簡單1/4

自定義控件其實很簡單1/4已更新

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