一、概述
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,那么就把這個矩陣放到已有矩陣之前進行乘法。
那么,上面代碼運算過程如下:
- [0.5f,1] * 原始矩陣 = 矩陣01 (因為是pre,所以設置的值放在原始矩陣之前相乘)
- [1,0.6f] -> 矩陣01 = [1,0.6f] = 矩陣02 (因為是set,所以不會進行運算,直接重置所有值)
- 矩陣02 * [0.7f,1] = 矩陣03 (因為是post,所以把設置的值放在后面相乘)
- [15,0] * 矩陣03 = 最終結果 (因為是pre,所以把設置值放在矩陣之前相乘)
現在,把上面用等量代換就可以得到運算過程啦:
- [1,0.6f]->([0.5f,1]*原始矩陣) = [1,0.6f] = 矩陣02
- [1,0.6f] * [0.7f,1] = 矩陣03
- [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);
上面代碼運算過程如下:
- [0.5f,1] * 原始矩陣 = 矩陣01 (因為是pre,所以設置的值放在原始矩陣之前相乘)
- [10,0] * 矩陣01 = 矩陣02 (因為是pre,所以不會進行運算,直接重置所有值)
- 矩陣02 * [0.7f,1] = 矩陣03 (因為是post,所以把設置的值放在后面相乘)
- 矩陣03 * [15,0] = 最終結果 (因為是post,所以把設置值放在矩陣之前相乘)
現在,把上面用等量代換就可以得到運算過程啦:
- [10,0] * ([0.5f,1] * 原始矩陣) = 矩陣02
- [10,0] * ([0.5f,1] * 原始矩陣) * [0.7f,1]= 矩陣03
- ([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 ,尊重原作者,感謝作者的無私分享。