詳解Paint的setShader(Shader shader)


一、概述

setShader(Shader shader)中傳入的自然是shader對象了,shader類是Android在圖形變換中非常重要的一個類。Shader在三維軟件中我們稱之為着色器,其作用是來給圖像着色。它有五個子類,像PathEffect一樣,它的每個子類都實現了一種Shader。下面來看看文檔中的解釋:

子類:BitmapShader, ComposeShader, LinearGradient, RadialGradient, SweepGradient

 

二、BitmapShader

2.1 構造方法

只有有一個含參的構造方法:

BitmapShader (Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)

顧名思義,它是給bitmap做處理的類,傳入的參數也有bitmap對象。從字面理解,傳入的第一個參數是bitmap對象,應該會對bitmap做一定的處理,后面兩個常量都是mode(模式),應該是設定處理效果的。理解了這個,我們就可以正式介紹下傳入的三個參數了。

第一個參數:要處理的bitmap對象

第二個參數:在X軸處理的效果,Shader.TileMode里有三種模式:CLAMP、MIRROR和REPETA

第三個參數:在Y軸處理的效果,Shader.TileMode里有三種模式:CLAMP、MIRROR和REPETA

下面我們就來用代碼進行各種模式的演示,演示之前自然要准備一個演示圖片了:

 

說明:為了講解需要,我給這個圖片邊界PS了幾個像素的紅色。

 

2.2 Shader.TileMode.CLAMP

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.kale);
        // 設置shader
        mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));  
        // 用設置好的畫筆繪制一個矩形
        canvas.drawRect(180, 200, 600, 600, mPaint);
    }

效果:

從效果看,我們看不明白這是什么東西,只看到了大片的紅色區域,左上角漏出了一個原圖的小腳。我們索性把繪制區域放大,看看效果會有什么變化。

canvas.drawRect(0, 0, 800, 800, mPaint);

效果:

這下我們的圖片終於完全顯示了出來,仔細分析發現圖片邊界的紅邊是在的,但是為啥右邊、下邊都沒有呢?因為我們設定的Shader.TileMode.CLAMP會將邊緣的一個像素進行拉伸、擴展。所以整個的紅色區域其實就是紅色邊框擴展后的結果。

 

2.3 Shader.TileMode.MIRROR

上面的例子我們知道CLAMP模式會拉伸邊緣的一個像素來填充,可以說是邊緣拉伸模式,那么這個MIRROR模式會有什么作用呢?顧名思義是鏡像,那么就來測試一下。測試的代碼就是從上面的改動的,僅僅把X軸的模式換成了MIRROR而已。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.kale);
        // 設置shader
        mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.MIRROR, Shader.TileMode.CLAMP)); // 用設置好的畫筆繪制一個矩形
        canvas.drawRect(0, 0, 800, 800, mPaint);
    }

結果:

可見,在繪制的矩形區域內,X軸方向上出現了鏡面翻轉,就像翻牌子一樣一個個翻開,和復印一樣。而Y軸我們還是用的CLAMP,繼續是拉伸邊緣的紅色像素,直到布滿畫布。

注意:繪制過程是先采用Y軸模式,再使用X軸模式的。所以是先繪制一幅圖片,先采用Y軸模式,向下拉伸了邊緣的紅色,然后采用X軸模式,將圖片和拉伸的紅色區域進行鏡像翻轉,再翻轉。

那么如果我們X,Y都用鏡面效果呢?

mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR));  

 

2.4 Shader.TileMode.REPEAT

顧名思義,這個應該是重復模式,和鏡像十分類似,但是不是翻轉復制,而是平移復制。我們接着把上面鏡像的代碼的X軸模式進行修改:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.kale);
        // 設置shader
        mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.MIRROR)); // 用設置好的畫筆繪制一個矩形
        canvas.drawRect(0, 0, 800, 800, mPaint);
    }

結果:

可以明顯的看到,第一層的圖片都是一個個復制的,豎直方向上的圖片是鏡面翻轉復制的。

繪制過程:

先在左上角繪制一個完整的圖片,因為Y軸是用了鏡像模式,所以翻轉了原圖,圖片倒立了;第二次翻轉了第二層第一列的一個圖片,所以第三層的圖片又變正了。接着,開始把第一列的圖片進行平移復制,產生最終的結果。

擴展:

我們上面繪制的是一個矩形,繪制圓形也是一樣的思路。需要明白的是這個setShader是畫筆的屬性,你用這個畫筆繪制的區域就有這個效果,和你畫圓的、方的都沒有任何關系,如果你小時候玩過金山畫王就能很好的理解了。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.kale);
        // 設置shader
        mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));  
        // 用設置好的畫筆繪制一個圓形
        //canvas.drawRect(0, 0, 800, 800, mPaint);
        canvas.drawCircle(300, 300, 300, mPaint);
    }

結果:

 

三、LinearGradient

3.1 構造方法

它就是一個線性漸變的處理類,有兩個構造方法:

第一個:

public LinearGradient (float x0, float y0, float x1, float y1, int color0, int color1, Shader.TileMode tile)

這是LinearGradient最簡單的一個構造方法,參數雖多其實很好理解(x0,y0)表示漸變的起點坐標而(x1,y1)則表示漸變的終點坐標,這兩點都是相對於屏幕坐標系而言的,而color0和color1則表示起點的顏色和終點的顏色。TileMode和上面講的完全一致,不贅述了。

第二個:

public LinearGradient (float x0, float y0, float x1, float y1, int[] colors, float[] positions, Shader.TileMode tile)

這個構造方法和第一個類似,坐標都是一樣的,但這里的colors和positions都是數組,也就是說我們可以傳入多個顏色和顏色的位置,產生更加豐富的漸變效果。

 

3.2 用代碼測試第一個效果

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Shader shader = new LinearGradient(0, 0, 400, 400, Color.RED, Color.YELLOW, Shader.TileMode.REPEAT);
        // 設置shader
        mPaint.setShader(shader);  

        canvas.drawRect(0, 0, 400, 400, mPaint);
    }

我們的畫布是(0,0)-(400,400)的區域,LinearGradient的區域(執行漸變的區域)也是(0,0)-(400,400),開始的顏色是紅色,結束的顏色是黃色,模式是重復模式。

結果:

這里我們的重復模式沒有起作用,是因為漸變的區域正好等於畫布繪制的區域,填充模式使用的前題是,填充的區域小於繪制的區域。就和用圖片做桌面一樣,如果圖片大小大於等於桌面的大小,自然就不會出現平鋪拉伸的效果了,如果是用小圖做桌面,那么就要看看是怎么一個拉伸法。下面我稍作修改,把漸變的區域縮小一點,看看有什么不一樣的效果:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Shader shader = new LinearGradient(0, 0, 200, 200, Color.RED, Color.YELLOW, Shader.TileMode.REPEAT);
        // 設置shader
        mPaint.setShader(shader);  
        canvas.drawRect(0, 0, 400, 400, mPaint);
    }

 

3.3 用代碼測試第二個效果

第二個效果是傳入不同漸變的顏色,然后設置顏色的顯示位置,最后產生絢麗的漸變效果。這里我的漸變區域是小於繪制區域的,設置的模式是鏡像模式。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Shader shader = new LinearGradient(0, 0, 200, 200, 
                new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, 
                new float[] { 0, 0.1F, 0.5F, 0.7F, 0.8F }, Shader.TileMode.MIRROR);
        // 設置shader
        mPaint.setShader(shader);  
        canvas.drawRect(0, 0, 400, 400, mPaint);
    }

colors是一個int型數組,我們用來定義所有漸變的顏色,positions表示的是漸變的相對區域,其取值只有0到1,上面的代碼中我們定義了一個[0, 0.1F, 0.5F, 0.7F, 0.8F],意思就是紅色到黃色的漸變起點坐標在整個漸變區域(left, top, right, bottom定義了漸變的區域)的起點,而終點則在漸變區域長度 * 10%的地方,而黃色到綠色呢則從漸變區域10%開始到50%的地方以此類推,其中positions是可以為空的,但模式不能為null。

結果:

當我們的position為null時,顏色是均勻的填充整個漸變區域,顯示的比較柔和。

Shader shader = new LinearGradient(0, 0, 200, 200, 
                new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, 
                null, Shader.TileMode.MIRROR);

結果:

 

3.4 繪制圖片陰影

線性漸變有什么用呢?我相信美工肯定會給你一個很好的回答,總之它肯定很有用啦。我們下面的例子會演示用線性漸變繪制圖片的倒影效果。

思路:

繪制倒影肯定要一個原圖,然后我們拷貝一張進行矩陣翻轉,放在它下面,作為倒影圖。倒影圖需要漸變消失,但bitmap沒有提供漸變消失的效果,所以我們可以去嘗試混合模式。既然用到了混合模式,就需要兩個圖片,一個圖片是倒影圖,還有一個肯定是漸變圖了。倒影的漸變肯定是線性漸變,漸變是從有到無。

接着我們去挑選合適的混合模式,找到了DST_IN。

PorterDuff.Mode.DST_IN

計算方式:[Sa * Da, Sa * Dc];

說明:只在源圖像和目標圖像相交的地方繪制目標圖像

原圖和漸變圖相交的地方才繪制目標圖像,所以目標圖像是我們的倒影圖,原圖是線性漸變圖。

效果圖:

  

實現代碼:

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

        // 為了突出效果,先繪制一個灰色的畫布
        canvas.drawColor(Color.GRAY);
        
        int x = 200, y = 200;

        // 獲取源圖
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.kale);

        // 實例化一個矩陣對象
        Matrix matrix = new Matrix();
        matrix.setScale(1F, -1F);
        // 產生和原圖大小一樣的倒影圖
        Bitmap refBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);

        // 繪制好原圖
        canvas.drawBitmap(bitmap, x, y, null);
        // 保存圖層。這里保存的圖層寬度是原圖繪制區域的寬度,高度是原圖繪制區域兩倍的高度,包含了繪制倒影的區域。
        int sc = canvas.saveLayer(x, y + bitmap.getHeight(), x + bitmap.getWidth(), y + bitmap.getHeight() * 2, null, Canvas.ALL_SAVE_FLAG);
        
        
        
        
        
        // 繪制倒影圖片,繪制的區域緊貼原圖的底部
        canvas.drawBitmap(refBitmap, x, y + bitmap.getHeight(), null);
        
        // 設置好混合模式
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        /*
         *  設置線性漸變模式。
         *  這里繪制的高度是原圖的1/4,也就說倒影最終的區域也就是原圖的1/4
         *  顏色是從Color.BLACK到透明,用於和倒影圖做混合模式。
         *  模式是邊緣拉伸模式,這里沒用到
         */
        mPaint.setShader(new LinearGradient(x, y + bitmap.getHeight(), 
                x, y + bitmap.getHeight() + bitmap.getHeight() / 4, 
                Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP));  
        // 畫一個矩形區域,作為目標圖片,用來做混合模式
        canvas.drawRect(x, y + bitmap.getHeight(), x + refBitmap.getWidth(), y + bitmap.getHeight() * 2, mPaint);

        mPaint.setXfermode(null);

        // 回復圖層
        canvas.restoreToCount(sc);
    }

 

四、SweepGradient

它的意思是梯度漸變,也稱之為掃描式漸變,因為其效果有點類似雷達的掃描效果,他也有兩個構造方法:

第一個:

SweepGradient(float cx, float cy, int color0, int color1)  

跟LinearGradient差不多,(cx,cy)是遠行的坐標,產生從color0到color1的漸變。來一個實例:

mPaint.setShader(new SweepGradient(screenX, screenY, Color.RED, Color.YELLOW)); 

第二個:

SweepGradient(float cx, float cy, int[] colors, float[] positions)  

實例代碼:

mPaint.setShader(new SweepGradient(screenX, screenY, new int[] { Color.GREEN, Color.WHITE, Color.GREEN }, null));  

 

五、RadialGradient

徑向漸變,徑向漸變說的簡單點就是個圓形中心向四周漸變的效果,他也一樣有兩個構造方法

第一個:

RadialGradient (float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)  

(centerX,centerY)是圓心的坐標,radius是半徑,centerColor是邊緣的顏色,edgeColor是外圍的顏色,最后是模式。

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

        Shader shader = new RadialGradient(200, 200, 200, Color.RED, Color.GREEN, Shader.TileMode.CLAMP);
        // 設置shader
        mPaint.setShader(shader);  
        canvas.drawRect(0, 0, 400, 400, mPaint);
    }

結果:

 

第二個:

RadialGradient (float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode) 

這個就是添加了多個色彩,設置色彩的位置,沒啥特別的。

 

六、ComposeShader

就是組合Shader的意思,顧名思義就是兩個Shader組合在一起作為一個新Shader,它有兩個構造方法:

ComposeShader (Shader shaderA, Shader shaderB, Xfermode mode)  

ComposeShader (Shader shaderA, Shader shaderB, PorterDuff.Mode mode)  

兩個都差不多的,只不過一個指定了只能用PorterDuff的混合模式而另一個只要是Xfermode下的混合模式都沒問題!

你可以把兩種漸變進行疊加,比如這樣:

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

         Shader shader01 = new RadialGradient(200, 200, 200, Color.RED, Color.GREEN, Shader.TileMode.CLAMP);
         Shader shader02 = new SweepGradient(200, 200, new int[] { Color.GREEN, Color.WHITE, Color.GREEN }, null);
                
        // 設置shader
        mPaint.setShader(new ComposeShader(shader01, shader02, PorterDuff.Mode.DARKEN));  
        
        canvas.drawRect(0, 0, 400, 400, mPaint);
    }

 

七、Shader和Matrix

7.1 前言

Shader是根據矩陣來繪制的,我們如果沒有做任何設置,下面的代碼會運行出這樣的效果。

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

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.kale);  
          
        // 實例化一個Shader  
        BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); // 設置shader
        mPaint.setShader(bitmapShader);  
        
        canvas.drawRect(0, 0, 400, 400, mPaint);
    }

如果我們引入矩陣這個類,然后進行矩陣平移,看看能不能改變圖片的位置。

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

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.kale);  
          
        // 實例化一個Shader  
        BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 
        
     
        Matrix matrix = new Matrix();
        // 設置矩陣變換  
        matrix.setTranslate(20, 20);  
        // 設置Shader的變換矩陣  
        bitmapShader.setLocalMatrix(matrix);
        
        // 設置shader
        mPaint.setShader(bitmapShader);  
        
        canvas.drawRect(0, 0, 400, 400, mPaint);
    }

這說明我們可以改變繪制的初始點,如果沒有做任何設定,那么繪制的初始點就是畫布的左上角(0,0)。上面的代碼我通過變換移改變了圖片繪制的初始位置,產生了位置的偏移,效果也符合我們的預期。

如果我們最終繪制的區域和左上角有一段距離,那么就可能會出現文章開頭的情形:

 

7.2 用Matrix進行變換

好了,現在我們正式引入Matrix。值得說明的是,Matrix可以做自身的各種變換,但:

注:除了平移外,縮放、旋轉、錯切、透視都是需要一個中心點作為參照的,如果沒有平移,默認為圖形的[0,0]點,平移只需要指定移動的距離即可,平移操作會改變中心點的位置!非常重要!記牢了!

僅僅做平移:

        Matrix matrix = new Matrix();
        // 設置矩陣變換  
        matrix.setTranslate(80, 80);  
        // 設置Shader的變換矩陣  
        bitmapShader.setLocalMatrix(matrix); 

結果:

   平移后→   

 

 

平移+旋轉:

     Matrix matrix = new Matrix();
        // 設置矩陣變換  
        matrix.setTranslate(80, 80);  
        matrix.setRotate(5);  
        // 設置Shader的變換矩陣  
        bitmapShader.setLocalMatrix(matrix);

結果:

   平移+旋轉后→   

我們發現,平移的效果不見了,僅僅有了旋轉。為什么呢?

其實是這樣的,在我們new了一個Matrix對象后,這個Matrix對象中已經就為我們封裝了一組原始數據:

float[]{  
        1, 0, 0  
        0, 1, 0  
        0, 0, 1  
} 

我們的setXXX方法執行的操作是把原本Matrix對象中的數據重置,重新設置新的數據,比如:

matrix.setTranslate(500, 500);  

會變為:

float[]{  
        1, 0, 500  
        0, 1, 500  
        0, 0, 1  
}  

如果我們再設置了旋轉?比如:

        matrix.setTranslate(500, 500);  
        matrix.setRotate(5);  

這樣就會覆蓋我們平移的數據,變成了這樣:

float[]{  
        cos, sin, 0  
        sin, cos, 0  
        0, 0, 1  
} 

具體參數值我也就不計算了,我們知道了setxxx方法會重置數據,所以做變換的時候需要多多注意。從這里大家也可以看出Android給我們封裝的方法是多么的體貼到位。你只需要setRotate個角度,即可壓根就不需要你關心如何去算的。

 

7.3 preXXX和postXXX方法

preXXX和postXXX一個是前乘一個是后乘。我們知道Matrix是一個矩陣,而我們設置的參數也是一個矩陣,最終的結果肯定是這兩個矩陣相互運算后得出的。

對於Matrix可以這樣說:圖形的變換實質上就是圖形上點的變換,而我們的Matrix的計算也正是基於此,比如點P(x0,y0)經過上面的矩陣變換后會去到P(x,y)的位置。學過矩陣的就知道了,矩陣乘法是不能交換左右位置的,哪個矩陣在前面就很重要了。我們現在再來看這句話:

preXXX和postXXX一個是前乘一個是后乘.setxxx是設置當前矩陣,不進行運算。

那么具體表現是什么樣的呢?,非常簡單!

 

舉例01:

matrix.preScale(0.5f, 1);   
matrix.setScale(1, 0.6f);   
matrix.postScale(0.7f, 1);   
matrix.preTranslate(15, 0);  

我們來分析一下系統是怎么運算的。為了說明方便,我做如下規定:

把上面的代碼,變為用兩個數字組成的 [x,x] 運算對象。

pre、post表示運算順序。

遇到post的矩陣,就把這個矩陣放在已有矩陣后面進行乘法;

遇到pre,那么就把這個矩陣放到已有矩陣之前進行乘法。

那么,上面代碼運算過程如下:

  1.  [0.5f,1] * 原始矩陣 = 矩陣01  (因為是pre,所以設置的值放在原始矩陣之前相乘)
  2.  [1,0.6f] -> 矩陣01 = [1,0.6f] = 矩陣02  (因為是set,所以不會進行運算,直接重置所有值)
  3.  矩陣02 * [0.7f,1] = 矩陣03    (因為是post,所以把設置的值放在后面相乘)
  4.  [15,0] * 矩陣03 = 最終結果    (因為是pre,所以把設置值放在矩陣之前相乘)

現在,把上面用等量代換就可以得到運算過程啦:

  1. [1,0.6f]->([0.5f,1]*原始矩陣) = [1,0.6f] = 矩陣02
  2. [1,0.6f] * [0.7f,1] = 矩陣03
  3. [15,0] * ([1,0.6f] * [0.7f,1]) = 最終結果

可見,計算過程即為:translate (15, 0) -> scale (1, 0.6f) -> scale (0.7f, 1)

因為set會重置數據,所以第一行的[0.5f,1]就沒有效果了。

 

舉例02:

matrix.preScale(0.5f, 1);   
matrix.preTranslate(10, 0);  
matrix.postScale(0.7f, 1);    
matrix.postTranslate(15, 0);  

上面代碼運算過程如下:

  1. [0.5f,1] * 原始矩陣 = 矩陣01  (因為是pre,所以設置的值放在原始矩陣之前相乘)
  2. [10,0] * 矩陣01 = 矩陣02    (因為是pre,所以不會進行運算,直接重置所有值)
  3. 矩陣02 * [0.7f,1] = 矩陣03  (因為是post,所以把設置的值放在后面相乘)
  4. 矩陣03 * [15,0] = 最終結果  (因為是post,所以把設置值放在矩陣之前相乘)

現在,把上面用等量代換就可以得到運算過程啦:

  1. [10,0] * ([0.5f,1] * 原始矩陣) = 矩陣02
  2. [10,0] * ([0.5f,1] * 原始矩陣) * [0.7f,1]= 矩陣03
  3. ([10,0] * ([0.5f,1] * 原始矩陣) * [0.7f,1])* [15,0] = 最終結果

其計算過程為:translate (10, 0) -> scale (0.5f, 1) -> scale (0.7f, 1) -> translate (15, 0)

 

有一個方法可以由自己去驗證,Matrix有一個getValues方法可以獲取當前Matrix的變換浮點數組,也就是我們之前說的矩陣:

/* 
 * 新建一個9個單位長度的浮點數組 
 * 因為我們的Matrix矩陣是9個單位長的對吧 
 */  
float[] fs = new float[9];  
  
// 將從matrix中獲取到的浮點數組裝載進我們的fs里  
matrix.getValues(fs);  
Log.d("Aige", Arrays.toString(fs));// 輸出看看唄!  

 

 

本文的大部分內容來自愛哥的博客,本文對原文有少量的刪減,全部代碼由本人自行驗證。記錄在此,僅作學習筆記。

參考自:http://blog.csdn.net/aigestudio/article/details/41799811

From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige ,尊重原作者,感謝作者的無私分享。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM