前言
我们先来聊聊,在我们生活中如何绘制一张如下的图。
我们需要两样东西来绘制:
- 一张纸(Android 中的 canvas):用来承载我们绘制的内容。
- 一支笔(Android 中的 paint):负责绘制内容的轨迹。
有了这两样,我们就能在现实的场景中开始绘制了。
1、绘图坐标系
在 Android 的体系中,我们所谓的 “笔Paint” 和 “纸Canvas” 都是由App持有的,所以我们在绘制时就出现一个问题:我们怎么“告诉”App,确定我们想要绘制图形的落笔点?当然需要一个坐标系来进行交流。
这个 坐标系 便是我们经常所说的 绘图坐标系。初始状态下,Canvas的左上角为原点,此时我们想画图中的红点,就非常的容易,只需要“告诉” App 在坐标(200,500)处画一个红点,这就达到了画图的效果了。所以我们可以明确的一点是 我们所有的画图坐标都是根据原点进行确定。
所以我们可以移动原点,达到整体坐标点的移动,例如还是画刚才的红点,我们可以先将原点水平移动100,垂直移动400。然后在进行绘制,这时红点的坐标就变为(100,100),具体如下图所示。
经过上面的简单讲述,我们可以知道,绘图过程中,我们的绘图坐标永远是跟随当前的原点,而画布的原点可以进行移动。
2、视图坐标系
理论上 Canvas 这张纸是没有边界的,但是我们的手机屏幕是有界的。我们可以理解为我们透过一个方形的洞(手机屏幕)看一张巨画(Canvas)。
又存在一个问题了,因为刚才的移动,我们是移动的原点,也就是说我们的画布是静止不动的,只是落笔点一直在变动,这就导致我们绘制的图对于用户来说是看不全的,所以我们需要进行移动 方形的洞 来查看这幅画。
可以通过移动 Screen框来查看这幅画,而这里又出现了一个坐标系,这一坐标系则为 视图坐标系,通过 scrollerTo
和 scrollerBy
进行移动该Screen框,正数则往正半轴,负数则往负半轴。
3、小结
自定义控件中存在两个坐标系需要明确,用一句话总结如下:
- 绘图坐标系:决定我们的绘制的坐标
- 视图坐标系:决定我们所看到的画布范围
=========================================================================
介绍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);
效果:
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比较难以理解和使用,故封装了一些常用的方法。 |
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轴增大方向。
绘制直线:
绘制直线需要两个点,初始点和结束点,同样绘制直线也可以绘制一条或者绘制一组:
// 在坐标(300,300)(500,600)之间绘制一条直线
canvas.drawLine(300,300,500,600,mPaint);
// 绘制一组线 每四数字(两个点的坐标)确定一条线
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(单精度浮点型)的。除了精度不同,两种提供的方法也稍微存在差别。
绘制圆角矩形:
绘制圆角矩形也提供了两种重载方式,如下:
// 第一种
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 rectF = new RectF(100,100,800,400);
canvas.drawOval(rectF,mPaint);
// 第二种
canvas.drawOval(100,100,800,400,mPaint);
同样,以上两种方法效果完全一样,但一般使用第一种。
绘制圆:
绘制圆形也比较简单, 如下:
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)的作用,就是确定角度的起始位置和扫过角度。
useCenter 使用了中心点之后绘制出来类似于一个扇形,而不使用中心点则是圆弧起始点和结束点之间的连线加上圆弧围成的图形。
绘制不规则多边形
使用Path
类来绘制不规则多边形。
例子:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
public class CustomPolygonView extends View {
private Path polygonPath = new Path();
private Paint polygonPaint = new Paint();
public CustomPolygonView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
polygonPaint.setAntiAlias(true);
polygonPaint.setColor(0xFF0000FF); // 蓝色填充
polygonPaint.setStyle(Paint.Style.FILL); // 设置填充样式
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 创建不规则多边形路径
polygonPath.reset();
polygonPath.moveTo(100, 50); // 第一个顶点
polygonPath.lineTo(200, 50); // 第二个顶点
polygonPath.lineTo(150, 100); // 第三个顶点
polygonPath.lineTo(100, 150); // 第四个顶点
polygonPath.close(); // 首尾连接关闭路径
// 绘制多边形
canvas.drawPath(polygonPath, polygonPaint);
}
}
我们创建了一个自定义View
,在其onDraw
方法中使用Path
绘制了一个有四个顶点的不规则多边形。您可以通过调整polygonPath
的顶点来创建不同形状的多边形。
绘制一串点路径虚线
要使用Canvas绘制一串点路径上的虚线,可以使用Paint
的setPathEffect
方法,并提供一个DashPathEffect
对象。
例子:
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.view.View;
public class DashedLineView extends View {
private Paint paint;
private Path path;
public DashedLineView(Context context) {
super(context);
init();
}
private void init() {
paint = new Paint();
paint.setColor(Color.BLACK);
paint.setStrokeWidth(5f);
paint.setStyle(Paint.Style.STROKE);
// 设置虚线效果,参数分别为:“点的间隔”,“点的长度”,“总的长度”
paint.setPathEffect(new DashPathEffect(new float[]{10f, 5f}, 0f));
path = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 定义路径,这里以一个简单的直线为例
path.reset();
path.moveTo(10, 10);
path.lineTo(300, 10);
// 绘制虚线
canvas.drawPath(path, paint);
}
}
DashPathEffect
被设置为每10个点一个间隔,每个点5个单位长度,总长度为50个单位。这将导致虚线由5个黑点和5个空白点组成的序列。你可以根据需要调整DashPathEffect
构造函数的参数来改变虚线的样式。
画布的操作:
相关操作 | 简要介绍 |
---|---|
save | 保存当前画布状态 |
restore | 回滚到上一次保存的状态 |
translate | 相对于当前位置位移 |
rotate | 旋转 |
tips:
自定义View流程梳理一遍(确定各个步骤应该做的事情):
步骤 | 关键字 | 作用 |
---|---|---|
1 | 构造函数 | 初始化(初始化画笔Paint) |
2 | onMeasure | 测量View的大小(暂时不用关心) |
3 | onSizeChanged | 确定View大小(记录当前View的宽高) |
4 | onLayout | 确定子View布局(无子View,不关心) |
5 | onDraw | 实际绘制内容(绘制饼状图) |
6 | 提供接口 | 提供接口(提供设置数据的接口) |