前面在学习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 paint = new Paint( )。常用方法如下:参考菜鸟教程
- setARGB(int a,int r,int g,int b): 设置绘制的颜色,A代表透明度,R,G, B代表红绿蓝三原色
- setAlpha(int a): 设置绘制图形的透明度
- setColor(int color): 设置绘制的颜色,使用颜色值来表示,该颜色值包括透明度和RGB颜色
- setAntiAlias(boolean aa): 设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢
- setDither(boolean dither): 设定是否使用图像抖动处理,会使绘制出来的图像更加清晰
- setFilterBitmap(boolean filter):如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作,加快显示速度,本设置项依赖于dither和xfermode的设置
- setXfermode(Xfermode xfermode): 设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果
- setColorFilter(ColorFilter colorfilter): 设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果
- setPathEffect(PathEffect effect) 设置绘制路径的效果,如点画线等
- setShader(Shader shader): 设置图像效果,使用Shader可以绘制出各种渐变效果
- setShadowLayer(float radius ,float dx,float dy,int color):在图形下面设置阴影层,产生阴影效果, radius为阴影的角度,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色
- setStyle(Paint.Style style): 设置画笔的样式,Paint.Style.FILL:仅填充内部,Paint.Style.STROKE:仅描边,Paint.Style.FILL_AND_STROKE:描边且填充内部
- setStrokeCap(Paint.Cap cap): 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样Cap.ROUND,或方形样式Cap.SQUARE
- setSrokeJoin(Paint.Join join): 设置绘制时各图形的结合方式,如平滑效果等
- setStrokeWidth(float width): 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度
- setFakeBoldText(boolean fakeBoldText): 模拟实现粗体文字,设置在小字体上效果会非常差
- setSubpixelText(boolean subpixelText): 设置该项为true,将有助于文本在LCD屏幕上的显示效果
- setTextAlign(Paint.Align align): 设置绘制文字的对齐方向
- setTextScaleX(float scaleX): 设置绘制文字x轴的缩放比例,可以实现文字的拉伸的效果
- setTextSize(float textSize): 设置绘制文字的字号大小
- setTextSkewX(float skewX): 设置斜体文字,skewX为倾斜弧度
- setTypeface(Typeface typeface): 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
- setUnderlineText(boolean underlineText): 设置带有下划线的文字效果
- setStrikeThruText(boolean strikeThruText): 设置带有删除线的效果
- setStrokeJoin(Paint.Join join): 设置结合处的样子,Miter:结合处为锐角, Round:结合处为圆弧:BEVEL:结合处为直线
- setStrokeMiter(float miter):设置画笔倾斜度
- setStrokeCap (Paint.Cap cap):设置转弯处的风格
其他常用方法:
- float ascent( ):测量baseline之上至字符最高处的距离
- float descent():baseline之下至字符最低处的距离
- int breakText(char[] text, int index, int count, float maxWidth, float[] measuredWidth): 检测一行显示多少文字
- clearShadowLayer( ):清除阴影层
Canvas
Canvas相当于画布,常用的方法如下:
drawxxx()方法:以一定的坐标值在当前画图区域画图,并且图层会叠加, 即后面绘画的图层会覆盖前面绘画的图层
- drawRect(RectF rect, Paint paint) :绘制矩形,
- drawPath(Path path, Paint paint) :绘制一个路径,参数一为Path路径对象
- drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) : 贴图,参数一是Bitmap对象,参数二是源区域(这里是bitmap), 参数三是目标区域(应该在canvas的位置和大小),参数四是Paint对象, 因为用到了缩放和拉伸的可能,当原始Rect不等于目标Rect时性能将会有大幅损失,除此之外还有其他不同参数的drawBitmap方法:
- drawLine(float startX, float startY, float stopX, float stopY, Paintpaint) : 画线,参数一起始点的x轴位置,参数二起始点的y轴位置,参数三终点的x轴水平位置, 参数四终点的y轴位置,最后一个参数为Paint 对象
- drawPoint(float x, float y, Paint paint): 画点,参数一水平x轴,参数二垂直y轴,第三个参数为Paint对象
- drawOval(RectF oval, Paint paint):画椭圆,参数一是RectF区域,参数二为paint对象
- drawCircle(float cx, float cy, float radius,Paint paint): 绘制圆,参数一是中心点的x轴,参数二是中心点的y轴,参数三是半径,参数四是paint对象
- drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint): 画弧,参数一是RectF对象,一个矩形区域椭圆形的界限用于定义在形状、大小、电弧,参数二是起始角 (度)在电弧的开始,参数三扫描角(度)开始顺时针测量的,参数四如果为true,包括椭圆中心的电弧,并关闭它,如果为false这将是一个弧线,参数五是Paint对象
- drawText(String text, float x, floaty, Paint paint) : 渲染文本,Canvas类除了上面的还可以描绘文字,参数一是String类型的文本, 参数二x轴,参数三y轴,参数四是Paint对象
clipxxx()方法:在当前的画图区域裁剪(clip)出一个新的画图区域,这个画图区域就是canvas 对象的当前画图区域。注意裁剪操作要在画图前进行,如果画图后再对Canvas进行Clip的话将不会影响 到已经画好的图形。例如:
- clipRect(new Rect()),那么该矩形区域就是canvas的当前画图区域
save()和restore()方法:
- save( )用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作
- restore()用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。save()和restore()要配对使用(restore可以比save少,但不能多),若restore调用次数比save多,会报错
其他的一些常用方法:
- translate(float dx, float dy):平移,将画布的坐标原点向左右方向移动x,向上下方向移动y.canvas的默认位置是在(0,0)
- scale(float sx, float sy):扩大,x为水平方向的放大倍数,y为竖直方向的放大倍数
- rotate(float degrees):旋转,angle指旋转的角度,顺时针旋转
Path
描点连线,在创建好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 为结束角度
Canvas API使用
这里学习一下上面说到的一些常见方法的使用。
- translate(float dx, float dy): Canvas默认位置在(0,0),该方法将Canvas的坐标原点向左右方向移动x,向上下方向移动y
新建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
}
}
}
修改activity_main.xml:
运行后效果如下:
-
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提供了七个重载方法:
参数介绍如下:
- rect:Rect/RectF对象,用于定义裁剪区的范围
- left:矩形裁剪区的左边位置
- top:矩形裁剪区的上边位置
- right:矩形裁剪区的右边位置
- bottom:矩形裁剪区的下边位置
- op:裁剪区域的组合方式
不过Q好像不推荐使用后三种方法了。
通过Demo学习一下,修改MyView.java内容:
如果注释掉clipRect方法,效果是:
加上裁剪方法后:
由上图也可以看到绘制是在这个裁剪后的Canvas上进行的,超过该区域的不显示。
clipPath
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方法参数为:
- float x:圆心X轴坐标
- float y:圆心Y轴坐标
- float radius:圆半径
- Path.Direction有两个值:Path.Direction.CCW:指创建逆时针方向的矩形路径;Path.Direction.CW:是clockwise的缩写,指创建顺时针方向的矩形路径
原图片为:
裁剪后的效果图为:
接下来看一下上面参数中提到的op,对应的枚举值如下:
- DIFFERENCE:A和B的差集范围,即A - B,只有在此范围内的绘制内容才会被显示
- INTERSECT:即A和B的交集范围,只有在此范围内的绘制内容才会被显示
- UNION:即A和B的并集范围,即两者所包括的范围的绘制内容都会被显示
- XOR:A和B的补集范围,此例中即A除去B以外的范围,只有在此范围内的绘制内容才会被显示
- REVERSE_DIFFERENCE:B和A的差集范围,即B - A,只有在此范围内的绘制内容才会被显示
- REPLACE:不论A和B的集合状况,B的范围将全部进行显示,如果和A有交集,则将覆盖A的交集范围
这里以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中的几个常用的变换方法如下:
- setTranslate(float dx, float dy):控制Matrix进行平移
- setRotate(float degrees, float px, float py):旋转,参数依次是:旋转角度,轴心(x,y)
- setScale(float sx, float sy, float px, float py):缩放, 参数依次是:X,Y轴上的缩放比例;缩放的轴心
- setSkew(float kx, float ky):倾斜(扭曲),参数依次是:X,Y轴上的缩放比例
通过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
最后用几个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;
}
}
也可以把 mcanvas.drawPath(path, paint)放在onTouchEvent中:
效果: