1. 程式人生 > >自定義元件開發五 陰影、 漸變和點陣圖運算

自定義元件開發五 陰影、 漸變和點陣圖運算

介紹陰影、漸變和點陣圖運算等技術
陰影只是一個狹義的說法,實際上也包括髮光等效果;Android 也提供了強大的漸變功能,漸變能為物體帶來更真實的質感,比如可以用漸變繪製一顆五子棋或一根金屬圓棒;點陣圖運算就更有趣了,Android 為 Bitmap 的運算提供了多達16 種運算方法,獲得的結果也不盡相同。
繪圖技術是自定義元件和遊戲開發的基礎

陰影

可以為文字和圖形指定陰影(Shader)。在繪圖中,有一個叫 layer(層)的概念,預設情況下,我們的文字和圖形繪製在主層(main layer)上,其實也可以將內容繪製在新建的 layer 上。實際上陰影就是在 main layer 的下面添加了一個陰影層(shader layer),可以為陰影指定模糊度、偏移量和陰影顏色。

Paint 類定義了一個名為 setShadowLayer 的方法:
public void setShadowLayer(float radius, float dx, float dy, int shadowColor),引數意義如下:
radius:陰影半徑
dx:x 方向陰影的偏移量
dy:y 方向陰影的偏移量
shadowColor:陰影的顏色

陰影 layer 顯示陰影時 , shader layer 有 兩 種 類 型 : View.LAYER_TYPE_SOFTWARE 和 View.LAYER_TYPE_HARDWARE,laye的預設型別為 LAYER_TYPE_HARDWARE,但陰影只能在View.LAYER_TYPE_SOFTWARE 環境下工作 , 所以 , 我們需要呼叫 View 類 的 public voidsetLayerType(int layerType, Paint paint) 方法為 Paint 物件指定層的型別 :setLayerType(View.LAYER_TYPE_SOFTWARE, paint)。

public class ShaderView extends View {
    public ShaderView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setTextSize(100
); this.setLayerType(View.LAYER_TYPE_SOFTWARE, paint); paint.setShadowLayer(10, 1, 1, Color.RED); canvas.drawText("Android 開發", 100, 100, paint); paint.setShadowLayer(10, 5, 5, Color.BLUE); canvas.drawText("Android 繪圖技術", 100, 220, paint); } }

這裡寫圖片描述

漸變

漸變(Gradient)是繪圖過程中顏色或點陣圖以特定規律進行變化,能增強物體的質感和審美情趣。生活中的漸變非常多,例如公路兩邊的電線杆、樹木、建築物的陽臺、鐵軌的枕木延伸到遠方等等,很多的自然理象都充滿了漸變的形式特點。Android 同樣對漸變進行了完善支援,通過漸變,可以繪製出更加逼真的效果。
Graphics2D 漸變種類有:
線性漸變:LinearGradient
徑向漸變:RadialGradient
掃描漸變:SweepGradient
點陣圖漸變:BitmapShader
混合漸變:ComposeShader
其中,線性漸變、徑向漸變和掃描漸變屬於顏色漸變,指定 2 種或 2 種以上的顏色,根據顏色過渡演算法自動計算出中間的過渡顏色,從而形成漸變效果,對於開發人員來說,無需關注中間的漸變顏色。點陣圖漸變則不再是簡單的顏色漸變,而是以圖片做為貼片有規律的變化,類似於桌布平鋪。混合漸變則能將多種漸變進行組合,實現更加複雜的漸變效果。
如果 A、B 分別代表 2 種不同的顏色,我們將漸變分為三種:
Ø ABAB 型:A、B 兩種顏色重複變化,通過 TileMode 類的 REPEAT 常量來表示;
Ø ABBA 型:A、B 兩種顏色映象變化,通過 TileMode 類的 MIRROR 常量來表示;
Ø AABB 型:A、B 兩種顏色只出現一次,通過 TileMode 類的 CLAMP 常量來表示。
這裡寫圖片描述
定義漸變時,必須指定一個漸變區域,根據定義的漸變內容和漸變模式填滿該區域。每一種漸變都被定義成了一個類,他們都繼承自同一個父類——Shader。繪圖時,呼叫 Paint 類的setShader(Shader shader)方法指定一種漸變型別,繪製出來的繪圖填充區域都將使用指定的漸變顏色或點陣圖進行填充。本質上來說,前面談到的填充(Fill)和漸變(Gradient)都大同小異。我們需要重點掌握每個漸變類的構造方法的引數以及意義。
討論漸變時雖然更多的是指填充區域的漸變,但繪圖樣式為 STOKE 時,線條同樣可以應用漸變效果。

線性漸變 ( LinearGradient )

線性漸變(LinearGradient)根據指定的角度、顏色和模式使用漸變顏色填充繪圖區域。我們必須定義兩個點(x0,y0)和(x1,y1),漸變的方向與這兩個點的連線垂直
這裡寫圖片描述
LinearGradient 的構造方法如下;
public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile):本方法用於兩種顏色的漸變,各引數意義如下:
x0、y0:用於決定線性方向的第一個點的座標(x0,y0);
x1、y1:用於決定線性方向的第二個點的座標(x1,y1);
color0:第一種顏色;
color1:第二種顏色;
tile:漸變模式

假設我們繪製了三個矩形,第一個矩形的漸變區域與矩形恰好一致,第二個矩形的漸變區域大於矩形區域,第三個矩形的漸變區域小於矩形區域,均採用 TileMode 的 CLAMP 模式,程式碼如下:

public class GradientView extends View {
    private static final int OFFSET = 100;
    private Paint paint;
    public GradientView(Context context, AttributeSet attrs) {
        super(context, attrs);

                paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.FILL);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Rect rect1 = new Rect(100, 100, 500, 400);
        LinearGradient lg = new LinearGradient(
                rect1.left, rect1.top, rect1.right, rect1.bottom,
                Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
        paint.setShader(lg);
        canvas.drawRect(rect1, paint);
        //座標往下移動
        canvas.translate(0, rect1.height() + OFFSET);
        //放大漸變矩形
        Rect rect2 = new Rect(rect1);
        rect2.inset(-100, -100);
        lg = new LinearGradient(
                rect2.left, rect2.top, rect2.right, rect2.bottom,
                Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
        paint.setShader(lg);
        canvas.drawRect(rect1, paint);
        //座標往下移動
        canvas.translate(0, rect1.height() + OFFSET);
        //縮小漸變矩形
        Rect rect3 = new Rect(rect1);
        rect3.inset(100, 100);
        lg = new LinearGradient(
                rect3.left, rect3.top, rect3.right, rect3.bottom,
                Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
        paint.setShader(lg);
        canvas.drawRect(rect1, paint);
    }
}

這裡寫圖片描述

我們看到,當漸變區域和矩形區域大小不同時,表現出來的效果也有不同。請讀者仔細體會。另外,您也可以修改漸變模式,看看其他兩種漸變有什麼特點。
如果兩種顏色無法滿足繪圖需求,LinearGradient 支援三種或者三種以上顏色的漸變,對應的構造方法如下:
public LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[], TileModetile),這是一個功能更加強大的構造方法,我們來看看該構造方法引數的作用:
x0、y0:起始點的座標
x1、y1:終止點的座標
colors:多種顏色
positions:顏色的位置(比例)
TileMode:漸變模式
引數 colors 和 positions 都是陣列,前者用於指定多種顏色,後者用於指定每種顏色的起始
比例位置。positions 陣列中的元素個數與 colors 要相同,且是 0 至 1 的數值,[0,1]是臨界區,如果小於 0 則當 0 處理,如果大於 1 則當 1 處理。假如在繪圖區域和漸變區域大小相同的情況下,
colors 包含了三種顏色:red、yellow、green,在漸變區域中這三種顏色的起始比例位置為 0、0.3、1,則顏色漸變如圖 5-6 所示,比例位置為 0.2、0.5、0.8,則顏色漸變如圖所示
這裡寫圖片描述

從圖可以看出,0.2 比例位置處設定為 red,0~0.2 之間的顏色也是 red,0.8 比例位置
處為 green,0.8~1 之間的顏色也是 green。當然,這裡呈現出來的效果是在繪圖區域和漸變區域相同的前提條件下,修改漸變區域的大小或者 TileMode 漸變模式,結果必定又不相同。

徑向漸變( RadialGradient )

徑向漸變是以指定的點為中心,向四周以漸變顏色進行圓周擴散,和線性漸變一樣,支援兩種或多種顏色。
這裡寫圖片描述

徑向漸變的主要構造方法如下:
public RadialGradient(float x, float y, float radius, int color0, int color1, TileMode tile),該構造方法支援兩種顏色,下面是引數的作用:
x、y:中心點座標
radius:漸變半徑
color0:起始顏色
color1:結束顏色
TileMode:漸變模式
public RadialGradient(float x, float y, float radius, int colors[], float positions[], TileModetile),該構造方法支援 3 種或 3 種以上顏色的漸變,各引數的作用如下:x、y:中心點座標
radius:漸變半徑
colors:多種顏色
positions:顏色的位置(比例)
TileMode:漸變模式
接下來我們在 View 上繪製相同大小的正方形和圓,使用一致的徑向漸變,模式為TileMode.MIRROR,在大部分時候,映象模式的漸變效果看起來會更舒服更討人喜歡。

public class RadialGradientView extends View {

    public RadialGradientView(Context context, AttributeSet attrs) {
    super(context, attrs);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Rect rect = new Rect(100, 100, 500, 500);
        RadialGradient rg = new RadialGradient(
                300, 300, 200, Color.RED, Color.GREEN, Shader.TileMode.MIRROR);
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setShader(rg);
        canvas.drawRect(rect, p);
        canvas.translate(510, 0);
        canvas.drawOval(new RectF(rect), p);
    }
}

上面程式碼中,我們定義了一個 RadialGradient 物件,中心點座標為(300, 300),正好是正方形和圓的中心,半徑為 200,意味著漸變區域與正方形和圓的大小相同。
這裡寫圖片描述
利用徑向漸變,我們可以畫出五子棋的棋子,五子棋分為黑色和白色兩種不同的棋子,為了畫出更逼真的效果,需要考慮棋子的反光效果,光點不能是正中心,而應該向右下角偏移;同時,為棋子加上陰影,棋子似乎躍然紙上。黑色棋子使用黑白兩色繪製,白色棋子則使用灰白兩色繪製

這裡寫圖片描述

有了棋子,就應該有棋盤,棋盤是一個 m*n 的矩陣,按照一定的規律畫好水平線和垂直線
就可以了。

public class FiveChessView extends View {
    private static final int SIZE = 120;//棋子的尺寸
    private static final int OFFSET = 10; //發光點的偏移大小
    private Paint paint;
    public FiveChessView(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = this.getMeasuredWidth();
        int height = this.getMeasuredHeight();
        int rows = height / SIZE;
        int cols = width / SIZE;
        //畫棋盤
        drawChessBoard(canvas, rows, cols);
        //畫棋子
        drawChess(canvas, 4, 4, ChessType.BLACK);
        drawChess(canvas, 4, 5, ChessType.BLACK);
        drawChess(canvas, 5, 4, ChessType.WHITE);
        drawChess(canvas, 3, 5, ChessType.WHITE);
    }
    /**
     * 畫棋盤
     */
    private void drawChessBoard(Canvas canvas, int rows, int cols) {
        paint.setColor(Color.GRAY);
        paint.setShadowLayer(0, 0, 0, Color.GRAY);//取消陰影
        for(int i = 0; i < rows + 1; i ++){
            canvas.drawLine(0, i * SIZE, cols * SIZE, i * SIZE, paint);
        }
        for(int i = 0; i < cols + 1; i ++){

                    canvas.drawLine(i * SIZE, 0, i * SIZE, rows * SIZE, paint);
        }
    }
    /**
     * 畫棋子
     * @param x 行
     * @param y 列
     * @param chessType 棋子型別
     */
    private void drawChess(Canvas canvas, int x, int y, ChessType chessType){
        //定義棋子顏色
        int colorOuter = chessType == ChessType.BLACK ? Color.BLACK: Color.GRAY;
        int colorInner = Color.WHITE;
        //定義漸變,發光點向右下角偏移 OFFSET
        RadialGradient rg = new RadialGradient(x * SIZE + OFFSET, y * SIZE + OFFSET, SIZE / 1.5f,
                colorInner, colorOuter, Shader.TileMode.CLAMP);
        paint.setShader(rg);
        //畫棋子
        this.setLayerType(View.LAYER_TYPE_SOFTWARE, paint);
        paint.setShadowLayer(6, 4, 4, Color.parseColor("#AACCCCCC"));//給棋子加陰影
        canvas.drawCircle(x * SIZE, y * SIZE, SIZE / 2, paint);
    }
    enum ChessType{
        BLACK, WHITE
    }
}

掃描漸變(SweepGradient )

Sweep,這是什麼單詞?英文字典裡翻譯為“清掃、掃除”,不過,我覺得叫掃描漸變也挺好,不管了,就這麼叫吧。
SweepGradient 類似於軍事題材電影中的雷達掃描效果,固定圓心,將半徑假想為有形並旋轉一週而繪製的漸變顏色。SweepGradient 定義了兩個主要的構造方法:
public SweepGradient(float cx, float cy, int color0, int color1)
支援兩種顏色的掃描漸變,引數的作用如下:
cx、cy:圓點座標;
color0:起始顏色;
color1:結束顏色。
public SweepGradient(float cx, float cy, int colors[], float positions[])
支援多種顏色的掃描漸變,引數的作用如下:
cx、cy:圓點座標;
colors:多種顏色;
positions:顏色的位置(比例)。
引數的作用和使用在前面的內容中都有說明,不再贅述,我們通過一個簡單的案例說明SweepGradient 的使用方法。

public class SweepGradientView extends View {
public SweepGradientView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
SweepGradient sg =
new SweepGradient(300, 300, Color.GREEN, Color.YELLOW);
paint.setShader(sg);
canvas.drawRect(new Rect(0, 0, 600, 600), paint);
}
}

這裡寫圖片描述
如圖所示,漸變的起始顏色為綠色,終止顏色為黃色,和RadialGradient 不同,RadialGradient的漸變是以圓點為中心向四圍擴散(想像為漣漪),擴散的距離由引數radius決定,一旦超過該距離,將根據 TileMode 漸變模式重複繪製漸變顏色;SweepGradient 是從 0 度方向開始,以指定點為中心,儲存中心不動,將半徑旋轉一週,不需要指定半徑和漸變模式,因為顏色的漸變指向無窮遠處,而且只旋轉 360 度。

上圖所示的效果在綠色和黃色之間沒有過渡色,顯得特別突兀,可以使用第二個構造方法,定義三種或三種以上的顏色,第一種顏色和最後一種顏色相同就即可。下面的程式碼演示了這種用法,定義 SweepGradient 物件時,引數 positions 為 null 表示各顏色所佔比例平均分配

public class SweepGradient2View extends View {
public SweepGradient2View(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
SweepGradient sg = new SweepGradient(300, 300,
new int[]{Color.GREEN, Color.YELLOW, Color.RED, Color.GREEN}, null);
paint.setShader(sg);
canvas.drawOval(new RectF(0, 0, 600, 600), paint);
}
}

這裡寫圖片描述

點陣圖漸變(BitmapShader)

點陣圖漸變其實就是在繪製的圖形中將指定的點陣圖作為背景,如果圖形比點陣圖小,則通過漸變模式進行平鋪,TileMode.CLAMP 模式不平鋪,TileMode.REPEAT 模式表示平鋪,TileMode.MIRROR模式也表示平鋪,但是交錯的點陣圖是彼此的映象,方向相反。可以同時指定水平和垂直兩個方向的漸變模式。

BitmapShader 只有一個構造方法:
public BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY),引數如下:
bitmap:點陣圖;
tileX:x 方向的重複模式;
tileY:y 方向的重複模式。
使用 Android Studio 開發 Android 應用程式時,在 res/mipmap 目錄下有一張名為 ic_launcher的預設圖片,我們編寫一個案例用該圖片填充到繪製的圖形中。

public class BitmapShaderView extends View {

    public BitmapShaderView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap bmp = BitmapFactory.decodeResource(
                getResources(), R.mipmap.ic_launcher);
        BitmapShader bs = new BitmapShader(bmp,
                Shader.TileMode.REPEAT, Shader.TileMode.MIRROR);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setShader(bs);
        canvas.drawRect(new Rect(0, 0,
                getMeasuredWidth(), getMeasuredHeight()), paint);
    }
}

這裡寫圖片描述
從圖中看出,水平方向的漸變模式為 TileMode.REPEAT,所以小機器人重複出現了,而垂直方向的漸變模式為 TileMode.MIRROR,偶數行的小機器人與奇數行是垂直翻轉的。

混合漸變( ComposeShader )

混合漸變ComposeShader是將兩種不同的漸變通過點陣圖運算後得到的一種更加複雜的漸變。點陣圖運算有 16 種之多,在下一個小節中將介紹,本節我們無法向您詳細解釋,我們暫且先掌握ComposeShader 的基本使用方法。
ComposeShader 有兩個構造方法:
public ComposeShader(Shader shaderA, Shader shaderB, Xfermode mode)
public ComposeShader(Shader shaderA, Shader shaderB, Mode mode)
shaderA 和 shaderB 是兩個漸變物件,mode 為點陣圖運算型別,16 種運算模式如圖
所示,其實從命名就能大概知道每種點陣圖運算的含義,大家不妨好好研究一下,理解
了自然就記住了。
這裡寫圖片描述

下面的案例是將兩種漸變(線性漸變和點陣圖漸變)進行 Mode.SRC_ATOP 運算得到的新的混合漸變,如下圖所示,Mode.SRC_ATOP 運算是指顯示第一個點陣圖的全部,而第二個點陣圖只顯示二者的交集部分並顯示在上面。

public class ComposeShaderView extends View{
public ComposeShaderView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//點陣圖漸變
Bitmap bmp = BitmapFactory.decodeResource(
getResources(), R.mipmap.ic_launcher);
BitmapShader bs = new BitmapShader(bmp,
Shader.TileMode.REPEAT, Shader.TileMode.MIRROR);
//線性漸變
LinearGradient lg = new LinearGradient(0, 0, getMeasuredWidth(), 0,
Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
//混合漸變
ComposeShader cs =
new ComposeShader(bs, lg, PorterDuff.Mode.SRC_ATOP);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setShader(cs);
canvas.drawRect(new Rect(0, 0, getMeasuredWidth(), 600), paint);
}
}

程式碼中,shaderA 為點陣圖漸變,shaderB 為線性漸變,根據 Mode.SRC_ATOP 的運算規則,shaderA 會全部顯示(此處為小機器人),shaderB 只顯示二者相交部分並位於最上面(TOP),所以,得到的運算效果如圖所示。大家可以試試其他的點陣圖運算模式,從結果中找出運算規律。
這裡寫圖片描述

漸變 與 Matrix

漸變類都繼承自同一個父類——Shader,該類並不複雜,不過定義了一個非常有用的方法:public void setLocalMatrix(Matrix localM),該方法能和漸變結合,在填充漸變顏色的時候實現移位、旋轉、縮放和拉斜的效果。
下面的案例中,我們做了一個旋轉的圓,圓內使用 SweepGradient 漸變填充,看起來像一張光碟。首先,我們建立了一個 Matrix 物件 mMatrix,mMatrix 定義了以圓點為中心漸變的旋轉效果,注意不是旋轉 Canvas 而是旋轉 SweepGradient。onDraw()方法中不斷呼叫 invalidate()重繪自己,每重繪一次就旋轉 3 度,於是就形成了一個旋轉的動畫。

public class Sweep extends View {
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private float mRotate;
    private Matrix mMatrix = new Matrix();
    private Shader mShader;
    public Sweep(Context context, AttributeSet attrs) {
        super(context, attrs);

                setFocusable(true);
        setFocusableInTouchMode(true);
        float x = 160;
        float y = 100;
        mShader = new SweepGradient(x, y, new int[] { Color.GREEN,
                Color.RED,
                Color.BLUE,
                Color.GREEN }, null);
        mPaint.setShader(mShader);
    }
    @Override protected void onDraw(Canvas canvas) {
        Paint paint = mPaint;
        float x = 160;
        float y = 100;
        canvas.translate(300, 300);
        canvas.drawColor(Color.WHITE);
        mMatrix.setRotate(mRotate, x, y);
        mShader.setLocalMatrix(mMatrix);
        mRotate += 3;
        if (mRotate >= 360) {
            mRotate = 0;
        }
        invalidate();
        canvas.drawCircle(x, y, 380, paint);
    }
}

點陣圖運算

PorterDuffXfermode

點陣圖運算為點陣圖的功能繁衍提供了強大的技術基礎,大大增強了點陣圖的可塑性和延伸性,使很多看起來非常複雜的效果和功能都能輕易實現,比如圓形頭像、不規則圖片、橡皮擦、稀奇古怪的自定義進度條等等。但因為運算模式多,加上大部分人對圖形學缺少了解和研究,往往會造成心理上難以逾越的門檻。所以,對本節的案例要多理解、多對比、多分析。

在 Graphics2D 中,類 PorterDuffXfermode 提供對點陣圖運算模式的定義與支援, “ProterDuff”是兩個人名組合:Tomas Proter 和 Tom Duff,他們是最早在 SIGGRAPH 上提出圖形混合概念的大神級人物。建立 PorterDuffXfermode 物件時,可以提供多達 16 種運算模式,執行官方提供的 APIDemos App,找到 Graphics/Xfermodes,能看到如圖 5-17 所示的執行結果。
這裡寫圖片描述

點陣圖運算模式定義在 PorterDuff 類的內部列舉型別 Mode 中,對應了 16 個不同的列舉值:
public static enum Mode {
ADD,
CLEAR,
DARKEN,
DST,
DST_ATOP,
DST_IN,
DST_OUT,
DST_OVER,
LIGHTEN,
MULTIPLY,
OVERLAY,
SCREEN,
SRC,
SRC_ATOP,
SRC_IN,
SRC_OUT,
SRC_OVER,
XOR
}
我們通過下面的表格來理解各運算模式的作用。
這裡寫圖片描述
這裡寫圖片描述
為了實現點陣圖運算,建立 PorterDuffXfermode 物件後,呼叫 Paint 類的 public Xfermode
setXfermode(Xfermode xfermode) 方 法 , PorterDuffXfermode 是 Xfermode 的 子 類 , 將PorterDuffXfermode 物件作為實際引數傳入即可,形如:
paint.setXfermode(new PorterDuffXfermode(Mode.CLEAR));

圖層( Layer )

Canvas 在一般的情況下可以看作是一張畫布,所有的繪圖操作如點陣圖、圓、直線等都在這
張畫布上繪製,Canvas 同時還定義了相關屬性如 Matrix、顏色等等。但是,倘若需要實現一些相對複雜的繪圖操作,比如多層動畫、地圖(地圖可以有多個地圖層疊加而成,比如:政區層、道路層、興趣點層)等,需要 Canvas 提供的圖層(layer)支援,預設情況下可以看作只有一個圖層 layer。如果需要按層次來繪圖, Canvas 需要建立一些中間層。layer 按照“棧結構”來管理,如圖 所示。
這裡寫圖片描述
既然是棧結構,自然存在入棧和出棧兩種行為。layer 入棧時,後續的繪圖操作都發生在這
個 layer 上,而 layer 出棧時,將把本圖層繪製的影象“繪製”到上層或是 Canvas 上,複製 layer到 Canvas 上時,還可以指定 layer 的透明度。

其實在《5.2 陰影》這一小節中,我們向大家介紹了 layer 的概念,我們可以將它翻譯成“圖
層”,Canvas 預設的圖層稱之為“主圖層(main layer)”,陰影顯示在“陰影圖層(shader layer)”中,實際上,我們還能自己建立新的圖層併入棧,建立圖層通過 saveLayer()方法,該方法有下面幾個過載的版本:
public int saveLayer(float left, float top, float right, float bottom, Paint paint, int saveFlags)
public int saveLayer(RectF bounds, Paint paint, int saveFlags)
public int saveLayer(RectF bounds, Paint paint)
public int saveLayer(float left, float top, float right, float bottom, Paint paint)
還能通過 saveLayerAlpha()方法為圖層(layer)指定透明度:
public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha(RectF bounds, int alpha)
public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, int saveFlags)
public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha)
saveLayer()方法中,left、top、right 和 bottom 用於確定圖層的位置和大小;引數 paint 官方文件對作用的描述是“This is copied, and is applied to the offscreen when restore() is called.”,可以指定為 null;saveFlags 用於指定儲存標識位,雖然也有好幾個值可供選擇,但官方推薦使用Canvas.ALL_SAVE_FLAG。在 saveLayerAlpha()方法中,引數 alpha 自然就是指定圖層的透明度(0~255)了。這兩個方法的返回值為一個 int 值,代表當前入棧的 layer 的 id,通過該 id 能明確是哪一個layer 出棧。layer 從棧中彈出(出棧),需要呼叫 public void restoreToCount(int saveCount)方法,該方法的引數就是 saveLayer()或 saveLayerAlpha()的返回值。

點陣圖運算技巧

要實現點陣圖的混合運算,一方面需要通過 PorterDuffXfermode 指定運算的模式,另一方面還需要藉助 layer 進行“離屏快取”,達到類似 Photoshop 中“遮罩層”的效果。歸納起來,大概有下面幾個參考步驟(其實可以有更加簡化的步驟):
1) 準備好分別代表 DST 和 SRC 的點陣圖,同時準備第三個點陣圖,該點陣圖用於繪製 DST 和
SRC 運算後的結果;
2) 建立大小合適的圖層(layer)併入棧;
3) 先將 DST 點陣圖繪製在第三個點陣圖上;
4) 呼叫 Paint 的 setXfermode()方法定義點陣圖運算模式;
5) 再將 SRC 點陣圖繪製在第三個點陣圖上;
6) 清除點陣圖運算模式;
7) 圖層(layer)出棧
8) 將第三個點陣圖繪製在 View 的 Canvas 上以便顯示。
為了讓大家能理解 layer 在點陣圖運算中的作用,我們層層遞進由淺入深地進行學習。首先,
在不使用 layer 的情況下,按照上面的思路繪製一個圓和一個正方形,圓作為 DST,正方形作為SRC,並執行 Mode.SRC 的運算模式。

public class PorterDuffXferView extends View {
    public PorterDuffXferView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap dst = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
        Bitmap src = dst.copy(Bitmap.Config.ARGB_8888, true);
        Bitmap b3 = Bitmap.createBitmap(450, 450, Bitmap.Config.ARGB_8888);
        Canvas c1 = new Canvas(dst);
        Canvas c2 = new Canvas(src);
        Canvas c3 = new Canvas(b3);
        Paint p1 = new Paint();
        p1.setColor(Color.GRAY);
        c1.drawCircle(150, 150, 150, p1);
        Paint p2 = new Paint();
        p2.setColor(Color.GREEN);
        c2.drawRect(0, 0, 300, 300, p2);
//定義畫筆
        Paint paint = new Paint();
//畫圓
        c3.drawBitmap(dst, 0, 0, null);
//定義點陣圖的運算模式
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
//畫正方形
        c3.drawBitmap(src, 150, 150, paint);
//清除運算效果
        paint.setXfermode(null);
//繪製到 Canvas 上
        canvas.drawBitmap(b3, 0, 0, null);
    }
}

執行這段程式碼,結果卻大失所望,從圖 中看出,與我們前面介紹的相差甚遠,根本不
是我們想要的結果,正確的效果應該如圖所示。
這裡寫圖片描述
該問題需要用到圖層(layer)來解決。事實上,在 Mode.SRC 運算模式中,如果不願意看到DST(圓)的非交集部分(左上角的灰色部分),不使用 layer 是解決不了問題的。我們必須在正方形區域定義一個圖層,繪圖後,圖層區域內的部分將會顯示,而圖層區域外的部分即會消失,示意圖可以幫助大家理解前面這段話。
這裡寫圖片描述
虛線部分表示圖層(layer),繪圖時,先建立圖層併入棧,該圖層的 left 和top 應該與圓的圓點座標相同,right 和 bottom 則應該與正方形的 right 和 bottom 相同,接下來依次繪製圓形和正方形,所有繪製都作用在前面建立的圖層上,通過 restoreToCount()方法將圖層出棧後,顯示出來的其實是圖層之內的部分,圖層之外的部分不會顯示了。我們重構一下上面的程式碼。

public class PorterDuffXferView extends View {
    public PorterDuffXferView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap dst = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
        Bitmap src = dst.copy(Bitmap.Config.ARGB_8888, true);
        Bitmap b3 = Bitmap.createBitmap(450, 450, Bitmap.Config.ARGB_8888);
        Canvas c1 = new Canvas(dst);
        Canvas c2 = new Canvas(src);
        Canvas c3 = new Canvas(b3);
        Paint p1 = new Paint();
        p1.setColor(Color.GRAY);
        c1.drawCircle(150, 150, 150, p1);
        Paint p2 = new Paint();
        p2.setColor(Color.GREEN);
        c2.drawRect(0, 0, 300, 300, p2);
//定義畫筆
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
//建立圖層
        int layer = c3.saveLayer(150, 150, 450, 450, null, Canvas.ALL_SAVE_FLAG);
//畫圓
        c3.drawBitmap(dst, 0, 0, null);
//定義點陣圖的運算模式
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
//畫正方形
        c3.drawBitmap(src, 150, 150, paint);
//清除運算效果
        paint.setXfermode(null);
//恢復
                c3.restoreToCount(layer);
//繪製到 Canvas 上
        canvas.drawBitmap(b3, 0, 0, null);
    }
}

不出所料,效果出來了。該示例只是演示了 Mode.SRC 的用法,另外的 15 種大家最好都能畫出來,確保執行結果與圖所示一模一樣,千萬不要想當然的以為程式碼差不多(這樣的想法讓您在學習過程中忽略了很多細節,不可取)。再次強調說明一下——圖層 layer 的位置和大小要根據實際情況進行設定。
本節我們為大家提供了一個位圖運算的基本思路,需要定義 3 個 Bitmap 物件,這是為了方便您理解,但這不是必要條件,具體要建立多少 Bitmap 物件取決於開發人員自己,在下面的案例中,可能會有些微的變化

案例 1 : 圓形 頭像

現在很多 App 或網頁在顯示頭像時(如微博),不再使用千遍一律的方形,而是使用更加活潑的圓形。手機或相機拍出來的照片都是矩形,如果要顯示為圓形,必須採用技術手段來解決。我們先來分析一下基本的解決思路。將照片作為DST,SRC則是新建立的畫了實心圓的點陣圖,其實也是遮罩層。如果既要顯示出 SRC 的形狀,又要顯示出 DST 的內容,則必須使用 DST_IN 運算模式(DST 表示顯示 DST 內容,IN 表示只顯示相交部分)。我們首先來看一下初級程式碼實現。

public class CirclePhotoView extends View {
    private Bitmap bmpCat;

    private Bitmap bmpCircleMask;
    private Canvas cvsCircle;
    private Paint paint;
    public CirclePhotoView(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        bmpCat = BitmapFactory.decodeResource(getResources(), R.mipmap.cat);
        int minWidth = Math.min(bmpCat.getWidth(), bmpCat.getHeight());
        bmpCircleMask = Bitmap.createBitmap(minWidth, minWidth,
                Bitmap.Config.ARGB_8888);
        cvsCircle = new Canvas(bmpCircleMask);
        int r = minWidth / 2;
        cvsCircle.drawCircle(r, r, r, paint);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bmpCat, 0, 0, null);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        canvas.drawBitmap(bmpCircleMask, 0, 0, paint);
    }
}

R.mipmap.cat 是一張可愛的小貓圖片(其實我更喜歡小狗),載入到 bmpCat 物件中,另外,建立了一個名為 bmpCircleMask 的 Bitmap 物件,併為該物件建立了一個關聯的 Canvas,在bmpCircleMask 上畫了一個實心圓,實心圓的直徑為圖片的短的邊長。bmpCircleMask 同時也是遮罩層。執行上面的程式碼,效果如圖所示。
這裡寫圖片描述

邊!大黑邊!!!!遮罩層圓形點陣圖的 4 個角變成了黑色,我們要的應該是透明,要解決這個問題,必須使用圖層(layer)。建立一個圖層,大小和 bmpCircleMask 一樣,將 DST(小貓)和SRC(實心圓)都繪製在該圖層上,奇蹟立刻出現了。

public class CirclePhotoView extends View {
//省略部分程式碼
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int w = bmpCircleMask.getWidth();
int layer = canvas.saveLayer(0, 0, w, w, null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(bmpCat, 0, 0, null);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas.drawBitmap(bmpCircleMask, 0, 0, paint);
canvas.restoreToCount(layer);
}
}

這裡寫圖片描述
利用這個方法,我們其實可以實現任意形狀的圖片,本質上,遮罩層是什麼形狀,圖片就會顯示什麼形狀,如果讀者熟悉 PhotoShop 中的蒙版,其實 layer 和蒙版概念基本相同。如圖 所示就是將小貓和 Android 機器人通過 DST_IN 點陣圖運算後得到的效果。需要注意的是,遮罩層最好使用 png 圖片,這種格式的圖片才支援透明畫素,才能有真正的不規則照片。
這裡寫圖片描述


public class AnomalousPhotoView extends View {
    private Bitmap bmpCat;
    private Bitmap bmpMask;
    private Paint paint;

    private static final int OFFSET = 100;
    public AnomalousPhotoView(Context context, AttributeSet attrs) {
        super(context, attrs);
        bmpCat = BitmapFactory.decodeResource(getResources(), R.mipmap.cat);
        bmpMask = BitmapFactory.decodeResource(
                getResources(), R.mipmap.ic_launcher);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int id = canvas.saveLayer(OFFSET, OFFSET, bmpMask.getWidth() + OFFSET,
                bmpMask.getHeight() + OFFSET,
                null, Canvas.ALL_SAVE_FLAG);
        canvas.drawBitmap(bmpCat, 0, 0, null);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        canvas.drawBitmap(bmpMask, OFFSET, OFFSET, paint);
        canvas.restoreToCount(id);
    }
}

案例 2 : 刮刮樂

刮刮樂是個很好玩的小應用,娛樂性很強,靠的是運氣。在圖片上隨機產生一箇中獎資訊,蒙上一層顏色,使用者使用手指在螢幕上塗刮,顏色即被擦除,最後看到中獎資訊。
這裡寫圖片描述
從技術實現上來說,我們發現刮刮樂有兩個圖層,一個是不會變化的中獎資訊圖層,一個是蒙上了一層灰色的圖層。當用戶手指在螢幕上塗抹時,我們需要將灰色抹掉。中獎資訊其實並不需要改變,換句話說,塗抹時,無需重繪中獎資訊。
所以,對於中獎資訊點陣圖來說,應該採用更簡單的實現,可以在圖片上寫上中獎資訊後作為View 的背景(Background),當手指在螢幕上塗抹時就不需要考慮他的重繪問題了。我們再建立一個 Bitmap 物件,初始蒙上一層灰色,手指在螢幕上移動時同時繪製線條,將線條與灰色做Mode.CLEAR 運算,相交的部分即被清除,變成了透明效果,於是我們就能看到背景了。
實現刮刮樂需要經歷下面兩個步驟:
1) 繪製背景
背景需要一張圖片,資源中的圖片不能編輯,所以必須呼叫 Bitmap 的 copy()方法複製
一張同樣的圖片並設定可編輯標識,畫上隨機生成的中獎資訊,呼叫 View 類的 public
void setBackground(Drawable background)方法設定為背景(該方法有相容性問題)。
2) 在螢幕上繪製線條
定義一個 Bitmap 物件,初始畫上一層灰色,當手指在螢幕上移動時,不斷繪製曲線,
曲線和灰色做 Mode.CLEAR 運算,實現清除的效果。
下面一起來看原始碼,原始碼比較長,相關的技術點在前面的章節內容中都已涉及,在此不再贅述。執行效果如圖所示。

public class GuaGuaLeView extends View {
    private Random rnd;
    private Paint paint;
    private Paint clearPaint;
    private static final String[] PRIZE = {
            "恭喜,您中了一等獎,獎金 1 億元",
            "恭喜,您中了二等獎,獎金 5000 萬元",
            "恭喜,您中了三等獎,獎金 100 元",
            "很遺憾,您沒有中獎,繼續加油哦"
    };
    /**塗抹的粗細*/
    private static final int FINGER = 50;
    /**緩衝區*/
    private Bitmap bmpBuffer;
    /**緩衝區畫布*/
    private Canvas cvsBuffer;
    private int curX, curY;
    public GuaGuaLeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        rnd = new Random();
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setTextSize(100);
        paint.setColor(Color.WHITE);
        clearPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));

                clearPaint.setStrokeJoin(Paint.Join.ROUND);
        clearPaint.setStrokeCap(Paint.Cap.ROUND);
        clearPaint.setStrokeWidth(FINGER);
//畫背景
        drawBackground();
    }
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
//初始化緩衝區
        bmpBuffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        cvsBuffer = new Canvas(bmpBuffer);
//為緩衝區蒙上一灰色
        cvsBuffer.drawColor(Color.parseColor("#FF808080"));
    }