前面在学习Animation中的ValueAnimator高级用法时,用到了Canvas,这次就系统的学习一下Canvas。
Canvas的作用涉及到View的绘制,要想在View中绘制出相应的图像,就必须在Canvas上进行绘制,它就像是一个画板,然后可以使用Paint(画笔)在上面绘制或者用Path(路径)来绘制多个点,Paint会进行一些颜色等之类的初始化,如上图在学习Animation时所示。通常需要继承View并重写其onDraw方法来完成绘制。
由上图可以看到,onDraw方法的参数就是一个Canvas对象,我们绘制时就是使用的这个Canvas对象。如果想要在其他地方进行绘制,就要使用代码创建一个Canvas对象并且传入一个Bitmap对象作为参数:Canvas canvas = new Canvas(bitmap)。传入的bitmap与通过其创建的Canvas画布是紧紧联系在一起的,bitmap存储了所有绘制在这个Canvas上的像素信息。因此通过这种方式创建的Canvas对象后,后面调用的所有Canvas.drawxxx方法都发生在这个bitmap上。
那么我们就先从上面提到的这几个点:Paint、Canvas、Path开始,之后主要学习Canvas。
Paint就相当于是画笔,可以设置绘制风格,如:线宽(粗细)、颜色、透明度、填充风格等, 创建方法如下Paint paint = new Paint( )。常用方法如下:参考菜鸟教程
Canvas相当于画布,常用的方法如下:
drawxxx()方法:以一定的坐标值在当前画图区域画图,并且图层会叠加, 即后面绘画的图层会覆盖前面绘画的图层
clipxxx()方法:在当前的画图区域裁剪(clip)出一个新的画图区域,这个画图区域就是canvas 对象的当前画图区域。注意裁剪操作要在画图前进行,如果画图后再对Canvas进行Clip的话将不会影响 到已经画好的图形。例如:
save()和restore()方法:
其他的一些常用方法:
描点连线,在创建好Path路径后,可以调用上面提到的Canvas的drawPath(Path path, Paint paint)将图形绘制出来,常用方法如下:
addArc(RectF oval, float startAngle, float sweepAngle:为路径添加一个多边形,startAngle为起始角度,sweepAngle为跨越角度
addCircle(float x, float y, float radius, Path.Direction dir):给path添加圆圈
addOval(RectF oval, Path.Direction dir):添加椭圆形
addRoundRect(RectF rect, float[] radii, Path.Direction dir):添加一个圆角区域
addRect(RectF rect, Path.Direction dir):添加一个区域
isEmpty():判断路径是否为空
transform(Matrix matrix):应用矩阵变换
transform(Matrix matrix, Path dst):应用矩阵变换并将结果放到新的路径中,即第二个参数
Matrix(矩阵):用于图形特效处理,颜色矩阵(ColorMatrix),还有使用Matrix进行图像的 平移,缩放,旋转,倾斜等
高级效果PathEffect类:
moveTo(float x, float y):不会进行绘制,只用于移动移动画笔
lineTo(float x, float y):用于直线绘制,默认从(0,0)开始绘制,用moveTo移动。 例如mPath.lineTo(300, 300); canvas.drawPath(mPath, mPaint)
quadTo(float x1, float y1, float x2, float y2): 用于绘制圆滑曲线,即贝塞尔曲线,同样可以结合moveTo使用
rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 同样是用来实现贝塞尔曲线的。 (x1,y1) 为控制点,(x2,y2)为控制点,(x3,y3) 为结束点
arcTo(RectF oval, float startAngle, float sweepAngle): 绘制弧线(实际是截取圆或椭圆的一部分)ovalRectF为椭圆的矩形,startAngle 为开始角度, sweepAngle 为结束角度
这里学习一下上面说到的一些常见方法的使用。
新建MyView.java:
public class MyView extends View {
private Paint paint;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.BLUE);
}
@Override
protected void onDraw(Canvas canvas) {
for(int i=0; i < 10; i++) {
canvas.drawCircle(50, 50, 50, paint);
canvas.translate(100, 100);//将Canvas的坐标原点向左右方向移动100,向上下方向移动100
}
}
}
rotate:rotate(float degrees) / rotate(float degrees, float px, float py)围绕坐标点旋转degrees度,值为正顺时针;px和py为指定旋转的中心点坐标(px,py)
修改MyView代码:
效果如下图所示:
这里注意旋转的时候旋转的是坐标轴,例如rotate(45)再rotate(45):坐标轴变化如下图所示
scale:scale(float sx, float sy)对Canvas进行缩放,sx为水平方向缩放比例,sy为竖直方向的缩放比例
修改MyView.java代码:
缩放后再绘制一次,效果如下:
skew::倾斜方法skew(float sx, float sy),sx为x轴方向上倾斜的对应角度,sy为y轴方向上倾斜的对应角度
修改MyView.java代码:
效果如下:
Canvas图层、save()与restore()
以translate平移为例,它的流程图如下图所示:
实际上就是将Canvas坐标原点的分别在x,y轴上移动100,如果在平移之后又想在未平移之前的那个Canvas上绘制,就得用到我们前面提到的save()与restore()方法了。Canvas为我们提供了图层(Layer)的支持,而Layer(图层)是按"栈结构"来进行管理的,也就是Canvas在做平移变换之前会将当前Canvas的状态进行保存然后入栈,这个操作是由**save()方法完成的;如果想恢复之前Canvas的状态,就要调用restore()**方法,此时Canvas的Layer栈会弹出栈顶的那个Layer,这样先一个入栈的Layer会来到栈顶,此时的Canvas恢复到这个新栈顶保存的Canvas时状态。
通过Demo学习一下:
修改MyView.java内容:
对应的效果如下图所示:前面说过rotate是旋转的坐标轴,所以4,5都是在旋转后的坐标轴上进行的平移
这里一层层的弹出确实有点麻烦,Canvas还提供了restoreToCount()方法可以直接传入要恢复到的Layer层数, 直接就跳到对应的那一层,同时会将该层上面所有的Layer踢出栈,让该层成为栈顶,还是上面那个例子,我们直接跳到最先入栈的Layer层实现上图标注的5:
修改onDraw方法:
效果如下:
同时还同工了saveLayer()方法,与save()方法类似,不同的是saveLayer()可以选择性的保存某个区域的状态而不是保存的是整个Canvas,这个之后有时间再研究。
clipRect
clipRect提供了七个重载方法:
参数介绍如下:
通过Demo学习一下,修改MyView.java内容:
如果注释掉clipRect方法,效果是:
加上裁剪方法后:
由上图也可以看到绘制是在这个裁剪后的Canvas上进行的,超过该区域的不显示。
clipPath有两个重载方法,Q也不推荐后一种了。
通过绘制圆形ImageView的Demo学习一下:
修改MyView.java代码:
public class MyView extends View {
private Paint paint;
private Bitmap bitmap;
private Path path;
private Rect rect = new Rect();
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
paint = new Paint();
path = new Path();
paint.setStyle(Paint.Style.STROKE);
paint.setAntiAlias(true);
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLACK);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.nav_icon);
}
@Override
protected void onDraw(Canvas canvas) {
rect.set(0,0,getWidth(),getHeight());//dst区域为整个屏幕大小
path.addCircle(getWidth() / 2, getHeight() / 2, getWidth() / 3, Path.Direction.CCW);
canvas.clipPath(path);
canvas.drawBitmap(bitmap, null, rect, paint);
}
}
path.addCircle方法参数为:
接下来看一下上面参数中提到的op,对应的枚举值如下:
这里以DIFFERENCE为例:
在(10,10)以及(50,50)为起点,裁剪了两个100*100的矩形,并指定Region.Op为DIFFERENCE,即裁剪结果是A和B的差集 = A - (A和B相交的部分)。效果如下:
drawBitmap
前面提到过drawBitmap其实有多种方法可被调用,如下图所示,这里我们主要学习其中几个方法:
1.drawBitmap(Bitmap bitmap,Rect src,Rect dst,Paint paint)
前面说过,第一个Rect 代表要绘制的bitmap 区域,第二个 Rect 代表的是要将bitmap 绘制在屏幕的什么地方。通过Demo来验证一下:
首先设置srcRect 取值为整个Bitmap 区域 ,dstRect 取值为view左上方和bitmap同样大小:
效果如下:
如果我们想把这个图像移到屏幕中心,除过平移的方法,还可以试试改变Rect的方法:
效果如下图所示:
2.drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)
参数里面涉及到Matrix矩阵,Matrix中的几个常用的变换方法如下:
通过Demo验证一下,Demo来自:
public class MyView extends View {
private Bitmap mbitmap;
private Matrix matrix;
private int flag;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
matrix = new Matrix();
mbitmap = BitmapFactory.decodeResource(getResources(), R.drawable.nav_icon);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
switch (flag){
case 0:
matrix.reset();
break;
case 1:
matrix.setSkew(0.5f,0);
break;
case 2:
matrix.setSkew(-0.5f,0);
break;
case 3:
matrix.setScale(1.5f,1.5f);
break;
case 4:
matrix.setScale(0.4f,0.4f);
break;
}
//根据原始位图与Matrix创建新位图
Bitmap bitmap = Bitmap.createBitmap(mbitmap,0,0,mbitmap.getWidth(),mbitmap.getHeight(),matrix,true);
//绘制新位图
canvas.drawBitmap(bitmap,matrix,null);
}
public void setMethod(int id){
flag = id;
invalidate();
}
}
MainActivity中:
效果图如下所示:
其他的方法之后有时间再继续学习。
最后用几个Demo来加固本次知识,会涉及到自定义控件的一些知识,现在也还不太懂,当作是学习自定义控件的学习前奏了。
Demo1-绘制自定义图形时钟
涉及的API可查看前面介绍Canvas时的说明:
public class MyView extends View {
private Paint paint;
private Path path;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
paint = new Paint();
path = new Path();
paint.setAntiAlias(true);
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
paint.setTextSize(30);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制圆形表盘
canvas.translate(200, 200);
canvas.drawCircle(0, 0, 150, paint);
//绘制"时钟"文字
canvas.save();
canvas.translate(-100, -100);
path.addArc(new RectF(0,0,150,150), 180, 270);
//drawTextOnPath第三个参数代表与路径起始点的水平偏移距离,第四个参数代表与路径中心的垂直偏移量
canvas.drawTextOnPath("时钟", path, 30, -60, paint);
//出栈
canvas.restore();
//绘制刻度
int time = 60;
int y = 150;
for(int i=0;i<time;i++){
if(i%5 == 0){
canvas.drawLine(0,y,0,y-15f,paint);//整除5时刻指针长
canvas.drawText(String.valueOf((i+30)%time), -4f, y-25f, paint);
}else{
canvas.drawLine(0,y,0,y-5f,paint);
}
canvas.rotate(6,0f,0f);//坐标轴每次旋转6度 360/60
}
//绘制指针
canvas.drawCircle(0, 0, 7, paint);
paint.setColor(Color.BLACK);
canvas.drawLine(0, 0, 0, -60, paint);
}
}
Demo2-简单画图板的实现:
首先初始化画笔(Paint),画布(Canvas),路径(Path)记录绘制路线。这里的主要存在的问题是画图的时候,每次都是从上次按下时间的发生点到本次移动结束时间的发生点即一次按下-移动的时间间隔,那么如果我们直接在onDraw方法中的Canvas上进行绘制时,造成的结果就是之前绘制的会丢失,为了保存之前绘制的内容,引入"双缓冲"技术,参考: 每次不是直接绘制到onDraw方法中的Canvas上,而是先绘制到Bitmap上,等Bitmap上的绘制完了, 再一次性地绘制到View,具体创建地方在在View的onMeasure()方法中,创建一个View大小的Bitmap, 同时创建一个Canvas,通过刚开始时说的在其他地方得到Canvas对象的方法。传入这个View大小的Bitmap对象作为参数,这样这个Canvas就和这个Bitmap绑定了,Bitmap上存储了所有绘制在这个Canvas上的像素信息;然后在onTouchEvent中获得X,Y坐标,做绘制连线,最后调用invalidate(),invalidate()会调用onDraw()方法重绘:
public class MyView extends View {
private Bitmap mbitmap;
private Paint paint;
private Path path;
private Canvas mcanvas;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
path = new Path();
paint = new Paint();
paint.setColor(Color.BLACK);
paint.setAntiAlias(true);
paint.setDither(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(10);
}
@Override
protected void onDraw(Canvas canvas) {
mcanvas.drawPath(path, paint);
canvas.drawBitmap(mbitmap,0,0,null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
mbitmap = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
mcanvas = new Canvas(mbitmap);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN://按下事件
path.moveTo(x,y);//移动画笔到屏幕刚按下的地方但不绘制
break;
case MotionEvent.ACTION_MOVE://移动事件
path.lineTo(x, y);
//mcanvas.drawPath(path, paint);//也可以放在这里
break;
}
invalidate();//调用onDraw方法重绘
return true;
}
}