Canvas之绘制基本形状
作者微博: @GcsSloop
【本系列相关文章】
在上一篇自己定义View分类与流程中我们了解自己定义View相关的基本知识,只是,这些东西依然还是理论,并不能拿来(zhuang)用(B), 这一次我们就了解一些能(zhaung)用(B)的东西。
在本篇文章中,我们先了解Canvas的基本用法,最后用一个小演示样例来结束本次教程。
一.Canvas简单介绍
Canvas我们能够称之为画布,能够在上面绘制各种东西,是安卓平台2D图形绘制的基础,非常强大。
**一般来说,比較基础的东西有两大特点:
1.可操作性强:因为这些是构成上层的基础。所以可操作性必定十分强大。
2.比較难用:各种方法太过基础,想要完美的将这些操作组合起来有一定难度。**
只是不必操心。本系列文章不仅会介绍到Canvas的操作方法,还会简单介绍一些设计思路和技巧。
二.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经常用法在上面表格中已经所有列出了,当然还存在一些其它的方法未列出,具体能够參考官方文档 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,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);
简要介绍Paint
看了上面这么多。相信有一部分人会产生一个疑问,假设我想绘制一个圆,仅仅要边不要里面的颜色怎么办?
非常easy,绘制的基本形状由Canvas确定。但绘制出来的颜色,具体效果则由Paint确定。
假设你注意到了的话,在一開始我们设置画笔样式的时候是这种:
mPaint.setStyle(Paint.Style.FILL); //设置画笔模式为填充
为了展示方便,easy看出效果,之前使用的模式一直为填充模式,实际上画笔有三种模式,例如以下:
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 | 旋转 |
制作一个饼状图
在展示百分比数据的时候经常会用到饼状图。像这样:
简单分析
事实上依据我们上面的知识已经能自己制作一个饼状图了。只是制作东西最重要的不是制作结果,而是制作思路。
相信我贴上代码大家一看就立马明确了,非常easy的东西。只是嘛,咱们还是想了解一下制作思路:
先分析饼状图的构成,非常明显,饼状图就是一个又一个的扇形构成的,每一个扇形都有不同的颜色。相应的有名字。数据和百分比。
经以上信息能够得出饼状图的最基本数据应包含:名字 数据值 百分比 相应的角度 颜色。
用户关心的数据 : 名字 数据值 百分比
须要程序计算的数据: 百分比 相应的角度
当中颜色这一项能够用户指定也能够用程序指定(我们这里採用程序指定)。
封装数据:
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 {
// 颜色表
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;
initDate(mData);
invalidate(); // 刷新
}
// 初始化数据
private void initDate(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: 这个饼状图并没有加入百分比等数据。仅作为演示样例使用。
总结:
事实上自己定义View仅仅要依照流程一步步的走。也是比較easy的。只是里面也有不少坑,这些坑还是自己踩过印象比較深,建议大家不要直接copy源代码,自己手打体验一下。