只要朝着一个方向努力,一切都会变得得心应手。——勃朗宁
我喜欢写博客之前用一句励志名言激励自己!
Canvas的意思是画布,表现在屏幕上就是一块区域,我们可以再上面使用各种API绘制我们想要的东西。可以说,Canvas贯穿整个2D Graphics,android.graphics中的所有类,几乎都于Canvas有直接或间接的联系。所以了解Canvas是学习2D Graphics的基础。
如何获得一个Canvas对象?
第一种我们通过重写View类中的onDraw方法,View中的Canvas对象会被当做参数传递过来,我们操作这个Canvas,效果会直接反应在View中。
第二种就是当你想自己创建一个Canvas对象。从上面的基本要素可以明白,一个Canvas对象一定是结合了一个Bitmap对象的。所以一定要为一个Canvas对象设置一个Bitmap对象。
//得到一个Bitmap对象,当然也可以使用别的方式得到。但是要注意,改bitmap一定要是mutable(异变的)
Bitmap b = Bitmap.createBitmap(100,100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
/*先new一个Canvas对象,在调用setBitmap方法,一样的效果
* Canvas c = new Canvas();
* c.setBitmap(b);
*/
第三种方式,是调用SurfaceHolder.lockCanvas(),返回一个Canvas对象。
我们有了画布对象后,接下来我们先看看它的平移,canvas 中有一个函数 translate()是用来实现画布坐标系平移的,画布坐标是以左上角为原点(0,0),向右是X轴正方向,向下是Y轴正方向,如下图所示
void translate(float dx, float dy)
// float dx:水平方向平移的距离,正数指向正方向(向右)平移的量,负数为向负方向(向左)平移的量
// float dy: 垂直方向平移的距离,正数指向正方向 (向下) 平移量,负数为向负方向 (向上) 平移量
我们创建一个新的类,MyCanvasView.java类
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//translate 平移,即改变坐标系原点位置
Paint paint = new Paint();
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.FILL);
// canvas.translate(100, 100);
Rect rect1 = new Rect(0, 0, 400, 220);
canvas.drawRect(rect1, paint);
}
上面这段代码,先把canvas.translate(100, 100);注释掉,看原来矩形的位置,然后打开注释,看平移后的位置,对比如下图:
这段代码中,同一个矩形,在画布平移前画一次,平移后再画一次,大家会觉得结果会怎样?
package com.as.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.RegionIterator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;
public class MyCanvasView extends View {
public MyCanvasView(Context context) {
this(context, null);
}
public MyCanvasView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs, 0);
}
public MyCanvasView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//构造两个画笔,一个红色,一个绿色
Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 3);
Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);
//构造一个矩形
Rect rect = new Rect(0, 0, 400, 220);
//在平移画布前用绿色画下边框
canvas.drawRect(rect, paintGreen);
////平移画布后,再用红色边框重新画下这个矩形
canvas.translate(100, 100);
canvas.drawRect(rect, paintRed);
}
private Paint generatePaint(int color, Paint.Style style, int stroke) {
Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(style);
paint.setStrokeWidth(stroke);
return paint;
}
}
这段代码中,对于同一个矩形,在平移画布前利用绿色画下矩形边框,在平移后,再用红色画下矩形边框。大家是不是会觉得这两个边框会重合?实际结果是这样的。
从到这个结果大家可能会蛋疼,我第一次看到这个结果的时候蛋都碎一地了要。淡定……
这个结果的关键问题在于,为什么绿色框并没有移动?
这是由于屏幕显示与Canvas根本不是一个概念!Canvas是一个很虚幻的概念,相当于一个透明图层,每次Canvas画图时(即调用draw系列函数),都会产生一个透明图层,然后在这个透明图层上画图,在透明图层画完之后覆盖在屏幕上显示。所以上面的两个结果是由下面几个步骤形成的:
1、调用canvas.drawRect(rect, paintGreen0) 时,产生一个Canvas透明图层,由于当时还没有对坐标系平移,所以坐标原点是(0,0);系统在Canvas上画好之后,覆盖到屏幕上显示出来,过程如下图:
2、然后再第二次调用canvas.drawRect(rect, paintRed)时,又会重新产生一个全新的Canvas画布,但此时画布坐标已经改变了,即向右和向下分别移动了100像素,所以此时的绘图方式为:(合成视图,从上往下看的合成方式)
上图展示了,Canvas图层与屏幕的合成过程,由于Canvas画布已经平移了100像素,所以在画图时是以新原点来产生视图的,然后合成到屏幕上,这就是我们上面最终看到的结果了。我们看到画布移动之后,有一部分超出了屏幕的范围,那超出范围的图像显不显示呢,当然不显示了!也就是说,Canvas上虽然能画上,但超出了屏幕的范围,是不会显示的。当然,我们这里也没有超出显示范围,两框框而已。
下面对上面的知识做一下总结
- 每次调用canvas.drawXXXX系列函数来绘图,都会产生一个全新的Canvas画布。
- 如果在DrawXXX前,调用平移、旋转等函数来对Canvas进行了操作,那么这个操作是不可逆的!每次产生的画布最新位置都是这些操作后的位置。(关于Save()、Restore()的画布可逆问题的后面再讲)
- 在Canvas与屏幕合成时,超出屏幕范围的图像是不会显示出来的。
旋转(Rotate)
画布的旋转是默认是围绕坐标原点来旋转的,这里容易产生错觉,看起来觉得是图片旋转了,其实我们旋转的是画布,以后在此画布上画的东西显示出来的时候全部看起来都是旋转的。其实Roate函数有两个构造函数
void rotate(float degrees)
void rotate (float degrees, float px, float py)
// 第一个构造函数直接输入旋转的度数,正数是顺时针旋转,负数指逆时针旋转,它的旋转中心点是原点(0,0)
// 第二个构造函数除了度数以外,还可以指定旋转的中心点坐标(px,py)
下面以第一个构造函数为例,旋转一个矩形,先画出未旋转前的图形,然后再画出旋转后的图形;
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//构造两个画笔,一个红色,一个绿色
Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 3);
Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);
//构造一个矩形
Rect rect = new Rect(300, 10, 500, 100);
//画出原轮廓
canvas.drawRect(rect, paintGreen);
//顺时针旋转画布 30度
canvas.rotate(30);
canvas.drawRect(rect, paintRed);
}
接下来我们看看第二个构造函数,以某个坐标点旋转
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//构造两个画笔,一个红色,一个绿色
Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 2);
Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 2);
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
//画横线(水平方向居中)
canvas.drawLine(0, displayMetrics.heightPixels / 2 - 5 / 2, displayMetrics.widthPixels, displayMetrics.heightPixels / 2 + 5 / 2, paintGreen);
//画竖线(垂直方向剧中)
canvas.drawLine(displayMetrics.widthPixels / 2 - 5 / 2, 0, displayMetrics.widthPixels / 2 + 5 / 2, displayMetrics.heightPixels, paintRed);
// 画布旋转10度
canvas.rotate(10, displayMetrics.widthPixels / 2, displayMetrics.heightPixels / 2);
//再画横线(垂直方向剧中)
canvas.drawLine(0, displayMetrics.heightPixels / 2 - 5 / 2, displayMetrics.widthPixels, displayMetrics.heightPixels / 2 + 5 / 2, paintGreen);
//再画竖线(水平方向剧中)
canvas.drawLine(displayMetrics.widthPixels / 2 - 5 / 2, 0, displayMetrics.widthPixels / 2 + 5 / 2, displayMetrics.heightPixels, paintRed);
}
private Paint generatePaint(int color, Paint.Style style, int stroke) {
Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(style);
paint.setStrokeWidth(stroke);
paint.setAntiAlias(true);
return paint;
}
我们在屏幕上花了四条线段,其中两条垂线,画布旋转10度后,有画了两条线。
接下来复习之前学习过的路径Path类,直接看看完成代码
package com.as.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.RegionIterator;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;
public class MyCanvasView extends View {
public MyCanvasView(Context context) {
this(context, null);
}
public MyCanvasView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs, 0);
}
public MyCanvasView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// protected void onDraw(Canvas canvas) {
// // TODO Auto-generated method stub
// super.onDraw(canvas);
// canvas.drawColor(Color.RED);
// //保存的画布大小为全屏幕大小
// canvas.save();
//
// canvas.clipRect(new Rect(100, 100, 700, 700));
// canvas.drawColor(Color.GREEN);
// //保存画布大小为Rect(100, 100, 700, 700)
// canvas.save();
//
// canvas.clipRect(new Rect(200, 200, 600, 600));
// canvas.drawColor(Color.BLUE);
// //保存画布大小为Rect(200, 200, 600, 600)
// canvas.save();
//
// canvas.clipRect(new Rect(300, 300, 500, 500));
// canvas.drawColor(Color.BLACK);
// //保存画布大小为Rect(300, 300, 500, 500)
// canvas.save();
//
// canvas.clipRect(new Rect(370, 370, 430, 430));
// canvas.drawColor(Color.WHITE);
//
// canvas.restore();
// canvas.restore();
// canvas.restore();
// canvas.drawColor(Color.YELLOW);
// //构造一个矩形
// Rect rect = new Rect(300, 0, 500, 100);
//
// //画出原轮廓
// canvas.drawRect(rect, paintGreen);
// Rect rect2 = new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels);
// for (int i = 10; i <= 90; i += 10) {
// canvas.save();
// canvas.rotate(i);
// canvas.drawRect(rect2, paintGreen);
// canvas.restore();
// }
// }
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//构造两个画笔,一个红色,一个绿色
Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 2);
Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 2);
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
//画横线(水平方向居中)
canvas.drawLine(0, displayMetrics.heightPixels / 2 - 5 / 2, displayMetrics.widthPixels, displayMetrics.heightPixels / 2 + 5 / 2, paintGreen);
//画竖线(垂直方向剧中)
canvas.drawLine(displayMetrics.widthPixels / 2 - 5 / 2, 0, displayMetrics.widthPixels / 2 + 5 / 2, displayMetrics.heightPixels, paintRed);
// 画布旋转10度
canvas.rotate(10, displayMetrics.widthPixels / 2, displayMetrics.heightPixels / 2);
//画横线(垂直方向剧中)
canvas.drawLine(0, displayMetrics.heightPixels / 2 - 5 / 2, displayMetrics.widthPixels, displayMetrics.heightPixels / 2 + 5 / 2, paintGreen);
//画竖线(水平方向剧中)
canvas.drawLine(displayMetrics.widthPixels / 2 - 5 / 2, 0, displayMetrics.widthPixels / 2 + 5 / 2, displayMetrics.heightPixels, paintRed);
//设置画笔的文本大小为 20个px
paintRed.setTextSize(20);
paintGreen.setTextSize(20);
String textGreen = "第2条绿线";
canvas.drawText(textGreen, displayMetrics.widthPixels - 120, displayMetrics.heightPixels / 2 - 10, paintGreen);
//复习下之前学习过的路径
Path path = new Path();
String textRed = "第2条红线";
int textWidth = (int) paintRed.measureText(textRed);
//直线的起始点
path.moveTo(displayMetrics.widthPixels / 2 + 10, textWidth / 2);
//是直线的终点,又是下一次绘制直线路径的起始点
path.lineTo(displayMetrics.widthPixels / 2 + 10, textWidth + textWidth / 2);
canvas.drawTextOnPath(textRed, path, 0, 0, paintRed);
}
private Paint generatePaint(int color, Paint.Style style, int stroke) {
Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(style);
paint.setStrokeWidth(stroke);
paint.setAntiAlias(true);
return paint;
}
}
缩放(scale )
public void scale (float sx, float sy)
public final void scale (float sx, float sy, float px, float py)
其实我也没弄懂第二个构造函数是怎么使用的,我就先讲讲第一个构造函数的参数吧
float sx:水平方向伸缩的比例,假设原坐标轴的比例为n,不变时为1,在变更的X轴密度为n*sx;所以,sx为小数为缩小,sx为整数为放大
float sy:垂直方向伸缩的比例,同样,小数为缩小,整数为放大
//构造两个画笔,一个红色,一个绿色
Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 3);
Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);
//构造一个矩形
Rect rect = new Rect(10, 10, 200, 100);
//画出原轮廓
canvas.drawRect(rect, paintGreen);
canvas.scale(0.5f, 1f);
canvas.drawRect(rect, paintRed);
扭曲(skew)
其实我觉得译成斜切更合适,在PS中的这个功能就差不多叫斜切。但这里还是直译吧,大家都是这个名字。看下它的构造函数:
void skew (float sx, float sy)
float sx:将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,
float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值,
注意,这里全是倾斜角度的tan值哦,比如我们打算在X轴方向上倾斜60度,tan60=根号3,小数对应1.732
package com.as.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.RegionIterator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;
public class MyCanvasView extends View {
public MyCanvasView(Context context) {
this(context, null);
}
public MyCanvasView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs, 0);
}
public MyCanvasView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//构造两个画笔,一个红色,一个绿色
Paint paintGreen = generatePaint(Color.GREEN, Paint.Style.STROKE, 3);
Paint paintRed = generatePaint(Color.RED, Paint.Style.STROKE, 3);
//构造一个矩形
Rect rect = new Rect(10, 10, 200, 100);
//画出原轮廓
canvas.drawRect(rect, paintGreen);
//X轴倾斜60度,Y轴不变
canvas.skew(1.732f, 0);
canvas.drawRect(rect, paintRed);
}
private Paint generatePaint(int color, Paint.Style style, int stroke) {
Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(style);
paint.setStrokeWidth(stroke);
return paint;
}
}
裁剪画布(clip系列函数)
裁剪画布是利用clip系列函数,通过与Rect、Path、Region取交、并、差等集合运算来获得最新的画布形状。除了调用Save、Restore函数以外,这个操作是不可逆的,一但Canvas画布被裁剪,就不能再被恢复!
boolean clipPath(Path path)
boolean clipPath(Path path, Region.Op op)
boolean clipRect(Rect rect, Region.Op op)
boolean clipRect(RectF rect, Region.Op op)
boolean clipRect(int left, int top, int right, int bottom)
boolean clipRect(float left, float top, float right, float bottom)
boolean clipRect(RectF rect)
boolean clipRect(float left, float top, float right, float bottom, Region.Op op)
boolean clipRect(Rect rect)
boolean clipRegion(Region region)
boolean clipRegion(Region region, Region.Op op)
以上就是根据Rect、Path、Region来取得最新画布的函数,难度都不大,就不再一一讲述。利用clipRect()来稍微一讲。
canvas.drawColor(Color.RED);
canvas.clipRect(new Rect(100, 100, 200, 200));
canvas.drawColor(Color.GREEN);
先把背景色整个涂成红色,显示在屏幕上。然后裁切画布,最后最新的画布整个涂成绿色。可见绿色部分,只有一小块,而不再是整个屏幕了。
关于两个画布与屏幕合成,我就不再画图了,跟上面的合成过程是一样的。
画布的保存与恢复(save()、restore())
前面我们讲的所有对画布的操作都是不可逆的,这会造成很多麻烦,比如,我们为了实现一些效果不得不对画布进行操作,但操作完了,画布状态也改变了,这会严重影响到后面的画图操作。如果我们能对画布的大小和状态(旋转角度、扭曲等)进行实时保存和恢复就最好了。
这小节就给大家讲讲画布的保存与恢复相关的函数——save()、restore()。
int save ()
void restore()
这两个函数没有任何的参数,很简单。
Save():每次调用Save()函数,都会把当前的画布的状态进行保存,然后放入特定的栈中;
restore():每当调用Restore()函数,就会把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布,并在这个画布上做画。
为了更清晰的显示这两个函数的作用,下面举个例子:
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
canvas.drawColor(Color.RED);
canvas.save();//保存当前画布大小即整屏
canvas.clipRect(new Rect(100, 100, 600, 600));
canvas.drawColor(Color.GREEN);
canvas.restore(); //恢复整屏画布
canvas.drawColor(Color.YELLOW);
}
private Paint generatePaint(int color, Paint.Style style, int stroke) {
Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(style);
paint.setStrokeWidth(stroke);
return paint;
}
他图像的合成过程为:(最终显示为全屏幕蓝色)
下面我通过一个多次利用save()、restore()来讲述有关保存Canvas画布状态的栈的概念:代码如下:
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
canvas.drawColor(Color.RED);
//保存的画布大小为全屏幕大小
canvas.save();
canvas.clipRect(new Rect(100, 100, 700, 700));
canvas.drawColor(Color.GREEN);
//保存画布大小为Rect(100, 100, 700, 700)
canvas.save();
canvas.clipRect(new Rect(200, 200, 600, 600));
canvas.drawColor(Color.BLUE);
//保存画布大小为Rect(200, 200, 600, 600)
canvas.save();
canvas.clipRect(new Rect(300, 300, 500, 500));
canvas.drawColor(Color.BLACK);
//保存画布大小为Rect(300, 300, 500, 500)
canvas.save();
canvas.clipRect(new Rect(370, 370, 430, 430));
canvas.drawColor(Color.WHITE);
}
在这段代码中,总共调用了四次save操作。上面提到过,每调用一次save()操作就会将当前的画布状态保存到栈中,所以这四次save()所保存的状态的栈的状态如下:
注意在,第四次save()之后,我们还对画布进行了canvas.clipRect(new Rect(370, 370, 430, 430));操作,并将当前画布画成白色背景。也就是上图中最小块的白色部分,是最后的当前的画布。
如果,现在使用restore(),会怎样呢,会把栈顶的画布取出来,当做当前画布的画图,试一下:
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
canvas.drawColor(Color.RED);
//保存的画布大小为全屏幕大小
canvas.save();
canvas.clipRect(new Rect(100, 100, 700, 700));
canvas.drawColor(Color.GREEN);
//保存画布大小为Rect(100, 100, 700, 700)
canvas.save();
canvas.clipRect(new Rect(200, 200, 600, 600));
canvas.drawColor(Color.BLUE);
//保存画布大小为Rect(200, 200, 600, 600)
canvas.save();
canvas.clipRect(new Rect(300, 300, 500, 500));
canvas.drawColor(Color.BLACK);
//保存画布大小为Rect(300, 300, 500, 500)
canvas.save();
canvas.clipRect(new Rect(370, 370, 430, 430));
canvas.drawColor(Color.WHITE);
canvas.restore();
canvas.drawColor(Color.YELLOW);
}
上段代码中,把栈顶的画布状态取出来,作为当前画布,然后把当前画布的背景色填充为黄色
那如果我连续Restore()三次,会怎样呢?
我们先分析一下,然后再看效果:Restore()三次的话,会连续出栈三次,然后把第三次出来的Canvas状态当做当前画布,也就是Rect(100, 100, 700, 700),所以如下代码:
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
canvas.drawColor(Color.RED);
//保存的画布大小为全屏幕大小
canvas.save();
canvas.clipRect(new Rect(100, 100, 700, 700));
canvas.drawColor(Color.GREEN);
//保存画布大小为Rect(100, 100, 700, 700)
canvas.save();
canvas.clipRect(new Rect(200, 200, 600, 600));
canvas.drawColor(Color.BLUE);
//保存画布大小为Rect(200, 200, 600, 600)
canvas.save();
canvas.clipRect(new Rect(300, 300, 500, 500));
canvas.drawColor(Color.BLACK);
//保存画布大小为Rect(300, 300, 500, 500)
canvas.save();
canvas.clipRect(new Rect(370, 370, 430, 430));
canvas.drawColor(Color.WHITE);
canvas.restore();
canvas.restore();
canvas.restore();
canvas.drawColor(Color.YELLOW);
}
上段代码中,把栈顶的画布状态取出来,作为当前画布,然后把当前画布的背景色填充为黄色