影像操縱大師Xfermode講解與實戰——Android高階UI

猛猛的小盆友發表於2019-02-19

正值猿宵佳節,小盆友在此祝大家新年無BUG。?

目錄

一、前言

二、PorterDuffXfermode

三、實戰

四、寫在最後

一、前言

自定義UI中,少不了對多種影像的疊加覆蓋,而需要達到預期的目的,我們便需要今天的主角XfermodeXfermode 有三個孩子,分別是:

  1. AvoidXfermode
  2. PixelXorXfermode
  3. PorterDuffXfermode

而 AvoidXfermode 和 PixelXorXfermode 已經在 API 16之後被標記為removed,所以就只剩下小兒子 PorterDuffXfermode 為我們合成影像,理所當然我們今天的重點也就在他身上。老規矩,先上幾張實戰圖,然後開始我們今天的分享。

Xfermode小工具

影像操縱大師Xfermode講解與實戰——Android高階UI

刮刮卡

影像操縱大師Xfermode講解與實戰——Android高階UI

心跳

影像操縱大師Xfermode講解與實戰——Android高階UI

二、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 圖。小盆友也跟著手寫了一遍,需要看原始碼的童鞋進傳送門

影像操縱大師Xfermode講解與實戰——Android高階UI
但是,這個 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),其值決定了這張合成圖的顏色值。

聰明的童鞋還會注意到 SaDaScDc這幾個值。他們各自代表(結合著英文記,更容易):

  • Sa(Source Alpha):源影像的透明值;
  • Da(Destination Alpha):目標影像的透明值;
  • Sc(Source Color):源影像的色值;
  • Dc(Destination Color):目標影像的色值;

源影像目標影像 又是什麼呢?記住一句話就可以,先設定的為目標圖(Dst),後設定的為源圖(Src)

所有的疑惑我們已經先點破,接下里就給出我們比較全面的Demo,這是小盆友以官方所示的十六種模式提供的Xfermode小工具,如果有時候拿捏不準具體使用什麼模式時,可以進行加入這個工具來進行琢磨。對該小工具感興趣的請進傳送門

影像操縱大師Xfermode講解與實戰——Android高階UI

接下來我們便逐個講解模式,所使用的圖片均來自 Xfermode工具 的zinc例子。

1、CLEAR

註釋給出的是 [0, 0] , 透明度 為0,即完全看不見顏色 為0,即無色; 最終呈現如下圖,什麼都沒有。

影像操縱大師Xfermode講解與實戰——Android高階UI

2、SRC

註釋給出的是[Sa, Sc], 透明度 為Sa,即取決於源圖的透明值顏色 為Sc,即取源圖的色值; 最終呈現如下圖,因為都是取源圖的值,所以最終就是顯示 源圖

影像操縱大師Xfermode講解與實戰——Android高階UI

3、DST

註釋給出的是[Da, Dc],透明度 為Da,即取目標圖的透明度顏色 為Dc,即取目標圖的色值; 最終呈現如下圖,因為都取目標圖的值,所以最終呈現的就是 目標圖

影像操縱大師Xfermode講解與實戰——Android高階UI

4、SRC_OVER

註釋給出的是 [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] ,其實就是源圖蓋於目標圖上,若有透明度,則會看到下一層,從名字也可以很好的記憶。

影像操縱大師Xfermode講解與實戰——Android高階UI

5、DST_OVER

註釋給出的是 [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc],和 SRC_OVER相反目標圖蓋於源圖上,有透明度的地方可以看到下一層

影像操縱大師Xfermode講解與實戰——Android高階UI

6、SRC_IN

註釋給出的是 [Sa * Da, Sc * Da]

透明度為 Sa * Da,說明 透明度取決源圖和目標圖的各自透明度,只有兩者的透明均為1時(完全可見),最終成像區域的透明才為完全可見,否則會被相應弱化。

色值為 Sc * Da,說明呈現影像 色值以源圖渲染

最終呈現效果如下,成像的結果是 目標圖和源圖的交集

影像操縱大師Xfermode講解與實戰——Android高階UI

7、DST_IN

註釋給出的是 [Sa * Da, Sa * Dc]

透明度為 Sa * Da,說明 透明度取決源圖和目標圖的各自透明度,只有兩者的透明均為1時(完全可見),最終成像區域的透明才為完全可見,否則會被相應弱化。

色值為 Sa * Dc,說明呈現影像 色值以目標圖渲染

最終呈現效果如下,成像的結果是 目標圖和源圖的交集

影像操縱大師Xfermode講解與實戰——Android高階UI

8、SRC_OUT

註釋給出的是 [Sa * (1 - Da), Sc * (1 - Da)]

透明度為 Sa * (1 - Da),說明 透明度取決源圖和目標圖的透明度,值得注意的是,目標圖的透明值越大,反而最終結果越弱,即目標圖透明度為1的地方,則最終影像不顯示該地方。目標圖透明度不為1的區域,則會對最終圖進行削弱透明度。目標圖透明度為0的區域,則不會影響到最終影像。

色值為 Sc * (1 - Da),說明呈現影像 色值以源圖渲染

最終呈現效果如下,成像的結果是 以源圖為主,剔除與目標圖交集的地方 (因為還受透明度影響)。

影像操縱大師Xfermode講解與實戰——Android高階UI

9、DST_OUT

註釋給出的是 [Da * (1 - Sa), Dc * (1 - Sa)]

透明度為 Da * (1 - Sa),說明 透明度取決源圖和目標圖的透明度,值得注意的是,源圖的透明值越大,反而最終結果越弱,即源圖透明度為1的地方,則最終影像不顯示該地方。源圖透明度不為1的區域,則會對最終圖進行削弱透明度。源圖透明度為0的區域,則不會影響到最終影像。

色值為 Dc * (1 - Sa),說明呈現影像 色值以目標圖渲染

最終呈現效果如下,成像的結果是 以目標圖圖為主,剔除與源圖交集的地方 (因為還受透明度影響)。

影像操縱大師Xfermode講解與實戰——Android高階UI

10、SRC_ATOP

註釋給出的是 [Da, Sc * Da + (1 - Sa) * Dc]

透明度為 Da,說明 最終影像的可見區域只取決於目標影像

色值 Sc * Da + (1 - Sa) * Dc,說明由 目標圖和源圖共同決定

最終呈現的效果如下,成像的結果是在 目標圖的區域內,源圖覆蓋在它上面。

影像操縱大師Xfermode講解與實戰——Android高階UI

11、DST_ATOP

註釋給出的是 [Sa, Sa * Dc + Sc * (1 - Da)]

透明度為 Sa,說明 最終影像的可見區域只取決於源影像

色值 Sa * Dc + Sc * (1 - Da),說明由 目標圖和源圖共同決定

最終呈現的效果如下,成像的結果是在 源圖的區域內,目標圖覆蓋在它上面。

影像操縱大師Xfermode講解與實戰——Android高階UI

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),說明由 目標圖和源圖共同決定

最終呈現的效果如下,成像的結果為 不相交的地方,以各自的影像呈現。相交的地方受兩者的透明度影響

影像操縱大師Xfermode講解與實戰——Android高階UI

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),說明由 目標圖和源圖共同決定

最終呈現的效果如下,成像的結果為 影像的顏色會稍微偏重些

影像操縱大師Xfermode講解與實戰——Android高階UI

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_INSRC_IN 有些類似,只是以灰度顯示。

影像操縱大師Xfermode講解與實戰——Android高階UI

16、SCREEN

註釋給出的是 [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc],最終成像如下,會削弱相交部分的顏色,呈現出更為亮的色澤。

在這裡插入圖片描述

17、ADD

註釋給出的是 Saturate(S + D),效果圖如下

影像操縱大師Xfermode講解與實戰——Android高階UI

18、OVERLAY

影像操縱大師Xfermode講解與實戰——Android高階UI

三、實戰

1、刮刮卡

(1)效果圖

影像操縱大師Xfermode講解與實戰——Android高階UI

(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)效果圖

影像操縱大師Xfermode講解與實戰——Android高階UI

(2)動畫分析

影像操縱大師Xfermode講解與實戰——Android高階UI
我們藉助以上小盆友手繪的一張圖來講解,綠色的心跳作為目標圖,藍色的作為源圖,通過不斷的增大dx的距離,從而讓藍色的源圖寬度不斷縮小,最終使用 DST_IN 模式合成就可以達到一點點出現的效果。

至於如何讓dx一點點增大,我們使用了屬性動畫。這個例子比較簡單,我們就不再貼上程式碼。有興趣的童鞋請進傳送門

關於 屬性動畫 小盆友在另一篇部落格中有詳細講述其原理和應用,感興趣的話,可以進傳送門

四、寫在最後

通過Xfermode的多種模式組合可以繪製出一些酷炫的影像和效果,限制我們的永遠還是我們的想象力和那懶惰的雙手?。最後如果你從這篇文章有所收穫,請給我個贊❤️,並關注我吧。文章中如有理解錯誤或是晦澀難懂的語句,請評論區留言,我們進行討論共同進步。你的鼓勵是我前進的最大動力。

高階UI系列的Github地址:請進入傳送門,如果喜歡的話給我一個star吧?

相關文章