Android 中 Canvas的功能
一. 如何在一屏幕上绘图
三个概念:
- 需要有画布Canvas
- 需要有笔Paint
- 需要有坐标系,画笔默认是在左上角(0,0)位置绘制的。我们可以通过移动画布坐标原点的形式,实现在不同位置绘制。
1. 移动坐标原点
简单绘制一个图
我们将画笔Canvas.translate(100,400)移动之后再进行绘制,这时画布的原点已经变化了。
2. 视图坐标系
理论上 Canvas 这张纸是没有边界的,但是我们的手机屏幕是有界的。我们可以理解为我们透过一个方形的洞(手机屏幕)看一张巨画(Canvas)。
而这里我们就又存在一个问题了,因为刚才的移动,我们是移动的原点,也就是说我们的画布是静止不动的,只是落笔点一直在变动,这就导致我们绘制的图对于用户来说是看不全的,所以我们需要进行移动 方形的洞 来查看这幅画。
举个例子,我们要查看最开始所说的画,可以通过移动 Screen框来查看这幅画,而这里又出现了一个坐标系,这一坐标系则为 视图坐标系,通过 scrollerTo 和 scrollerBy 进行移动该Screen框,正数则往正半轴,负数则往负半轴。
3. 小结
自定义控件中存在两个坐标系需要明确,用一句话总结如下:
- 绘图坐标系:决定我们的绘制的坐标
- 视图坐标系:决定我们所看到的画布范围
二、Canvas的剪刀手API
Canvas 中以 clip开头 的公有方法,用于裁剪画布的内容。裁剪之后画布之外将无法绘制, 我们抽取比较好玩的参数类型为Path的方法来分享,其余的都可以一一映射进来。
1、clipPath裁剪任意形状画布
public boolean clipPath(@NonNull Path path)
描述: 只留下 path内 的画布区域,而处于path范围之外的则不显示。
举个例子: 我们先准备好一个心形的路径Path,然后调用 clipPath 从画布中将此路径内的区域 “裁剪” 下来,最后为了我们观察,使用drawColor “染”上酒红色。
....省略,具体请移步demo(我会贴在末尾的)
canvas.clipPath(mPath);
canvas.drawColor(mBgColor);
【这个有一个网上的】 自带美感的贝塞尔曲线原理与实战
画心型路径基本是使用贝塞尔曲线的( 路径关系可以参考这个画心),路径关系如文章所示本章仅讲解和绘制相关的知识。
效果如图所示:
代码如下:
public class HartView extends View {
public HartView(Context context) {
super(context);
init();
}
public HartView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public HartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL);
}
private Paint mPaint = new Paint();
private Path mPath = new Path();
@Override
protected void onDraw(Canvas canvas) {
int mWidth = getWidth();
int mHeight = getHeight();
for (int i = 0 + 180; i < 361 + 180; i++) {
double sit = i * 2 * Math.PI / 360d;
double x = 20 * 16 * Math.pow(Math.sin(sit), 3);
double y = 20 * (13 * Math.cos(sit) - 5 * Math.cos(2 * sit) - 2 * Math.cos(3 * sit) - Math.cos(4 * sit));
if (i == 180) {
mPath.moveTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));scrcpy
}
mPath.lineTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
if (canvas != null)
canvas.drawPath(mPath, mPaint);
}
}
}
public class OutsideHartView extends View {
public OutsideHartView(Context context) {
super(context);
init();
}
public OutsideHartView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public OutsideHartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL);
}
private Paint mPaint = new Paint();
private Path mPath = new Path();
@Override
protected void onDraw(Canvas canvas) {
int mWidth = getWidth();
int mHeight = getHeight();
for (int i = 0 + 180; i < 361 + 180; i++) {
double sit = i * 2 * Math.PI / 360d;
double x = 20 * 16 * Math.pow(Math.sin(sit), 3);
double y = 20 * (13 * Math.cos(sit) - 5 * Math.cos(2 * sit) - 2 * Math.cos(3 * sit) - Math.cos(4 * sit));
if (i == 180) {
mPath.moveTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
mPath.lineTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(mPath);
}
canvas.drawColor(getContext().getResources().getColor(R.color.color_cd6155));
}
2. clipRect裁剪矩形
public boolean clipRect(float left, float top, float right, float bottom)
public boolean clipRect(int left, int top, int right, int bottom)
public boolean clipRect(@NonNull Rect rect)
public boolean clipRect(@NonNull RectF rect)
3. clipOutPath裁剪保留path之外的区域
public boolean clipOutPath(@NonNull Path path)
描述: 只留下 path外 的画布区域,而处于path范围之内的则不显示。(与clipPath的作用范围正好相反)
值得注意的是,该方法只能在API26版本以上调用。 低版本我们使用下一小节介绍的方法
举个例子:
我们先准备好一个心形的路径Path,然后调用 clipOutPath 从画布中将此路径之外的区域 “裁剪” 下来,最后为了我们观察,使用 drawColor “染”上酒红色。
....省略,具体请移步github
canvas.clipOutPath(mPath);
canvas.drawColor(mBgColor);
实现效果如下:
代码如下:
public class OutsideHartView extends View {
public OutsideHartView(Context context) {
super(context);
init();
}
public OutsideHartView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public OutsideHartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL);
}
private Paint mPaint = new Paint();
private Path mPath = new Path();
@Override
protected void onDraw(Canvas canvas) {
int mWidth = getWidth();
int mHeight = getHeight();
for (int i = 0 + 180; i < 361 + 180; i++) {
double sit = i * 2 * Math.PI / 360d;
double x = 20 * 16 * Math.pow(Math.sin(sit), 3);
double y = 20 * (13 * Math.cos(sit) - 5 * Math.cos(2 * sit) - 2 * Math.cos(3 * sit) - Math.cos(4 * sit));
if (i == 180) {
mPath.moveTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
mPath.lineTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(mPath);
}
值得注意的是,该方法只能在API26版本以上调用。 低版本我们使用下一小节介绍的方法
举个例子:
我们先准备好一个心形的路径Path,然后调用 clipOutPath 从画布中将此路径之外的区域 “裁剪” 下来,最后为了我们观察,使用 drawColor “染”上酒红色。
canvas.drawColor(getContext().getResources().getColor(R.color.color_cd6155));
}
此类型的方法还有以下这几个,但他们的裁剪范围均为矩形
public boolean clipOutRect(float left, float top, float right, float bottom)
public boolean clipOutRect(int left, int top, int right, int bottom)
public boolean clipOutRect(@NonNull Rect rect)
public boolean clipOutRect(@NonNull RectF rect)
4、clipPath 裁剪保留区域
clipPath 和 clipOutPath 一样,都是对画布的裁剪,不会保留空白像素,之所以这么说是因为笔者在实际工作过程中遇到过,因为通过混色方案实现过View的压感效果,但是如果这个View在可见区域外其实保留了空白像素,这种情况混色将会出现原本圆角位置出现四个阴影直角效果的情况。
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op)
描述: 在画布上进行使用 path 路径进行操作,至于其作用由 op 决定。
描述比较抽象,我们通过例子来体会。但在上例子前,我们需要先了解下 Region.Op 这个枚举类型,具体内容代码如下
public enum Op {
DIFFERENCE(0),
INTERSECT(1),
UNION(2),
XOR(3),
REVERSE_DIFFERENCE(4),
REPLACE(5);
}
通过源码可以知道共有六种类型。值得一提的有以下两点:
1)clipOutPath 方法中使用的类型就是 DIFFERENCE ,换而言之,我们可以使用以下代码代替,解决在API26 以下无法使用的问题clipOutPath 方法的问题
clipPath(mPath, Region.Op.DIFFERENCE)
2)clipPath 方法中使用的类型就是 INTERSECT ,换而言之,我们可以使用以下代码代替
clipPath(mPath, Region.Op.INTERSECT)
复制代码
举些例子:
接下来我们一个个讲解这六种类型,两次裁剪比较能体现出 Region.Op 参数的作用,所以我们接下来的例子需要使用两个路径:
1、心形路径 (下列例子中的 A)
2、圆形路径 (下列例子中的 B)
![](http://shubin.noip.cn:9000/blogimg/16273717161891.png">
(1)DIFFERENCE
描述: A形状中不同于B的部分显示出来
效果图: 红色即为最终裁剪留下区域
![](http://shubin.noip.cn:9000/blogimg/16273715237707.png">
代码如下:
@Override
protected void onDraw(Canvas canvas) {
int mWidth = getWidth();
int mHeight = getHeight();
for (int i = 0 + 180; i < 361 + 180; i++) {
double sit = i * 2 * Math.PI / 360d;
double x = 20 * 16 * Math.pow(Math.sin(sit), 3);
double y = 20 * (13 * Math.cos(sit) - 5 * Math.cos(2 * sit) - 2 * Math.cos(3 * sit) - Math.cos(4 * sit));
if (i == 180) {
mPath.moveTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
mPath.lineTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
canvas.clipPath(mPath, Region.Op.DIFFERENCE);
canvas.save();
mPaint.setColor(0xaa6200EE);
canvas.drawCircle(mWidth / 2, mHeight / 2 + 400, 400, mPaint);
}
(2)INTERSECT
描述: A和B交集的形状
效果图: 红色和蓝色交界的部分即为最终裁剪留下区域
![](http://shubin.noip.cn:9000/blogimg/16273733514089.png">
代码如下:
@Override
protected void onDraw(Canvas canvas) {
int mWidth = getWidth();
int mHeight = getHeight();
for (int i = 0 + 180; i < 361 + 180; i++) {
double sit = i * 2 * Math.PI / 360d;
double x = 20 * 16 * Math.pow(Math.sin(sit), 3);
double y = 20 * (13 * Math.cos(sit) - 5 * Math.cos(2 * sit) - 2 * Math.cos(3 * sit) - Math.cos(4 * sit));
if (i == 180) {
mPath.moveTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
mPath.lineTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
canvas.clipPath(mPath, Region.Op.INTERSECT);
canvas.save();
mPaint.setColor(0xaa6200EE);
canvas.drawCircle(mWidth / 2, mHeight / 2 + 400, 400, mPaint);
}
(3)UNION
描述: A和B的全集,安卓版本大于等于28不支持 ,Android.P = 28;
效果图: 红色即为最终裁剪留下区域
效果如下:
【TODO需要补齐效果】
代码如下:
@Override
protected void onDraw(Canvas canvas) {
int mWidth = getWidth();
int mHeight = getHeight();
for (int i = 0 + 180; i < 361 + 180; i++) {
double sit = i * 2 * Math.PI / 360d;
double x = 20 * 16 * Math.pow(Math.sin(sit), 3);
double y = 20 * (13 * Math.cos(sit) - 5 * Math.cos(2 * sit) - 2 * Math.cos(3 * sit) - Math.cos(4 * sit));
if (i == 180) {
mPath.moveTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
mPath.lineTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
canvas.clipPath(mPath, Region.Op.UNION);
canvas.save();
mPaint.setColor(0xaa6200EE);
canvas.drawCircle(mWidth / 2, mHeight / 2 + 400, 400, mPaint);
}
4)XOR
描述: A和B的全集形状,去除交集形状之后的部分 ,安卓版本大于等于28不支持 ,Android.P = 28;
效果图: 红色即为最终裁剪留下区域
效果如下:
【TODO需要补齐效果】
代码如下:
@Override
protected void onDraw(Canvas canvas) {
int mWidth = getWidth();
int mHeight = getHeight();
for (int i = 0 + 180; i < 361 + 180; i++) {
double sit = i * 2 * Math.PI / 360d;
double x = 20 * 16 * Math.pow(Math.sin(sit), 3);
double y = 20 * (13 * Math.cos(sit) - 5 * Math.cos(2 * sit) - 2 * Math.cos(3 * sit) - Math.cos(4 * sit));
if (i == 180) {
mPath.moveTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
mPath.lineTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
canvas.clipPath(mPath, Region.Op.XOR);
canvas.save();
mPaint.setColor(0xaa6200EE);
canvas.drawCircle(mWidth / 2, mHeight / 2 + 400, 400, mPaint);
}
(5)REVERSE_DIFFERENCE
描述: B形状中不同于A的部分显示出来,安卓版本大于等于28不支持 ,Android.P = 28;
效果图: 红色即为最终裁剪留下区域
效果如下:
【TODO需要补齐效果】
代码如下:
@Override
protected void onDraw(Canvas canvas) {
int mWidth = getWidth();
int mHeight = getHeight();
for (int i = 0 + 180; i < 361 + 180; i++) {
double sit = i * 2 * Math.PI / 360d;
double x = 20 * 16 * Math.pow(Math.sin(sit), 3);
double y = 20 * (13 * Math.cos(sit) - 5 * Math.cos(2 * sit) - 2 * Math.cos(3 * sit) - Math.cos(4 * sit));
if (i == 180) {
mPath.moveTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
mPath.lineTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
canvas.clipPath(mPath, Region.Op.REVERSE_DIFFERENCE);
canvas.save();
mPaint.setColor(0xaa6200EE);
canvas.drawCircle(mWidth / 2, mHeight / 2 + 400, 400, mPaint);
}
(6)REPLACE
描述: 只显示B的形状,安卓版本大于等于28不支持 ,Android.P = 28;
效果图: 红色即为最终裁剪留下区域
效果如下:
【TODO需要补齐效果】
此类
代码如下:
@Override
protected void onDraw(Canvas canvas) {
int mWidth = getWidth();
int mHeight = getHeight();
for (int i = 0 + 180; i < 361 + 180; i++) {
double sit = i * 2 * Math.PI / 360d;
double x = 20 * 16 * Math.pow(Math.sin(sit), 3);
double y = 20 * (13 * Math.cos(sit) - 5 * Math.cos(2 * sit) - 2 * Math.cos(3 * sit) - Math.cos(4 * sit));
if (i == 180) {
mPath.moveTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
mPath.lineTo(mWidth / 2 + (float) (x), mHeight / 2 - (float) (y));
}
canvas.clipPath(mPath, Region.Op.REPLACE);
canvas.save();
mPaint.setColor(0xaa6200EE);
canvas.drawCircle(mWidth / 2, mHeight / 2 + 400, 400, mPaint);
}
4. 代码实战
绘制心型水波纹
- 绘制心形
- 绘制波纹
- 组合
- 动画
效果如下:
通过画布裁剪去掉无关部分
三、理解Canvas.save(),Canvas.restore(),Canvas.restoreToCount()函数
API | | 备注 |
---|
**Canvas save ** | | 把Canvas 的信息保存,压入栈 | **Canvas restore ** | | 恢复到最近的一个保存点。出栈。 | **restoreToCount ** | | 恢复到特定的保存点 | | | |
当前矩阵变换例如:平移translate(),缩放scale(),以及旋转rotate()等
Canvas的save()、restore()这两个方法字面意思就是保存、恢复,但为什么要保存和回复呢?不保存会怎么样?
其实可以理解为Canvas.store()就是将当前的Canvas压入栈做备份,中间可能会经过若干的矩阵变换,这些变化可能是平移、也可能是旋转、缩放等。在我们绘制完成之后只需要调用Canvas.restore()就可以将画布恢复到原来的位置。但是这个操作过程中是不会改变画布上已经绘制的图像内容的,也就是说Path、bitmap等是不受影响的。压栈记录的其实是对画布的操作。
下面我们来做一个简单的实验我们就可以理解了。
-
我们创建一个画布 绘制如下Path protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFF000000);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(10);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.translate(100, 100);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.translate(100, 100);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
}
效果如下:
-
增加Canvas.save() 代码,然后继续绘制发现没有任何影响 @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFF000000);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(10);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.translate(100, 100);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.translate(100, 100);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.save();
canvas.translate(100, 100);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
}
? 效果如下:
?
- 使用
Canvas.restore() 后,然后在执行重新执行绘制
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFF000000);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(10);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.translate(100, 100);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.translate(100, 100);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.save();
canvas.translate(100, 100);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.restore();
canvas.translate(100, 300);
canvas.drawLine(0, 0, 100, 0, mPaint);
}
绘制效果如下:
可以看到前面设置的canvas.translate(100, 100) 被还原了,可以得出结论,canvas.translate(x,y) 是可以被还原的。
- 使用
Canvas.restore() 还原Canvas.rotate() 和 Canvas.scale()
@Overridehttp://shubin.noip.cn:9000/image/image_20210723_400128987.png
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFF000000);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(10);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.translate(100, 100);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.translate(100, 100);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.save();
canvas.translate(100, 100);
canvas.drawLine(0, 0, 100, 0, mPaint);
canvas.drawLine(0, 100, 0, 0, mPaint);
canvas.restore();
canvas.translate(100, 300);
canvas.drawLine(0, 0, 100, 0, mPaint);
mPaint.setColor(0xffff00ff);
canvas.save();
canvas.translate(100, 100);
canvas.rotate(90);
canvas.drawLine(0, 0, 50, 0, mPaint);
canvas.drawLine(0, 50, 0, 0, mPaint);
canvas.translate(100, 100);
canvas.scale(2, 2);
mPaint.setColor(0xff0000ff);
canvas.drawLine(0, 0, 50, 0, mPaint);
canvas.drawLine(0, 50, 0, 0, mPaint);
canvas.restore();
canvas.drawLine(0, 0, 200, 0, mPaint);
}
绘制效果如下:
通过这个示例可以看出,Canvas.rotate() 和 Canvas.scale() 都是可以被还原的。
5.裁剪整个画布,然后执行绘制
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFF000000);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(10);
mPaint.setColor(0xffff00ff);
canvas.save();http://shubin.noip.cn:9000/image/image_20210723_400128987.png
canvas.drawRect(new Rect(0, 0, 600, 600), mPaint);
canvas.translate(100, 100);
Path path = new Path();
path.addRect(new RectF(0, 0, 200, 200), Path.Direction.CW);
canvas.clipPath(path);
mPaint.setColor(0xff00ffff);
canvas.drawRect(new Rect(0, 0, 600, 600), mPaint);
canvas.restore();
}
实现效果如下:
- 通过Canvas.restore()整个画布只后重新绘制矩形块。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFF000000);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(10);
mPaint.setColor(0xffff00ff);
canvas.save();
canvas.drawRect(new Rect(0, 0, 600, 600), mPaint);
canvas.translate(100, 100);
Path path = new Path();
path.addRect(new RectF(0, 0, 200, 200), Path.Direction.CW);
canvas.clipPath(path);
mPaint.setColor(0xff00ffff);
canvas.drawRect(new Rect(0, 0, 600, 600), mPaint);
canvas.restore();
mPaint.setColor(0xff00ffff);
canvas.drawRect(new Rect(0, 0, 600, 600), mPaint);
}
实现效果:
Canvas的其他功能
如下图当使用 canvas.translate(100,100)方法后canvas的坐标中心就不是左上角,而是(100,100),如图左边数第一个黑色直角就是 第一次canvas.translate(50,50)后的坐标中心。我用了一个for循环对同一个canvas(注意是同一个canvas)执行了5次canvas.translate(50,50),从图中发现canvas每次都以当前坐标中心为基础移动(50,50),如上图。但是如果遇到在for循环中对canvas执行translate后不想canvas改变坐标中心怎么办?那就在canvas translate前save,后再restore。
|