Canvas&Paint 知識梳理(5) Paint#setShader

澤毛發表於2017-12-21

一、概述

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

Canvas&Paint 知識梳理(5)   Paint#setShader
我們首先採用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原本的寬高大,因此對於多出的部分,取了邊緣最後一個畫素的顏色進行重複:

Canvas&Paint 知識梳理(5)   Paint#setShader
現在有個疑問,因為整個圖片的大小為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);
複製程式碼

得到的效果如下圖,可以看到,邊緣部分被切割掉了。

Canvas&Paint 知識梳理(5)   Paint#setShader

2.1.2 REPEAT/MIRROR

對於這兩種模式,實現方式和上面類似,我們就不再重複描述了,只給出下面執行的結果:

BitmapShader shader = new BitmapShader(mOriginalBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
複製程式碼

Canvas&Paint 知識梳理(5)   Paint#setShader

BitmapShader shader = new BitmapShader(mOriginalBitmap, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR);
複製程式碼

Canvas&Paint 知識梳理(5)   Paint#setShader
得到的結果都是和描述相符的。

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中留下的疑問。

Canvas&Paint 知識梳理(5)   Paint#setShader

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);
    }
複製程式碼

這時候的影象為:

Canvas&Paint 知識梳理(5)   Paint#setShader
下面,我們將終點的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軸,順時針旋轉了一定的度數,那麼最終的影象也相對於上面那種情況順時針旋轉了相應的度數。

Canvas&Paint 知識梳理(5)   Paint#setShader

2.2.2 colorspositions

這兩個引數很好理解,因為在顏色由起點顏色變到終點顏色的過程中,我們可能還希望中間會經過別的顏色,那麼這時候,我們就可以在陣列的第一個和最後一個元素當中插入別的元素,這些元素就是中間會經過的顏色,並且當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);
    }
複製程式碼

結果為:

Canvas&Paint 知識梳理(5)   Paint#setShader

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));
    }
複製程式碼

唯一不同的就是去掉了colorsposition陣列,變成了color0color1,那麼我們就只能指定起點和終點的顏色了,其它的原理和上面那個建構函式是相同的。

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);
    }
複製程式碼

最後的結果為:

Canvas&Paint 知識梳理(5)   Paint#setShader

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中討論的一樣,color0color1就是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);
    }
複製程式碼

最終的結果為:

Canvas&Paint 知識梳理(5)   Paint#setShader

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的不同而發生改變,下面我們用一個簡單的例子,來看一下BitmapShaderRadialGradient的組合:

    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,因此就會出現朦朧的效果。

Canvas&Paint 知識梳理(5)   Paint#setShader

相關文章