Canvas和PorterDuffXfermode的秘密

前要:此篇主要以Android舉例博敬,iOS可以參考CGBlendMode

Canvas

繪制四要素:
1個Bitmap用來承載像素信息缩多,
1個繪圖基元(例如呆奕,Rect,Path衬吆,text梁钾,Bitmap),
1個Canvas用來管理Draw相關(guān)方法(把繪圖基元寫入Bitmap中)逊抡,
1個畫筆(用于描繪圖像的顏色和風格)姆泻。
這和我們?nèi)粘@斫獾睦L畫異曲同工,Bitmap作為畫布秦忿,Canvas管理著繪畫的手法麦射,繪圖基元代表著要繪制的目標,Paint就是你手里的畫筆和顏料灯谣。

獲取Canvas
1.onDraw方法的入口參數(shù)就是Canvas潜秋,直接可以使用,而我們操作這個Canvas最終的效果會反應(yīng)在這個View上胎许。
2.新建Canvas峻呛。1個Canvas對象一定要結(jié)合1個Bitmap對象,所以一定要為新建的Canvas對象設(shè)置1個Bitmap對象辜窑。但是要注意屿脐,該bitmap一定要是mutable(可變的)

Bitmap.createBitmap(mWidth, mHeight, Config.ARGB_8888) ;
或者
BimtapFactory.decodeResource().copy(configu_argb_8888, true);
(BimapFactory.decodeResource() 得到的mutable 為false)

Canvas繪制
Canvas內(nèi)部維持了一個mutable Bitmap肋联,所以我們有一系列方法:
drawRGB(int r, int g, int b)
drawARGB(int a, int r, int g, int b)
drawColor(int color)
drawColor(int color, PorterDuff.Mode mode)
drawPaint(Paint paint)
繪制圖形
canvas.drawArc (扇形)
canvas.drawCircle(圓)
canvas.drawOval(橢圓)
canvas.drawLine(線)
canvas.drawPoint(點)
canvas.drawRect(矩形)
canvas.drawRoundRect(圓角矩形)
canvas.drawVertices(頂點)
cnavas.drawPath(路徑)
繪制圖片
canvas.drawBitmap(位圖)
canvas.drawPicture(圖片)
文本
canvas.drawText

Canvas變換
Canvas不僅僅可以draw一些圖形肤无、圖片论泛,其本身也提供了可操作的方法:rorate(旋轉(zhuǎn))、scale(壓縮)、translate(平移)方面、skew(扭曲)等放钦。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawRect(new Rect(0, 0, 200, 200), new Paint());
    canvas.scale(0.5f, 0.5f);//縮放了
    canvas.drawRect(new Rect(400, 400, 600, 600), new Paint());

    canvas.translate(600, 600);//平移了
    canvas.rotate(45);//旋轉(zhuǎn)了
    canvas.drawRect(new Rect(0, 0, 200, 200), new Paint());

    canvas.translate(200, 200);
    canvas.skew(.5f, .5f);//扭曲了
    canvas.drawRect(new Rect(0, 0, 200, 200), new Paint());
}

Canvas保存和回滾
如繪制表盤:

@Override
protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);

 for (int i = 0; i < 360; i = i + 6) {
     canvas.save();
     canvas.rotate(i, 100, 100);
     canvas.drawLine(100, 0, 100, 10, new Paint());
     canvas.restore();
 }
}

PorterDuffXfermode

概述

PorterDuffXfermode extends Xfermode。它將所繪制的圖形的像素與Canvas中對應(yīng)位置的像素按照一定規(guī)則進行混合恭金,形成新的像素值操禀,從而更新Canvas中最終的像素顏色值。

PorterDuffXfermode這個類中的Porter和Duff是兩個人名横腿,這兩個人在1984年一起寫了一篇名為《Compositing Digital Images》的論文颓屑。

我們下面會分析幾個代碼片段研究PorterDuffXfermode使用及工作原理詳解。

示例一

我們在演示如何使用PorterDuffXfermode之前耿焊,先看一下下面這個例子揪惦,代碼如下所示:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //設(shè)置背景色
        canvas.drawARGB(255, 139, 197, 186);
 
        int canvasWidth = canvas.getWidth();
        int r = canvasWidth / 3;
        //繪制黃色的圓形
        paint.setColor(0xFFFFCC44);
        canvas.drawCircle(r, r, r, paint);
        //繪制藍色的矩形
        paint.setColor(0xFF66AAFF);
        canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint);
    }

我們重寫了View的onDraw方法,首先將View的背景色設(shè)置為綠色搀别,然后繪制了一個黃色的圓形丹擎,然后再繪制一個藍色的矩形,效果如下所示:

上面演示就是Canvas正常的繪圖流程歇父,后來繪制的圖形就會覆蓋之前繪制的圖形!

示例二

下面我們使用PorterDuffXfermode對上面的代碼進行一下修改再愈,修改后的代碼如下所示:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //設(shè)置背景色
        canvas.drawARGB(255, 139, 197, 186);
 
        int canvasWidth = canvas.getWidth();
        int r = canvasWidth / 3;
        //正常繪制黃色的圓形
        paint.setColor(0xFFFFCC44);
        canvas.drawCircle(r, r, r, paint);
        //使用CLEAR作為PorterDuffXfermode繪制藍色的矩形
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        paint.setColor(0xFF66AAFF);
        canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint);
        //最后將畫筆去除Xfermode
        paint.setXfermode(null);
    }

效果如下所示:

下面我們對以上代碼進行一下分析:

在canvas.drawRect()之前setXfermode榜苫,那么所繪制的矩形中的像素稱作源像素(Src),所繪制的矩形在Canvas中對應(yīng)位置的矩形內(nèi)的像素稱作目標像素(Dst)翎冲。
即根據(jù)Xfermode的規(guī)則垂睬,Src是繪制內(nèi)容像素,Dst是Canvas像素抗悍。Src和Dst的ARGB值會重新計算驹饺。
本例中Xfermode是PorterDuff.Mode.CLEAR,直接將目標像素的ARGB四個分量全置為0缴渊,即(0赏壹,0,0衔沼,0)蝌借,即透明色,所以實際上繪制了一個透明的矩形指蚁。但是效果圖為什么是白色的呢菩佑,因為屏幕本身是白色的。

示例三

我們在對示例二中的代碼進行一下修改凝化,將繪制圓形和繪制矩形相關(guān)的代碼放到canvas.saveLayer()和canvas.restoreToCount()之間稍坯,代碼如下所示:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //設(shè)置背景色
        canvas.drawARGB(255, 139, 197, 186);
 
        int canvasWidth = canvas.getWidth();
        int canvasHeight = canvas.getHeight();
        int layerId = canvas.saveLayer(0, 0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG);
            int r = canvasWidth / 3;
            //正常繪制黃色的圓形
            paint.setColor(0xFFFFCC44);
            canvas.drawCircle(r, r, r, paint);
            //使用CLEAR作為PorterDuffXfermode繪制藍色的矩形
            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
            paint.setColor(0xFF66AAFF);
            canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint);
            //最后將畫筆去除Xfermode
            paint.setXfermode(null);
        canvas.restoreToCount(layerId);
    }

效果如下所示:

下面對上述代碼進行一下分析:

關(guān)于canvas繪圖中的layer有以下幾點需要說明:

  1. canvas是支持圖層layer渲染這種技術(shù)的,canvas默認就有一個layer搓劫,當我們平時調(diào)用canvas的各種drawXXX()方法時瞧哟,其實是把所有的東西都繪制到canvas這個默認的layer上面袜蚕。

  2. 我們還可以通過canvas.saveLayer()新建一個layer,新建的layer放置在canvas默認layer的上部绢涡,當我們執(zhí)行了canvas.saveLayer()之后牲剃,我們所有的繪制操作都繪制到了我們新建的layer上,而不是canvas默認的layer雄可。

  3. 用canvas.saveLayer()方法產(chǎn)生的layer所有像素的ARGB值都是(0凿傅,0,0数苫,0)聪舒,即canvas.saveLayer()方法產(chǎn)生的layer初始時時完全透明的。

  4. canvas.saveLayer()方法會返回一個int值虐急,用于表示layer的ID箱残,在我們對這個新layer繪制完成后可以通過調(diào)用canvas.restoreToCount(layer)或者canvas.restore()把這個layer繪制到canvas默認的layer上去,這樣就完成了一個layer的繪制工作止吁。

那你可能感覺到很奇怪被辑,我們只是將繪制圓形與矩形的代碼放到了canvas.saveLayer()和canvas.restoreToCount()之間,為什么不再像示例二那樣顯示白色的矩形了敬惦?

在將一個新建的layer繪制到Canvas上去時盼理,Android會用整個layer上面的像素顏色去更新Canvas對應(yīng)位置上像素的顏色,并不是簡單的替換俄删,而是Canvas和新layer進行Alpha混合宏怔,可參見此處鏈接

大部分情況下畴椰,我們想要本例中實現(xiàn)的效果臊诊,而不是想要示例二中形成的白色矩形,所以大部分情況下在使用PorterDuffXfermode時都是結(jié)合canvas.saveLayer()斜脂、canvas.restoreToCount()的抓艳,將關(guān)鍵代碼寫在這兩個方法之間。

一張被不經(jīng)大腦瘋傳的神圖

這張圖是Android的sdk下自帶的API的Demo示例中的一個秽褒,其源碼對應(yīng)的物理路徑是C:\Users\iSpring\AppData\Local\Android\sdk\samples\android-23\legacy\ApiDemos\src\com\example\android\apis\graphics\Xfermodes.java壶硅。

這張圖演示了先繪制黃色的圓形,然后將畫筆paint設(shè)置為16種不同的PorterDuffXfermode销斟,然后再繪制藍色矩形的效果庐椒。

但是上圖是錯誤的,它實現(xiàn)這個效果是在該代碼中對所繪制的黃色圖形和藍色圖形大小都做了手腳蚂踊。

不同混合模式的計算規(guī)則

為了方便觀察對比约谈,整個View的背景設(shè)置為綠色,最終運行效果應(yīng)該是如下所示:

上面的例子演示了了16種混合模式的效果,并且關(guān)鍵代碼都放在了canvas.saveLayer()與canvas.restoreToCount()之間棱诱。DST是黃色圓泼橘,SRC是藍色矩形,PorterDuffXfermode公式應(yīng)用于該Rectangle(canvas.drawRect())區(qū)域迈勋。

我們知道一個像素的顏色由四個分量組成炬灭,即ARGB,第一個分量A表示的是Alpha值靡菇,后面三個分量RGB表示了顏色重归。我們用S代表源像素,源像素的顏色值可表示為[Sa, Sc]厦凤,Sa中的a是alpha的縮寫鼻吮,Sa表示源像素的Alpha值,Sc中的c是顏色color的縮寫较鼓,Sc表示源像素的RGB椎木。我們用D代表目標像素,目標像素的顏色值可表示為[Da, Dc]博烂,Da表示目標像素的Alpha值香椎,Dc表示目標像素的RGB。

源像素與目標像素在不同混合模式下計算顏色的規(guī)則如下所示:

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 – SaDa, Sc(1 – Da) + Dc*(1 – Sa) + min(Sc, Dc)]

LIGHTEN:[Sa + Da – SaDa, Sc(1 – Da) + Dc*(1 – Sa) + max(Sc, Dc)]

MULTIPLY:[Sa * Da, Sc * Dc]

SCREEN:[Sa + Da – Sa * Da, Sc + Dc – Sc * Dc]

ADD:Saturate(S + D)

OVERLAY:Saturate(S + D)

最后需要說明一下,DARKEN们颜、LIGHTEN吕朵、OVERLAY等幾種混合規(guī)則在GPU硬件加速下不起效,如果你覺得混合模式?jīng)]有正確使用窥突,可以讓調(diào)用View.setLayerType(View.LAYER_TYPE_SOFTWARE, null)方法努溃,把我們的View禁用掉GPU硬件加速,切換到軟件渲染模式阻问,這樣所有的混合模式都能正常使用了梧税,具體可參見博文《Android中GPU硬件加速控制及其在2D圖形繪制上的局限》

最后總結(jié)一下,PorterDuffXfermode用于實現(xiàn)新繪制的像素與Canvas上對應(yīng)位置已有的像素按照混合規(guī)則進行顏色混合第队。

參考:
Android中Canvas繪圖之PorterDuffXfermode使用及工作原理詳解
Android中Canvas繪圖基礎(chǔ)詳解(附源碼下載)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哮塞,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子凳谦,更是在濱河造成了極大的恐慌忆畅,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尸执,死亡現(xiàn)場離奇詭異家凯,居然都是意外死亡,警方通過查閱死者的電腦和手機剔交,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門肆饶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人岖常,你說我怎么就攤上這事驯镊。” “怎么了竭鞍?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵板惑,是天一觀的道長。 經(jīng)常有香客問我偎快,道長冯乘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任晒夹,我火速辦了婚禮裆馒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘丐怯。我一直安慰自己喷好,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布读跷。 她就那樣靜靜地躺著梗搅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪效览。 梳的紋絲不亂的頭發(fā)上无切,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音丐枉,去河邊找鬼哆键。 笑死,一個胖子當著我的面吹牛矛洞,可吹牛的內(nèi)容都是我干的洼哎。 我是一名探鬼主播烫映,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼噩峦!你這毒婦竟也來了锭沟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤识补,失蹤者是張志新(化名)和其女友劉穎族淮,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凭涂,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡祝辣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了切油。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蝙斜。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖澎胡,靈堂內(nèi)的尸體忽然破棺而出孕荠,到底是詐尸還是另有隱情,我是刑警寧澤攻谁,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布稚伍,位于F島的核電站,受9級特大地震影響戚宦,放射性物質(zhì)發(fā)生泄漏个曙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一受楼、第九天 我趴在偏房一處隱蔽的房頂上張望垦搬。 院中可真熱鬧,春花似錦艳汽、人聲如沸悼沿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至慌植,卻和暖如春甚牲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蝶柿。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工丈钙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人交汤。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓雏赦,卻偏偏與公主長得像劫笙,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子星岗,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容