1. 程式人生 > >PorterDuffXfermode 影象混合技術在漫畫APP中的應用

PorterDuffXfermode 影象混合技術在漫畫APP中的應用

此文已由作者遊葳授權網易雲社群釋出。

歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。



寫在開頭

隨著應用開發的深入,視覺同學在完成了頁面的基本設計後,再也按耐不住心中的寂寞,開始對各種細節不滿意,於是乎就會提出各種視覺優化的方案。作為開發人員,啥也別說了,你懂的,有困難要上,沒困難,製造困難也要上。既然是優化提升的方案,那很多時候只使用系統提供的各種控制元件,或者只是簡單的用Paint去進行圖形顏色的繪製,已經滿足不了視覺同志的胃口了,這就要求我們必須掌握Paint的進階技巧,比如本文介紹的影象混合技術 - PorterDuffXfermode。

PorterDuffXfermode 簡介

相信很多android開發同學和我一樣,第一次看到這個ProterDuff單詞都會覺得奇怪,這是個啥子意思呢。作為一個豬場員工,我當然是立刻馬上用有道詞典翻譯了一下,結果啥也沒搜出來。後來上網查了才知道,ProterDuff是兩個人名的組合: Tomas Proter和 Tom Duff. 這兩個人在1984年一起寫了一篇名為《Compositing Digital Images》的論文。我們知道,一個畫素是由ARGB四個分量組成的,該論文就論述瞭如何實現不同數字影象的畫素之間是如何進行混合的,並提出了多種畫素混合的模式。PorterDuffXfermode支援以下十幾種畫素顏色的混合模式,分別為:

CLEAR        計算方式:[0, 0],效果:清除

SRC             計算方式:[Sa, Sc];效果:只繪製源影象

DST              計算方式:[Da, Dc];效果:只繪製目標影象

SRC_OVER 計算方式:[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] 說明:在目標影象的上方繪製源影象

DST_OVER  計算方式:[Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc];說明:在源影象的上方繪製目標影象

SRC_IN       計算方式:[Sa * Da, Sc * Da];說明:只在源影象和目標影象相交的地方繪製目標影象

DST_IN        計算方式:[Sa * Da, Sa * Dc];說明:只在源影象和目標影象相交的地方繪製目標影象

SRC_OUT   計算方式:[Sa * (1 - Da), Sc * (1 - Da)];說明:只在目標影象和源影象不相交的地方繪製目標影象

DST_OUT    計算方式:[Da * (1 - Sa), Dc * (1 - Sa)];說明:只在源影象和目標影象不相交的地方繪製源影象

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

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

XOR      計算方式:[Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc];說明:在源影象和目標影象不相交的地方各自繪製,在重疊的地方不繪製任何內容

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

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

MULTIPLY 計算方式:[Sa * Da, Sc * Dc];說明:混合

ADD      計算方式:Saturate(S + D);說明:飽和度相加   

S代表源畫素,源畫素的顏色值表示為[Sa, Sc],Sa中的a是alpha的縮寫,Sa表示源畫素的Alpha值,Sc中的c是顏色color的縮寫,Sc表示源畫素的RGB。D代表目標畫素,目標畫素的顏色值表示為[Da, Dc],Da表示目標畫素的Alpha值,Dc表示目標畫素的RGB。

合成後[]逗號前面的這一部分的值代表計算後的Alpha通道,而逗號後的這一部分的值代表計算後的顏色值,圖形混合後的圖片依靠這個向量來計算ARGB的值。

一張容易被誤解的神圖

相信很多人在用到PorterDuffXfermode的時候都有看過這張圖吧,這張圖是Android的sdk下自帶的API的Demo示例。但是如果按照這張圖的示例進行開發的話,有時可能會達不到預期效果。比如第一種的CLEAR效果,乍一看該圖,CLEAR達到的效果應該是把dst和src的圖片全部都清空了,但這個其實是不對的,因為PorterDuffXfermode 的機制就是src與dst進行各種混合變化,在超出src範圍內的區域是不起作用的,所以CLEAR只是把src所包含部分清除了,但是在圖上看了,卻是整個圖層上啥都沒有了,這個又是為什麼呢?

這個祕密就藏在Demo的原始碼中,開啟位於/Users/netease/Library/Android/sdk/samples/android-19/legacy/ApiDemos/src/com/example/android/apis/graphics目錄下的Xfermodes.java檔案,示例中建立dst和src圖片的原始碼如下:

    // create a bitmap with a circle, used for the "dst" image
    static Bitmap makeDst(int w, int h) {
        Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bm);
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);

        p.setColor(0xFFFFCC44);
        c.drawOval(new RectF(0, 0, w*3/4, h*3/4), p);        return bm;
    }


    // create a bitmap with a rect, used for the "src" image
    static Bitmap makeSrc(int w, int h) {
        Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bm);
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);

        p.setColor(0xFF66AAFF);
        c.drawRect(w/3, h/3, w*19/20, h*19/20, p);        return bm;
    }


發現了嗎?原來在示例中建立的dst和src的大小,不只是我們從圖中看到的只是那兩個圓圈和方塊而已,而是整個圖的範圍,黃色和藍色的區域其實只是dst和src的一部分而已,只是其他部分是透明的,讓人容易誤以為dst和src就那麼大而已。所以,當都是w * h範圍的src和dst用CLEAR模式進行混合後,才會出現全部都沒有的效果。

假如就以實際看到的兩個區域作為dst和src的大小來進行演示,效果又會是怎麼樣的呢?修改後的混合效果如下( 為了方便觀察比較,我將填滿整個頁面充當背景的View的顏色設定成白色,每一個顯示效果的小View的背景色設定為綠色):

    

可以看到,當dst和src都只是圓圈和方塊大小是,CLEAR模式下就僅僅只是把方塊區域給清除了,這個才是CLEAR的真實效果。那為什麼上下兩張圖的清除效果又會有所差異呢,一個是顯示當前View的綠色背景,一個則直接顯示了底層View的白色背景呢?

嘿嘿,原因就在於對canvas圖層(layer)的使用。第一個圖的流程是先繪製綠色背景,然後再呼叫saveLayer新生成一個圖層進行dst和src的CLEAR操作,操作完後呼叫restore退出該圖層,將該圖層合成到原圖層上。第二個圖的流程是首先生成一個新圖層,然後在圖層上繪製綠色背景,進行dst和src的操作,這個時候由於綠色背景和dst,src是處於同一layer,因此CLEAR操作會將該layer上方塊區域的RGB色值全部設定為0,再將該layer合成到原圖層上後,就把底下的白色背景顯示出來了(詳細程式碼可見附件)。很多時候我們應該需要的是第一個圖的效果,因此在操作的時候就要使用saveLayer、restoreToCount的方法把混合操作放在新圖層上進行了。


一個食慄

有一天,做視覺的胖大叔(是的,負責給我們做視覺的是個大叔,原來的妹紙被拉去做官網視覺了。。。)跑過來給我說,誒,這個意見反饋傳送圖片的效果要改啊,要改的和微信的效果一樣(微信聊天介面傳送圖片是什麼效果,我覺得我就不用上圖了吧)。額,是不是做聊天的都要向微信學(抄)啊,嘿嘿。好吧,原來那種簡單的直接給ImageView新增一個背景的方式是用不了咯,想想怎麼搞吧。

一開始的想法是利用剪裁的方式,把圖片按照背景泡泡圖片的尺寸進行裁剪,但是這個計算就比較累,而且那個尖角是什麼鬼?這個要怎麼計算。在紙上畫了半天了,突然腦子裡閃過“ PorterDuffXfermode”(好吧,其實當時肯定拼錯了)幾個字,哈哈,終於找到了解決問題的那把key了。於是乎,趕緊上網複習了一下PorterDuffXfermode的相關資料,確定了應該要使用的是SRC_IN的模式,將泡泡圖片作為dst,需要顯示的圖片作為src,保證這兩者長寬一致,那合成後就是具有泡泡形狀的圖片了。想清楚了就直接開始實踐,最終完美的達到了胖大叔的要求:

具體實現流程如下:

1 自定義一個繼承自ImageView的控制元件OverlapImageView,因為ImageView裡面正好可以配置背景圖片(dst)和前景圖片(src)。由於我們使用的背景泡泡圖是一張.9圖片,因此初始化的時候如果背景是.9圖,還需要進行一下處理:


        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.OverlapImageView,defStyleAttr,0);        boolean isNinePatch = a.getBoolean(R.styleable.OverlapImageView_isNinePatch,false);        final int srcResId = a.getResourceId(R.styleable.OverlapImageView_dst, 0);        final TypedValue value = new TypedValue();        final Resources r = a.getResources();        try {            final InputStream is = r.openRawResource(srcResId, value);            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inScreenDensity = (int) r.getDisplayMetrics().scaledDensity;            final Rect padding = new Rect();
            dstBp = BitmapFactory.decodeResourceStream(r, value, is, padding, options);
            is.close();            if (isNinePatch && dstBp != null){
                mNinePatch = new NinePatch(dstBp,dstBp.getNinePatchChunk(),null);
            }
        } catch (IOException e) {            // Ignore
            e.printStackTrace();
            Drawable d = a.getDrawable(R.styleable.OverlapImageView_dst);
            dstBp = drawableToBitmap(d);
        }

2 複寫onDraw()方法:

    @Override
    protected void onDraw(Canvas canvas) {        if(srcBp != null && (dstBp != null || mNinePatch != null)){            int width = getRight() - getLeft() > 0 ? getRight() - getLeft() : srcBp.getWidth();            int height = getBottom() - getTop() > 0 ? getBottom() - getTop() :srcBp.getHeight();            //1.建立一個新圖層Layer進行效果合成
            int sc = canvas.saveLayer(0,0,width,height,null,Canvas.ALL_SAVE_FLAG);
            Rect r = new Rect(0,0,width,height);            //2.繪製DST
            if (mNinePatch != null){
                mNinePatch.draw(canvas,r,mPaint);
            }else {
                canvas.drawBitmap(dstBp,null,r,mPaint);
            }            //3.設定混合模式,一旦呼叫該方法,當前Layer上的內容會被作為DST
            mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));            //4.繪製SRC
            canvas.drawBitmap(srcBp,null,r,mPaint);
            mPaint.setXfermode(null);            //5.當前Layer退棧,將其內容儲存到canvas預設的Layer上
            canvas.restoreToCount(sc);
        }else {            super.onDraw(canvas);
        }
    }

需要注意的一點就是一旦呼叫setXfermode()方法後,當前Layer上的內容就會被當做dst的內容進行處理。canvas預設是自帶了一個Layer,因此如果沒有呼叫saveLayer(),那當前canvas上的所有內容都是dst了。因此必須搞清楚哪些內容是dst,不然的話合成出來的就可能達不到預期效果了。

另一個栗子

又過了幾天,胖大叔又呼哧呼哧的跑過來找我,說是那個粉絲榜的進度條效果不好看,要改!然後他就給我發了一張具有指導性意見的圖片,就按這個效果改啊:

有了上次的經驗,這個我略微思索,掐指一算,嗯,你小子不就是XOR模式嗎,當然,還需要對progressBar進行一下改造,預設的進度條是無法顯示文字的。大致的實現思路就是先新建一個Layer,繪製ProgressBar的邊框,繪製進度條,然後把這兩個影象作為dst,再繪製文字作為src,最後使用XOR模式進行合成,就可以達到上面的效果啦,具體程式碼如下:

1 新建進度條背景的xml檔案:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape android:shape="rectangle">
            <corners android:radius="3dp"></corners>
            <stroke android:width="1px"
            android:color="@color/bg_color_edc550"></stroke>
        </shape>
    </item>
    <item android:id="@android:id/progress">
        <clip>
            <shape android:shape="rectangle">
                <corners android:radius="3dp"></corners>
                <solid android:color="@color/bg_color_ffd53a"/>
            </shape>
        </clip>
    </item>
</layer-list>

2 自定義一個控制元件繼承ProgressBar,然後複寫onDraw()方法:

    @Override
    protected synchronized void onDraw(Canvas canvas) {        if (mText.length() == 0){            super.onDraw(canvas);            return;
        }else if (!mRevertMode){            super.onDraw(canvas);
            drawText(canvas);            return;
        }        //1.新建一個圖層Layer
        int sc = canvas.saveLayer(0,0,getMeasuredWidth(),getMeasuredHeight(),null,Canvas.ALL_SAVE_FLAG);        //2. 繪製背景邊框
        final Drawable backgound = getBackground();        if (backgound != null){
            backgound.draw(canvas);
        }        //3. 繪製進度條
        final Drawable d = getProgressDrawable();        if(d != null){            final int saveCount = canvas.save();
            d.draw(canvas);
            canvas.restoreToCount(saveCount);            if (mText.length() > 0){                //4. 設定混合模式XOR,
                mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));                //5. 繪製進度文字
                drawText(canvas);
                mPaint.setXfermode(null);
            }
        }        //6.退棧,將效果合成到canvas中
        canvas.restoreToCount(sc);
    }

最終的效果如下:


TIPS:

假如你設定的混合模式沒有生效,試著關閉一下硬體加速功能。



免費體驗雲安全(易盾)內容安全、驗證碼等服務

更多網易技術、產品、運營經驗分享請點選




相關文章:
【推薦】 PaaS服務之路漫談(三)
【推薦】 雲端計算互動設計師的正確出裝姿勢