1. 程式人生 > >Android高階進階——繪圖篇(五)setXfermode 設定混合模式

Android高階進階——繪圖篇(五)setXfermode 設定混合模式

一、GPU硬體加速

  • 1、概述

GPU英文全稱Graphic Processing Unit,中文翻譯為“圖形處理器”。與CPU不同,GPU是專門為處理圖形任務而產生的晶片。
在GPU出現之前,CPU一直負責著所有的運算工作,CPU的架構是有利於X86指令集的序列架構,CPU從設計思路上適合儘可能快的完成一個任務。但當面對類似多媒體、圖形影象處理型別的任務時,就顯得力不從心。因為在多媒體計算中通常要求更高的運算密度、多併發執行緒和頻繁地儲存器訪問;顯然當你打遊戲時,螢幕上的動畫是需要實時重新整理的,這些都需要頻繁的計算、存取動作;如果CPU不能及時響應,那麼螢幕就會顯得很卡……你的隊友可能會發一句……我等的花都謝了,你咋還不動呢……
為了專門處理多媒體的計算、儲存任務,GPU就應運而生了,GPU中自帶處理器和儲存器,以用來專門計算和儲存多媒體任務。
對於Andorid來講,在API 11之前是沒有GPU的概念的,在API 11之後,在程式集中加入了對GPU加速的支援,在API 14之後,硬體加速是預設開啟的!我們可以顯式地強制影象計算時使用GPU而不使用CPU.

在CPU繪製和GPU繪製時,在流程上是有區別的:

  • 在基於軟體的繪製模型下,CPU主導繪圖,檢視按照兩個步驟繪製:

    • 讓View層次結構失效
    • 繪製View層次結構
  • 在基於硬體加速的繪製模式下,GPU主導繪圖,繪製按照三個步驟繪製:

    • 讓View層次結構失效
    • 記錄、更新顯示列表
    • 繪製顯示列表

可以看到在GPU加速時,流程中多了一項“記錄、更新顯示列表”,它表示在第一步View層次結構失效後,並不是直接開始逐層繪製,而是首先把這些View的繪製函式作為繪製指令記錄一個顯示列表中,然後再讀取顯示列表中的繪製指令呼叫OpenGL相關函式完成實際繪製。所以在GPU加速時,實際是使用OpenGL的函式來完成繪製的。

所以使用GPU加速的優點顯而易見:硬體加速提高了Android系統顯示和重新整理的速度;

  • 它有缺點也顯而易見:
    • 1、 相容性問題:由於是將繪製函式轉換成OpenGL命令來繪製,定然會存在OpenGL並不能完全支援原始繪製函式的問題,所以這就會造成在開啟GPU加速時,效果會失效的問題。
    • 2、記憶體消耗問題:由於需要OpenGL的指令,所以需要把系統中的OpenGL相關的包載入到記憶體中來,所以單純OpenGL API呼叫就會佔用8MB,而實際上會佔用更多記憶體;
    • 3、電量消耗問題:多使用了一個部件,當然會更耗電……

    下圖顯示了一些特殊函式硬體加速開始支援的平臺等級:(紅叉表示任何平臺都不支援,不在列表中的預設在API 11就開始支援)
![](https://upload-images.jianshu.io/upload_images/11455341-72cdf224fbe0260d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ![](https://upload-images.jianshu.io/upload_images/11455341-b1cda5ae66bb1340.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ![](https://upload-images.jianshu.io/upload_images/11455341-56fab787fef40b17.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 圖片摘自[《google官方文件:硬體加速》 ](http://developer.android.com/guide/topics/graphics/hardware-accel.html) 我再重複一遍,上面我們涉及了兩個API等級,在API 11以後,在程式集中加入了對GPU加速的支援,在API 14之後,硬體加速是預設開啟的!也就是說在API 11——API 13雖然是支援硬體加速的,但是預設是關閉的。
  • 2、禁用GPU硬體加速方法
    那麼問題就來了,如果你的APP跑在API 14版本以後,而你洽好要用那些不支援硬體加速的函式要怎麼辦?
    那就只好禁用硬體加速嘍,針對不同型別的東東,Android給我們提供了不同的禁用方法:
    硬體加速分全域性(Application)、Activity、Window、View 四個層級

  • 1.在AndroidManifest.xml檔案為application標籤新增如下的屬性即可為整個應用程式開啟/關閉硬體加速:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent"  
    android:orientation="vertical"  
    android:paddingLeft="2dp"  
    android:layerType="software"  
    android:paddingRight="2dp" >  
二、setXfermode(Xfermode xfermode)之PorterDuffXfermode 這個函式是影象混合裡最難的一個了,它的功能也是相當強大的,這個模式叫做圖形混合模式。 與setColorFilter一樣,派生自Xfermode的有三個類: ![image.png](https://upload-images.jianshu.io/upload_images/11455341-d205f06c14c3e563.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 1、概述——基本流程
    從上面可以看出,派生自Xfermode的有AvoidXfermode,PixelXorXfermode,PorterDuffXfermode;
    從硬體加速不支援的函式列表中,我們可以看到AvoidXfermode,PixelXorXfermode是完全不支援的,而PorterDuffXfermode是部分不支援的。

image.png

所以在使用Xfermode時,為了保險起見,我們需要做兩件事:

  • 1、禁用硬體加速:
    setLayerType(View.LAYER_TYPE_SOFTWARE, null);
  • 2、使用離屏繪製
//新建圖層  
int layerID = canvas.saveLayer(0,0,width,height,mPaint,Canvas.ALL_SAVE_FLAG);  

//TODO 核心繪製程式碼  

//還原圖層  
canvas.restoreToCount(layerID);  
有關離屏繪製的原因,這節就先不給大家引申了,後面會單獨拉出來一篇文章講離屏繪製(Canvas圖層相關),大家只需要知道,我們需要把繪製的核心程式碼放在saveLayer()和restoreToCount()之間即可。 下面我們先簡單講解PorterDuffXfermode的用法,然後寫個例子,看下SetXfermode()的使用方法和效果 PorterDuffXfermode的宣告如下:

public PorterDuffXfermode(PorterDuff.Mode mode)

它只有一個引數PorterDuff.Mode,它的可取值有如下幾個:
Mode.CLEAR  
Mode.SRC  
Mode.DST  
Mode.SRC_OVER  
Mode.DST_OVER  
Mode.SRC_IN  
Mode.DST_IN  
Mode.SRC_OUT  
Mode.DST_OUT  
Mode.SRC_ATOP  
Mode.DST_ATOP  
Mode.XOR  
Mode.DARKEN  
Mode.LIGHTEN  
Mode.MULTIPLY  
Mode.SCREEN  
Mode.OVERLAY  
Mode.ADD  
上面每一個模式都對應著一個演算法: ![image.png](https://upload-images.jianshu.io/upload_images/11455341-5471f7aaf8af8589.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 摘自[《google:PorterDuff.Mode》 ](http://developer.android.com/reference/android/graphics/PorterDuff.Mode.html) 比如LIGHTEN的計算方式為[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)],其中Sa全稱為Source alpha表示源圖的Alpha通道;Sc全稱為Source color表示源圖的顏色;Da全稱為Destination alpha表示目標圖的Alpha通道;Dc全稱為Destination color表示目標圖的顏色,在每個公式中,都會被分為兩部分[……,……],其中“,”前的部分為“Sa + Da - Sa*Da”這一部分的值代表計算後的Alpha通道而“,”後的部分為“Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)”這一部分的值代表計算後的顏色值,圖形混合後的圖片就是依據這個公式來對DST和SRC兩張影象中每一個畫素進行計算,得到最終的結果的。  Google給我們了一張圖,顯示的是兩個圖形一圓一方通過一定的計算產生不同的組合效果,其中圓形是底部的目標影象,方形是上方的源影象。 ![](https://upload-images.jianshu.io/upload_images/11455341-bb5f1599fee93468.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 在上面的公式中涉及到一個概念,目標圖DST,源圖SRC。那什麼是源圖,什麼是目標圖呢?我們簡單舉例子來說明一下:
    private void init() {
        //初始化畫筆
        paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //禁用硬體加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        //使用離屏繪製
        int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);

        canvas.drawBitmap(createDstBigmap(width, height), 0, 0, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(createSrcBigmap(width, height), width / 2, height / 2, paint);
        paint.setXfermode(null);

        canvas.restoreToCount(layerID);

    }

    public Bitmap createDstBigmap(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint scrPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        scrPaint.setColor(0xFFFFCC44);
        canvas.drawCircle(width / 2, height / 2, width / 2, scrPaint);
        return bitmap;
    }

    public Bitmap createSrcBigmap(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint dstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        dstPaint.setColor(0xFF66AAFF);
        canvas.drawRect(new Rect(0, 0, width, height), dstPaint);
        return bitmap;
    }
![image.png](https://upload-images.jianshu.io/upload_images/11455341-c8c64de702af663a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 它會在源影象所在區域與目標影象運算,在得到結果以後,將結果覆蓋到目標影象上。整個過程如下: 首先在兩個矩形的所在位置 ![image.png](https://upload-images.jianshu.io/upload_images/11455341-68e26d592c03364f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 然後是源影象計算結果的覆蓋過程: ![image.png](https://upload-images.jianshu.io/upload_images/11455341-6c99d0b213530c6e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 其中藍色小塊是源影象所在區域與目標影象經過運算的結果(有關這個結果為什麼是一小塊藍色,下面會具體講),在得到結果以後,把結果對應區域的影象先清空,然後把結果覆蓋上去。 這裡還需要強調一點,源影象在運算時,只是在源影象所在區域與對應區域的目標影象做運算。所以目標影象與源影象不相交的地方是不會參與運算的!這一點非常重要!不相交的地方不會參與運算,所以不相交的地方的影象也不會是髒資料,也不會被更新,所以不相交地方的影象也永遠顯示的是目標影象。
  • 2、Google的誤導
    大家仔細看我們示例程式碼的結果,同樣是SRC_IN模式,為什麼我們的結果與google的影象不一樣呢?
    google給出的SRC_IN的結果是這樣的

image.png

而我們的運算結果確是這樣的:

image.png

在Android\sdk\samples\android-XX\legacy\ApiDemos\src\com\example\android\apis\graphics\Xfermodes.java中可以找到google所給影象的原始碼:(我仿照上面的示例,對原始碼進行更改,只演示SRC_IN的合成樣式,具體原始碼大家可以從上面路徑查詢)

public class Xfermodes extends View {  

        // 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;  
    }  
    private int width = 400;  
    private int height = 400;  
    private Bitmap dstBmp;  
    private Bitmap srcBmp;  
    private Paint mPaint;  

    public Xfermodes(Context context,AttributeSet attrs) {  
        super(context,attrs);  

        srcBmp = makeSrc(width, height);  
        dstBmp = makeDst(width, height);  
        mPaint = new Paint();  
    }  

    @Override   
    protected void onDraw(Canvas canvas) {  
        canvas.drawColor(Color.WHITE);  

        int layerID = canvas.saveLayer(0,0,width*2,height*2,mPaint,Canvas.ALL_SAVE_FLAG);  

        canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
        canvas.drawBitmap(srcBmp, 0, 0, mPaint);  
        mPaint.setXfermode(null);  

        canvas.restoreToCount(layerID);  
    }  
}  

效果圖如下:

image.png

我們從程式碼中分析下,google是如何來得到這個影象的。
看原始碼中,他也是通過makeDst(int w, int h)和makeSrc(int w, int h)生成兩個bitmap,一圓一方,但仔細看他的程式碼,就會發現問題
先看makeDst(int w, int h)

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;  
}

這段函式是用來生成圓形的目標影象的,它首先生成一個寬度為w,高度為h的空白影象:

Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);

然後在這個空白影象上畫圓形:

c.drawOval(new RectF(0, 0, w*3/4, h*3/4), p);

現在問題來了,它這裡bitmap的寬和高是w和h,但畫的圓形的大小卻是new RectF(0, 0, w*3/4, h*3/4)!並沒有完全填滿bitmap,畫出來的效果圖是這樣的:

image.png

其中紅色矩形區域是整個bitmap的大小。

然後再看makeSrc(int w, int h)

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;  
}  

這裡同樣是新建一個寬度為w和高度為h的空白影象:

Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);

然後再在這個空白bitmap上畫一個矩形:

c.drawRect(w/3, h/3, w*19/20, h*19/20, p);

大家注意到了沒,它同樣是比空白bitmap小的,它竟然是從(w/3, h/3)開始畫,right和bottom的座標在( w*19/20, h*19/20)!這個比bitmap小太多了,我們來看下它的影象

image.png

同樣,紅色框表示bitmap的位置。從中可以看到明顯矩形框只佔整個bitmap的其中小部分

最後我們來看看是如何將這兩個bitmap繪出來的

protected void onDraw(Canvas canvas) {  
    canvas.drawColor(Color.WHITE);  

    int layerID = canvas.saveLayer(0,0,width*2,height*2,mPaint,Canvas.ALL_SAVE_FLAG);  

    canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
    canvas.drawBitmap(srcBmp, 0, 0, mPaint);  
    mPaint.setXfermode(null);  

    canvas.restoreToCount(layerID);  
}  

最最關鍵的是看這裡:

canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
canvas.drawBitmap(srcBmp, 0, 0, mPaint);  

注意,這兩個bitmap的大小都是width和height,而且都是從(0,0)位置開始繪製的!!!!這說明這兩個bitmap是完全重合的!!!!
所以它的合成過程是這樣的:

image.png

即兩個同樣大小的bitmap合併。正是由於這兩個bitmap所在位置和大小是完全一樣的,所以在以源影象所在區域與目標影象做計算時,是將兩個影象完全重合計算的,而不是像我們前面示例中那樣,只有一部分相交區域。所以google這麼做是對開發者的誤導,利用兩個完全不同的完全相同大小的圖片,只在其中一部分畫矩形和圓形,但在做計算時卻是以整個bitmap大小來做計算的,這顯然是不正確的。
最後,仿照示例程式碼,在(0,0,width,height)畫一個圓形,然後在(width/2,height/2,3*width/2,3*height/2)的位置畫一個矩形,然後應用各個Mode樣式結果如下:

image.png

模式的具體使用

下面將逐個講解每個模式的意義,在開始講解之前,我們隨便拿一個效果圖來看一下,我們在這個效果圖中需要關注哪兩點

image.png

對應程式碼:

canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN));  
canvas.drawBitmap(srcBmp,width/2,height/2, mPaint);  

其實在最後一句計算效果影象時,是以源影象所在區域為計算目標的,把計算後的源影象更新到對應區域內。

所以如上圖所示,我們在計算源影象所在區域效果圖時,需要著重關注兩個區域:
1、如圖示示區域一:區域一是源影象和目標影象的相交區域,由於在這個區域源影象和目標影象畫素都不是空白畫素,所以可以明顯看出顏色的計算效果。
2、如圖示示區域二:在區域二中,源影象所在區域的目標影象是空白畫素,所以這塊區域所表示的意義就是,當某一方區域是空白畫素時,此時的計算結果。
總而言之:我們在下面的各個模式計算時,只需要關注圖示中的區域一和區域二;其中區域一表示當源影象和目標影象畫素都不是空白畫素時的計算結果,而區域二則表示當某一方區域是空白畫素時,此時的計算結果。

一、顏色疊加相關模式

這部分涉及到的幾個模式有Mode.ADD(飽和度相加)、Mode.DARKEN(變暗),Mode.LIGHTEN(變亮)、Mode.MULTIPLY(正片疊底)、Mode.OVERLAY(疊加),Mode.SCREEN(濾色)

  • 1 、Mode.ADD(飽和度相加)
    它的公式是Saturate(S + D);ADD模式簡單來說就是對SRC與DST兩張圖片相交區域的飽和度進行相加
    同樣使用上篇中的示例,一個矩形,一個圓形來做相加 (程式碼就不給了,程式碼還是一樣的程式碼就是改了個模式)

效果圖如下:
image.png

從效果圖中可以看出,只有源圖與目標影象相交的部分的影象的飽和度產生了變化,沒相交的部分是沒有變的,因為對方的飽和度是0,當然不相交的位置飽和度是不會變的。
這個模式的應用範圍比較少,暫時想不到哪裡會用到;

  • 2、Mode.LIGHTEN(變亮)
    它的演算法是: [Sa + Da - Sa*Da,Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)]
    圓形和矩形的效果圖為:

image.png

這個效果比較容易理解,兩個影象重合的區域才會有顏色值變化,所以只有重合區域才有變亮的效果,源影象非重合的區域,由於對應區域的目標影象是空白畫素,所以直接顯示源影象。

我們在實際應用中,會有下面的這個情況,當選中一本書時,給這本書加上燈光效果

其實它是兩張圖合成的:
DST:目標影象
image.png

SRC:源影象

image.png

可以看到,在這張圖片的最上方中間的位置有些白色半透明的填充,其它位置都是透明的。

  • 3、Mode.DARKEN(變暗)

對應公式是: [Sa + Da - Sa*Da,Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)]
示例影象為:

  • 4、Mode.MULTIPLY(正片疊底)
    公式是:[Sa * Da, Sc * Dc]
    示例影象為:

image.png

有些同學會奇怪了,Photoshop中也有正片疊底啊,相交區域正片疊底後的顏色確實是綠色的,但源影象的非相交區域怎麼沒了?
我們來看下他的計算公式:[Sa * Da, Sc * Dc],在計算alpha值時的公式是Sa * Da,是用源影象的alpha值乘以目標影象的alpha值;由於源影象的非相交區域所對應的目標影象畫素的alpha是0,所以結果畫素的alpha值仍是0,所以源影象的非相交區域在計算後是透明的。
在兩個影象的相交區域的混合方式是與photoshop中的正片疊底效果是一致的。

  • 5、Mode.OVERLAY(疊加)
    這個沒有給出公式……
    示例影象為:

image.png

雖然沒有給出公式,但從效果圖中可以看到,源影象交合部分有效果,非交合部分依然是存在的,這就可以肯定一點,當目標影象透明時,在這個模式下源影象的色值不會受到影響;

  • 6、Mode.SCREEN(濾色)
    對應公式是:[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc]
    示例影象為:
    image.png

同樣,只是源影象與目標影象交合部分有效果,源影象非交合部分保持原樣。

  • 7、示例——twitter標識的描邊效果
    由於這些模式在photoshop中都存在,直接拿目標影象和源影象在photoshop中就可以演示出來,就沒有多舉例子,其實,在實現時實現兩影象混合時,也經常會用到這些模式的,比如這裡twitter的暗光效果
    我們這裡有兩張源圖:
    圖一:

image.png

圖二:
image.png

合成圖如下:
image.png

我們先想想這個要實現的效果有哪些特性:
首先,
在圖一中,小鳥整個都是藍色的
在圖二中,只有小鳥的邊緣部分是白色的,中間部分是透明的。
在最終的合成圖中:圖一和圖二中小鳥與邊緣的是顯示的,而且還有某種效果,但小鳥中間的區域變透明瞭!顯示的是底部Activity的背景色。
想到我們前面學到的幾種樣式中,Mode.MULTIPLY(正片疊底)、SRC_IN 和 DST_IN 會在兩個影象的一方透明時,結果畫素就是透明的。所以這裡使用的模式就是Mode.MULTIPLY
對應程式碼如下:

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

        int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);  

        BmpDST = BitmapFactory.decodeResource(getResources(),R.drawable.twiter_bg,null);  
        BmpSRC = BitmapFactory.decodeResource(getResources(),R.drawable.twiter_light,null);  

        canvas.drawBitmap(BmpDST,0,0,mBitPaint);  
        mBitPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));  
        canvas.drawBitmap(BmpSRC,0,0,mBitPaint);  

        mBitPaint.setXfermode(null);  
        canvas.restoreToCount(layerId);  
    }  
}  

二、SRC_IN 和 DST_IN 模式

  • 1、Mode.SRC_IN
    示例影象為:

image.png

  • 2、MODE.DST_IN

image.png

共同點:

當模式為 SRC_IN 和 DST_IN 時,不管是 目標影象 還是 源影象 只要是和空白畫素(完全透明)相交的畫素,計算結果也將為空白畫素(完全透明)
不同點:
相交局域視情況顯示(SRC_IN 時,相交區域顯示為 源影象,相反顯示 目標影象)

我們來驗證一下我們上面得出的結論:
- 驗證一:
首先把 目標影象 的畫筆設為 Color.TRANSPARENT(完全透明)我們來試一下當 模式 為 PorterDuff.Mode.SRC_IN 時 效果是怎樣的?

什麼都沒顯示是對的,因為 目標影象 是空白畫素,本身就顯示不出來,而 源影象 與 目標影象 的空白畫素相交的畫素,計算結果也將為空白畫素(完全透明),就是這麼個情況

  • 驗證二:
    首先把 源影象 的畫筆設為 Color.TRANSPARENT(完全透明)我們來試一下當 模式 為 PorterDuff.Mode.DST_IN 時 效果是怎樣的?

效果是一樣的,必將規則就是 與 空白畫素相交變空白畫素,理所當然

三、SRC_OUT 和 DST_OUT 模式

  • 1、SRC_OUT 模式
    示例影象為:
    image.png

可以發現,當模式為 SRC_OUT 時,當前 目標影象 和 源影象 均不是空白畫素,相交區域變為空白畫素(完全透明),而未相交區域則正常顯示原本畫素

  • 假設一下,如果 目標畫素為空白畫素(完全透明)時,會變成什麼樣子呢?來試一下,把目標元素的畫筆顏色改為 Color.TRANSPARENT(完全透明):
    效果圖如下:

image.png

發現,源影象 和 目標影象 的相交區域並沒有變透明

  • 那如果 源影象 為空表畫素(完全透明)呢?會是什麼效果,來看一下:
    效果圖如下:
    image.png

結論:當模式為 SRC_OUT 時, 如果 目標影象 不是空白畫素時,相交區域會變成空白畫素,未相交區域正常顯示,如果 目標影象 為空白畫素(完全透明)時,則顯示 源影象。

  • 2、DST_OUT 模式
    示例影象為:
    image.png

可以發現,這個顯示效果和 模式為 SRC_OUT 同時 源影象 為空白畫素時的顯示效果一樣,是因為 目標畫素 和 源畫素 都不是空白畫素,所以相交區域變成了空白畫素,那麼 源影象 的其他區域為什麼也變成了空白畫素呢?是因為 源影象 的其他區域的相交區域是空白畫素導致的。

應用場景:(橡皮擦效果實現)
效果圖如下:
Jietu20180423-211846.gif

程式碼:

   private void init() {
        //初始化畫筆
        paint = new Paint();
        //設定畫筆顏色(不能為完全透明)
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(100);
        // 源影象
        srcBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dog, null);
        //目標影象
        dstBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        //路徑(貝塞爾曲線)
        path = new Path();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //禁用硬體加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        //使用離屏繪製
        int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);

        //先將路徑繪製到 bitmap上
        Canvas dstCanvas = new Canvas(dstBitmap);
        dstCanvas.drawPath(path, paint);

        //繪製 目標影象
        canvas.drawBitmap(dstBitmap, 100, 100, paint);
        //設定 模式 為 SRC_OUT
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
        //繪製源影象
        canvas.drawBitmap(srcBitmap, 100, 100, paint);
        paint.setXfermode(null);

        canvas.restoreToCount(layerID);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                eventX = event.getX();
                eventY = event.getY();
                path.moveTo(eventX, eventY);
                break;
            case MotionEvent.ACTION_MOVE:
                float endX = (event.getX() - eventX) / 2 + eventX;
                float endY = (event.getY() - eventY) / 2 + eventY;
                path.quadTo(eventX, eventY, endX, endY);
                eventX = event.getX();
                eventY = event.getY();
                break;
        }
                invalidate();
        return true;
    }

程式碼非常簡單,利用的原理就是使用 SRC_OUT 模式時,當 目標影象 和 源影象 均不為 空白畫素時,相交局域會變成空白畫素(完全透明),所以在繪製途徑時,使用的畫筆顏色不能採用 Color.TRANSPARENT,必須要有顏色值。

  • 2、刮刮卡效果

Jietu20180423-222757.gif

程式碼:

    private void init() {
        //初始化畫筆
        paint = new Paint();
        //設定畫筆顏色(不能為完全透明)
        paint.setColor(Color.RED);
        paint.setStrokeWidth(100);
        paint.setStyle(Paint.Style.STROKE);
        // 源影象
        srcBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.guaguaka, null);
        //目標影象
        dstBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        //中獎資訊
        bitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        //路徑(貝塞爾曲線)
        path = new Path();
        //繪製中獎資訊文字的畫筆
        textPaint = new Paint();
        textPaint.setColor(Color.RED);
        textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        textPaint.setTextSize(50);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //禁用硬體加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        Canvas canvas1 = new Canvas(bitmap);

        String text = "別傻了,寶貝,洗洗睡去吧!!!";
        //獲取文字寬度
        float textWidth = textPaint.measureText(text);
        //居中繪製文字,這裡沒有考慮高度居中
        canvas1.drawText(text, (bitmap.getWidth() - textWidth) / 2, bitmap.getHeight() / 2, textPaint);

        canvas.drawBitmap(bitmap, 100, 100, paint);

        //使用離屏繪製
        int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);


        //先將路徑繪製到 bitmap上
        Canvas dstCanvas = new Canvas(dstBitmap);
        dstCanvas.drawPath(path, paint);

        //繪製 目標影象
        canvas.drawBitmap(dstBitmap, 100, 100, paint);
        //設定 模式 為 SRC_OUT
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));

        canvas.drawBitmap(srcBitmap, 100, 100, paint);

        //繪製源影象
        paint.setXfermode(null);

        canvas.restoreToCount(layerID);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                eventX = event.getX();
                eventY = event.getY();
                path.moveTo(eventX, eventY);
                break;
            case MotionEvent.ACTION_MOVE:
                float endX = (event.getX() - eventX) / 2 + eventX;
                float endY = (event.getY() - eventY) / 2 + eventY;
                path.quadTo(eventX, eventY, endX, endY);
                eventX = event.getX();
                eventY = event.getY();
                break;

        }
        invalidate();
        return true;
    }

這裡需要注意的地方有那麼一個:

        Canvas canvas1 = new Canvas(bitmap);

        String text = "別傻了,寶貝,洗洗睡去吧!!!";
        //獲取文字寬度
        float textWidth = textPaint.measureText(text);
        //居中繪製文字,這裡沒有考慮高度居中
        canvas1.drawText(text, (bitmap.getWidth() - textWidth) / 2, bitmap.getHeight() / 2, textPaint);

        canvas.drawBitmap(bitmap, 100, 100, paint);

在繪製中獎資訊的時候,是在離屏繪製之外繪製的,為什麼要在離屏繪製之前繪製呢?自己先想想,這個問題放到後面介紹 Canvas 圖層時會詳細說,這裡由於篇幅問題就先放放……

其他模式在實際應用中並不是很常見,等用到了在說吧,就這樣,下篇會在結合 這幾個模式在測試幾個例子,後面開始圖層,在後面就開始寫實際自定義 View 以及 自定義繪製View 大概還會有個五六篇的樣子,爭取在月底之前搞定,加油!!!

相關推薦

Android高階——繪圖setXfermode 設定混合模式

一、GPU硬體加速 1、概述 GPU英文全稱Graphic Processing Unit,中文翻譯為“圖形處理器”。與CPU不同,GPU是專門為處理圖形任務而產生的晶片。 在GPU出現之前,CPU一直負責著所有的運算工作,CPU的架構是有利於X86

Android高階——繪圖Canvas 與 圖層

開篇 前面很多篇文章都用到了圖層的概念,但是一直沒有詳細介紹,今天這篇文章將詳細的介紹 Canvas 與 圖層的概念 一、如何獲得一個Canvas物件 方法一:自定義view時, 重寫onDraw、dispatchDraw方法 protect

Android高階——繪圖Canvas基本操作

開篇 前面在介紹 onDraw 過程時,有提到 View 的繪製(Canvas 的使用),後續的幾篇會詳細的介紹有關 Canvas 以及 Paint 的相關操作。 Canvas 和 Paint Canvas 和 Paint 之間的關係就像我們平時畫畫需要的

Kubernetes 1.3 從入門到 安裝1

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

Kubernetes 1 3 從入門到 安裝1

Kubernetes 1.3 從入門到進階 安裝篇:minikube Kubernetes單機執行環境一直是一個沒有得到重視的問題。現在我們有了minikube,一個用go語言開發的可以在本地執行kubernetes的利器,不過目前應該只是支援kubernetes1.3。如果你只有一臺機器或

Python基礎之路之字串

字串 字串的定義 字串 就是 一串字元,是程式語言中表示文字的資料型別 在 Python 中可以使用 一對雙引號 " 或者 一對單引號 ' 定義一個字串 雖然可以使用 \" 或者 \' 做字串的轉義,但是在實際開發中: 如果字串內部需要使用 ",可以使用 ' 定義字串 如果字串內部需要使用

Recycleview實現複雜頁面 三種以上佈局 瀑布流 多佈局 scrollview巢狀recyclerView 顯示不全 滑動衝突 之終極 轉載

=============================================================================================== 相信很多安卓開發的朋友,尤其是剛從事安卓開發的朋友, 當產品經理遞過來一張複雜頁面的

Kubernetes 1 3 從入門到 安裝2

pri http com block .com 整理 希望 over role Kubernetes 1.3 從入門到進階 安裝篇: kubernetes-ansible 上一篇文章我們介紹了使用minikube快速部署kubernetes1.3到單機上. 多臺機器構成

React 之路

之前的文章我們介紹了  React 事件,方法, React定義方法的幾種方式 獲取資料 改變資料 執行方法傳值。接下來我們將介紹 React 表單事件 鍵盤事件 事件物件以及 React中 的 ref 獲取 dom 節點 、雙向資料繫結,約束性和非約束性元件。 1 im

koa2 從入門到之路

之前的文章我們介紹了一下 koa 中使用 ejs 模板及頁面渲染,本篇文章我們來看一下 koa post提交資料及 koa-bodyparser中介軟體。 在前端頁面中,不免會用到 form 表單和 post 請求向後端提交資料,接下來我們看一下 koa 是如何獲取到前端通過 post 請求傳過來的資料。

Python 從入門到之路

之前的文章我們簡單介紹了一下 Python 的函式,本篇文章我們來看一下 Python 中的面向物件。  Python從設計之初就已經是一門面向物件的語言,正因為如此,在Python中建立一個類和物件是很容易的。 面向物件技術簡介 類(Class): 用來描述具有相同的屬性和方法

Python 爬蟲從入門到之路

在之前的文章中我們帶入了 opener 方法,接下來我們看一下 opener 應用中的 ProxyHandler 處理器(代理設定)。 使用代理IP,這是爬蟲/反爬蟲的第二大招,通常也是最好用的。 很多網站會檢測某一段時間某個IP的訪問次數(通過流量統計,系統日誌等),如果訪問次數多的不像正常人,它會禁止

Java 從入門到之路

之前的文章我們介紹了 Java 的迴圈結構,本章我們來看一下 Java 的陣列 陣列對於每一門程式語言來說都是重要的資料結構之一,當然不同語言對陣列的實現及處理也不盡相同。 陣列   - 相同資料型別的元素組成的集合   - 元素按線性順序排列。所謂線性順序是指除第一個元素外,每一個元素都有唯一的前驅

Java——Java的I/O技術

  程式中,為了永久的儲存建立的資料,需要將其儲存在磁碟檔案中,以便在其它程式中使用它們。Java的I/O技術可以將資料儲存到文字檔案、二進位制檔案甚至是ZIP壓縮檔案中,以達到永久性儲存資料的要求。   本篇我們要介紹的內容就是Java的I/O技術,即輸入/輸出。 一、輸入/輸出流   流是一組有序的資料序

Java多執行緒知識點總結——之多執行緒下的單例模式

餓漢式 餓漢式多執行緒和單執行緒的程式碼是一樣的,如下: class Single { private static final Single s = new Single(); p

python------程線程

dex locked cep 陌生 cnblogs 信號 fault sig utf8 Python中的IO模型 同步(synchronous) IO和異步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分別是什麽,

Android高階之自定義View

前言 在網上看到一張圖,花了些時間自己嘗試著寫了一個自定義View,裡面涉及到了自定義屬性、自定義View padding屬性的處理、畫筆(Paint)和畫布(Canvas)的使用、解析度適配問題、效能問題、屬性動畫等,覺得還是有些東西值的記錄一下的,效果圖如下: 自定義屬

Android高階ToolBar

我記得之前Android4.0的時候是actionBar,到了Android5.0以後google新出了ToolBar用以彌補ActionBar的不足。今天我們就來講一下 ToolBar的用法。其實現在Android系統的UI設計有些已經超越了IOS... 一、基本用法 1. 我們新建立一個

Android高階之 TextInputLayout用法

TextInputLayout見名知義與文字輸入有關係,TextInputLayout控制元件通過內嵌EditText來實現輸入文字時,根據預先設定的屬性向使用者展示相應的提醒文字並附有酷炫的動畫效果。例如,當文字框裡的字元長度大於10的時候自動給使用者提示,無需編寫額外的程

Android高階--插曲-從Android5.0到Android9.0各版本變化

                            從Android5.0到Android9.0 自從公司提出了一些出其不意的需求之後我就下定了決心去了解安卓各版本系統的差異。這些出其不意的需求要麼屬於黑客行為,要麼還不成熟,很難順利的實現,例如做一個殺不死的APP,又