自定義View進階篇《二》——Canvas之繪制圖形

一、Canvas簡介

Canvas我們可以稱之為畫布,能夠在上面繪制各種東西,是安卓平臺2D圖形繪制的基礎,非常強大。
一般來說,比較基礎的東西有兩大特點:

  • 1.可操作性強:由于這些是構成上層的基礎,所以可操作性必然十分強大。
  • 2.比較難用:各種方法太過基礎,想要完美的將這些操作組合起來有一定難度。

不過不必擔心,本系列文章不僅會介紹到Canvas的操作方法,還會簡單介紹一些設計思路和技巧。

二、Canvas的常用操作速查表

PS: Canvas常用方法在上面表格中已經全部列出了,當然還存在一些其他的方法未列出,具體可以參考官方文檔 Canvas

三、Canvas詳解

本篇內容主要講解如何利用Canvas繪制基本圖形。
繪制顏色:
繪制顏色是填充整個畫布,常用于繪制底色。

canvas.drawColor(Color.BLUE); //繪制藍色

創建畫筆:
要想繪制內容,首先需要先創建一個畫筆,如下:

// 1.創建一個畫筆
private Paint mPaint = new Paint();

// 2.初始化畫筆
private void initPaint() {
    mPaint.setColor(Color.BLACK);       //設置畫筆顏色
    mPaint.setStyle(Paint.Style.FILL);  //設置畫筆模式為填充
    mPaint.setStrokeWidth(10f);         //設置畫筆寬度為10px
}

// 3.在構造函數中初始化
public SloopView(Context context, AttributeSet attrs) {
   super(context, attrs);
   initPaint();
}

在創建完畫筆之后,就可以在Canvas中繪制各種內容了。

繪制點:

可以繪制一個點,也可以繪制一組點,如下:

canvas.drawPoint(200, 200, mPaint);     //在坐標(200,200)位置繪制一個點
canvas.drawPoints(new float[]{          //繪制一組點,坐標位置由float數組指定
      500,500,
      500,600,
      500,700
},mPaint);

關于坐標原點默認在左上角,水平向右為x軸增大方向,豎直向下為y軸增大方向。

繪制直線:

繪制直線需要兩個點,初始點和結束點,同樣繪制直線也可以繪制一條或者繪制一組:

canvas.drawLine(300,300,500,600,mPaint);    // 在坐標(300,300)(500,600)之間繪制一條直線
canvas.drawLines(new float[]{               // 繪制一組線 每四數字(兩個點的坐標)確定一條線
    100,200,200,200,
    100,300,200,300
},mPaint);
繪制矩形:

確定確定一個矩形最少需要四個數據,就是對角線的兩個點的坐標值,這里一般采用左上角和右下角的兩個點的坐標。
關于繪制矩形,Canvas提供了三種重載方法,第一種就是提供四個數值(矩形左上角和右下角兩個點的坐標)來確定一個矩形進行繪制。 其余兩種是先將矩形封裝為Rect或RectF(實際上仍然是用兩個坐標點來確定的矩形),然后傳遞給Canvas繪制,如下:

// 第一種
canvas.drawRect(100,100,800,400,mPaint);

// 第二種
Rect rect = new Rect(100,100,800,400);
canvas.drawRect(rect,mPaint);

// 第三種
RectF rectF = new RectF(100,100,800,400);
canvas.drawRect(rectF,mPaint);

看到這里,相信很多觀眾會產生一個疑問,為什么會有Rect和RectF兩種?兩者有什么區別嗎?
答案當然是存在區別的,兩者最大的區別就是精度不同,Rect是int(整形)的,而RectF是float(單精度浮點型)的。除了精度不同,兩種提供的方法也稍微存在差別,在這里我們暫時無需關注,想了解更多參見官方文檔 Rect 和 RectF。

繪制圓角矩形:

繪制圓角矩形也提供了兩種重載方式,如下:

// 第一種
RectF rectF = new RectF(100,100,800,400);
canvas.drawRoundRect(rectF,30,30,mPaint);

// 第二種
canvas.drawRoundRect(100,100,800,400,30,30,mPaint);

上面兩種方法繪制效果也是一樣的,但鑒于第二種方法在API21的時候才添加上,所以我們一般使用的都是第一種。

下面簡單解析一下圓角矩形的幾個必要的參數的意思。

很明顯可以看出,第二種方法前四個參數和第一種方法的RectF作用是一樣的,都是為了確定一個矩形,最后一個參數Paint是畫筆,無需多說,與矩形相比,圓角矩形多出來了兩個參數rx 和 ry,這兩個參數是干什么的呢?

稍微分析一下,既然是圓角矩形,他的角肯定是圓弧(圓形的一部分),我們一般用什么確定一個圓形呢?
答案是圓心 和 半徑,其中圓心用于確定位置,而半徑用于確定大小。

由于矩形位置已經確定,所以其邊角位置也是確定的,那么確定位置的參數就可以省略,只需要用半徑就能描述一個圓弧了。
但是,半徑只需要一個參數,但這里怎么會有兩個呢?
好吧,讓你發現了,這里圓角矩形的角實際上不是一個正圓的圓弧,而是橢圓的圓弧,這里的兩個參數實際上是橢圓的兩個半徑,他們看起來個如下圖:


紅線標注的 rx 與 ry 就是兩個半徑,也就是相比繪制矩形多出來的那兩個參數。

我們了解到原理后,就可以為所欲為了,通過計算可知我們上次繪制的矩形寬度為700,高度為300,當你讓 rx大于350(寬度的一半), ry大于150(高度的一半) 時奇跡就出現了, 你會發現圓角矩形變成了一個橢圓, 他們畫出來是這樣的 ( 為了方便確認我更改了畫筆顏色, 同時繪制出了矩形和圓角矩形 ):

// 矩形
RectF rectF = new RectF(100,100,800,400);  

// 繪制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF,mPaint);

// 繪制圓角矩形
mPaint.setColor(Color.BLUE);
canvas.drawRoundRect(rectF,700,400,mPaint);

其中灰色部分是我們所選定的矩形,而里面的圓角矩形則變成了一個橢圓,實際上在rx為寬度的一半,ry為高度的一半時,剛好是一個橢圓,通過上面我們分析的原理推算一下就能得到,而當rx大于寬度的一半,ry大于高度的一半時,實際上是無法計算出圓弧的,所以drawRoundRect對大于該數值的參數進行了限制(修正),凡是大于一半的參數均按照一半來處理。

繪制橢圓:

相對于繪制圓角矩形,繪制橢圓就簡單的多了,因為他只需要一個矩形矩形作為參數:

// 第一種
RectF rectF = new RectF(100,100,800,400);
canvas.drawOval(rectF,mPaint);

// 第二種
canvas.drawOval(100,100,800,400,mPaint);

同樣,以上兩種方法效果完全一樣,但一般使用第一種。
繪制橢圓實際上就是繪制一個矩形的內切圖形,原理如下,就不多說了:


PS: 如果你傳遞進來的是一個長寬相等的矩形(即正方形),那么繪制出來的實際上就是一個圓。

繪制圓:

繪制圓形也比較簡單, 如下:

canvas.drawCircle(500,500,400,mPaint);  // 繪制一個圓心坐標在(500,500),半徑為400 的圓。

繪制圓形有四個參數,前兩個是圓心坐標,第三個是半徑,最后一個是畫筆。

繪制圓弧:

繪制圓弧就比較神奇一點了,為了理解這個比較神奇的東西,我們先看一下它需要的幾個參數:

// 第一種
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint){}
    
// 第二種
public void drawArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle, boolean useCenter, @NonNull Paint paint) {}

從上面可以看出,相比于繪制橢圓,繪制圓弧還多了三個參數:

startAngle  // 開始角度
sweepAngle  // 掃過角度
useCenter   // 是否使用中心

通過字面意思我們基本能猜測出來前兩個參數(startAngle, sweepAngel)的作用,就是確定角度的起始位置和掃過角度, 不過第三個參數是干嘛的?試一下就知道了,上代碼:

RectF rectF = new RectF(100,100,800,400);
// 繪制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF,mPaint);

// 繪制圓弧
mPaint.setColor(Color.BLUE);
canvas.drawArc(rectF,0,90,false,mPaint);

//-------------------------------------

RectF rectF2 = new RectF(100,600,800,900);
// 繪制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF2,mPaint);

// 繪制圓弧
mPaint.setColor(Color.BLUE);
canvas.drawArc(rectF2,0,90,true,mPaint);

上述代碼實際上是繪制了一個起始角度為0度,掃過90度的圓弧,兩者的區別就是是否使用了中心點,結果如下:


可以發現使用了中心點之后繪制出來類似于一個扇形,而不使用中心點則是圓弧起始點和結束點之間的連線加上圓弧圍成的圖形。這樣中心點這個參數的作用就很明顯了,不必多說想必大家試一下就明白了。 另外可以關于角度可以參考一下這篇文章: 角度與弧度

相比于使用橢圓,我們還是使用正圓比較多的,使用正圓展示一下效果:

RectF rectF = new RectF(100,100,600,600);
// 繪制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF,mPaint);

// 繪制圓弧
mPaint.setColor(Color.BLUE);
canvas.drawArc(rectF,0,90,false,mPaint);

//-------------------------------------

RectF rectF2 = new RectF(100,700,600,1200);
// 繪制背景矩形
mPaint.setColor(Color.GRAY);
canvas.drawRect(rectF2,mPaint);

// 繪制圓弧
mPaint.setColor(Color.BLUE);
canvas.drawArc(rectF2,0,90,true,mPaint);
簡要介紹Paint

看了上面這么多,相信有一部分人會產生一個疑問,如果我想繪制一個圓,只要邊不要里面的顏色怎么辦?
很簡單,繪制的基本形狀由Canvas確定,但繪制出來的顏色,具體效果則由Paint確定。
如果你注意到了的話,在一開始我們設置畫筆樣式的時候是這樣的:

mPaint.setStyle(Paint.Style.FILL);  //設置畫筆模式為填充

為了展示方便,容易看出效果,之前使用的模式一直為填充模式,實際上畫筆有三種模式,如下:

STROKE                //描邊
FILL                  //填充
FILL_AND_STROKE       //描邊加填充

為了區分三者效果我們做如下實驗:

Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStrokeWidth(40);     //為了實驗效果明顯,特地設置描邊寬度非常大

// 描邊
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(200,200,100,paint);

// 填充
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(200,500,100,paint);

// 描邊加填充
paint.setStyle(Paint.Style.FILL_AND_STROKE);
canvas.drawCircle(200, 800, 100, paint);

一圖勝千言,通過以上實驗我們可以比較明顯的看出三種模式的區別,如果只需要邊緣不需要填充內容的話只需要設置模式為描邊(STROKE)即可。

其實關于Paint的內容也是有不少的,這些只是冰山一角,在后續內容中會詳細的講解Paint。

小示例

簡要介紹畫布的操作:

畫布操作詳細內容會在下一篇文章中講解, 不是本文重點,但以下示例中可能會用到,所以此處簡要介紹一下。

制作一個餅狀圖

在展示百分比數據的時候經常會用到餅狀圖,像這樣:


簡單分析

其實根據我們上面的知識已經能自己制作一個餅狀圖了。不過制作東西最重要的不是制作結果,而是制作思路。 相信我貼上代碼大家一看就立刻明白了,非常簡單的東西。不過嘛,咱們還是想了解一下制作思路:

先分析餅狀圖的構成,非常明顯,餅狀圖就是一個又一個的扇形構成的,每個扇形都有不同的顏色,對應的有名字,數據和百分比。

經以上信息可以得出餅狀圖的最基本數據應包括:名字 數據值 百分比 對應的角度 顏色。

  • 用戶關心的數據 : 名字 數據值 百分比
  • 需要程序計算的數據: 百分比 對應的角度
  • 其中顏色這一項可以用戶指定也可以用程序指定(我們這里采用程序指定)。

封裝數據:

public class PieData {
    // 用戶關心數據
    private String name;        // 名字
    private float value;        // 數值
    private float percentage;   // 百分比
    
    // 非用戶關心數據
    private int color = 0;      // 顏色
    private float angle = 0;    // 角度

    public PieData(@NonNull String name, @NonNull float value) {
        this.name = name;
        this.value = value;
    }
}

PS: 以上省略了get set方法

自定義View:

先按照自定義View流程梳理一遍(確定各個步驟應該做的事情):

代碼如下:

public class PieView extends View {
    // 顏色表 (注意: 此處定義顏色使用的是ARGB,帶Alpha通道的)
    private int[] mColors = {0xFFCCFF00, 0xFF6495ED, 0xFFE32636, 0xFF800000, 0xFF808000, 0xFFFF8C69, 0xFF808080,
            0xFFE6B800, 0xFF7CFC00};
    // 餅狀圖初始繪制角度
    private float mStartAngle = 0;
    // 數據
    private ArrayList<PieData> mData;
    // 寬高
    private int mWidth, mHeight;
    // 畫筆
    private Paint mPaint = new Paint();

    public PieView(Context context) {
        this(context, null);
    }

    public PieView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (null == mData)
            return;
        float currentStartAngle = mStartAngle;                    // 當前起始角度
        canvas.translate(mWidth / 2, mHeight / 2);                // 將畫布坐標原點移動到中心位置
        float r = (float) (Math.min(mWidth, mHeight) / 2 * 0.8);  // 餅狀圖半徑
        RectF rect = new RectF(-r, -r, r, r);                     // 餅狀圖繪制區域

        for (int i = 0; i < mData.size(); i++) {
            PieData pie = mData.get(i);
            mPaint.setColor(pie.getColor());
            canvas.drawArc(rect, currentStartAngle, pie.getAngle(), true, mPaint);
            currentStartAngle += pie.getAngle();
        }

    }

    // 設置起始角度
    public void setStartAngle(int mStartAngle) {
        this.mStartAngle = mStartAngle;
        invalidate();   // 刷新
    }

    // 設置數據
    public void setData(ArrayList<PieData> mData) {
        this.mData = mData;
        initData(mData);
        invalidate();   // 刷新
    }

    // 初始化數據
    private void initData(ArrayList<PieData> mData) {
        if (null == mData || mData.size() == 0)   // 數據有問題 直接返回
            return;

        float sumValue = 0;
        for (int i = 0; i < mData.size(); i++) {
            PieData pie = mData.get(i);

            sumValue += pie.getValue();       //計算數值和

            int j = i % mColors.length;       //設置顏色
            pie.setColor(mColors[j]);
        }

        float sumAngle = 0;
        for (int i = 0; i < mData.size(); i++) {
            PieData pie = mData.get(i);

            float percentage = pie.getValue() / sumValue;   // 百分比
            float angle = percentage * 360;                 // 對應的角度

            pie.setPercentage(percentage);                  // 記錄百分比
            pie.setAngle(angle);                            // 記錄角度大小
            sumAngle += angle;

            Log.i("angle", "" + pie.getAngle());
        }
    }
}

PS: 在更改了數據需要重繪界面時要調用invalidate()這個函數重新繪制。
文章來自 GcsSloop

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容