一、概述
Shader
稱為著色器,通過給Paint
設定Shader
,我們可以對影像進行渲染,在實際的使用當中,我們一般使用Shader
的以下五個子類來實現不同的效果:
BitmapShader
LinearGradient
SweepGradient
RadialGradient
ComposeShader
其中第1
個用來設定Bitmap
的變換,第2~4
用來設定顏色的變換,第5
個用來組合上面的幾個Shader
,下面我們一起來看以下各個子類的使用和應用場景。
二、使用示例
2.1 BitmapShader
BitmapShader
是所有五個子類當中唯一一個對Bitmap
進行操作的,我們看一下它的建構函式:
/**
* Call this to create a new shader that will draw with a bitmap.
*
* @param bitmap The bitmap to use inside the shader
* @param tileX The tiling mode for x to draw the bitmap in.
* @param tileY The tiling mode for y to draw the bitmap in.
*/
public BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY) {
mBitmap = bitmap;
mTileX = tileX;
mTileY = tileY;
init(nativeCreate(bitmap, tileX.nativeInt, tileY.nativeInt));
}
複製程式碼
第一個引數很好理解,就是需要繪製的Bitmap
,我們看一下後面的兩個引數,它的取值有:
public enum TileMode {
/**
* replicate the edge color if the shader draws outside of its
* original bounds
*/
CLAMP (0),
/**
* repeat the shader's image horizontally and vertically
*/
REPEAT (1),
/**
* repeat the shader's image horizontally and vertically, alternating
* mirror images so that adjacent images always seam
*/
MIRROR (2);
TileMode(int nativeInt) {
this.nativeInt = nativeInt;
}
final int nativeInt;
}
複製程式碼
需要注意的是,下面幾種模式都是建立在繪製的區域要比原來的bimtap
大的情況下的。
the shader draws outside of its original bounds
複製程式碼
CLAMP
:取bitmap
邊緣的最後一個畫素進行擴充套件。REPEAT
:水平地重複整張bitmap
。MIRROR
:和REPEAT
類似,但是每次重複的時候,將bitmap
進行翻轉。
2.1.1 CLAMP
首先,我們取一張寬高為200dp * 200dp
的圖片,我們整個View
的寬高為300dp * 300dp
,
CLAMP
的模式:
private Bitmap mOriginalBitmap;
private Paint mPaint;
private void init() {
mOriginalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.shader_pic);
mPaint = new Paint();
}
private void drawBitmapShader(Canvas canvas) {
BitmapShader shader = new BitmapShader(mOriginalBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint.setShader(shader);
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mPaint);
}
複製程式碼
最終得到的結果為下圖,可以看到,由於Paint
繪製的寬高要比Bitmap
原本的寬高大,因此對於多出的部分,取了邊緣最後一個畫素的顏色進行重複:
600 * 600
,而我們繪製的大小為900 * 900
,按前面的說法,對於(600,0) - (899, 600)
的區域,取的是(599, 0) - (599, 599)
這一列的顏色,而對於(0, 600) - (600, 899)
取的是(0, 599) - (599, 599)
這一行的顏色,那麼(600, 600) - (899, 899)
這一區域是怎麼取的呢?
現在,我們試一下,把最後drawRect
的起始點改為(100, 100)
:
canvas.drawRect(100, 100, canvas.getWidth(), canvas.getHeight(), mPaint);
複製程式碼
得到的效果如下圖,可以看到,邊緣部分被切割掉了。
2.1.2 REPEAT/MIRROR
對於這兩種模式,實現方式和上面類似,我們就不再重複描述了,只給出下面執行的結果:
BitmapShader shader = new BitmapShader(mOriginalBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
複製程式碼
BitmapShader shader = new BitmapShader(mOriginalBitmap, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR);
複製程式碼
得到的結果都是和描述相符的。
2.1.3 當X
軸和Y
軸的TileMode
不同時
上面討論的情況,都是x
軸和y
軸的TileMode
相同的情況,現在,我們來看一下,當兩者不同時,會發生什麼情況:
BitmapShader shader = new BitmapShader(mOriginalBitmap, Shader.TileMode.CLAMP, Shader.TileMode.MIRROR);
複製程式碼
最終的結果如下,可以看到,我們是先按x
軸的模式進行處理,然後將x
軸處理完畢後的影像再按y
軸的模式進行處理,這也解釋了我們前面在2.1.1
中留下的疑問。
2.2 LinearGradient
LinearGradient
用來處理線性漸變,同理我們先來看它的建構函式說明,和前面不同,它有兩個建構函式,其中一種是另一種的簡化版,我們直接來看複雜的一種:
/** Create a shader that draws a linear gradient along a line.
@param x0 The x-coordinate for the start of the gradient line
@param y0 The y-coordinate for the start of the gradient line
@param x1 The x-coordinate for the end of the gradient line
@param y1 The y-coordinate for the end of the gradient line
@param colors The colors to be distributed along the gradient line
@param positions May be null. The relative positions [0..1] of
each corresponding color in the colors array. If this is null,
the the colors are distributed evenly along the gradient line.
@param tile The Shader tiling mode
*/
public LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[], TileMode tile) {
複製程式碼
下面,我們從幾個方面來分析一下這個建構函式中的引數。
2.2.1 起點座標和終點座標
對於這兩個點的座標我們可以這麼理解,起點的顏色就是color[]
陣列的第一個元素,終點的顏色就是color[]
陣列的最後一個元素,這兩個點的連線決定了線性變化的方向,如果兩點連線和x
軸的正方向是重合的時候,那麼就是水平地變化,當和x
軸正方向有度數時,那麼這個連線相對於x
軸旋轉了多少,最後線性變化的影像也就會相對於水平變化的影像旋轉了多少,下面我們用兩個例子來說明。
首先是水平方向的:
private void drawLinearGradient(Canvas canvas) {
LinearGradient gradient = new LinearGradient(0, 0, 100, 0, new int[]{ Color.WHITE, Color.BLACK }, null, Shader.TileMode.REPEAT);
mPaint.setShader(gradient);
canvas.drawRect(0, 0, 900, 900, mPaint);
}
複製程式碼
這時候的影像為:
下面,我們將終點的y
軸座標下移一點,讓起點座標和終點座標的連線,與x
軸形成一定的角度:
private void drawLinearGradient(Canvas canvas) {
LinearGradient gradient = new LinearGradient(0, 0, 100, 10, new int[]{ Color.WHITE, Color.BLACK }, null, Shader.TileMode.REPEAT);
mPaint.setShader(gradient);
canvas.drawRect(0, 0, 900, 900, mPaint);
}
複製程式碼
這時候的影像為,可以看到,由於此時連線相對於x
軸,順時針旋轉了一定的度數,那麼最終的影像也相對於上面那種情況順時針旋轉了相應的度數。
2.2.2 colors
和positions
這兩個引數很好理解,因為在顏色由起點顏色變到終點顏色的過程中,我們可能還希望中間會經過別的顏色,那麼這時候,我們就可以在陣列的第一個和最後一個元素當中插入別的元素,這些元素就是中間會經過的顏色,並且當positions
不為null
的時候,colors
的大小要和positions
相同。
private void drawLinearGradient(Canvas canvas) {
LinearGradient gradient = new LinearGradient(0, 0, 100, 10, new int[]{ Color.WHITE, Color.BLUE, Color.BLACK }, new float[]{0, 0.5f, 1f}, Shader.TileMode.REPEAT);
mPaint.setShader(gradient);
canvas.drawRect(0, 0, 900, 900, mPaint);
}
複製程式碼
結果為:
2.2.3 TileMode
和BitmapShader
不同,此時我們只用指定一個方向的變化,這個方向就是顏色線性變化對應的方向。
2.2.4 另一個建構函式
/** Create a shader that draws a linear gradient along a line.
@param x0 The x-coordinate for the start of the gradient line
@param y0 The y-coordinate for the start of the gradient line
@param x1 The x-coordinate for the end of the gradient line
@param y1 The y-coordinate for the end of the gradient line
@param color0 The color at the start of the gradient line.
@param color1 The color at the end of the gradient line.
@param tile The Shader tiling mode
*/
public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1,
TileMode tile) {
mType = TYPE_COLOR_START_AND_COLOR_END;
mX0 = x0;
mY0 = y0;
mX1 = x1;
mY1 = y1;
mColor0 = color0;
mColor1 = color1;
mTileMode = tile;
init(nativeCreate2(x0, y0, x1, y1, color0, color1, tile.nativeInt));
}
複製程式碼
唯一不同的就是去掉了colors
和position
陣列,變成了color0
和color1
,那麼我們就只能指定起點和終點的顏色了,其它的原理和上面那個建構函式是相同的。
2.3 SweepGradient
它用來提供類似雷達的效果,同理,我們看一下建構函式:
/**
* A subclass of Shader that draws a sweep gradient around a center point.
*
* @param cx The x-coordinate of the center
* @param cy The y-coordinate of the center
* @param colors The colors to be distributed between around the center.
* There must be at least 2 colors in the array.
* @param positions May be NULL. The relative position of
* each corresponding color in the colors array, beginning
* with 0 and ending with 1.0. If the values are not
* monotonic, the drawing may produce unexpected results.
* If positions is NULL, then the colors are automatically
* spaced evenly.
*/
public SweepGradient(float cx, float cy, int colors[], float positions[])
複製程式碼
2.3.1 中心點座標(cx, cy)
對於(cx, cy)
中心點的座標,我們可以把它想象成一個時鐘的指標,這個指標開始時指向3
點鐘方向,它初始的顏色就是起點顏色,那麼它會以此為起點,順時針旋轉360
度,在旋轉的過程中,這個指標的顏色不斷變化,當旋轉到360
度後,指標就變成了終點顏色,在旋轉過程中,指標所形成的軌跡就是最終的影像。
2.3.2 TileMode
需要注意到,它和LinearGradient
不同的是,由於指標是無限長的,所以形成的影像在x
軸和y
軸所拼接成的區域是無限大的,因此也就不存在了TileMode
這個引數的必要了。
2.3.3 colors[]
和positions[]
這兩個陣列的作用和上面LinearGradient
的兩個陣列的作用是相同的,這裡就不重複說明了。
2.3.4 舉例
下面舉個簡單的例子:
private void drawSweepGradient(Canvas canvas) {
SweepGradient gradient = new SweepGradient(450, 450, Color.WHITE, Color.BLACK);
mPaint.setShader(gradient);
canvas.drawRect(0, 0, 900, 900, mPaint);
}
複製程式碼
最後的結果為:
2.3.5 另一個建構函式
/**
* A subclass of Shader that draws a sweep gradient around a center point.
*
* @param cx The x-coordinate of the center
* @param cy The y-coordinate of the center
* @param color0 The color to use at the start of the sweep
* @param color1 The color to use at the end of the sweep
*/
public SweepGradient(float cx, float cy, int color0, int color1) {
mType = TYPE_COLOR_START_AND_COLOR_END;
mCx = cx;
mCy = cy;
mColor0 = color0;
mColor1 = color1;
init(nativeCreate2(cx, cy, color0, color1));
}
複製程式碼
和前面LinearGradient
中討論的一樣,color0
和color1
就是colors[]
和positions[]
的簡化版本。
2.4 RadialGradient
它被稱為圓形漸變,建構函式如下:
/** Create a shader that draws a radial gradient given the center and radius.
@param centerX The x-coordinate of the center of the radius
@param centerY The y-coordinate of the center of the radius
@param radius Must be positive. The radius of the circle for this gradient.
@param colors The colors to be distributed between the center and edge of the circle
@param stops May be <code>null</code>. Valid values are between <code>0.0f</code> and
<code>1.0f</code>. The relative position of each corresponding color in
the colors array. If <code>null</code>, colors are distributed evenly
between the center and edge of the circle.
@param tileMode The Shader tiling mode
*/
public RadialGradient(float centerX, float centerY, float radius, @NonNull int colors[], @Nullable float stops[], @NonNull TileMode tileMode)
複製程式碼
2.4.1 原點座標(centerX, centerY)
和半徑radius
對於圓形漸變,我們可以這麼理解,開始的時候,有一個半徑無限小的圓環位於(centerX, centerY)
,它的顏色就是起點顏色,之後它開始慢慢變大,直到變為半徑是radius
為止,在此期間,圓環的顏色慢慢變為終點顏色,在整個變化的過程中,圓環所形成的軌跡就是最終的影像。
2.4.2 TileMode
由於在這種情況下,影像的大小是有限的,最大就是radius
指定的範圍,因此對於超出範圍的影像,我們需要定義它的行為,但是原理還是和前面討論的TileMode
的三種情況一樣的。
2.4.3 colors[]
和stops[]
原理和上面討論的colors[]
和positions[]
一樣。
2.4.4 示例
private void drawRadialGradient(Canvas canvas) {
RadialGradient gradient = new RadialGradient(200, 200, 50, Color.BLUE, Color.RED, Shader.TileMode.REPEAT);
mPaint.setShader(gradient);
canvas.drawRect(0, 0, 900, 900, mPaint);
}
複製程式碼
最終的結果為:
2.5 ComposeShader
上面,我們已經學習了四種Shader
的實現方式,但是有時候,我們希望能夠將它組合起來,ComposeShader
就為我們提供了這種途徑,可以組合兩種Shader
的實現。
/** Create a new compose shader, given shaders A, B, and a combining mode.
When the mode is applied, it will be given the result from shader A as its
"dst", and the result from shader B as its "src".
@param shaderA The colors from this shader are seen as the "dst" by the mode
@param shaderB The colors from this shader are seen as the "src" by the mode
@param mode The mode that combines the colors from the two shaders. If mode
is null, then SRC_OVER is assumed.
*/
public ComposeShader(Shader shaderA, Shader shaderB, Xfermode mode)
複製程式碼
這就涉及到之前我們學過的PorterDuff.Mode
,第一個Shader
作為DST
,而第二個Shader
作為SRC
,兩個組合的結果會根據Mode
的不同而發生改變,下面我們用一個簡單的例子,來看一下BitmapShader
和RadialGradient
的組合:
private void drawComposeShader(Canvas canvas) {
BitmapShader bitmapShader = new BitmapShader(mOriginalBitmap, Shader.TileMode.CLAMP, Shader.TileMode.MIRROR);
RadialGradient radialGradient = new RadialGradient(300, 300, 300, Color.TRANSPARENT, Color.WHITE, Shader.TileMode.CLAMP);
ComposeShader composeShader = new ComposeShader(bitmapShader, radialGradient, PorterDuff.Mode.SRC_OVER);
mPaint.setShader(composeShader);
canvas.drawCircle(300, 300, 300, mPaint);
}
複製程式碼
最終的結果為下圖,可以看到,由於我們採用了SRC_OVER
,因此就會出現朦朧的效果。