自定義view (一) Android Canvas詳解

目錄

座標系以及view的位置信息

API簡介

Canvas基本操作

Canvas變化

save和restore

分層的概念layer

總結


  • 座標系以及view的位置信息

         自定義view在平時有很多應用,我們知道自定義view,其實就是通過Canvas進行繪製,但是在繪製之前,一些基本的知識要明確,安卓的座標系和我們實際在數學中用的座標系還有一些區別, 在安卓中初始化以屏幕的左上角爲原點,

           這是關於座標的解釋,還有我們再繪畫view的時候。經常用到view的長寬,left,top,right,bottom的座標。view獲取左,上,右下這些座標都是基於parent的基礎,是指相對parent的距離。不是相對於整個屏幕的。

          通過procession畫的,還處於摸索階段,如果哪位同學知道畫數學函數之類好用的軟件可以推薦一下。

  • API簡介

        作用                                                     Api                                說明
繪製顏色 drawColor, drawRGB, drawARGB 通過ARGB設置畫布顏色
繪製基本形狀 drawPoints, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc,drawPath 繪製點,線,長方形,圓形,橢圓,弧形,以及路徑
繪製圖片 drawBitmap, drawPicture 繪製位圖
繪製文本 drawText, drawPosText, drawTextOnPath 繪製各式文本,其中根據路徑繪製比較常用
畫布裁剪 clipPath, clipRect 可以設置畫布的展示區域
畫布狀態 save, restore, saveLayerXxx, restoreToCount, getSaveCount 此部分api就是保存圖層狀態、 回滾到指定狀態、 獲取保存次數,可以看成類似git保存版本狀態
畫布變換 translate, scale, rotate, skew 位移、縮放、 旋轉、錯切
Matrix getMatrix, setMatrix, concat 實際上畫布的位移,縮放等操作的都是圖像矩陣Matrix,通過C++源碼可以看出。在這裏google已經把matrix封裝好,但是如果需要特殊效果,需要自己使用matrix操作,比如窗口抖動等效果沒有封裝好的api可用
  • Canvas基本操作

         要畫東西,你需要4個基本組件:一個用來容納像素的位圖,一個用來承載畫布的畫布繪製調用(寫入位圖),一個繪製素材(例如Rect,

路徑,文本,位圖),和畫筆(描述顏色和樣式畫)。其中關於繪製基本形狀沒有什麼可說的。其中繪製弧度:

        這裏是畫了一個圓弧,其實畫圓弧就是截取一個長方形內切橢圓的一段弧度,代碼如下:

   @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawArc(canvas);
    }

    private void drawArc(Canvas canvas){

        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(5);
        mPaint.setAntiAlias(true);

        //表示空心,這樣畫出的是線,否則就是實心的圖形
        mPaint.setStyle(Paint.Style.STROKE);

        canvas.translate(500,500);
        RectF rf = new RectF(0,0,300,300);

        canvas.drawArc(rf,0,-80,false,mPaint);
        canvas.drawRect(rf,mPaint);
    }

       畫出這個長方形是爲了更好的理解弧度的由來。這裏需要注意的有幾點,1 Rect和RectF都是表示長方形,只是RectF參數是float,Rect參數是int。 2 畫長方形他的四個參數分別是,左,上,右,下,要注意底部的座標必須大於頂部,右部必須大於左部。 3 在ondraw千萬不允許用new函數,因爲ondraw會頻繁調用,如果使用new,分配大量內存,會造成內存抖動,這裏只是爲了演示代碼,所以使用了new。

       對於path其實可以看出一連串的點連接而成。 其中還可以包括貝塞爾曲線之類的。我們可以根據曲線來畫文字,比如

       代碼如下:

 private void drawTexts(Canvas canvas){
        Path paths  = new Path();
        canvas.translate(500,500);
        RectF rf = new RectF(0,-400,400,0);
        paths.addArc(rf, 60, 180);
        canvas.drawPath(paths,mPaint);
        mPaint.setTextSize(50);
        canvas.drawTextOnPath("中國人民萬歲", paths, 0, -20, mPaint);
    }

  drawTextOnPath中的3,4個參數是指文字相對path水平和豎直方向的位移。

  • Canvas變化

      我們可以大致把canvas的變化分爲四類,位移(translate),旋轉(rotate),縮放(scale),傾斜(skew).

 1  translate 位移比較簡單。 就是將當前的原點一定到指定的x,y的位置。

比如:

 

 private void drawTranslate(Canvas canvas){
        //初始的時候原點爲(0,0),畫一個圓心爲(200,200)半徑爲100的圓
        mPaint.setColor(Color.GREEN);
        canvas.drawCircle(200,200,100,mPaint);

        //將原點移動到200,200
        canvas.translate(200,200);

        //移動之後(200,0)就相當於移動前,(400,200.)
        mPaint.setColor(Color.parseColor("#ff00ff"));
        canvas.drawCircle(200,0,100,mPaint);
    }

   移動是可以疊加的。 第二次移動是以第一次移動後的原點位置爲標準。其他以此類推。

2 scale,縮放。 api提供了2中縮放的方法:

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

   
  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);
  }

 可以看出兩種縮放的方式,第一個就直接進行縮放,第二種是以px,py爲原點進行縮放。區別可看如下代碼:

  我們做如下效果的縮放:

private void drawScale(Canvas canvas){

        canvas.translate(400,800);
        mPaint.setColor(Color.RED);

        RectF rt = new RectF(0,-300,400,0);
       //簡單的 畫2條線當做X,Y軸
        mPaint.setColor(Color.BLACK);
        canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
        canvas.drawLine(0,-800,0,getHeight()-800,mPaint);

        //首先畫出黑色的基礎矩形
        mPaint.setColor(Color.BLACK);
        canvas.drawRect(rt,mPaint);

        //保存當前設置
        canvas.save();
        //將座標軸按x軸,y軸分別擴大1.3倍,然後再用藍色畫這個矩形,
        //會發現藍色矩形長寬都是原來基礎矩形的1.3被,當這裏設置(0,1)時縮小,(1,+∞)時擴大
        canvas.scale(1.3f,1.3f);
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(rt,mPaint);
        //恢復保存以前的狀態。就是canvas沒有放大前的狀態,和save成對出現
        canvas.restore();

        canvas.save();
        //以200,0爲原點進行縮放,將x,y縮小爲原來的0.5倍,然後再畫。
        //其實他是分爲三步,第一個進行唯一,translate(200,0),然後縮放scal(0.5,0.5)
        //然後在進行位移(-200,0);但是第二次位移在縮放的基礎上了。所以再位移-200,並沒有回到原來的縮放前的原點。
        //因爲是在(200,0)的基礎上唯一,x軸縮小0.5,位移-200,變爲-100.所以實際上如果沒有縮放,他是在(200,0)
        //的基礎上進行爲(-100,0)的位移。所以雖然看着(200,0)和(-200,0)正好互補,但是因爲縮放的存在不能回到原來了。
        //所以紅色的矩形如圖所示的位置
        canvas.scale(0.5f,0.5f,200,0);
        mPaint.setColor(Color.RED);
        canvas.drawRect(rt,mPaint);
        //再次回到save之前的狀態,即canvas沒有做任何變化的狀態
        canvas.restore();


        canvas.save();
        //(2,-0.5)x軸變爲原來的2倍,y軸首先變爲原來的0.5被,然後需要y軸的反轉。
        canvas.scale(2,-0.5f);
        mPaint.setColor(Color.parseColor("#ff00ff"));
        canvas.drawRect(rt,mPaint);
        //畫一個紫色的變化後的矩形。如圖所示
        canvas.restore();

    }

           我們看最初展示的android源碼可以看出。scale(float sx, float sy, float px, float py)是先進行位移再旋轉,然後再次位移。translate(px, py)移動的物理距離分別是px和py,經過scale(sx, sy)縮放後再通過translate(-px, -py)位移,移動的物理距離就是-px*sx和-py*sy。

 所以我們可以看出縮放情況如下:

倍數(n) 說明
(-∞, 0) 首先對x,y軸進行n倍伸縮,然後對應的座標軸
(0,+∞) 直接將對應的x,y軸進行n倍伸縮
n==1 根據代碼可以看出不做任何變化

3 rotate(旋轉),旋轉同樣提供了2中方法

 public void rotate(float degrees) {
        if (degrees == 0.0f) return;
        nRotate(mNativeCanvasWrapper, degrees);
    }

   
 public final void rotate(float degrees, float px, float py) {
        if (degrees == 0.0f) return;
        translate(px, py);
        rotate(degrees);
        translate(-px, -py);
    }

         第一種是直接以原點爲中心進行旋轉degrees度, 第二種是以px,py爲原點進行旋轉。它也是分爲3步,1是進行位移(px,py),然後旋轉,之後再進行位移(-px,-py),同理,旋轉之後,表面看兩次位移正好互補,但是不能回到原來的點了。首先來看以原點爲標準旋轉

座標軸變了,所有的座標點也就變了,計算的時候。 你可以仍然以水平爲X軸思考,無論是畫線,還是圖形。在原來X軸上的操作,旋轉之後會和X軸一樣旋轉。比如下圖:

  private void drawRotate(Canvas canvas){

        canvas.translate(400,800);
        mPaint.setColor(Color.RED);
        RectF rt = new RectF(0,-300,400,0);
        //簡單的 畫2條線當做X,Y軸
        mPaint.setColor(Color.BLACK);
        canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
        canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
        canvas.drawCircle(500,-100,100,mPaint);


        mPaint.setStrokeWidth(30);
        mPaint.setColor(Color.GREEN);
        canvas.drawPoint(0,0,mPaint);

        mPaint.setStrokeWidth(5);
        canvas.save();
        //旋轉60度
        canvas.rotate(60);
        mPaint.setColor(Color.RED);
        //畫x,y軸還有圓形的代碼和旋轉一直一模一樣
        canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
        canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
        canvas.drawCircle(500,-100,100,mPaint);
        canvas.restore();
    }

    通過看代碼我們會發現。 繪畫黑色的座標軸與圓形, 和旋轉之後畫紅色的座標與圓形的代碼一模一樣。 所以無論怎樣旋轉,任何圖形與其座標軸的相對位置不會發生改變。這就是網上很多圓形進度條的原理。如下效果:

這種氣勢就是畫一條水平的線,然後不停的旋轉。

4 傾斜skew

 public void skew(float sx, float sy) {
        if (sx == 0.0f && sy == 0.0f) return;
        nSkew(mNativeCanvasWrapper, sx, sy);
    }

參數sx,sy分別是x,y軸上傾斜角度的tan值。比如skew(1,1)則分別是切斜45度

這是x軸切斜45度的效果,代碼如下:

private void drawSkew(Canvas canvas){
        canvas.translate(400,800);
        RectF rt = new RectF(0,-300,400,0);
        //簡單的 畫2條線當做X,Y軸
        mPaint.setColor(Color.BLACK);
        canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
        canvas.drawLine(0,-800,0,getHeight()-800,mPaint);

        canvas.drawRect(0,0,200,200,mPaint);

        canvas.skew(1,0);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0,0,200,200,mPaint);
    }

         其實可以把canvas的操作看成是畫很多層,移動之後就屬於在另一層重新畫,無論怎樣移動,都可以想象成再水平面操作。因爲所有的繪畫元素與原點的相對位置不變。

        需要注意一點,繪畫的移動,縮放,旋轉,傾斜,都是可以連續多次進行的。多次操作效果疊加。

  • save和restore

        實際開發中繪畫的時候經常會有移動,縮放等操作。但是變換完成之後,還想恢復到原來的狀態。再進行繪製。如果沒有save,和restore,比如我們translate(100,20),操作完成之後,還要進行translate(-100,-20)進行復原,這還是一次操作。如果多次操作就會很麻煩。canvas提供save和restore解決了這個問題。save就相當於git中保存狀態,restore就相當於回滾操作。回到save的狀態。

 private void drawSaveRestore(Canvas canvas){

        //畫一個以300,300位圓心,100位半徑的圓,這個時候座標原點爲左上角(0,0)
        mPaint.setColor(Color.RED);
        canvas.drawCircle(300,300,100,mPaint);

        mPaint.setStrokeWidth(15);
        canvas.drawLine(0,0,getRight()-100,0,mPaint);
        canvas.drawLine(0,0,0,getBottom()-100,mPaint);

        mPaint.setStrokeWidth(8);
        //保存這個時候的狀態。即原點爲(0,0)的狀態
        canvas.save();
        //進行移動,將原點移動到(500,500)的位置
        canvas.translate(500,500);
        mPaint.setColor(Color.BLACK);
        canvas.drawLine(0,0,500,0,mPaint);
        canvas.drawLine(0,0,0,500,mPaint);

        //畫一個以(500,500)爲原點長寬都是200的正方形
        canvas.drawRect(0,0,200,200,mPaint);

        //恢復到save的狀態。這個時候,座標原點又是(0,0)了,
        canvas.restore();
        mPaint.setColor(Color.parseColor("#ff00ff"));
        canvas.drawRect(0,0,200,200,mPaint);
    }

          save和restore是成對出現的。是一對一的。canvas還提供了restoreToCount(int saveCount)方法,去恢復到指定的保存狀態。我們可以查看源碼。一切說的很清楚。

 /**
     * Efficient way to pop any calls to save() that happened after the save
     * count reached saveCount. It is an error for saveCount to be less than 1.
     *
     * Example:
     *    int count = canvas.save();
     *    ... // more calls potentially to save()
     *    canvas.restoreToCount(count);
     *    // now the canvas is back in the same state it was before the initial
     *    // call to save().
     *
     * @param saveCount The save level to restore to.
     */
    public void restoreToCount(int saveCount) {
        if (saveCount < 1) {
            if (!sCompatibilityRestore || !isHardwareAccelerated()) {
                // do nothing and throw without restoring
                throw new IllegalArgumentException(
                        "Underflow in restoreToCount - more restores than saves");
            }
            // compat behavior - restore as far as possible
            saveCount = 1;
        }
        nRestoreToCount(mNativeCanvasWrapper, saveCount);
    }

其實我們可以把save操作當做入棧操作,然後restore可以簡單看做是出棧操作。但是save都有編號,也可以指定編號進行恢復。

  • 分層的概念layer

     如果做過地圖的項目大家都清楚。在標誌某個地點或者其他類似效果。會通過層的概念在指定地點添加覆蓋物。canvas也有分層的概念,通過savelayer的方式保存, 

代碼如下:

 private void drawLayer(Canvas canvas) {
        canvas.translate(100, 100);
        mPaint.setColor(Color.RED);
        canvas.drawCircle(75, 75, 75, mPaint);
        canvas.saveLayerAlpha(0, 0, 200, 200, 0x88);
        mPaint.setColor(Color.BLUE);
        canvas.drawCircle(125, 125, 75, mPaint);
        canvas.restore();
    }

在savexxx的函數中,存在flag的參數,在api28中已經全部改爲:ALL_SAVE_FLAGS,其他已經無效:

/**
     * Restore everything when restore() is called (standard save flags).
     * <p class="note"><strong>Note:</strong> for performance reasons, it is
     * strongly recommended to pass this - the complete set of flags - to any
     * call to <code>saveLayer()</code> and <code>saveLayerAlpha()</code>
     * variants.
     *
     * <p class="note"><strong>Note:</strong> all methods that accept this flag
     * have flagless versions that are equivalent to passing this flag.
     */
    public static final int ALL_SAVE_FLAG = 0x1F;

    private static void checkValidSaveFlags(int saveFlags) {
        if (sCompatiblityVersion >= Build.VERSION_CODES.P
                && saveFlags != ALL_SAVE_FLAG) {
            throw new IllegalArgumentException(
                    "Invalid Layer Save Flag - only ALL_SAVE_FLAGS is allowed");
        }
    }

通過這裏的源碼可以看出。

  • 總結

        其實view的繪製原理是一個很複雜的過程。相信的大家都可以大致說出,measure,layout,draw這三個流程,此篇只是簡單介紹canvas的應用,甚至不用去管他的流程,先明白怎樣畫的即可。所有複雜的view繪製,其實都可以拆分成簡單的操作,繪製可以從簡單做起。

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