正值猿宵佳節,小盆友在此祝大家新年無BUG。?
目錄
一、前言
二、PorterDuffXfermode
三、實戰
四、寫在最後
一、前言
自定義UI中,少不了對多種影像的疊加覆蓋,而需要達到預期的目的,我們便需要今天的主角Xfermode。Xfermode 有三個孩子,分別是:
- AvoidXfermode
- PixelXorXfermode
- PorterDuffXfermode
而 AvoidXfermode 和 PixelXorXfermode 已經在 API 16之後被標記為removed,所以就只剩下小兒子 PorterDuffXfermode 為我們合成影像,理所當然我們今天的重點也就在他身上。老規矩,先上幾張實戰圖,然後開始我們今天的分享。
Xfermode小工具
刮刮卡
心跳
二、PorterDuffXfermode
我們看以下兩段原始碼,可知 PorterDuffXfermode 作用時通過 Paint的setXfermode 設定,而 PorterDuffXfermode 的例項化其實還需要一個引數,型別為 PorterDuff.Mode。
// Paint 類
public Xfermode setXfermode(Xfermode xfermode) {
int newMode = xfermode != null ? xfermode.porterDuffMode : Xfermode.DEFAULT;
int curMode = mXfermode != null ? mXfermode.porterDuffMode : Xfermode.DEFAULT;
if (newMode != curMode) {
nSetXfermode(mNativePaint, newMode);
}
mXfermode = xfermode;
return xfermode;
}
複製程式碼
// PorterDuffXfermode 類
public class PorterDuffXfermode extends Xfermode {
public PorterDuffXfermode(PorterDuff.Mode mode) {
porterDuffMode = mode.nativeInt;
}
}
複製程式碼
所以經過上面得知,最終起作用的是 PorterDuff.Mode。進入原始碼,會看到以下可用的模式,這段程式碼是API 22 的片段,如果你在比較高的版本看的話會有些許不同,但相同模式的計算公式一樣。
public enum Mode {
/** [0, 0] */
CLEAR (0),
/** [Sa, Sc] */
SRC (1),
/** [Da, Dc] */
DST (2),
/** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
SRC_OVER (3),
/** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
DST_OVER (4),
/** [Sa * Da, Sc * Da] */
SRC_IN (5),
/** [Sa * Da, Sa * Dc] */
DST_IN (6),
/** [Sa * (1 - Da), Sc * (1 - Da)] */
SRC_OUT (7),
/** [Da * (1 - Sa), Dc * (1 - Sa)] */
DST_OUT (8),
/** [Da, Sc * Da + (1 - Sa) * Dc] */
SRC_ATOP (9),
/** [Sa, Sa * Dc + Sc * (1 - Da)] */
DST_ATOP (10),
/** [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] */
XOR (11),
/** [Sa + Da - Sa*Da,
Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] */
DARKEN (12),
/** [Sa + Da - Sa*Da,
Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] */
LIGHTEN (13),
/** [Sa * Da, Sc * Dc] */
MULTIPLY (14),
/** [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] */
SCREEN (15),
/** Saturate(S + D) */
ADD (16),
OVERLAY (17);
Mode(int nativeInt) {
this.nativeInt = nativeInt;
}
/**
* @hide
*/
public final int nativeInt;
}
複製程式碼
每個模式的效果是怎樣的呢? 我們先看看官方給出的 Demo 圖。小盆友也跟著手寫了一遍,需要看原始碼的童鞋進傳送門
但是,這個 demo 少了一樣東西,那就是透明度,不能全面的體現出Xfermode的威力。所以我們需要先說明下引數的意思,然後給出我們較為全面的demo。PorterDuff.Mode 原始碼中每個模式的組成都是 [xx, yy] 形式,我們拿 SRC_OUT 來舉例。
/** [Sa * (1 - Da), Sc * (1 - Da)] */
SRC_OUT (7),
複製程式碼
"xx" 指的就是 Sa * (1 - Da),其值決定了這張合成圖的透明度。而透明度的取值範圍為 [0, 1]。0代表著完全透明,而1代表完全可見。
“yy” 指的就是 Sc * (1 - Da),其值決定了這張合成圖的顏色值。
聰明的童鞋還會注意到 Sa、Da、Sc、Dc這幾個值。他們各自代表(結合著英文記,更容易):
- Sa(Source Alpha):源影像的透明值;
- Da(Destination Alpha):目標影像的透明值;
- Sc(Source Color):源影像的色值;
- Dc(Destination Color):目標影像的色值;
而 源影像 和 目標影像 又是什麼呢?記住一句話就可以,先設定的為目標圖(Dst),後設定的為源圖(Src)。
所有的疑惑我們已經先點破,接下里就給出我們比較全面的Demo,這是小盆友以官方所示的十六種模式提供的Xfermode小工具,如果有時候拿捏不準具體使用什麼模式時,可以進行加入這個工具來進行琢磨。對該小工具感興趣的請進傳送門。
接下來我們便逐個講解模式,所使用的圖片均來自 Xfermode工具 的zinc例子。
1、CLEAR
註釋給出的是 [0, 0] , 透明度 為0,即完全看不見;顏色 為0,即無色; 最終呈現如下圖,什麼都沒有。
2、SRC
註釋給出的是[Sa, Sc], 透明度 為Sa,即取決於源圖的透明值;顏色 為Sc,即取源圖的色值; 最終呈現如下圖,因為都是取源圖的值,所以最終就是顯示 源圖。
3、DST
註釋給出的是[Da, Dc],透明度 為Da,即取目標圖的透明度;顏色 為Dc,即取目標圖的色值; 最終呈現如下圖,因為都取目標圖的值,所以最終呈現的就是 目標圖。
4、SRC_OVER
註釋給出的是 [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] ,其實就是源圖蓋於目標圖上,若有透明度,則會看到下一層,從名字也可以很好的記憶。
5、DST_OVER
註釋給出的是 [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc],和 SRC_OVER相反 ,目標圖蓋於源圖上,有透明度的地方可以看到下一層。
6、SRC_IN
註釋給出的是 [Sa * Da, Sc * Da]
透明度為 Sa * Da,說明 透明度取決源圖和目標圖的各自透明度,只有兩者的透明均為1時(完全可見),最終成像區域的透明才為完全可見,否則會被相應弱化。
色值為 Sc * Da,說明呈現影像 色值以源圖渲染。
最終呈現效果如下,成像的結果是 目標圖和源圖的交集 。
7、DST_IN
註釋給出的是 [Sa * Da, Sa * Dc]
透明度為 Sa * Da,說明 透明度取決源圖和目標圖的各自透明度,只有兩者的透明均為1時(完全可見),最終成像區域的透明才為完全可見,否則會被相應弱化。
色值為 Sa * Dc,說明呈現影像 色值以目標圖渲染。
最終呈現效果如下,成像的結果是 目標圖和源圖的交集 。
8、SRC_OUT
註釋給出的是 [Sa * (1 - Da), Sc * (1 - Da)]
透明度為 Sa * (1 - Da),說明 透明度取決源圖和目標圖的透明度,值得注意的是,目標圖的透明值越大,反而最終結果越弱,即目標圖透明度為1的地方,則最終影像不顯示該地方。目標圖透明度不為1的區域,則會對最終圖進行削弱透明度。目標圖透明度為0的區域,則不會影響到最終影像。
色值為 Sc * (1 - Da),說明呈現影像 色值以源圖渲染。
最終呈現效果如下,成像的結果是 以源圖為主,剔除與目標圖交集的地方 (因為還受透明度影響)。
9、DST_OUT
註釋給出的是 [Da * (1 - Sa), Dc * (1 - Sa)]
透明度為 Da * (1 - Sa),說明 透明度取決源圖和目標圖的透明度,值得注意的是,源圖的透明值越大,反而最終結果越弱,即源圖透明度為1的地方,則最終影像不顯示該地方。源圖透明度不為1的區域,則會對最終圖進行削弱透明度。源圖透明度為0的區域,則不會影響到最終影像。
色值為 Dc * (1 - Sa),說明呈現影像 色值以目標圖渲染。
最終呈現效果如下,成像的結果是 以目標圖圖為主,剔除與源圖交集的地方 (因為還受透明度影響)。
10、SRC_ATOP
註釋給出的是 [Da, Sc * Da + (1 - Sa) * Dc]
透明度為 Da,說明 最終影像的可見區域只取決於目標影像。
色值 Sc * Da + (1 - Sa) * Dc,說明由 目標圖和源圖共同決定。
最終呈現的效果如下,成像的結果是在 目標圖的區域內,源圖覆蓋在它上面。
11、DST_ATOP
註釋給出的是 [Sa, Sa * Dc + Sc * (1 - Da)]
透明度為 Sa,說明 最終影像的可見區域只取決於源影像。
色值 Sa * Dc + Sc * (1 - Da),說明由 目標圖和源圖共同決定。
最終呈現的效果如下,成像的結果是在 源圖的區域內,目標圖覆蓋在它上面。
12、XOR
註釋給出的是 [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc]
透明度 Sa + Da - 2 * Sa * Da,說明 透明受源圖和目標圖的共同影響,當兩者透明度為1時,最終此區域的透明度反而會為0。
色值 Sa * Dc + Sc * (1 - Da),說明由 目標圖和源圖共同決定。
最終呈現的效果如下,成像的結果為 不相交的地方,以各自的影像呈現。相交的地方受兩者的透明度影響。
13、DARKEN
註釋給出的是 [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)]
透明度為 Sa + Da - Sa*Da,從公式可以知道 透明度受源圖和目標圖的共同影響,並且最終的透明度數值會大些或是保持原值。
色值 Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc),說明由 目標圖和源圖共同決定。
最終呈現的效果如下,成像的結果為 影像的顏色會稍微偏重些。
14、LIGHTEN
註釋給出的是 [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)]
透明度為 Sa + Da - Sa*Da,從公式可以知道 透明度受源圖和目標圖的共同影響,並且最終的透明度數值會大些或是保持原值。
色值 Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc),說明由 目標圖和源圖共同決定。
最終呈現的效果如下,成像的結果為 相交部分影像的顏色會偏亮些。
15、MULTIPLY
註釋給出的是 [Sa * Da, Sc * Dc],最終成像如下,與 DST_IN 和 SRC_IN 有些類似,只是以灰度顯示。
16、SCREEN
註釋給出的是 [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc],最終成像如下,會削弱相交部分的顏色,呈現出更為亮的色澤。
17、ADD
註釋給出的是 Saturate(S + D),效果圖如下
18、OVERLAY
三、實戰
1、刮刮卡
(1)效果圖
(2)效果分析
想必大家能看出,這裡需要兩層圖,一層為“黑蜘蛛”的圖,一層為灰色遮罩。根據我們手指的滑動軌跡“擦拭掉”該地方的灰色遮罩。最後在手指抬起時,判斷被“擦拭掉”的區域是否已經超出20%,如果超出,則不再繪製遮罩,達到底層圖顯現的效果。
(3)具體實現
第一步,我們通過 onTouchEvent 實現記錄手指滑動的軌跡。 但值得注意的是,這裡做了一個小優化,使用了貝塞爾曲線,使滑動軌跡會更加的順滑,具體程式碼如下
對 “貝塞爾曲線” 感興趣的童鞋,可以檢視小盆友的另一片文章 自帶美感的貝塞爾曲線原理與實戰。
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPreX = event.getX();
mPreY = event.getY();
mPath.moveTo(mPreX, mPreY);
break;
case MotionEvent.ACTION_MOVE:
float endX = (mPreX + event.getX()) / 2;
float endY = (mPreY + event.getY()) / 2;
// 此處使用貝塞爾曲線
mPath.quadTo(mPreX, mPreY, endX, endY);
mPreX = endX;
mPreY = endY;
break;
case MotionEvent.ACTION_UP:
post(calculatePixelsRunnable);
break;
}
postInvalidate();
return true;
}
複製程式碼
第二步,我們需要將獲取到的軌跡作用於 灰色塗層 上,達到“刮卡”效果。這裡其實可以使用的模式不止一個,主要看設定的 灰色塗層 和 手指路徑 的先後順序。我們使用的為 DST_OUT。
這裡值得注意的是,需要開闢一個新的圖層, 以免模式效果作用到其他的影像上。具體程式碼如下
// 開闢新的一個圖層
int layer = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(mCoatingLayerBitmap, 0, 0, mPaint);
mPaint.setXfermode(mXfermode);
canvas.drawPath(mPath, mPaint);
mCanvas.drawPath(mPath, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layer);
複製程式碼
經過這兩步,效果就已經達到,因為我們繼承的是 ImageView ,所以 “黑蜘蛛” 圖層的放入便已經實現。
第三步,自動去除 “灰色圖層” 的操作,在每次手指抬起時,就會開啟一個執行緒來計算 “灰色圖層” 的畫素色值,如果超過20%被擦拭,則說明可以去除該 “灰色圖層”。具體程式碼如下:
private Runnable calculatePixelsRunnable = new Runnable() {
@Override
public void run() {
int width = getWidth();
int height = getHeight();
float totalPixel = width * height;
int[] pixel = new int[width * height];
mCoatingLayerBitmap.getPixels(pixel, 0, width, 0, 0, width, height);
int cleanPixel = 0;
for (int col = 0; col < height; ++col) {
for (int row = 0; row < width; ++row) {
if (pixel[col * width + row] == 0) {
cleanPixel++;
}
}
}
float result = cleanPixel / totalPixel;
if (result >= PERCENT) {
isShowAll = true;
postInvalidate();
}
}
};
複製程式碼
核心三步便已經在以上實現,剩餘的便是組裝起來,這裡不再過多贅述,完整程式碼請進傳送門。
2、心跳
(1)效果圖
(2)動畫分析
我們藉助以上小盆友手繪的一張圖來講解,綠色的心跳作為目標圖,藍色的作為源圖,通過不斷的增大dx的距離,從而讓藍色的源圖寬度不斷縮小,最終使用 DST_IN 模式合成就可以達到一點點出現的效果。至於如何讓dx一點點增大,我們使用了屬性動畫。這個例子比較簡單,我們就不再貼上程式碼。有興趣的童鞋請進傳送門。
關於 屬性動畫 小盆友在另一篇部落格中有詳細講述其原理和應用,感興趣的話,可以進傳送門。
四、寫在最後
通過Xfermode的多種模式組合可以繪製出一些酷炫的影像和效果,限制我們的永遠還是我們的想象力和那懶惰的雙手?。最後如果你從這篇文章有所收穫,請給我個贊❤️,並關注我吧。文章中如有理解錯誤或是晦澀難懂的語句,請評論區留言,我們進行討論共同進步。你的鼓勵是我前進的最大動力。
高階UI系列的Github地址:請進入傳送門,如果喜歡的話給我一個star吧?
如果需要更為深入的探討,加我微信吧?。