安卓自定义View进阶-Canvas之绘制图形

在本篇文章中,咱们先了解Canvas的基本用法,最后用一个小示例来结束本次教程。css

一.Canvas简介

Canvas咱们能够称之为画布,可以在上面绘制各类东西,是安卓平台2D图形绘制的基础,很是强大。html

通常来讲,比较基础的东西有两大特色:java

  • 1.可操做性强:因为这些是构成上层的基础,因此可操做性必然十分强大。
  • 2.比较难用:各类方法太过基础,想要完美的将这些操做组合起来有必定难度。

不过没必要担忧,本系列文章不只会介绍到Canvas的操做方法,还会简单介绍一些设计思路和技巧。android

二.Canvas的经常使用操做速查表

操做类型 相关API 备注
绘制颜色 drawColor, drawRGB, drawARGB 使用单一颜色填充整个画布
绘制基本形状 drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc 依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧
绘制图片 drawBitmap, drawPicture 绘制位图和图片
绘制文本 drawText, drawPosText, drawTextOnPath 依次为 绘制文字、绘制文字时指定每一个文字位置、根据路径绘制文字
绘制路径 drawPath 绘制路径,绘制贝塞尔曲线时也须要用到该函数
顶点操做 drawVertices, drawBitmapMesh 经过对顶点操做可使图像形变,drawVertices直接对画布做用、 drawBitmapMesh只对绘制的Bitmap做用
画布剪裁 clipPath, clipRect 设置画布的显示区域
画布快照 save, restore, saveLayerXxx, restoreToCount, getSaveCount 依次为 保存当前状态、 回滚到上一次保存的状态、 保存图层状态、 回滚到指定状态、 获取保存次数
画布变换 translate, scale, rotate, skew 依次为 位移、缩放、 旋转、错切
Matrix(矩阵) getMatrix, setMatrix, concat 实际上画布的位移,缩放等操做的都是图像矩阵Matrix, 只不过Matrix比较难以理解和使用,故封装了一些经常使用的方法。

PS: Canvas经常使用方法在上面表格中已经所有列出了,固然还存在一些其余的方法未列出,具体能够参考官方文档 Canvasgit


三.Canvas详解

本篇内容主要讲解如何利用Canvas绘制基本图形。github

绘制颜色:

绘制颜色是填充整个画布,经常使用于绘制底色。canvas

canvas.drawColor(Color.BLUE); //绘制蓝色

关于颜色的更多资料请参考基础篇_颜色数组


建立画笔:

要想绘制内容,首先须要先建立一个画笔,以下:ide

// 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(单精度浮点型)的。除了精度不一样,两种提供的方法也稍微存在差异,在这里咱们暂时无需关注,想了解更多参见官方文档 RectRectF


绘制圆角矩形:

绘制圆角矩形也提供了两种重载方式,以下:

// 第一种
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。


小示例

简要介绍画布的操做:

画布操做详细内容会在下一篇文章中讲解, 不是本文重点,但如下示例中可能会用到,因此此处简要介绍一下。

相关操做 简要介绍
save 保存当前画布状态
restore 回滚到上一次保存的状态
translate 相对于当前位置位移
rotate 旋转

制做一个饼状图

在展现百分比数据的时候常常会用到饼状图,像这样:

简单分析

其实根据咱们上面的知识已经能本身制做一个饼状图了。不过制做东西最重要的不是制做结果,而是制做思路。 相信我贴上代码你们一看就马上明白了,很是简单的东西。不过嘛,我们仍是想了解一下制做思路:

先分析饼状图的构成,很是明显,饼状图就是一个又一个的扇形构成的,每一个扇形都有不一样的颜色,对应的有名字,数据和百分比。

经以上信息能够得出饼状图的最基本数据应包括:名字 数据值 百分比 对应的角度 颜色

用户关心的数据 : 名字 数据值 百分比

须要程序计算的数据: 百分比 对应的角度

其中颜色这一项能够用户指定也能够用程序指定(咱们这里采用程序指定)。

封装数据:

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流程梳理一遍(肯定各个步骤应该作的事情):

步骤 关键字 做用
1 构造函数 初始化(初始化画笔Paint)
2 onMeasure 测量View的大小(暂时不用关心)
3 onSizeChanged 肯定View大小(记录当前View的宽高)
4 onLayout 肯定子View布局(无子View,不关心)
5 onDraw 实际绘制内容(绘制饼状图)
6 提供接口 提供接口(提供设置数据的接口)

代码以下:

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()这个函数从新绘制。

效果图

PS: 这个饼状图并无添加百分比等数据,仅做为示例使用。

PieView源码下载

总结:

其实自定义View只要按照流程一步步的走,也是比较容易的。不过里面也有很多坑,这些坑仍是本身踩过印象比较深,建议你们不要直接copy源码,本身手打体验一下。

About

本系列相关文章

做者微博: GcsSloop

参考资料:

View
Canvas
Android Canvas绘图详解

相关文章
相关标签/搜索