正值猿宵佳節,小盆友在此祝你們新年無BUG。😄java
目錄git
1、前言github
2、PorterDuffXfermodecanvas
3、實戰微信
4、寫在最後ide
自定義UI中,少不了對多種圖像的疊加覆蓋,而須要達到預期的目的,咱們便須要今天的主角Xfermode。Xfermode 有三個孩子,分別是:工具
而 AvoidXfermode 和 PixelXorXfermode 已經在 API 16以後被標記爲removed,因此就只剩下小兒子 PorterDuffXfermode 爲咱們合成圖像,理所固然咱們今天的重點也就在他身上。老規矩,先上幾張實戰圖,而後開始咱們今天的分享。post
Xfermode小工具優化
刮刮卡 動畫
心跳
咱們看如下兩段源碼,可知 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 圖。小盆友也跟着手寫了一遍,須要看源碼的童鞋進傳送門
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這幾個值。他們各自表明(結合着英文記,更容易):
而 源圖像 和 目標圖像 又是什麼呢?記住一句話就能夠,先設置的爲目標圖(Dst),後設置的爲源圖(Src)。
全部的疑惑咱們已經先點破,接下里就給出咱們比較全面的Demo,這是小盆友以官方所示的十六種模式提供的Xfermode小工具,若是有時候拿捏不許具體使用什麼模式時,能夠進行加入這個工具來進行琢磨。對該小工具感興趣的請進傳送門。
接下來咱們便逐個講解模式,所使用的圖片均來自 Xfermode工具 的zinc例子。
註釋給出的是 [0, 0] , 透明度 爲0,即徹底看不見;顏色 爲0,即無色; 最終呈現以下圖,什麼都沒有。
註釋給出的是[Sa, Sc], 透明度 爲Sa,即取決於源圖的透明值;顏色 爲Sc,即取源圖的色值; 最終呈現以下圖,由於都是取源圖的值,因此最終就是顯示 源圖。
註釋給出的是[Da, Dc],透明度 爲Da,即取目標圖的透明度;顏色 爲Dc,即取目標圖的色值; 最終呈現以下圖,由於都取目標圖的值,因此最終呈現的就是 目標圖。
註釋給出的是 [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] ,其實就是源圖蓋於目標圖上,如有透明度,則會看到下一層,從名字也能夠很好的記憶。
註釋給出的是 [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc],和 SRC_OVER相反 ,目標圖蓋於源圖上,有透明度的地方能夠看到下一層。
註釋給出的是 [Sa * Da, Sc * Da]
透明度爲 Sa * Da,說明 透明度取決源圖和目標圖的各自透明度,只有二者的透明均爲1時(徹底可見),最終成像區域的透明才爲徹底可見,不然會被相應弱化。
色值爲 Sc * Da,說明呈現圖像 色值以源圖渲染。
最終呈現效果以下,成像的結果是 目標圖和源圖的交集 。
註釋給出的是 [Sa * Da, Sa * Dc]
透明度爲 Sa * Da,說明 透明度取決源圖和目標圖的各自透明度,只有二者的透明均爲1時(徹底可見),最終成像區域的透明才爲徹底可見,不然會被相應弱化。
色值爲 Sa * Dc,說明呈現圖像 色值以目標圖渲染。
最終呈現效果以下,成像的結果是 目標圖和源圖的交集 。
註釋給出的是 [Sa * (1 - Da), Sc * (1 - Da)]
透明度爲 Sa * (1 - Da),說明 透明度取決源圖和目標圖的透明度,值得注意的是,目標圖的透明值越大,反而最終結果越弱,即目標圖透明度爲1的地方,則最終圖像不顯示該地方。目標圖透明度不爲1的區域,則會對最終圖進行削弱透明度。目標圖透明度爲0的區域,則不會影響到最終圖像。
色值爲 Sc * (1 - Da),說明呈現圖像 色值以源圖渲染。
最終呈現效果以下,成像的結果是 以源圖爲主,剔除與目標圖交集的地方 (由於還受透明度影響)。
註釋給出的是 [Da * (1 - Sa), Dc * (1 - Sa)]
透明度爲 Da * (1 - Sa),說明 透明度取決源圖和目標圖的透明度,值得注意的是,源圖的透明值越大,反而最終結果越弱,即源圖透明度爲1的地方,則最終圖像不顯示該地方。源圖透明度不爲1的區域,則會對最終圖進行削弱透明度。源圖透明度爲0的區域,則不會影響到最終圖像。
色值爲 Dc * (1 - Sa),說明呈現圖像 色值以目標圖渲染。
最終呈現效果以下,成像的結果是 以目標圖圖爲主,剔除與源圖交集的地方 (由於還受透明度影響)。
註釋給出的是 [Da, Sc * Da + (1 - Sa) * Dc]
透明度爲 Da,說明 最終圖像的可見區域只取決於目標圖像。
色值 Sc * Da + (1 - Sa) * Dc,說明由 目標圖和源圖共同決定。
最終呈現的效果以下,成像的結果是在 目標圖的區域內,源圖覆蓋在它上面。
註釋給出的是 [Sa, Sa * Dc + Sc * (1 - Da)]
透明度爲 Sa,說明 最終圖像的可見區域只取決於源圖像。
色值 Sa * Dc + Sc * (1 - Da),說明由 目標圖和源圖共同決定。
最終呈現的效果以下,成像的結果是在 源圖的區域內,目標圖覆蓋在它上面。
註釋給出的是 [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc]
透明度 Sa + Da - 2 * Sa * Da,說明 透明受源圖和目標圖的共同影響,當二者透明度爲1時,最終此區域的透明度反而會爲0。
色值 Sa * Dc + Sc * (1 - Da),說明由 目標圖和源圖共同決定。
最終呈現的效果以下,成像的結果爲 不相交的地方,以各自的圖像呈現。相交的地方受二者的透明度影響。
註釋給出的是 [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),說明由 目標圖和源圖共同決定。
最終呈現的效果以下,成像的結果爲 圖像的顏色會稍微偏重些。
註釋給出的是 [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),說明由 目標圖和源圖共同決定。
最終呈現的效果以下,成像的結果爲 相交部分圖像的顏色會偏亮些。
註釋給出的是 [Sa * Da, Sc * Dc],最終成像以下,與 DST_IN 和 SRC_IN 有些相似,只是以灰度顯示。
註釋給出的是 [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc],最終成像以下,會削弱相交部分的顏色,呈現出更爲亮的色澤。
註釋給出的是 Saturate(S + D),效果圖以下
想必你們能看出,這裏須要兩層圖,一層爲「黑蜘蛛」的圖,一層爲灰色遮罩。根據咱們手指的滑動軌跡「擦拭掉」該地方的灰色遮罩。最後在手指擡起時,判斷被「擦拭掉」的區域是否已經超出20%,若是超出,則再也不繪製遮罩,達到底層圖顯現的效果。
第一步,咱們經過 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();
}
}
};
複製代碼
核心三步便已經在以上實現,剩餘的即是組裝起來,這裏再也不過多贅述,完整代碼請進傳送門。
至於如何讓dx一點點增大,咱們使用了屬性動畫。這個例子比較簡單,咱們就再也不粘貼代碼。有興趣的童鞋請進傳送門。
關於 屬性動畫 小盆友在另外一篇博客中有詳細講述其原理和應用,感興趣的話,能夠進傳送門。
經過Xfermode的多種模式組合能夠繪製出一些酷炫的圖像和效果,限制咱們的永遠仍是咱們的想象力和那懶惰的雙手😄。最後若是你從這篇文章有所收穫,請給我個贊❤️,並關注我吧。文章中若有理解錯誤或是晦澀難懂的語句,請評論區留言,咱們進行討論共同進步。你的鼓勵是我前進的最大動力。
高級UI系列的Github地址:請進入傳送門,若是喜歡的話給我一個star吧😄
若是須要更爲深刻的探討,加我微信吧😁。