自定義View起步:Canvas之繪製基本形狀

一、Canvas簡介
      Canvas在Android中被稱之爲畫布,可以幫助我們繪製各種各樣的圖形。是Android平臺繪製2D圖形的基礎。但是想要繪製出來一個完美的控件也是困難的,需要對各種基礎的方法非常的熟練加以運用。
二、Canvas的基本API
全部的API請參考官網的文檔:點擊打開鏈接
三、Canvas的一些基本操作
 3.1繪製顏色
 

3.2初始化畫筆

3.3繪製一個點或者一組點

3.4繪製直線
 直線的繪製是由起點和終點兩點的連線決定的,同樣我們也可以繪製一組點

3.5繪製矩形

確定一個矩形最少需要四個數據,就是對角線的兩個點的座標值,這裏一般採用左上角和右下角的兩個點的座標。

關於繪製矩形,Canvas提供了三種重載方法,第一種就是提供四個數值(矩形左上角和右下角兩個點的座標)來確定一個矩形進行繪製。 其餘兩種是先將矩形封裝爲Rect或RectF(實際上仍然是用兩個座標點來確定的矩形),然後傳遞給Canvas繪製,如下:

爲什麼會有Rect和RectF兩種?兩者有什麼區別嗎?

答案當然是存在區別的,兩者最大的區別就是精度不同,Rect是int(整形)的,而RectF是float(單精度浮點型)的。除了精度不同,兩種提供的方法也稍微存在差別,在這裏我們暫時無需關注,想了解更多參見官方文檔 RectRectF

3.6繪製圓角矩形

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

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

稍微分析一下,既然是圓角矩形,他的角肯定是圓弧(圓形的一部分),我們一般用什麼確定一個圓形呢?

答案是圓心 和 半徑,其中圓心用於確定位置,而半徑用於確定大小

由於矩形位置已經確定,所以其邊角位置也是確定的,那麼確定位置的參數就可以省略,只需要用半徑就能描述一個圓弧了。

但是,半徑只需要一個參數,但這裏怎麼會有兩個呢?

好吧,讓你發現了,這裏圓角矩形的角實際上不是一個正圓的圓弧,而是橢圓的圓弧,這裏的兩個參數實際上是橢圓的兩個半徑,他們看起來個如下圖:

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

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

實際上在rx爲寬度的一半,ry爲高度的一半時,剛好是一個橢圓,通過上面我們分析的原理推算一下就能得到,而當rx大於寬度的一半,ry大於高度的一半時,實際上是無法計算出圓弧的,所以drawRoundRect對大於該數值的參數進行了限制(修正),凡是大於一半的參數均按照一半來處理。

3.7繪製橢圓

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

繪製橢圓實際上就是繪製一個矩形的內切圖形,如果你傳遞進來的是一個長寬相等的矩形(即正方形),那麼繪製出來的實際上就是一個圓。原理如下,就不多說了:

3.8繪製圓形

繪製圓形有四個參數,前兩個是圓心座標,第三個是半徑,最後一個是畫筆。

3.9繪製圓弧

繪製圓弧就比較神奇一點了,爲了理解這個比較神奇的東西,我們先看一下它需要的幾個參數:關鍵是後邊的StartAngle是開始掃描的角度,Sweep angle是從開始位置計算起,掃描多少度。userCenter是否啓用中心位置,如果不啓用,扇形的面積是掃描之後起點和結束點的連線,如果是true將會包括中心位置,是一個真正的扇形。

我們來看一下實例代碼:


在看一下正圓的情況下是什麼樣子的:

四、簡單介紹一下畫筆

如果我想繪製一個圓,只要邊不要裏面的顏色怎麼辦?很簡單,繪製的基本形狀Canvas確定,但繪製出來的顏色,具體效果則由Paint確定。設置畫筆的填充模式就可以實現我們想要的效果。如果你注意到了的話,在一開始我們設置畫筆樣式的時候是這樣的:

實例代碼如下


五、下面我們來做一個百分比的餅狀圖來實際操作一下

簡要介紹畫布的操作:

製作一個餅狀圖如下:

簡單分析一下我們所需要的數據和思路

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

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

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

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

封裝數據:

package net.fitrun.mysvg;

/**
 * Created by 晁東洋 on 2017/3/28.
 * 繪製餅狀圖的數據
 */

public class PieData {
    // 用戶關心數據
    private String name;        // 名字
    private float value;        // 數值
    private float percentage;   // 百分比

    // 非用戶關心數據
    private int color = 0;      // 顏色
    private float angle = 0;    // 角度

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getValue() {
        return value;
    }

    public void setValue(float value) {
        this.value = value;
    }

    public float getPercentage() {
        return percentage;
    }

    public void setPercentage(float percentage) {
        this.percentage = percentage;
    }

    public int getColor() {
        return color;
    }

    public void setColor(int color) {
        this.color = color;
    }

    public float getAngle() {
        return angle;
    }

    public void setAngle(float angle) {
        this.angle = angle;
    }
}

自定義View:

package net.fitrun.mysvg;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;

/**
 * Created by 晁東洋 on 2017/3/24.
 */

public class CustomView extends View {
    private Paint mPaint;

    // 顏色表(注意: 此處定義顏色使用的是ARGB,帶Alpha通道的)
    private int[] mColors = {0xFFCCFF00, 0xFF6495ED, 0xFFE32636, 0xFF800000, 0xFF808000, 0xFFFF8C69, 0xFF808080,
            0xFFE6B800, 0xFF7CFC00};
    // 餅狀圖初始繪製角度
    private float mStartAngle = 0;

    //數據
    private ArrayList<PieData> mDate;
    //寬高
    private int mWidth,mHeight;



    public CustomView(Context context) {
        super(context);
        //初始化畫筆
        intiPaint();
    }

    private void intiPaint() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL); //設置畫筆模式爲填充
        //mPaint.setStrokeWidth(10f); //設置畫筆的寬度爲10px
        mPaint.setAntiAlias(true);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //取出寬度的確切數值
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        //取出寬度的測量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        //取出高度的確切數值
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //取出高度的測量模式
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    }

    @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 onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r, b);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (null == mDate)
            return;;
        float currentStartAngle = mStartAngle; //當前的起始角度,爲0度
        canvas.translate(mWidth/2,mHeight/2); //將畫布的中心圓點移動到中心位置
        float r = (float)(Math.min(mWidth,mHeight)/2*0.8); //繪圖的半徑,取寬和高較小的值
        RectF mRectf = new RectF(-r,-r,r,r); //繪圖的區域,其實是一個正方形

        //開始根據數據繪製扇形
        for (int i =0; i<mDate.size();i++){
            PieData pie = mDate.get(i);
            mPaint.setColor(pie.getColor());
            canvas.drawArc(mRectf,currentStartAngle,pie.getAngle(),true,mPaint);
            currentStartAngle += pie.getAngle();
        }


    }

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

    //設置數據
    public void setData(ArrayList<PieData> mData){
        this.mDate = mData;
        initData(mData);
        invalidate();
    }
    //計算數據
    private void initData(ArrayList<PieData> mData) {
        if (null == mData || mData.size() ==0){
            return;
        }else {
            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.e("總的角度",pie.getAngle()+"");

            }
        }
    }

    //監聽鍵盤的點擊事件
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        return super.onKeyDown(keyCode, event);
    }

    //監聽鍵盤的彈起事件
    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        return super.onKeyUp(keyCode, event);
    }


    // 監聽軌跡球的移動事件
    @Override
    public boolean onTrackballEvent(MotionEvent event) {
        return super.onTrackballEvent(event);
    }
    //觸摸的事件監聽
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

    //視圖焦點變化的監聽
    @Override
    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    }
    //包含這一視圖的窗口焦點的監聽
    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
    }

    //視圖被附加add時被調用
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
    }

    //視圖和窗口分離事調用
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
    }
    //調用視圖的窗口可見性變化時調用
    @Override
    protected void onWindowVisibilityChanged(int visibility) {
        super.onWindowVisibilityChanged(visibility);
    }
}


Activity中引用我們的控件

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final PorterDuffXfermodeView taiJi = new PorterDuffXfermodeView(this);
        ArrayList<PieData> mlist = new ArrayList<>();
        for (int i =0; i<5;i++){
            float mF = 20f;
            mF+= mF;
            PieData pieData = new PieData("生產",mF);
            mlist.add(pieData);
        }

        CustomView customView = new CustomView(this);
        customView.setData(mlist);
        customView.setStartAngle(180);

        setContentView(customView);


最後是我們的效果圖

最後如果大家喜歡我的學習筆記,可以掃描左邊的二維碼關注我的微信公衆號,有更多的乾貨,更及時的分享給大家。

參考GcsSloopgithub詳細內容




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