AndroidUi之Canvas,Path

        畫布,在我們android的View或者自定義View中佔有舉足輕重的地位,在android中它的意義也是字面的意思,我們可以通過畫筆繪製幾何圖形,文本,路徑或者位圖。我們可以將Canvas的API主要分爲三類,一類是繪製相關的,例如drawText,drawLine...,一類是變換相關的,canvas.translate、canvas.rotate....,還有一類是狀態保存和恢複相關的如canvas.save、canvas.restore....下面開始分別講解。

繪製相關

    和畫布繪製相關的也就是我們在Canvas類中定義的類似drawXXXX以draw開頭的方法:

上圖截取了一部分Canvas中的drawXXX的方法。

繪製文本drawText

     我們平時使用TextView的時候都是調用setText方法將我們需要顯示的文本內容顯示在TextView上面,其實最終也是調用Canvas的drawText方法。先看下面的方法,特地將text的初始座標放在(0,15)這個位置上,看一下效果:

private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        mPaint.setTextSize(30);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.RED);
        canvas.drawText("測試DrawText的功能的Demo",0,15,mPaint);
    }

效果就是綠色的文本只顯示了一部分,這是爲什麼呢。其實在android中當我們使用canvas.drawText的時候,設置的座標的位置其實是設置的文字的左下角那邊的位置。爲什麼這麼說呢?大家小學的時候,都用過那種作業本,漢語拼音,四線三格,我們都是以第三條線位標準寫拼音的,Android的標準也是差不多的。看下圖:

與字體相關的在上圖標註的幾個基準線:

baseline:字符基準線(其實就是我們上面drawText標註的(0,15)這個15的值)

ascent:字符最高點到baseLine的推薦距離。

top:字符最高點到baseline之間的距離。

descent:字符最低點到baseline之間的推薦距離

bottom:字符最低點到baseline之間的最大距離

leading:行間距,即前一行的descent到當前行的ascent之間的距離。

與繪製文本相關的還有FontMetrics,可以獲取上面我們標記解釋的幾個值:

  public static class FontMetrics {
        /**
         * The maximum distance above the baseline for the tallest glyph in
         * the font at a given text size.
         */
        public float   top;
        /**
         * The recommended distance above the baseline for singled spaced text.
         */
        public float   ascent;
        /**
         * The recommended distance below the baseline for singled spaced text.
         */
        public float   descent;
        /**
         * The maximum distance below the baseline for the lowest glyph in
         * the font at a given text size.
         */
        public float   bottom;
        /**
         * The recommended additional space to add between lines of text.
         */
        public float   leading;
    }

我們可以通過 下面方法來獲得,並讀取上面的屬性:

Paint.FontMetrics metrics = mPaint.getFontMetrics();

文字的測量

paint中提供給了測量兩種測量文字的方法,但是有一些細小的差別:分別位measureText()和getTextBounds()方法,他們都屬於Paint的方法,兩個函數的區別在於:

        如果你用代碼分別使用getTextBounds() 和 measureText() 來測量文字的寬度,你會發現 measureText() 測出來的寬度總是要比getTextBounds()d的 大一點點。這是因爲這兩個方法其實測量的是兩個不一樣的東西。

  • getTextBounds(): 它測量的是文字的顯示範圍(關鍵詞:顯示)。就是能夠包裹住這段文字的嘴角的矩形,就是這段文字的 bounds。

  • measureText() : 它測量的是文字繪製時所佔用的寬度。繪製文字的時候,往往需要佔用比他的實際顯示寬度更多一點的寬度,以此來讓文字和文字之間保留一些間距,不會顯得過於擁擠,也就是設置我們不同行文字之間的間距,導致了 measureText() 比getTextBounds()測量出的寬度要大一些

drawRect,drawCicle,drawBitmap,drawArc...等等都是比較簡單的,直接使用API就ok了。

Path

    path即路徑,可以用來繪製直線,曲線,以及這些直線或曲線構成的集合圖形,還可用於根據路徑繪製文字。常用的API有移動,連線,閉合,添加圖形等....

 private void init() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(6);
        mPaint.setColor(Color.RED);
        mPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.moveTo(100,70);    //路徑起始點移動到(100,70)的位置
        mPath.lineTo(140,800);   //從(100,70)向(140,800)畫一條線
        mPath.lineTo(250,600);   //從(140,800)向(250,600)畫一條線
        mPath.close();   //將線路閉合
        canvas.drawPath(mPath,mPaint);
    }

上圖的效果是這樣的,其實我們可以將mPath.lineTo(140,800)修改爲mPath.rLineTo(40,730),rLineTo的意思其中r就是“相對位置”的意思,就是相對於path的上一個所畫路徑的結束點的相對位置也即是140-100 = 40,800-70=730;上圖我們看到紅色的圖像是閉合的,有人可以以爲使是調用mPath.close的結果,其實上例中如果價格mPath.close去掉,也是這樣的結果,主要問題出在Paint的style上面,上例中將style設置爲FILL,如果修改爲STROKE的話

        

上面2圖體現的結果不一樣,圖1未封閉,圖2成爲一個閉合的圖像:

 private void init() {
        mPaint = new Paint();
        //修改畫筆的樣式 爲Style
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(6);
        mPaint.setColor(Color.RED);
        mPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.moveTo(100,70);
        mPath.lineTo(140,800);
        mPath.lineTo(250,600);
        //圖一需要去掉下面一句代碼
         mPath.close();
        canvas.drawPath(mPath,mPaint);
    }

上圖中設置畫筆都爲STROKE,左圖1註釋掉mPath.close(), 右圖2加上mPath.close()方法,結果就不一樣了。path.close() 方法的意思就是,將路徑閉合,如果路徑本來就是閉合的就不用管,如果不是閉合的就將首尾兩點連線閉合。

Path.addArc

  添加一個弧形。最終調用的都是含有6個參數的addArc方法,left,top表示的是弧形所在矩形的左上方的位置座標,right,bottom表示弧形所在矩形的右下方的位置座標,startAngle就是弧形開始的位置,sweepAngel表示弧形掃過的角度:

 public void addArc(RectF oval, float startAngle, float sweepAngle) {
        addArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle);
    }

public void addArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle) {
     
    }
 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.addArc(200,200,400,400,-225,225);
        mPath.close();
        canvas.drawPath(mPath,mPaint);
    }

Path.addRect

      addRect添加一個矩形,addRect方法中的參數除了矩形的left,top,right,bottom的座標外,還有一個參數表示繪製的順序:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.addRect(500,500,900,900,Path.Direction.CW);
        canvas.drawPath(mPath,mPaint);
    }

Path.Direction.CW   順時針方向繪製

Path.direction.CCW  逆時針方向繪製

其他的mPath.addCircle,mPath.addOval 都需要這個參數

Path追加圖形

例如下面一段代碼:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.addArc(200,200,400,400,-225,225);
        mPath.arcTo(400,200,600,400,-180,225,false);
        canvas.drawPath(mPath,mPaint);
    }

 

上例中首先調mPath.addArc畫一個弧形,然後調用mPath.arcTo在原來的圖像上面追加一個弧形,類似前面的mPath.lineTo方法,在arcTo方法中最後一個參數爲false,

boolean forceMoveTo

也就是forceMoveTo = false; 這個參數的意思是繪製的時候,是否移動起點的位置。當forceMoveTo = true,表示繪製的時候將起點移動到所要繪製的圖像的起點位置,當forceMoveTo = false的時候,意思就是不移動起點,但是必須繪製一條從前一個圖像的終點 指向 即將繪製的圖像的起點位置的直線:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.moveTo(0,0);
        mPath.lineTo(100,100);
        mPath.arcTo(400,200,600,400,0,270,true);
        canvas.drawPath(mPath,mPaint);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.lineTo(100,100);
        mPath.arcTo(400,200,600,400,0,270,false);
        canvas.drawPath(mPath,mPaint);
    }

      

左圖爲 forceMoveTo = true 的效果,右圖爲 forceMove = false的效果。

Path.addPath

 還可以在一條路徑上面,通過addPath將另外一條路徑添加到當前的Path中

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //添加一個路徑
        mPath.moveTo(100, 70);
        mPath.lineTo(140, 180);
        mPath.lineTo(250, 330);
        mPath.lineTo(400, 630);
        mPath.lineTo(100, 830);

        Path newPath = new Path();
        newPath.moveTo(100, 1000);
        newPath.lineTo(600, 1300);
        newPath.lineTo(400, 1700);
        mPath.addPath(newPath);
        canvas.drawPath(mPath,mPaint);
    }

Path添加多階貝塞爾曲線

      貝塞爾曲線是用一系列點來控制曲線狀態的,將這些點分爲兩類,一類是數據點,一類是控制點。

       在很多繪圖或者動畫效果中需要用到貝塞爾曲線,在API中爲我們提供了一階,二階,三階貝塞爾曲線的API。其實一節貝塞爾曲線就是一條直線用lineTo就可以繪製,二階貝塞爾曲線用quadTo或者rQuadTo(相對位置)方法繪製,三階貝塞爾曲線由cubicTo 或者rCubicTo方法進行繪製,例如下面是繪製二階貝塞爾曲線的一般使用方法

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //畫二階貝塞爾曲線
        mPath.moveTo(300, 500);
        mPath.quadTo(500, 100, 800, 500);
        //參數表示相對位置,等同於上面一行代碼
      //  mPath.rQuadTo(200, -400, 500, 0);
        canvas.drawPath(mPath,mPaint);
    }

  • 一階曲線原理

一階曲線是沒有控制點的,僅有兩個數據點(A 和 B),最終效果一個線段。

一階公式如下:

  

  • 二階曲線原理

  二階曲線由兩個數據點(A 和 C),一個控制點(B)來描述曲線狀態,大致如下:

          

連接DE,取點F,使得: ,這樣獲取到的點F就是貝塞爾曲線上的一個點,動態圖如下:

二階公式如下:

二階貝塞爾曲線公式推倒方法如下:

三階貝塞爾曲線去下:

公式如下:

高階貝塞爾曲線的通用公式如下,可以認爲是一個遞歸的操作過程

變換相關

Canvas中有許多與變換相關的API,可以幫助我們事先許多意想不到的效果。

canvas.translate

canvas.translate使畫布平移,先看下面的代碼,畫兩個矩形,一個紅色矩形座標爲(50,50),(400,400) 改變畫筆顏色之後,畫綠色矩形,座標還爲(50,50),(400,400),看效果:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(50,50,400,400,mPaint);
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(50,50,400,400,mPaint);
    }

看上面的圖像,綠色矩形在紅色矩形的下方,再看下方代碼和效果基本上可以看出平移的一些套路了

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(50,50,400,400,mPaint);
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(50,50,400,400,mPaint);
        canvas.translate(-50,-50);
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(50,50,400,400,mPaint);
    }

畫紅色矩形的座標爲(50,50),(400,400),但是canvas.translate(100,100)之後畫綠色矩形,我們看出同樣是(50,50),(400,400)的座標,但是綠色矩形和紅色矩形沒有重合,歸結原因是畫布平移之後,Canvas座標原點平移到(100,100),然後再畫綠色的矩形,實際上畫的綠色的矩形"相對於"原來紅色矩形的座標系應該爲(100+50,100+50),(100+400,100+400)。藍色矩形同理。可以得出一個結論,畫筆平移方法translate(x,y)方法傳遞x,y的值爲 正數 畫布座標原點向下平移,傳遞的值爲負數,畫布座標原點想上平移

canvas.scale

canvas.scale可以對畫布進行縮放,有2個重載的方法:

    public void scale(float sx, float sy) {
        if (sx == 1.0f && sy == 1.0f) return;
        nScale(mNativeCanvasWrapper, sx, sy);
    }

    /**
     * Preconcat the current matrix with the specified scale.
     *
     * @param sx The amount to scale in X
     * @param sy The amount to scale in Y
     * @param px The x-coord for the pivot point (unchanged by the scale)
     * @param py The y-coord for the pivot point (unchanged by the scale)
     */
    public final void scale(float sx, float sy, float px, float py) {
        if (sx == 1.0f && sy == 1.0f) return;
        translate(px, py);
        scale(sx, sy);
        translate(-px, -py);
    }

先看第一個函數,傳入float sx,float sy是縮放比率:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,700,700,mPaint);
        canvas.scale(0.5f,0.5f);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(200,200,700,700,mPaint);
    }

 

我們看到調用scale(0.5f,0.5f)之後,同樣座標的紅色矩形和綠色矩形,爲啥綠色矩形會出現那樣的位置呢。我個人對這個的理解是:其實我們畫同樣大小的紅色和綠色大小的矩形,但是顯示出的大小結果不一樣了,就像我們查看百度地圖或者高德地圖一樣,當我們縮小地圖的時候,我們看到的範圍其實更大了,其實是我們顯示地圖的比率變了而已。類比上面的效果,當我們畫(200,200),(700,700)的紅色矩形,我們的座標系,可以理解爲一個像素點爲1cm,那麼紅色矩形就是寬高爲500cm的矩形,但是當我們將畫布縮小之後,我們一個單位像素點就變成了1/0.5 = 2cm,所以我們的綠色圖形的寬高就爲原來的一半,起始座標相對於原來的畫布變爲(100,100)。所以按照這個理解,我們可以推測出canvas.scale(2,2)的效果爲綠的圖形放大了。起始座標爲(400,400).

第二個方法   public final void scale(float sx, float sy, float px, float py) 傳遞的是四個參數:

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);       
        canvas.drawRect(200,200,700,700,mPaint);
        canvas.scale(0.5f,0.5f,200,200);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(200,200,700,700,mPaint);
    }

看到最後的效果是這樣的。起始上面的代碼的四個參數的scale方法等同於下面的代碼

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,700,700,mPaint);
        canvas.translate(200,200);
        canvas.scale(0.5f,0.5f);
        canvas.translate(200,200);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(200,200,700,700,mPaint);
    }

那麼 public final void scale(float sx, float sy, float px, float py) 的意思就是畫布先平移translate(px,py)然後,scale(0.5f,0.5f),最後  translate(-px,-py).

canvas.rotate

canvas.rotate(float degress),傳遞的參數爲度數:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //旋轉操作
        canvas.drawRect(0,0,300,300,mPaint);
        canvas.rotate(45);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,300,300,mPaint);
    }

看到上面的綠色矩形旋轉了45°,有一部分旋轉到屏幕之外了。。當canvas.rotate(degree)傳入的 度數爲正數的時候是繞順時針旋轉,當傳入的度數爲負數的時候,繞逆時針旋轉,默認是繞畫布的座標原點旋轉,上例就是繞(0,0)順時針旋轉45°。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //旋轉操作
        canvas.drawRect(200,200,700,700,mPaint);
        canvas.rotate(-45,450,450);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(200,200,700,700,mPaint);
    }

 

上例中canvas.rotate(-45,450,450) 後面兩個參數是旋轉的中心450,450,畫布繞(450,450)逆時針旋轉45度。

canvas.skew(float sx,float sy)

畫布的錯切(傾斜),sx,sy值,分別表示將畫布在x方向和y方向上傾斜角度的tan 值。

當sx=1時,即將畫布在x方向上旋轉45度,其實就是x軸保持方向不變,y軸逆時針旋轉45度。

當sy=1時,即將畫布在y方向上旋轉45度,其實就是y軸保持方向不變,x軸順時針旋轉45度。

當sx、sy都改變時,兩者都進行相應的移動。

例如下面的代碼:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
       //傾斜 錯切操作
        canvas.drawRect(0,0,200,200,mPaint);
        canvas.skew(1,0);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,200,200,mPaint);
    }

紅色矩形爲錯切之前的矩形,將畫布向x方向傾斜45°之後,然後再繪製綠色矩形,可能不是很好理解,看下面一幅圖:

紅色線爲原先的x軸和y軸,當調用canvas.skew(1,0)之後,向x方向錯切45°,其實就是將y軸以x爲軸線旋轉45°,然後再畫矩形.如果上面圖中不畫紅色矩形直接錯切之後的效果:一切只是視角不一樣而已

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
       //傾斜 錯切操作
        canvas.skew(1,0);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,200,200,mPaint);
    }

canvas.clipXXX

    canvas.clipXXX系列方法可以對畫布進行裁剪,使裁剪之後額圖形只能在裁剪的區域內繪製,否則 “無效”。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.GRAY);    //繪製灰色背景
        canvas.drawRect(0,0,500,500,mPaint);  //畫一個矩形區域爲(0,0) (500,500)
        canvas.clipRect(new Rect(0,0,200,200));   //裁剪(0,0) (200,200)的區域
        mPaint.setColor(Color.GREEN);  //畫筆設置爲綠色
        canvas.drawRect(0,0,200,200,mPaint);  //繪製(0,0) (200,200)的綠色矩形
        canvas.drawRect(250,250,400,400,mPaint);  //繪製(250,250) (400,400)的綠色矩形
    }

效果圖爲:

看到(250,250),(400,400)的綠色矩形並沒有顯示出來,是因爲我們裁剪的大小區域爲(0,0),(200,200) 而我們的(250,250),(400,400)的矩形區域不在上面的矩形區域中,所以並沒有繪製出來。

canvas還有其他的裁剪方法,例如canvas.clipPath,和clipRect類似,只不過clipPath能夠裁剪的圖形多一些。clipOutRect反向裁剪。也就是說裁剪之後繪製的圖像,只能顯示在裁剪區域之外,在裁剪區域之內的不能顯示出來。

canvas.setMatrix

setMatrix其實可以做上面的平移 旋轉,縮放 等等操作。使用Matrix進行變換的一般操作步驟爲:

  1. 創建 Matrix 對象;
  2. 調用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法來設置幾何變換;
  3. 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 來把幾何變換應用到 Canvas
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //matrix變換
        canvas.drawRect(0,0,400,400,mPaint);
        Matrix matrix = new Matrix();
        matrix.setTranslate(200,200);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,400,400,mPaint);
    }
  1. Canvas.setMatrix(matrix):用 Matrix 直接替換 Canvas 當前的變換矩陣,即拋棄 Canvas 當前的變換,改用 Matrix 的變換。
  2. Canvas.concat(matrix):用 Canvas 當前的變換矩陣和 Matrix 相乘,即基於 Canvas 當前的變換,疊加上 Matrix 中的變換。
  3. 使用matrix時候 有的時候需要調用matrix.reset()清除之前的變換。

狀態保存和恢復

   Canvas調用translate,scale,rotate,skew,matrix等的變換之後,後續的操作都是基於變換之後的Canvas的,都會受到影響,對後續的操作很不方便,Canvas提供了save,saveLayer,saveLayerAlpha,restore,restorToCount等方法來保存和恢復狀態。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,500,500,mPaint);
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,300,300,mPaint);
        canvas.drawRect(200,200,500,500,mPaint);
    }

兩個矩形的大小雖然一致,但是明顯畫布平移之後,座標零點變換位置了,變爲原來畫布的(100,100)位置,如果我們想回到原來的座標零點,需要在調用translate(-100,-100)才能將綠色矩形調整至原來的座標零點。並且後面的(200,200),(500,500)的綠色矩形也是在畫布平移之後的畫布的基礎上繪製的。Android中Canvas提供一些保存狀態的API,讓後續的繪製可以不受這些變換的影響。

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,500,500,mPaint);
        canvas.save();
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,300,300,mPaint);
        mPaint.setColor(Color.BLUE);
        canvas.restore();
        canvas.drawRect(0,0,300,300,mPaint);
    }

上述代碼中調用了canvas.store()  和 canvas.restore()方法,store的意思就是保存當前的畫布狀態,restore就是恢復之前的畫布狀態,這裏也就是恢復到tanslate(100,100)之前的畫布狀態,然後再繪製藍色的矩形,就是在座標零點了。

Android中一般情況下store和restore是成對出現的,意思爲保存當前狀態,退回到上一步的畫布狀態。例如下面多次成對出現這個兩個方法(restore方法總是退回到最近的store方法調用之前的畫布的狀態)。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,500,500,mPaint);
        canvas.save();
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,300,300,mPaint);
        canvas.save();
        canvas.translate(100,100);
        canvas.drawLine(0,0,500,500,mPaint);

        canvas.restore();
        canvas.restore();
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(0,0,300,300,mPaint);
    }

先畫紅色矩形,調用store,然後平移畫布繪製綠色矩形,然後再調用store然後再平移畫布,繪製綠色直線,最後兩次調用restore,回到畫布最開始的狀態,在座標零點繪製藍色矩形。有點類似一個 “入棧出棧” 的過程 。還有一種restoreToCount的方法可以一步到位。

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(200,200,500,500,mPaint);
        int saveState = canvas.save();
        canvas.translate(100,100);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,300,300,mPaint);
        canvas.translate(100,100);
        canvas.drawLine(0,0,500,500,mPaint);
        canvas.restoreToCount(saveState);
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(0,0,300,300,mPaint);
    }

調用 int saveState = canvas.save() ; 保存當前的狀態,然後調用canvas.restoreToCount(saveState) 直接回退到那個狀態。出棧操作就是直接回退到指定的棧的那一層。

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //創建離屏繪製圖層
        int layerId = canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint);

        //  在圖層之間進行繪製      

        //最後將創建的圖層繪製到canvas中
        canvas.restoreToCount(layerId);
    }

下面看一個引導頁的效果實現:

 

多看幾遍,我們可以將效果圖分解爲幾個階段:

1.6個小球繞中心 旋轉的階段

2.6個小球 以球所在的中心 先擴大 後收縮的階段。

3.類似水波紋擴散的階段。

自定義View的成員變量

    private Paint mPaint;  //畫小圓的畫筆
    private Paint mHolePaint;   //最後白色的波浪的畫筆
    private int colors [] = null;
    private int mCenterX,mCenterY;
    private float mCircleRadius = 18;  //旋轉的小球的半徑
    private float mCurrentAngle = 0;  //小球旋轉的時候的初始角度,做動畫使用
    private float mDiatance;


    private SplashState mState;  //當前的狀態
    private ValueAnimator mValueAnimator;

    private float mRotateRadius = 100;  //旋轉大圓的半徑
    //當前大圓的半徑
    private float mCurrentRotateRadius = mRotateRadius;
    //擴散圓的半徑
    private float mCurrentHoleRadius = 0F;

定義上面分析的集中狀態:

   //抽象內部類  描述上面3種狀態
   private abstract class SplashState{
        abstract void drawState(Canvas canvas);
    }

    //小球的旋轉狀態
    private class RotateState extends  SplashState{

        .....
    }

   //小球的擴散聚合狀態
    private class MerginState extends  SplashState{

        .....
    }

    //擴散 水波紋狀態
    private class SpreadState extends  SplashState{

        .....
    }

自定義View中最重要的onDraw方法,但是看起來卻那麼簡單:第一個狀態也就是旋轉狀態

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(mState == null){
            mState = new RotateState();
        }
        mState.drawState(canvas);
    }

看旋轉狀態的RotateState的實現

private class RotateState extends  SplashState{
        public RotateState(){

            mValueAnimator = ValueAnimator.ofFloat(0f, (float) (Math.PI*2));
        //    mValueAnimator.setRepeatCount(1);
            mValueAnimator.setInterpolator(new LinearInterpolator());
            mValueAnimator.setDuration(1500);
            mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mCurrentAngle = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
            mValueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mState = new MerginState();
                }
            });
            mValueAnimator.start();
        }
        @Override
        void drawState(Canvas canvas) {
            drawBackground(canvas);
            drawCicle(canvas);
        }
    }

定義屬性動畫,使小球旋轉起來 看drawCicle方法:

 private void drawCicle(Canvas canvas){
        int length = colors.length;
        float angle = (float)((2*Math.PI)/length);
        for(int i=0;i<length;i++){
            float sweepAngle = i*angle+mCurrentAngle;  //mCurrentAngle爲屬性動畫中獲取的值
            //逆時針旋轉
          //  float cx = (float)(Math.sin(sweepAngle)*mCurrentRotateRadius+mCenterX);
           // float cy = (float)(Math.cos(sweepAngle)*mCurrentRotateRadius+mCenterY);
            //順時針旋轉
            float cx = (float)(Math.cos(sweepAngle)*mCurrentRotateRadius+mCenterX);
            float cy = (float)(Math.sin(sweepAngle)*mCurrentRotateRadius+mCenterY);
            mPaint.setColor(colors[i]);
            canvas.drawCircle(cx,cy,mCircleRadius,mPaint);
        }
    }

drawBackground是在3個狀態中都要繪製的:

 private void drawBackground(Canvas canvas){
        if(mCurrentHoleRadius>0){  //擴散半徑>0的時候 就是最好一個狀態
            float strokeWidth = mDiatance - mCurrentHoleRadius;  //線寬在減小
            float radius = strokeWidth / 2 + mCurrentHoleRadius; //半徑一直在增大
            mHolePaint.setStrokeWidth(strokeWidth);
            canvas.drawCircle(mCenterX,mCenterY, radius, mHolePaint);
        }else{
            canvas.drawColor(Color.WHITE);  //否則 只繪製白色的背景
        } 
    }

上面的float radius  =  strokeWidth / 2 + mCurrentHoleRadius; 需要加上線寬的一半 ,否則白色圓擴散的半徑就接不上了,這裏爲啥,可以看之前的AndroidUi之Paint 在最開始講解了線寬 對繪製圓的影響。

注意:在MerginState的時候,執行屬性動畫時候,調用了

//反轉執行動畫...
mValueAnimator.reverse();
     public MerginState(){
            mValueAnimator = ValueAnimator.ofFloat(0,mRotateRadius);
            mValueAnimator.setDuration(1000);
            mValueAnimator.setInterpolator(new OvershootInterpolator(5));
            mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mCurrentRotateRadius = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
            mValueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mState = new SpreadState();
                }
            });
            //反轉執行動畫...
            mValueAnimator.reverse();
        }

本來原來的屬性動畫,是旋轉半徑從0-mRotateRadius  並且攔截器爲 OvershootInterpolator。現在調用reverse效果之後,從最大半徑mRotateRadius的位置到0。

Demo中還有2個自定義View的效果,一個是爆炸效果,一個是類似QQ消息氣泡效果,可以自行查看。

      

 

Demo下載地址

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章