前言
网上自定义View的文章已经很多了,但不是我的。。。
1.还是那句话,别人的东西,永远在被人脑子里!哪怕看了很多遍,实践很多次,没有自己的总结,一段时间后,再拿起来还是很费力,又要重新寻找资源,效率太低。
2.自定义View,可以说是android UI的难点与核心。了解每个细节,熟练掌握自定义View,设计UI也就信手拈来。
于是,就开始了属于自己的自定义View的系列。内容多比较类似,站在巨人肩膀,聚集大家的智慧。
希望可以尽力完成这系列的文章~
开始
平时画画,一般需要两个工具,纸和笔。这里的绘制也是一样,需要Canvas和Paint 2个工具。Paint就是相当于笔,而Canvas就是纸,这里叫画布。
Paint:可以定义画笔的大小、粗细、颜色等属性;
Canvas:可以定义画布的形状、颜色、等属性;
注:这篇主要详细介绍Canvas的用,但会引伸其他工具类,如:Paint、Path等。可以暂时不管,或者跳到相关的链接简单了解再回来继续看。
实践:一切多是从onDraw()开始
反手就来一个实例热身,先对绘制有整体的认识,后面再一步一步介绍其中的细节。
首先,就是新建一个工程啦,自动生成一个MainActivity;
其次,新建一个TestCanvas视图类,派生子View,绘制的工作就在这类执行。
其实掌握了核心基础,自定义绘制非常简单,这里大致上三步即可:
- 定义画笔Paint;
- 重写onDraw()方法;
- 通过画布Canvas的drawXxx()绘制
具体实现的代码如下:
public class CanvasDemo extends View{
private Paint mPaint = null;
/**
* 直接在xml中定义并显示,所以只实现该构造函数即可
* @param context
* @param attrs
*/
public CanvasDemo(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 第一次绘制:绘制矩形
*/
canvas.drawRect(80,80,200,150,mPaint);//通过draw方法,将内容显示在屏幕上,此次画布的任务已经完成
/**
* 第二次绘制:先平移,再绘制圆
*/
canvas.translate(300,300);//画布还是原来的画布,只是将它平移。内容是在屏幕上的,所以画布平移不影响第一次绘制的内容
mPaint.setColor(Color.GREEN); //改变画笔的颜色
canvas.drawCircle(50,50,100,mPaint);//通过draw方法,将内容显示在屏幕上,此次画布的任务已经完成。(这里的坐标多是相对画布而言)
}
}
最后,将CanvasDemo 类 加载到MainActivity的xml文件中,并显示出来(根据构造函数来确定加载方式)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.excellent.frameworks.view.CanvasDemo
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
最终效果图:
就这样子给我写完了?是的,是不是觉得很简单。
接下来,就开始详细介绍Canvas和Paint的用法,本章先介绍Canvas。
Canvas API使用
Canvas工具类的API非常多,具体可以参考官方网址:https://developer.android.google.cn/reference/android/graphics/Canvas.html
这里也引荐一张概括图,清晰明了,也是较为常用的方法,如下图:(具体网址:点这里)
获取Canvas的几种方法
1.通过自定义Bitmap 对象,创建Canvas
Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
2.通过继承View类,重写onDraw(),使用参数中的Canvas(常用),一般用于 不需要大量的处理速度或帧率
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
}
3.通过继承SurfaceView类,并实现SurfaceHolder.Callback方法,然后通过SurfaceHolder来获取Canvas对象。 为应用程序提供辅助线程来绘图,以便应用程序不需要等待系统的View层次结构准备好绘制,性能高。 一般用于频繁刷新的绘制。
SurfaceHolder surfaceHolder = surfaceView.getHolder();
Canvas c = surfaceHolder.lockCanvas();
以上只是简单介绍而已,后面具体用到在具体说明。这里主要还是使用第2种。
绘制颜色
drawColor(int color)
参数:color 颜色值。
drawRGB(int r, int g, int b)
drawARGB(int a, int r, int g, int b)
参数:
a 透明度,取值范围0~255;
r 红色值,取值范围0~255
g 绿色值,取值范围0~255
b 蓝色值,取值范围0~255
drawColor(int color, PorterDuff.Mode mode) // 需设置PorterDuff模式,后面再讲。
使用:(注意:以下所有的api使用,多是在onDraw( )调用)
// canvas.drawColor(Color.RED);
canvas.drawColor(Color.parseColor("#66ff66ff"));
// canvas.drawRGB(200, 0, 0);
// canvas.drawARGB(100, 0, 0, 100);
这类颜色填充方法一般用于在绘制之前设置底色,或者在绘制之后为界面设置半透明蒙版。
绘制基本图形
1.绘制点
drawPoint(float x, float y, Paint paint)
参数:
x:点的X坐标
y:点的Y坐标
drawPoints(float[] pts, Paint paint)
drawPoints(float[] pts, int offset, int count, Paint paint)
参数:
pts:数值的合集,每2个数值组成1个点,样式为 [x0 y0 x1 y1 x2 y2 ...]
offset:绘制前,要跳过数值的个数
count:从offset开始,选取count个数值。一般取2的倍数,不然最后数值组成不了一个点,则不绘制。
使用:
mPaint.setStrokeWidth(20);//设置画笔的大小
// canvas.drawPoint(200,200,mPaint);
//绘制4个点
canvas.drawPoints(new float[]{300,300, 300,350, 300,400, 300,450}, mPaint);
//从数组第3个数开始,取出4个数值,再绘制点
canvas.translate(200,0);
canvas.drawPoints(new float[]{300,300, 300,350, 300,400, 300,450}, 4,4, mPaint);
绘制线
drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
参数:
startX:开始点的X坐标
startY:开始点的Y坐标
stopX:结束点的X坐标
stopY:结束点的Y坐标
drawLines(float[] pts, Paint paint)
drawLines(float[] pts, int offset, int count, Paint paint)
参数:
pts:数值的合集,每4个数值组成1条线,样式为 [x0 y0 x1 y1 x2 y2 ...]
offset:绘制前,要跳过数值的个数
count:从offset开始,选取count个数值。一般取4的倍数,不然最后数值组成不了一条线,则不绘制
具体使用与drawPoints( )类似,这里不贴代码了。
绘制矩形
绘制之前,先了解一下:
矩形工具类RectF与Rect
这2个类其实是矩形的封装类,将4个边坐标传入构造器参数中即可。两者最大的区别就是精度不同,Rect是int(整形)的,而RectF是float(单精度浮点型)的,其他貌似没什么大区别了。
drawRect(float left, float top, float right, float bottom, Paint paint)
参数:
left:矩形左边距离X轴的距离(X轴指定的是当前视图的)
top:矩形顶边距离Y轴的距离(Y轴指定的是当前视图的)
right:矩形右边距离X轴的距离
bottom:矩形底边距离X轴的距离
drawRect(Rect r, Paint paint)
参数:r:矩形类Rect()
drawRect(RectF rect, Paint paint)
参数:rect:矩形类RectF()
使用:
mPaint.setColor(Color.YELLOW);
canvas.drawRect(100,100,500,400,mPaint);
mPaint.setColor(Color.RED);
canvas.drawRect(new Rect(200,200,600,500), mPaint);
mPaint.setColor(Color.GREEN);
canvas.drawRect(new RectF(300,300,700,600), mPaint);
以上三种方法,最终效果多是一样的,主要理解坐标参数即可。
绘制圆角矩形
drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint)
drawRoundRect(RectF rect, float rx, float ry, Paint paint)
参数:
left、top、right、bottom 、rect 同绘制矩形
rx:用于圆角的椭圆X半径
ry:用于圆角的椭圆Y半径
主要理解rx、ry,示意图如下:
注:实际上,在rx为宽度的一半,ry为高度的一半时,刚好是一个椭圆;但由于当rx大于宽度一半,ry大于高度一半时,无法计算出圆弧,所以drawRoundRect对大于该数值的参数进行了修正,凡是大于一半的参数均按照一半来处理。
绘制椭圆
drawOval(float left, float top, float right, float bottom, Paint paint)
drawOval(RectF oval, Paint paint)
参数:
left、top、right、bottom 、oval 同绘制矩形
可以看到,他们参数和绘制矩形的参数一样的,其原理就是对角线顶点确定矩形,根据传入矩形的长宽作为长轴和短轴画椭圆,绘制一个内切图形。如果长宽一样,那么就会变像变成圆了。
示意图如下:
绘制圆形
drawCircle(float cx, float cy, float radius, Paint paint)
参数:
cx:圆心的X坐标
cy:圆心的Y坐标
radius:圆的半径
使用:
mPaint.setColor(Color.RED);
canvas.drawCircle(300,400,200,mPaint);
绘制圆弧
drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
参数:
left、top、right、bottom 、oval 同绘制矩形
startAngle:圆弧开始的角度
sweepAngle:扫描的角度(顺时针旋转)
useCenter:是否使用中心点
不使用中心点:圆弧的形状 = (起、止点连线+圆弧)构成的面积
使用中心店:圆弧面积 = (起点、圆心连线 + 止点、圆心连线+圆弧)构成的面积
使用:
canvas.drawArc(50,50,400,200,0,120,true,mPaint);//使用中心点
canvas.translate(300,0);
canvas.drawArc(50,50,400,200,0,120,false,mPaint); //不使用中心点
//注意:画出来的效果 与画笔Paint的填充效果有关。
canvas.translate(-300,300);
mPaint.setStyle(Paint.Style.STROKE);//设置画笔填充模式为描边
canvas.drawArc(50,50,400,200,0,120,true,mPaint);//使用中心点
canvas.translate(300,0);
canvas.drawArc(50,50,400,200,0,120,false,mPaint);//不使用中心点
谷歌API Demos有个有趣的demo,看效果图:
绘制路径
通过绘制基本图形可以看到,这些图形多是很规则。如果需要绘制一些不规则图形,那么就可以通过Path来定义,然后再绘制出来。不管Path是什么图形,最终多是通过drawPath()来绘制:
drawPath(Path path, Paint paint)
该方法同样能过绘制基本的图形,还有绘制所有你想要的不规则图形。涉及内容较多,将在后面的章节具体讲解。
绘制文本
这里通过Canvas绘制文本,是绘制文本整体的轮廓,比如字符位置、数量、绘制路径。如果需要设置文本的细节,使用过Paint来设置。这将会在下一节介绍。Canvas关于文本的方法有以下:
drawText(String text, float x, float y, Paint paint)
drawText(String text, int start, int end, float x, float y, Paint paint)
drawText(char[] text, int index, int count, float x, float y, Paint paint)
drawText(CharSequence text, int start, int end, float x, float y, Paint paint)
参数:
text:文本字符,可以是字符串类型String,字符数组char[],CharSequence 类型
x:绘制文本的X轴坐标
y:绘制文本的Y轴坐标
start、end:根据索引值,截取字符串,索引从0开始,格式是 [start,end),不包括end。
index,count:根据索引值和数量,截取char数组的字符,索引从0开始。
这几个构造函数比较简单,根据参数就知道具体的用法,这里不测试。具体的还会在Paint介绍。
drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)
drawTextOnPath(char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint)
参数:
path:绘制的路径;可以先不管,在后面会说到。
hOffset:是指文字,相对path在水平方向的偏移
vOffset:是指文字,相对path在竖直方向的偏移。
使用:
//设置画笔的属性
mPaint.setTextSize(40);
mPaint.setStrokeWidth(5);
mPaint.setStyle(Paint.Style.STROKE);
//根据路径path 绘制矩形
Path path = new Path();
path.addRect(100,100,500,400,Path.Direction.CW);
canvas.drawPath(path,mPaint);
//根据path的路径,绘制文本
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.RED);
canvas.drawTextOnPath("Hello World !厉害了,我的国!¥#@&*$",path,30,30,mPaint);
下面2个方法是给每个字符设置一个坐标。但已过期。
drawPosText(String text, float[] pos, Paint paint)
drawPosText(char[] text, int index, int count, float[] pos, Paint paint)
在API23新添加的方法,貌似对一些特殊字符的处理,暂没使用过
drawTextRun( char[] text, int index, int count, int contextIndex, int contextCount, float x, float y, boolean isRtl, Paint paint)
drawTextRun(CharSequence text, int start, int end, int contextStart,int contextEnd, float x, float y, boolean isRtl, Paint paint)
绘制图片
需关闭硬件加速!!!
绘制Bitmap图片
drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
参数:
bitmap:图片资源对象Bitmap
left:图片在X轴的距离
top:图片在Y轴的距离
drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)
参数:
src:截取图片区域的大小,意思可绘制Bitmap的一部分,为null时,即绘制整个图片
dst:指定Bitmap图片在屏幕显示的区域,当dst != src时,会根据dst大小,来缩放src
drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) // Matrix 变换,后面再讲(挖了好多坑)
使用:
构造函数很简单,但是我们怎么获取Bitmap对象呢?方式有很多种,这里推荐使用BitmapFactory工具类,它可以从不同的数据源(指定文件、网络、输入流等)来解析、创建Bitmap对象。(这里不多说,可先自行研究)
//通过BitmapFactory 从资源文件获取图片,
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.d);
//原始图片,在(0,0)开始显示
canvas.drawBitmap(bitmap,0,0, mPaint);
//剪切图片,并指定显示区域, 这时dst > src 会拉伸图片
Rect src = new Rect(100, 0, 400, bitmap.getHeight());
Rect dst = new Rect(0, 500, getWidth(), 800);
canvas.drawBitmap(bitmap,src,dst, mPaint);
drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)
这个方法用于扭曲图片,使用起来还是有点复杂,这里暂不演示,先介绍下方法。
参数:
meshWidth:该参数控制在X轴方向把该图片划分为多少格
meshHeight:该参数控制在Y轴方向把该图片划分为多少格
verts:长度为 (meshWidth+1)*(meshHeight+1)*2+ vertOffset 的数组,他记录了扭曲后的位图各顶点(网格线交点) 位置,虽然他是一个一维数组,但是实际上它记录的数据是形如(x0,y0),(x1,y1)..(xN,Yn)格式的数据, 这些数组元素控制对bitmap位图的扭曲效果
vertOffset:控制verts数组从第几个数组元素开始对bitmap进行扭曲(忽略verOffset之前数据 的扭曲效果),通常设置为0。
colors:长度为 (meshWidth+1) * (meshHeight+1) + colorOffset 的数组,设置网格顶点的颜色,该颜色会和位图对应像素的颜色叠加。
colorOffset:控制colors数组从第几个顶点开始对顶点转换颜色(忽略colorOffset之前数据的颜色),通常传 0。
绘制Picture 图片
drawPicture(Picture picture)
drawPicture(Picture picture, RectF dst)
drawPicture(Picture picture, Rect dst)
参数:
dst:指定Picture 图片在屏幕显示的区域,根据dst区域大小来缩放Picture
使用:
也是类似,主要是怎么获取Picture 对象?这里直接使用Picture的构造函数,然后对Picture 进行操作。
使用:
/**
* 绘制Picture图片
*/
Picture mPicture = new Picture();//一张空白的Picture
//该方法在初始化的时候调用
private void testDrawPicture(){
//1.开始录制
Canvas c = mPicture.beginRecording(getWidth(), getHeight());
//2.绘制内容
mPaint.setStyle(Paint.Style.STROKE);
c.drawCircle(300,300,100,mPaint);
mPaint.setTextSize(20);
c.drawText("画一个圈圈诅咒你",200,450,mPaint);
//3.结束录制
mPicture.endRecording();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//4.显示Picture图片
canvas.drawPicture(mPicture);
}
绘制NinePatch图片(先放着,后面用到再补充)
drawPatch(NinePatch patch, RectF dst, Paint paint)
drawPatch(NinePatch patch, Rect dst, Paint paint)
画布的操作
在介绍操作之前,再强调一次:
关于画布的操作,多是操作Canvas,与屏幕无关。上一次的操作已经绘制在屏幕上,所以不影响上一次的绘制。
平移
translate(float dx, float dy)
参数:
dx:在X轴平移的距离,值可以为正负
dy:在Y轴平移的距离,值可以为正负
在上面已经使用过该方法,这里不再示范。
旋转
rotate(float degrees) //默认轴点是画布的原坐标(0,0)
rotate(float degrees, float px, float py)
参数:
degrees:旋转的角度,正数向顺时针旋转,负数向逆时针旋转
px:旋转轴点的X坐标
py:旋转轴点的Y坐标
使用:
/**
* 注意:先将画布移动到中间点,不然使用旋转后,有时会看不到绘制的效果,显示在屏幕外
*/
mPaint.setStrokeWidth(10);
canvas.translate(300,300);//那么,对于旋转来说,相对于屏幕的坐标(300,300)就是画布原点
canvas.drawLine(0,0,getWidth(),0,mPaint);
/**
* 第一种方式
*/
// canvas.rotate(30);
// mPaint.setColor(Color.RED);
// canvas.drawLine(0,0,400,0,mPaint);
//
// canvas.rotate(60);
// mPaint.setColor(Color.GREEN);
// canvas.drawLine(0,0,400,0,mPaint);
/**
* 第二种方式
*/
canvas.rotate(30,getWidth()/2,0);//此次操作,是相对与上一次平移后的画布,不是整个屏幕旋转
mPaint.setColor(Color.RED);
canvas.drawLine(0,0,getWidth(),0,mPaint);
canvas.rotate(60,getWidth()/2,0); //此次操作,是相对与上一次的画布,所以最终旋转了90度
mPaint.setColor(Color.GREEN);
canvas.drawLine(0,0,getWidth(),0,mPaint);
这里对第2种方式,画出示意图简要分析:(还是不懂,那就撸起来吧~)
通过上面的示意图,应该大致的明白,也可以看出:
- 这里所有操作,多是针对Canvas画布,与屏幕无关,与视图无关(动画是可以根据视图来旋转,这里不是)
- 每次对Canvas画布操作,不影响上次的绘制结果,但会影响后面的绘制,即每次相对于上一次的位置(暂时不考虑save() 、restore());
缩放
scale(float sx, float sy) //默认轴点是画布的原坐标(0,0)
scale(float sx, float sy, float px, float py)
参数:
sx:水平方向缩放比例,正数向正方向缩放,负数向反方向缩放
sy:竖直方向缩放比例,正数向正方向缩放,负数向反方向缩放
px:缩放轴点的X坐标
py:缩放轴点的Y坐标
使用:
mPaint.setStrokeWidth(10);
mPaint.setStyle(Paint.Style.STROKE);
canvas.translate(300,300);
canvas.drawRect(0,0,200,200,mPaint);
/**
* 第一种方式
*/
// canvas.scale(1.5f, 1.5f);
// mPaint.setColor(Color.RED);
// canvas.drawRect(0,0,200,200,mPaint);
/**
* 第二种方式
*/
canvas.scale(-1.5f, -1.5f,100,0);//放大倍数为负数,向反方法缩放
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,200,200,mPaint);
错切
skew(float sx, float sy)
参数:
sx: 在X轴倾斜的系数 ,sx=0,不倾斜;sx>0,向X轴正方向倾斜;sx<0,向X轴负方向倾斜;
sy:在Y轴倾斜的系数,sy=0,不倾斜;sy>0,向Y轴正方向倾斜;sy<0,向Y轴负方向倾斜;
以上2个参数,具体算法,是倾斜角度的tan值,即:sx = tanθ ,如果算大致角度(用计算器):θ=arctansx
使用:
mPaint.setStrokeWidth(10);
mPaint.setStyle(Paint.Style.STROKE);
canvas.translate(300,300);
canvas.drawRect(0,0,200,200,mPaint);
canvas.skew(1,2);//设置倾斜系数
mPaint.setColor(Color.RED);
canvas.drawRect(0,0,200,200,mPaint);
裁剪
clipRect(int left, int top, int right, int bottom)
clipRect(Rect rect)
clipRect(float left, float top, float right, float bottom)
clipRect(RectF rect)
以上构造函数,使用矩形形状来剪切画布
clipPath(Path path) //使用path路径来剪切画布,定义自己想要的形状
使用:
Bitmap bitmap = ((BitmapDrawable)getResources().getDrawable(R.mipmap.d, null)).getBitmap();//定义一涨图片
/**
* 使用矩形形状
*/
canvas.clipRect(100,0,400,300);
canvas.drawBitmap(bitmap,0,0,mPaint);
/**
* 使用Path自定义形状
*/
Path path = new Path();
path.moveTo(150,0);
path.lineTo(250,400);
path.lineTo(450,400);
path.lineTo(550,0);
canvas.clipPath(path);
// canvas.clipOutPath(path); //clipPath() 剪切区域 取反
canvas.drawBitmap(bitmap,0,0,mPaint);
在API26,剪切的方法有了改变:
以下方法已经过期:
clipRect(Rect rect, Region.Op op)
clipRect(float left, float top, float right, float bottom, Region.Op op)
clipRect(RectF rect, Region.Op op)
clipPath(Path path, Region.Op op)
当需要剪切多个区域,可以根据Region.Op的值,处理重叠的部分(交集)。后面讲解Path也有类似参数。
既然过期了,那。。。这里不示范了!(又可以偷懒)
以下方法是重新添加:
clipOutRect(int left, int top, int right, int bottom)
clipOutRect(Rect rect)
clipOutRect(float left, float top, float right, float bottom)
clipOutRect(RectF rect)
clipOutPath(Path path)
以上构造函数,就是方法名多了“out”,其他没什么却别。意思clipRect()取反的区域?实践下:
使用:参考上面代码,已标注
在API 26,移除关于Region.Op的方法,又添加新的方法,猜测应该是有关联的。
是的,官方介绍,以后要使用类似Region.Op的效果(对重叠部分的处理),后面可以用clipRect( )和clipOutRect( )组合来替代。
保存与恢复
save()
把当前画布的快照保存在栈中,从下往上保存,可多次调用
save(int saveFlags) // 在API 26已经过期,只是用save(),默认saveFlags为ALL_SAVE_FLAG,性能更好
参数:(关于有 saveFlags参数的方法,在API 26都已经过期,下面只是简单介绍下)
saveFlags:根据saveFlags保存部分状态,其值可以有:
ALL_SAVE_FLAG(默认):保存全部状态
CLIP_SAVE_FLAG:保存剪辑区
CLIP_TO_LAYER_SAVE_FLAG:剪裁区作为图层保存
FULL_COLOR_LAYER_SAVE_FLAG:保存图层的全部色彩通道
HAS_ALPHA_LAYER_SAVE_FLAG:保存图层的alpha(不透明度)通道
MATRIX_SAVE_FLAG:保存Matrix信息
restore()
恢复栈种保存的画布快照,从上往下恢复,可多次调用
restoreToCount(int saveCount)
恢复指定位置以及以上所有状态(即恢复:saveCount ~ getSaveCount()),并恢复索引为saveCount的状态。
getSaveCount()
获取状态栈中保存状态的数量
在平时使用的时候,保存和恢复一般多是组合使用:
save()
... //绘制操作
restore()
如处理平移、旋转、缩放画布等,如果旋转30°,后面的画布一直是旋转的,希望这次旋转不影响后面的操作,那么就可以:
save(); //保存当前状态
canvas.rotate(30);
canvas.drawLine(0,0,400,0,mPaint);
restore(); //恢复保存的状态
......
具体原理图:(借鉴)
关于图层的方法:(可以了解Xfermode之后再来阅读)
saveLayer(RectF bounds, Paint paint)
saveLayer(float left, float top, float right, float bottom, Paint paint)
saveLayerAlpha(RectF bounds, int alpha) // 可以设置透明度
saveLayerAlpha(float left, float top, float right, float bottom, int alpha)
//以上方法都会返回一个ID值,即栈层索引,比如保存在第三层,则返回2
作用:类似save( ),但会重定向到一个新的图层,在新的图层上面渲染绘制,也称为离屏缓存 (Off-screen Buffer) 。
以上方法也是将图层放在同一个栈中,所以操作完成之后,同样可以通过restore()恢复上一次图层的状态,同时也是合并图层。正因为新建一个新的图层,所以性能消耗高,应避免使用。一般在应用alpha, Xfermode, ColorFilter会使用到。
这里是一个栗子:
在使用setXfermode() ,根据指定模式,需要组合2张图像,就需要新建一个图层,先将这2张图渲染之后,在整合到canvas上,这里借鉴一张动态图:(引用地址)
具体使用:
int saved = canvas.saveLayer(0, 0, W*2 ,H*2, mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(mDstBitmap, 0, 0, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(mSrcBitmap, W/2, H/2, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(saved);
这里,构造函数需要指定一个区域,这里不能随便写,需要控制好大小,不同的区域会有不同的结果,再借鉴一张图:
结论:这里,区域不能太小,这样将不会受到 Xfermode 的影响,也不能太大,会消耗太多性能。在图中,能够刚好覆盖矩形和圆形的大小,则较为合适。
save() 与 saveLayer()的区别
先看先官方的解释:
save()只是将某种状态(matrix、clip、rotate)保存在私有栈中。
比如操作旋转、缩放、错切时,先保存状态(),再操作,最后通过restore( )恢复保存的状态。这样可以达到的效果是:旋转、缩放、错切等操作,不会影响后面画布的绘制。
问题:有些人可能以为,restore( )之后,会把这次绘制的所有东西先清除,然后再恢复到上一次状态。
并不是这样子。save()保存只是画布的状态,并没有将这次绘制的内容保存作为一个还原点,和内容完全没有关系。也可以这样子理解,每次对画布的操作,其实画布都是干净的,因为他的内容已经绘制到屏幕上去了。
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.YELLOW);
canvas.drawRect(100,100,500,400,mPaint);
canvas.save(); //保存
canvas.rotate(45,250,200);
mPaint.setColor(Color.RED);
// canvas.drawRect(new Rect(200,200,600,500), mPaint);
canvas.drawRect(100,100,500,400,mPaint);
canvas.restore();//恢复
canvas.save();//保存
canvas.rotate(30,250,200);
mPaint.setColor(Color.GREEN);
// canvas.drawRect(new RectF(300,300,700,600), mPaint);
canvas.drawRect(100,100,500,400,mPaint);
canvas.restore();//恢复
//多次调用
canvas.restore();
canvas.restore();
canvas.restore();
canvas.restore();
最终,所有的内容还是会显示出来:
saveLayer( )也是与save()类似,但是会重定向到一个图层(离屛缓存区),后面的绘制多是在这个图层进行的。如果没有调用restore()方法,后面继续在这个图层绘制,继续按照某种模式绘制;如果调用了,就回到canvas(如果还嵌套其他图层,则回到上一个图层)。
看了很多博客,说每次drawXxx多会在新的透明图层绘制,这里是否会有新的图层?需要打个问号。
如果是新的图层,那和saveLayer( )类似,多将是耗能的工作,说不过去,所以我觉得不会。个人觉得drawXxx()之后,内容将会在canvas清空,然后又是一个干净的画布。但也只是猜测,具体应该还需要看底层实现看才知道,待功力加强后再研究下。
其实,也不用管太多,实践之后,有自己的理解。现在只要理解他们的功能即可,嘎嘎~
结语
好了,Canvas的介绍就到这里,里面还关联到其他很多知识点(Path、Paint、PorterDuff.Mode、Xfermode),后面会一点点补上。
内容属于个人理解,如果议论,欢迎指出,感谢!!!
(持续更改中......)
参考:
https://developer.android.google.cn/reference/android/graphics/Canvas
https://juejin.im/user/552f20a7e4b060d72a89d87f/posts
https://blog.csdn.net/harvic880925/article/details/50995268
https://blog.csdn.net/column/details/14815.html