上一節我們粗略地講了下如何去實現我們的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類是什么呢?追蹤源碼進去你會發現其里面很簡單幾乎沒有:
~~~
public class ColorFilter {
int native_instance;
/**
* @hide
*/
public int nativeColorFilter;
protected void finalize() throws Throwable {
try {
super.finalize();
} finally {
finalizer(native_instance, nativeColorFilter);
}
}
private static native void finalizer(int native_instance, int nativeColorFilter);
}
~~~
壓根沒有和圖像處理相關的方法對吧,那么說明該類必定是個父類或者說其必有一定的子類去實現一些方法,查看API文檔發現果然有三個子類:

ColorMatrixColorFilter、LightingColorFilter和PorterDuffColorFilter,也就是說我們在setColorFilter(ColorFilter filter)的時候可以直接傳入這三個子類對象作為參數,那么這三個子類又是什么東西呢?首先我們來看看
**ColorMatrixColorFilter**
中文直譯為色彩矩陣顏色過濾器,要明白這玩意你得先了解什么是色彩矩陣。在Android中圖片是以RGBA像素點的形式加載到內存中的,修改這些像素信息需要一個叫做ColorMatrix類的支持,其定義了一個4x5的float[]類型的矩陣:
~~~
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
});
~~~
其中,第一行表示的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!還是接著昨天那個圓環,不過我們今天把它改成繪制一個圓并且去掉線程動畫的效果因為我們不需要:
~~~
public class CustomView extends View {
private Paint mPaint;// 畫筆
private Context mContext;// 上下文環境引用
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
// 初始化畫筆
initPaint();
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆并打開抗鋸齒
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/*
* 設置畫筆樣式為描邊,圓環嘛……當然不能填充不然就么意思了
*
* 畫筆樣式分三種:
* 1.Paint.Style.STROKE:描邊
* 2.Paint.Style.FILL_AND_STROKE:描邊并填充
* 3.Paint.Style.FILL:填充
*/
mPaint.setStyle(Paint.Style.FILL);
// 設置畫筆顏色為自定義顏色
mPaint.setColor(Color.argb(255, 255, 128, 103));
/*
* 設置描邊的粗細,單位:像素px 注意:當setStrokeWidth(0)的時候描邊寬度并不為0而是只占一個像素
*/
mPaint.setStrokeWidth(10);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制圓形
canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2, 200, mPaint);
}
}
~~~
運行下是一個橙紅色的圓~~是不是有點蘿卜頭國旗幟的感腳?

下面我們為Paint設置一個色彩矩陣:
~~~
// 生成色彩矩陣
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
});
mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
~~~
再次運行發現沒變化啊!!!草!!!是不是感覺被我坑了?如果你真的那么認為我只能說你壓根就沒認真看上面的文字,我說過什么?值為1時表示什么?表示不改變原色彩的值!!這時我們改變色彩矩陣:
~~~
// 生成色彩矩陣
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
0.5F, 0, 0, 0, 0,
0, 0.5F, 0, 0, 0,
0, 0, 0.5F, 0, 0,
0, 0, 0, 1, 0,
});
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?那么我們換張圖片來試試唄!看看是什么樣的效果:
~~~
public class CustomView extends View {
private Paint mPaint;// 畫筆
private Context mContext;// 上下文環境引用
private Bitmap bitmap;// 位圖
private int x,y;// 位圖繪制時左上角的起點坐標
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
// 初始化畫筆
initPaint();
//初始化資源
initRes(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
/**
* 初始化資源
*/
private void initRes(Context context) {
// 獲取位圖
bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
* 屏幕坐標x軸向左偏移位圖一半的寬度
* 屏幕坐標y軸向上偏移位圖一半的高度
*/
x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;
y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制位圖
canvas.drawBitmap(bitmap, x, y, mPaint);
}
}
~~~
如代碼所示我們清除了所有的畫筆屬性設置因為沒必要,從資源獲取一個Bitmap繪制在畫布上:

一張灰常漂亮的風景圖,好!現在我們來為我們的畫筆添加一個顏色過濾:
~~~
// 生成色彩矩陣
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
0.5F, 0, 0, 0, 0,
0, 0.5F, 0, 0, 0,
0, 0, 0.5F, 0, 0,
0, 0, 0, 1, 0,
});
mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
~~~
大家看到還是剛才那個色彩矩陣,運行下看看什么效果呢:

變暗了對吧!沒意思,我們來點更刺激的,改下ColorMatrix矩陣:
~~~
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
0.33F, 0.59F, 0.11F, 0, 0,
0.33F, 0.59F, 0.11F, 0, 0,
0.33F, 0.59F, 0.11F, 0, 0,
0, 0, 0, 1, 0,
});
~~~

噢!變灰了!還是沒意思!繼續改:
~~~
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
-1, 0, 0, 1, 1,
0, -1, 0, 1, 1,
0, 0, -1, 1, 1,
0, 0, 0, 1, 0,
});
~~~

喲呵!!是不是有點類似PS里反相的效果?我們常看到的圖片都是RGB的,顛覆一下思維,看看BGR的試試:
~~~
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
0, 0, 1, 0, 0,
0, 1, 0, 0, 0,
1, 0, 0, 0, 0,
0, 0, 0, 1, 0,
});
~~~

這樣紅色的變成了藍色而藍色的就變成了紅色,繼續改:
~~~
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
0.393F, 0.769F, 0.189F, 0, 0,
0.349F, 0.686F, 0.168F, 0, 0,
0.272F, 0.534F, 0.131F, 0, 0,
0, 0, 0, 1, 0,
});
~~~

是不是有點類似于老舊照片的感腳?繼續:
~~~
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
1.5F, 1.5F, 1.5F, 0, -1,
1.5F, 1.5F, 1.5F, 0, -1,
1.5F, 1.5F, 1.5F, 0, -1,
0, 0, 0, 1, 0,
});
~~~

類似去色后高對比度的效果,繼續:
~~~
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
1.438F, -0.122F, -0.016F, 0, -0.03F,
-0.062F, 1.378F, -0.016F, 0, 0.05F,
-0.062F, -0.122F, 1.483F, 0, -0.02F,
0, 0, 0, 1, 0,
});
~~~

飽和度對比度加強,好了不演示了……累死我了!截圖粘貼上傳!!
這些各種各樣的圖像效果在哪見過?PS?對的!還有各種拍照軟件拍攝后的特效處理!大致原理都是這么來的!有人會問愛哥你傻逼么!這么多參數怎么玩!誰記得!而且TMD用參數調顏色?我映像中都是直接在各種繪圖軟件(比如PS)里拖進度條的!這怎么玩!淡定!如我所說很多時候你壓根不需要了解太多原理,只需站在巨人的丁丁上即可,所以稍安勿躁!再下一個系列教程“設計色彩”中愛哥教你玩轉色彩并且讓設計和開發無縫結合!
ColorMatrixColorFilter和ColorMatrix就是這么個東西,ColorMatrix類里面也提供了一些實在的方法,比如setSaturation(float sat)設置飽和度,而且ColorMatrix每個方法都用了陣列的計算,如果大家感興趣可以自己去深挖來看不過我是真心不推薦的~~~
下面我們來看看ColorFilter的另一個子類
**LightingColorFilter**
顧名思義光照顏色過濾,這肯定是跟光照是有關的了~~該類有且只有一個構造方法:
`LightingColorFilter (int mul, int add) `
這個方法非常非常地簡單!mul全稱是colorMultiply意為色彩倍增,而add全稱是colorAdd意為色彩添加,這兩個值都是16進制的色彩值0xAARRGGBB。這個方法使用也是非常的簡單。還是拿上面那張圖片來說吧,比如我們想要去掉綠色:
~~~
public class CustomView extends View {
private Paint mPaint;// 畫筆
private Context mContext;// 上下文環境引用
private Bitmap bitmap;// 位圖
private int x, y;// 位圖繪制時左上角的起點坐標
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
// 初始化畫筆
initPaint();
// 初始化資源
initRes(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 設置顏色過濾
mPaint.setColorFilter(new LightingColorFilter(0xFFFF00FF, 0x00000000));
}
/**
* 初始化資源
*/
private void initRes(Context context) {
// 獲取位圖
bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
* 屏幕坐標x軸向左偏移位圖一半的寬度
* 屏幕坐標y軸向上偏移位圖一半的高度
*/
x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;
y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制位圖
canvas.drawBitmap(bitmap, x, y, mPaint);
}
}
~~~
運行后你會發現綠色確實是沒了但是原來偏綠的部分現在居然成了紅色,為毛!敬請關注下一系列設計色彩文章!!哈哈哈!!當LightingColorFilter(0xFFFFFFFF, 0x00000000)的時候原圖是不會有任何改變的,如果我們想增加紅色的值,那么LightingColorFilter(0xFFFFFFFF, 0x00XX0000)就好,其中XX取值為00至FF。那么這個方法有什么存在的意義呢?存在必定合理,這個方法存在一定是有它可用之處的,前些天有個盆友在群里問點擊一個圖片如何直接改變它的顏色而不是為他多準備另一張點擊效果的圖片,這種情況下該方法就派上用場了!如下圖一個灰色的星星,我們點擊后讓它變成黃色

代碼如下,注釋很清楚我就不再多說了:
~~~
public class CustomView extends View {
private Paint mPaint;// 畫筆
private Context mContext;// 上下文環境引用
private Bitmap bitmap;// 位圖
private int x, y;// 位圖繪制時左上角的起點坐標
private boolean isClick;// 用來標識控件是否被點擊過
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
// 初始化畫筆
initPaint();
// 初始化資源
initRes(context);
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
/*
* 判斷控件是否被點擊過
*/
if (isClick) {
// 如果已經被點擊了則點擊時設置顏色過濾為空還原本色
mPaint.setColorFilter(null);
isClick = false;
} else {
// 如果未被點擊則點擊時設置顏色過濾后為黃色
mPaint.setColorFilter(new LightingColorFilter(0xFFFFFFFF, 0X00FFFF00));
isClick = true;
}
// 記得重繪
invalidate();
}
});
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
/**
* 初始化資源
*/
private void initRes(Context context) {
// 獲取位圖
bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a2);
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
* 屏幕坐標x軸向左偏移位圖一半的寬度
* 屏幕坐標y軸向上偏移位圖一半的高度
*/
x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;
y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制位圖
canvas.drawBitmap(bitmap, x, y, mPaint);
}
}
~~~
運行后點擊星星即可變成黃色再點擊變回灰色,當我們不想要顏色過濾的效果時,setColorFilter(null)并重繪視圖即可!那么為什么要叫光照顏色過濾呢?原因很簡單,因為它所呈現的效果就像有色光照在物體上染色一樣~~~哎,不說這方法了,看下一個也是最后一個ColorFilter的子類。
**PorterDuffColorFilter**
PorterDuffColorFilter跟LightingColorFilter一樣,只有一個構造方法:
`PorterDuffColorFilter(int color, PorterDuff.Mode mode) `
這個構造方法也接受兩個值,一個是16進制表示的顏色值這個很好理解,而另一個是PorterDuff內部類Mode中的一個常量值,這個值表示混合模式。那么什么是混合模式呢?混合混合必定是有兩種東西混才行,第一種就是我們設置的color值而第二種當然就是我們畫布上的元素了!,比如這里我們把Color的值設為紅色,而模式設為PorterDuff.Mode.DARKEN變暗:
~~~
public class CustomView extends View {
private Paint mPaint;// 畫筆
private Context mContext;// 上下文環境引用
private Bitmap bitmap;// 位圖
private int x, y;// 位圖繪制時左上角的起點坐標
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
// 初始化畫筆
initPaint();
// 初始化資源
initRes(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 設置顏色過濾
mPaint.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.DARKEN));
}
/**
* 初始化資源
*/
private void initRes(Context context) {
// 獲取位圖
bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
* 屏幕坐標x軸向左偏移位圖一半的寬度
* 屏幕坐標y軸向上偏移位圖一半的高度
*/
x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;
y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制位圖
canvas.drawBitmap(bitmap, x, y, mPaint);
}
}
~~~
我們嘗試在畫布上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一樣沒有公開的實現的方法:
~~~
public class Xfermode {
protected void finalize() throws Throwable {
try {
finalizer(native_instance);
} finally {
super.finalize();
}
}
private static native void finalizer(int native_instance);
int native_instance;
}
~~~
同理可得其必然有一定的子類去實現一些方法供我們使用,查看API文檔發現其果然有三個子類:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode,這三個子類實現的功能要比setColorFilter的三個子類復雜得多,主要是是涉及到圖像處理的一些知識可能對大家來說會比較難以理解,不過我會盡量以通俗的方式闡述它們的作用,那好先來看看我們的第一個子類
**AvoidXfermode**
首先我要告訴大家的是這個API因為不支持硬件加速在API 16已經過時了(大家可以在HardwareAccel查看那些方法不支持硬件加速)……如果想在高于API 16的機子上測試這玩意,必須現在應用或手機設置中關閉硬件加速,在應用中我們可以通過在AndroidManifest.xml文件中設置application節點下的android:hardwareAccelerated屬性為false來關閉硬件加速:
~~~
<application
android:allowBackup="true"
android:hardwareAccelerated="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" >
<activity
android:name="com.aigestudio.customviewdemo.activities.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</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)并不等于紅色值也會被“染”色,容差值越大“染”色范圍越廣反之則反,空說無憑我們來看看具體的實現和效果:
~~~
public class CustomView extends View {
private Paint mPaint;// 畫筆
private Context mContext;// 上下文環境引用
private Bitmap bitmap;// 位圖
private AvoidXfermode avoidXfermode;// AV模式
private int x, y, w, h;// 位圖繪制時左上角的起點坐標
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
// 初始化畫筆
initPaint();
// 初始化資源
initRes(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/*
* 當畫布中有跟0XFFFFFFFF色不一樣的地方時候才“染”色
*/
avoidXfermode = new AvoidXfermode(0XFFFFFFFF, 0, AvoidXfermode.Mode.TARGET);
}
/**
* 初始化資源
*/
private void initRes(Context context) {
// 獲取位圖
bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
* 屏幕坐標x軸向左偏移位圖一半的寬度
* 屏幕坐標y軸向上偏移位圖一半的高度
*/
x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;
y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;
w = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 + bitmap.getWidth() / 2;
h = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 + bitmap.getHeight() / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 先繪制位圖
canvas.drawBitmap(bitmap, x, y, mPaint);
// “染”什么色是由我們自己決定的
mPaint.setARGB(255, 211, 53, 243);
// 設置AV模式
mPaint.setXfermode(avoidXfermode);
// 畫一個位圖大小一樣的矩形
canvas.drawRect(x, y, w, h, mPaint);
}
}
~~~
在高于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來看看這兩個漸變色的顏色值在不同的混合模式下究竟發生了什么?先看看我們的測試代碼:
~~~
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class PorterDuffView extends View {
/*
* PorterDuff模式常量
* 可以在此更改不同的模式測試
*/
private static final PorterDuff.Mode MODE = PorterDuff.Mode.ADD;
private static final int RECT_SIZE_SMALL = 400;// 左右上方示例漸變正方形的尺寸大小
private static final int RECT_SIZE_BIG = 800;// 中間測試漸變正方形的尺寸大小
private Paint mPaint;// 畫筆
private PorterDuffBO porterDuffBO;// PorterDuffView類的業務對象
private PorterDuffXfermode porterDuffXfermode;// 圖形混合模式
private int screenW, screenH;// 屏幕尺寸
private int s_l, s_t;// 左上方正方形的原點坐標
private int d_l, d_t;// 右上方正方形的原點坐標
private int rectX, rectY;// 中間正方形的原點坐標
public PorterDuffView(Context context, AttributeSet attrs) {
super(context, attrs);
// 實例化畫筆并設置抗鋸齒
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 實例化業務對象
porterDuffBO = new PorterDuffBO();
// 實例化混合模式
porterDuffXfermode = new PorterDuffXfermode(MODE);
// 計算坐標
calu(context);
}
/**
* 計算坐標
*
* @param context
* 上下文環境引用
*/
private void calu(Context context) {
// 獲取包含屏幕尺寸的數組
int[] screenSize = MeasureUtil.getScreenSize((Activity) context);
// 獲取屏幕尺寸
screenW = screenSize[0];
screenH = screenSize[1];
// 計算左上方正方形原點坐標
s_l = 0;
s_t = 0;
// 計算右上方正方形原點坐標
d_l = screenW - RECT_SIZE_SMALL;
d_t = 0;
// 計算中間方正方形原點坐標
rectX = screenW / 2 - RECT_SIZE_BIG / 2;
rectY = RECT_SIZE_SMALL + (screenH - RECT_SIZE_SMALL) / 2 - RECT_SIZE_BIG / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 設置畫布顏色為黑色以便我們更好地觀察
canvas.drawColor(Color.BLACK);
// 設置業務對象尺寸值計算生成左右上方的漸變方形
porterDuffBO.setSize(RECT_SIZE_SMALL);
/*
* 畫出左右上方兩個正方形
* 其中左邊的的為src右邊的為dis
*/
canvas.drawBitmap(porterDuffBO.initSrcBitmap(), s_l, s_t, mPaint);
canvas.drawBitmap(porterDuffBO.initDisBitmap(), d_l, d_t, mPaint);
/*
* 將繪制操作保存到新的圖層(更官方的說法應該是離屏緩存)我們將在1/3中學習到Canvas的全部用法這里就先follow me
*/
int sc = canvas.saveLayer(0, 0, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);
// 重新設置業務對象尺寸值計算生成中間的漸變方形
porterDuffBO.setSize(RECT_SIZE_BIG);
// 先繪制dis目標圖
canvas.drawBitmap(porterDuffBO.initDisBitmap(), rectX, rectY, mPaint);
// 設置混合模式
mPaint.setXfermode(porterDuffXfermode);
// 再繪制src源圖
canvas.drawBitmap(porterDuffBO.initSrcBitmap(), rectX, rectY, mPaint);
// 還原混合模式
mPaint.setXfermode(null);
// 還原畫布
canvas.restoreToCount(sc);
}
}
~~~
代碼中我們使用到了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就很容易理解,我這里有兩張圖:

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

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

我們把這張美女圖畫在我們的屏幕上:
~~~
public class DisInView extends View {
private Paint mPaint;// 畫筆
private Bitmap bitmapDis;// 位圖
private int x, y;// 位圖繪制時左上角的起點坐標
private int screenW, screenH;// 屏幕尺寸
public DisInView(Context context) {
this(context, null);
}
public DisInView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化畫筆
initPaint();
// 初始化資源
initRes(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
/**
* 初始化資源
*/
private void initRes(Context context) {
// 獲取位圖
bitmapDis = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3);
// 獲取包含屏幕尺寸的數組
int[] screenSize = MeasureUtil.getScreenSize((Activity) context);
// 獲取屏幕尺寸
screenW = screenSize[0];
screenH = screenSize[1];
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
* 屏幕坐標x軸向左偏移位圖一半的寬度
* 屏幕坐標y軸向上偏移位圖一半的高度
*/
x = screenW / 2 - bitmapDis.getWidth() / 2;
y = screenH / 2 - bitmapDis.getHeight() / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制美女圖
canvas.drawBitmap(bitmapDis, x, y, mPaint);
}
}
~~~
運行后如下:

美女腦袋上有個文字標識巨惡心而且因為圖片畫質問題美圖周圍還有一片淡黃色的不好看,那我們就通過剛才那個黑色的透明通道圖把美女“摳”出來:
~~~
public class DisInView extends View {
private Paint mPaint;// 畫筆
private Bitmap bitmapDis, bitmapSrc;// 位圖
private PorterDuffXfermode porterDuffXfermode;// 圖形混合模式
private int x, y;// 位圖繪制時左上角的起點坐標
private int screenW, screenH;// 屏幕尺寸
public DisInView(Context context) {
this(context, null);
}
public DisInView(Context context, AttributeSet attrs) {
super(context, attrs);
// 實例化混合模式
porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
// 初始化畫筆
initPaint();
// 初始化資源
initRes(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
/**
* 初始化資源
*/
private void initRes(Context context) {
// 獲取位圖
bitmapDis = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3);
bitmapSrc = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3_mask);
// 獲取包含屏幕尺寸的數組
int[] screenSize = MeasureUtil.getScreenSize((Activity) context);
// 獲取屏幕尺寸
screenW = screenSize[0];
screenH = screenSize[1];
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
* 屏幕坐標x軸向左偏移位圖一半的寬度
* 屏幕坐標y軸向上偏移位圖一半的高度
*/
x = screenW / 2 - bitmapDis.getWidth() / 2;
y = screenH / 2 - bitmapDis.getHeight() / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
/*
* 將繪制操作保存到新的圖層(更官方的說法應該是離屏緩存)我們將在1/3中學習到Canvas的全部用法這里就先follow me
*/
int sc = canvas.saveLayer(0, 0, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);
// 先繪制dis目標圖
canvas.drawBitmap(bitmapDis, x, y, mPaint);
// 設置混合模式
mPaint.setXfermode(porterDuffXfermode);
// 再繪制src源圖
canvas.drawBitmap(bitmapSrc, x, y, mPaint);
// 還原混合模式
mPaint.setXfermode(null);
// 還原畫布
canvas.restoreToCount(sc);
}
}
~~~
看!只剩米女了~~~~:

當然該混合模式的用法絕不止這么簡單,這只是闡述了一個原理,更棒的用法就看你怎么用了~~~~
PorterDuff.Mode.DST_OUT
計算方式:[Da * (1 - Sa), Dc * (1 - Sa)];Chinese:只在源圖像和目標圖像不相交的地方繪制目標圖像

上面那個例子呢我們把米女摳了出來,而這次我們將從一個色塊中把米女的輪廓挖出來~~~~啦啦啦:
~~~
public class DisOutView extends View {
private Paint mPaint;// 畫筆
private Bitmap bitmapSrc;// 位圖
private PorterDuffXfermode porterDuffXfermode;// 圖形混合模式
private int x, y;// 位圖繪制時左上角的起點坐標
private int screenW, screenH;// 屏幕尺寸
public DisOutView(Context context) {
this(context, null);
}
public DisOutView(Context context, AttributeSet attrs) {
super(context, attrs);
// 實例化混合模式
porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
// 初始化畫筆
initPaint();
// 初始化資源
initRes(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
/**
* 初始化資源
*/
private void initRes(Context context) {
// 獲取位圖
bitmapSrc = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3_mask);
// 獲取包含屏幕尺寸的數組
int[] screenSize = MeasureUtil.getScreenSize((Activity) context);
// 獲取屏幕尺寸
screenW = screenSize[0];
screenH = screenSize[1];
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
* 屏幕坐標x軸向左偏移位圖一半的寬度
* 屏幕坐標y軸向上偏移位圖一半的高度
*/
x = screenW / 2 - bitmapSrc.getWidth() / 2;
y = screenH / 2 - bitmapSrc.getHeight() / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
/*
* 將繪制操作保存到新的圖層(更官方的說法應該是離屏緩存)我們將在1/3中學習到Canvas的全部用法這里就先follow me
*/
int sc = canvas.saveLayer(0, 0, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);
// 先繪制一層顏色
canvas.drawColor(0xFF8f66DA);
// 設置混合模式
mPaint.setXfermode(porterDuffXfermode);
// 再繪制src源圖
canvas.drawBitmap(bitmapSrc, x, y, mPaint);
// 還原混合模式
mPaint.setXfermode(null);
// 還原畫布
canvas.restoreToCount(sc);
}
}
~~~
看看美女那動人的輪廓~~~~~么么噠:

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提供的幾個色彩混合模式中最好的,它可以讓圖像焦媃幻化,有一種色調均和的感覺:
~~~
public class ScreenView extends View {
private Paint mPaint;// 畫筆
private Bitmap bitmapSrc;// 位圖
private PorterDuffXfermode porterDuffXfermode;// 圖形混合模式
private int x, y;// 位圖繪制時左上角的起點坐標
private int screenW, screenH;// 屏幕尺寸
public ScreenView(Context context) {
this(context, null);
}
public ScreenView(Context context, AttributeSet attrs) {
super(context, attrs);
// 實例化混合模式
porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);
// 初始化畫筆
initPaint();
// 初始化資源
initRes(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
/**
* 初始化資源
*/
private void initRes(Context context) {
// 獲取位圖
bitmapSrc = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3);
// 獲取包含屏幕尺寸的數組
int[] screenSize = MeasureUtil.getScreenSize((Activity) context);
// 獲取屏幕尺寸
screenW = screenSize[0];
screenH = screenSize[1];
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
* 屏幕坐標x軸向左偏移位圖一半的寬度
* 屏幕坐標y軸向上偏移位圖一半的高度
*/
x = screenW / 2 - bitmapSrc.getWidth() / 2;
y = screenH / 2 - bitmapSrc.getHeight() / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
/*
* 將繪制操作保存到新的圖層(更官方的說法應該是離屏緩存)我們將在1/3中學習到Canvas的全部用法這里就先follow me
*/
int sc = canvas.saveLayer(0, 0, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);
// 先繪制一層帶透明度的顏色
canvas.drawColor(0xcc1c093e);
// 設置混合模式
mPaint.setXfermode(porterDuffXfermode);
// 再繪制src源圖
canvas.drawBitmap(bitmapSrc, x, y, mPaint);
// 還原混合模式
mPaint.setXfermode(null);
// 還原畫布
canvas.restoreToCount(sc);
}
}
~~~
它比原圖多一層藍紫色調給人感覺更古典~~

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作遮罩遮掉填充的色塊顯示下層的圖像:
~~~
public class EraserView extends View {
private static final int MIN_MOVE_DIS = 5;// 最小的移動距離:如果我們手指在屏幕上的移動距離小于此值則不會繪制
private Bitmap fgBitmap, bgBitmap;// 前景橡皮擦的Bitmap和背景我們底圖的Bitmap
private Canvas mCanvas;// 繪制橡皮擦路徑的畫布
private Paint mPaint;// 橡皮檫路徑畫筆
private Path mPath;// 橡皮擦繪制路徑
private int screenW, screenH;// 屏幕寬高
private float preX, preY;// 記錄上一個觸摸事件的位置坐標
public EraserView(Context context, AttributeSet set) {
super(context, set);
// 計算參數
cal(context);
// 初始化對象
init(context);
}
/**
* 計算參數
*
* @param context
* 上下文環境引用
*/
private void cal(Context context) {
// 獲取屏幕尺寸數組
int[] screenSize = MeasureUtil.getScreenSize((Activity) context);
// 獲取屏幕寬高
screenW = screenSize[0];
screenH = screenSize[1];
}
/**
* 初始化對象
*/
private void init(Context context) {
// 實例化路徑對象
mPath = new Path();
// 實例化畫筆并開啟其抗鋸齒和抗抖動
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
// 設置畫筆透明度為0是關鍵!我們要讓繪制的路徑是透明的,然后讓該路徑與前景的底色混合“摳”出繪制路徑
mPaint.setARGB(128, 255, 0, 0);
// 設置混合模式為DST_IN
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
// 設置畫筆風格為描邊
mPaint.setStyle(Paint.Style.STROKE);
// 設置路徑結合處樣式
mPaint.setStrokeJoin(Paint.Join.ROUND);
// 設置筆觸類型
mPaint.setStrokeCap(Paint.Cap.ROUND);
// 設置描邊寬度
mPaint.setStrokeWidth(50);
// 生成前景圖Bitmap
fgBitmap = Bitmap.createBitmap(screenW, screenH, Config.ARGB_8888);
// 將其注入畫布
mCanvas = new Canvas(fgBitmap);
// 繪制畫布背景為中性灰
mCanvas.drawColor(0xFF808080);
// 獲取背景底圖Bitmap
bgBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a4);
// 縮放背景底圖Bitmap至屏幕大小
bgBitmap = Bitmap.createScaledBitmap(bgBitmap, screenW, screenH, true);
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制背景
canvas.drawBitmap(bgBitmap, 0, 0, null);
// 繪制前景
canvas.drawBitmap(fgBitmap, 0, 0, null);
/*
* 這里要注意canvas和mCanvas是兩個不同的畫布對象
* 當我們在屏幕上移動手指繪制路徑時會把路徑通過mCanvas繪制到fgBitmap上
* 每當我們手指移動一次均會將路徑mPath作為目標圖像繪制到mCanvas上,而在上面我們先在mCanvas上繪制了中性灰色
* 兩者會因為DST_IN模式的計算只顯示中性灰,但是因為mPath的透明,計算生成的混合圖像也會是透明的
* 所以我們會得到“橡皮擦”的效果
*/
mCanvas.drawPath(mPath, mPaint);
}
/**
* View的事件將會在7/12詳解
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
/*
* 獲取當前事件位置坐標
*/
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:// 手指接觸屏幕重置路徑
mPath.reset();
mPath.moveTo(x, y);
preX = x;
preY = y;
break;
case MotionEvent.ACTION_MOVE:// 手指移動時連接路徑
float dx = Math.abs(x - preX);
float dy = Math.abs(y - preY);
if (dx >= MIN_MOVE_DIS || dy >= MIN_MOVE_DIS) {
mPath.quadTo(preX, preY, (x + preX) / 2, (y + preY) / 2);
preX = x;
preY = y;
}
break;
}
// 重繪視圖
invalidate();
return true;
}
}
~~~
運行效果如下:

啊啊啊啊啊啊啊啊啊啊啊啊~~~~~PorterDuffXfermode算是暫告一段落,大家一定要學會PorterDuffXfermode各個混合模式的使用,這是android圖形繪制的重點之一!
再次強調:PorterDuffXfermode是重點~~~~一定要學會如何靈活使用
源碼地址:[傳送門](http://download.csdn.net/detail/aigestudio/8182033)
溫馨提示:自定義控件其實很簡單系列文章每周一、周四更新一篇~
下集精彩預告:你知道Shader是什么嗎?Xfermode和Colorfilter給我們帶來了炫酷的圖像混合效果,然而僅僅是這樣肯定是體現不出Android在圖形圖像處理上的逼格。鎖定本臺敬請關注:自定義控件其實很簡單1/4