前言
- 自定义View是Android开发者必须了解的基础;而Canvas类的使用在自定义View绘制中发挥着非常重要的作用
- 网上有大量关于自定义View中Canvas类的文章,但存在一些问题:内容不全、思路不清晰、简单问题复杂化等等
- 今天,我将全面总结自定义View中的Canvas类的使用,我能保证这是市面上的最全面、最清晰、最易懂的
- 文章较长,建议收藏等充足时间再进行阅读
- 阅读本文前请先阅读 自定义View基础 - 最易懂的自定义View原理系列
目录
![目录](https://i-blog.csdnimg.cn/blog_migrate/6eacbe41a6397fe33f4ee9e3a2a60a39.webp?x-image-process=image/format,png)
1. 简介
- 定义:画布,是一种绘制时的规则
是安卓平台2D图形绘制的基础
- 作用:规定绘制内容时的规则 & 内容
1. 记住:绘制内容是根据画布的规定绘制在屏幕上的
2. 理解为:画布只是绘制时的规则,但内容实际上是绘制在屏幕上的
2. Canvas的本质
请务必记住:
- 绘制内容是根据画布(Canvas)的规定绘制在屏幕上的
- 画布(Canvas)只是绘制时的规则,但内容实际上是绘制在屏幕上的
为了更好地说明绘制内容的本质和Canvas,请看下面例子:
2.1 实例
- 实例情况:先画一个矩形(蓝色);然后移动画布;再画一个矩形(红色)
- 代码分析:
canvas.drawRect(100, 100, 150, 150, mPaint1);
canvas.translate(400,500);
canvas.drawRect(100, 100, 150, 150, mPaint2);
![效果图](https://i-blog.csdnimg.cn/blog_migrate/4538e642f3affc51e9d40877359396f8.webp?x-image-process=image/format,png)
![流程分析](https://i-blog.csdnimg.cn/blog_migrate/a26f0f1d9570993e7126c60c86a37fb0.webp?x-image-process=image/format,png)
看完上述分析,你应该非常明白Canvas的本质了。
- 总结
绘制内容是根据画布的规定绘制在屏幕上的
- 内容实际上是绘制在屏幕上;
- 画布,即Canvas,只是规定了绘制内容时的规则;
- 内容的位置由坐标决定,而坐标是相对于画布而言的
注:关于对画布的操作(缩放、旋转和错切)原理都是相同的,下面会详细说明。
3. 基础
3.1 Paint类
- 定义:画笔
- 作用:确定绘制内容的具体效果(如颜色、大小等等)
在绘制内容时需要画笔Paint
- 具体使用:
步骤1
:创建一个画笔对象
步骤2
:画笔设置,即设置绘制内容的具体效果(如颜色、大小等等)
步骤3
:初始化画笔(尽量选择在View的构造函数) 具体使用如下:
private Paint mPaint = new Paint();
private void initPaint() {
mPaint.setColor(int color);
mPaint.setStyle(Style style);
mPaint.setStrokeWidth(float width)
mPaint.setStrokeWidth(10f);
mPaint.getColor()
Paint.setShader(Shader shader)
mPaint.setARGB(int a, int r, int g, int b)
mPaint.setAlpha(int a)
mPaint.getAlpha()
mPaint.setTextSize(float textSize)
mPaint.setStyle(Style style);
setTextAlign()
setUnderlineText(boolean underlineText)
setStrikeThruText(boolean strikeThruText)
setFakeBoldText(boolean fakeBoldText)
Paint.setTextSkewX(-0.5f);
Paint.setShadowLayer(5,5,5,Color.YELLOW);
}
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
Style模式效果如下:
![Style模式效果](https://i-blog.csdnimg.cn/blog_migrate/9eea5960c8c00b6d3deaae9c4df6a57a.webp?x-image-process=image/format,png)
3.2 Path类
具体请看我写的另外一篇文章:Path类的最全面详解 - 自定义View应用系列
3.3 关闭硬件加速
- 在Android4.0的设备上,在打开硬件加速的情况下,使用自定义View可能会出现问题
具体问题可以看
这里。
- 所以测试前,请先关闭硬件加速。具体关闭方式如下:
在AndroidMenifest.xml的application节点添加:
android:hardwareAccelerated="false"
4. Canvas的使用
4.1 对象创建 & 获取
Canvas对象 & 获取的方法有4个:
Canvas canvas = new Canvas();
Canvas canvas = new Canvas(bitmap)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
SurfaceView surfaceView = new SurfaceView(this);
SurfaceHolder surfaceHolder = surfaceView.getHolder();
Canvas c = surfaceHolder.lockCanvas();
surfaceHolder.unlockCanvasAndPost(c);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
官方推荐方法4来创建并获取Canvas,原因:
- SurfaceView里有一条线程是专门用于画图,所以方法4的画图性能最好,并适用于高质量的、刷新频率高的图形
- 而方法3刷新频率低于方法3,但系统花销小,节省资源
4.2 绘制方法使用
- 利用Canvas类可绘画出很多内容,如图形、文字、线条等等;
- 对应使用的方法如下:
仅列出常用方法,更加详细的方法可参考官方文档 Canvas
![Canvas绘制方法](https://i-blog.csdnimg.cn/blog_migrate/d75d5897c1a91ede9cf10b6f3d5b4b3b.webp?x-image-process=image/format,png)
下面我将逐个方法进行详细讲解
特别注意
Canvas具体使用时是在复写的onDraw()里:
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
// 对Canvas进行一系列设置
// 如画圆、画直线等等
canvas.drawColor(Color.BLUE);
// ...
}
}
具体为什么,请看我写的自定义View原理系列文章:
自定义View Measure过程 - 最易懂的自定义View原理系列(2)
自定义View Layout过程 - 最易懂的自定义View原理系列(3)
自定义View Draw过程- 最易懂的自定义View原理系列(4)
4.2.1 绘制颜色
canvas.drawColor(Color.BLUE);
![效果图](https://i-blog.csdnimg.cn/blog_migrate/3ef05b0fba40e80c12f03032e628273f.webp?x-image-process=image/format,png)
4.2.2 绘制基本图形
a. 绘制点(drawPoint)
-
原理:在某个坐标处绘制点
可画一个点或一组点(多个点)
-
具体使用
canvas.drawPoint(300, 300, mPaint1);
canvas.drawPoints(new float[]{
600,500,
600,600,
600,700
},mPaint2);
![效果图](https://i-blog.csdnimg.cn/blog_migrate/5fd71da1aade180d95f020cc9b3432cd.webp?x-image-process=image/format,png)
b. 绘制直线(drawLine)
- 原理:两点(初始点 & 结束点)确定一条直线
- 具体使用:
canvas.drawLine(100,200,700,200,mPaint1);
canvas.drawLines(new float[]{
400,500,500,500,
400,600,500,600
},mPaint2);
}
![效果图](https://i-blog.csdnimg.cn/blog_migrate/752397d0975a22bea1979cb6b675ab30.webp?x-image-process=image/format,png)
c. 绘制矩形(drawRect)
-
原理:矩形的对角线顶点确定一个矩形
一般是采用左上角和右下角的两个点的坐标。
-
具体使用
canvas.drawRect(100,100,800,400,mPaint);
Rect rect = new Rect(100,100,800,400);
canvas.drawRect(rect,mPaint);
RectF rectF = new RectF(100,100,800,400);
canvas.drawRect(rectF,mPaint);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
![效果图](https://i-blog.csdnimg.cn/blog_migrate/cf187691802ec56c1b1b8a776cf77a51.webp?x-image-process=image/format,png)
d. 绘制圆角矩形
-
原理:矩形的对角线顶点确定一个矩形
类似于绘制矩形
-
具体使用
canvas.drawRoundRect(100,100,800,400,30,30,mPaint);
RectF rectF = new RectF(100,100,800,400);
canvas.drawRoundRect(rectF,30,30,mPaint);
![效果图](https://i-blog.csdnimg.cn/blog_migrate/e8da463feb7d532d15fbbcaa16d68878.webp?x-image-process=image/format,png)
- 与矩形相比,圆角矩形多了两个参数rx 和 ry
- 圆角矩形的角是椭圆的圆弧,rx 和 ry实际上是椭圆的两个半径,如下图:
![椭圆示意图](https://i-blog.csdnimg.cn/blog_migrate/d3eef1963cad507e05832e1bde52e075.webp?x-image-process=image/format,png)
- 特别注意:当 rx大于宽度的一半, ry大于高度一半 时,画出来的为椭圆
实际上,在rx为宽度的一半,ry为高度的一半时,刚好是一个椭圆;但由于当rx大于宽度一半,ry大于高度一半时,无法计算出圆弧,所以drawRoundRect对大于该数值的参数进行了修正,凡是大于一半的参数均按照一半来处理
e. 绘制椭圆
RectF rectF = new RectF(100,100,800,400);
canvas.drawOval(rectF,mPaint);
canvas.drawOval(100,100,800,400,mPaint);
canvas.drawRect(100,100,800,400,mPaint);
![效果图](https://i-blog.csdnimg.cn/blog_migrate/889ef3e9b40bb9e9cb81104917d64b43.webp?x-image-process=image/format,png)
f. 绘制圆
canvas.drawCircle(500,500,400,mPaint);
![具体使用](https://i-blog.csdnimg.cn/blog_migrate/0b9072c61c5617d9ae87f142382d3616.webp?x-image-process=image/format,png)
g. 绘制圆弧
- 原理:通过圆弧角度的起始位置和扫过的角度确定圆弧
- 具体使用
startAngle
sweepAngle
useCenter
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint){}
public void drawArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean useCenter, @NonNull Paint paint) {}
为了理解第三个参数:useCenter
,看以下示例:
RectF rectF = new RectF(100, 100, 800,400);
canvas.drawRect(rectF, mPaint1);
canvas.drawArc(rectF, 0, 90, false, mPaint2);
RectF rectF2 = new RectF(100,600,800,900);
canvas.drawRect(rectF2, mPaint1);
canvas.drawArc(rectF2,0,90,true,mPaint2);
![效果图](https://i-blog.csdnimg.cn/blog_migrate/e963cb40e083f3a025adb1e37ff294e9.webp?x-image-process=image/format,png)
从示例可以发现:
- 不使用中心点:圆弧的形状 = (起、止点连线+圆弧)构成的面积
- 使用中心店:圆弧面积 = (起点、圆心连线 + 止点、圆心连线+圆弧)构成的面积
类似扇形
4.2.3 绘制文字
绘制文字分为三种应用场景:
-
情况1:指定文本开始的位置
- 即指定文本基线位置
- 基线x默认在字符串左侧,基线y默认在字符串下方
-
情况2:指定每个文字的位置
- 情况3:指定路径,并根据路径绘制文字
下面分别细说:
文字的样式(大小,颜色,字体等)具体由画笔Paint控制,详细请会看上面基础的介绍
情况1:指定文本开始的位置
public void drawText (String text, float x, float y, Paint paint)
// 实例
canvas.drawText("abcdefg",300,400,mPaint1);
public void drawText (String text, int start, int end, float x, float y, Paint paint)
public void drawText (CharSequence text, int start, int end, float x, float y, Paint paint)
// 对于字符数组char[]
// 截取文本使用起始位置(index)和长度(count)
public void drawText (char[] text, int index, int count, float x, float y, Paint paint)
// 实例:绘制从位置1-3的文本
canvas.drawText("abcdefg",1,4,300,400,mPaint1);
char[] chars = "abcdefg".toCharArray();
canvas.drawText(chars,1,3,200,500,textPaint);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
![效果图](https://i-blog.csdnimg.cn/blog_migrate/0d72b20a3eabd80a71494a95eff8b86a.webp?x-image-process=image/format,png)
情况2:分别指定文本的位置
public void drawPosText (String text, float[] pos, Paint paint)
public void drawPosText (char[] text, int index, int count, float[] pos, Paint paint)
canvas.drawPosText("abcde", new float[]{
100, 100,
200, 200,
300, 300,
400, 400,
500, 500
}, mPaint1);
char[] chars = "abcdefg".toCharArray();
canvas.drawPosText(chars, 1, 3, new float[]{
300, 300,
400, 400,
500, 500,
}, mPaint1);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
![效果图](https://i-blog.csdnimg.cn/blog_migrate/7aa0b1a6d3768fa0e9e23cfcb45771ef.webp?x-image-process=image/format,png)
情况3:指定路径,并根据路径绘制文字
关于Path类的使用请看我写的文章:Path类的最全面详解 - 自定义View应用系列
Path path = new Path();
path.cubicTo(540, 750, 640, 450, 840, 600);
canvas.drawPath(path,mPaint2);
canvas.drawTextOnPath("在Path上写的字:Carson_Ho", path, 50, 0, mPaint2);
![效果图](https://i-blog.csdnimg.cn/blog_migrate/ad32c91bc8e4183125bc22e6c7be45ca.webp?x-image-process=image/format,png)
4.2.4 绘制图片
绘制图片分为:绘制矢量图(drawPicture)和 绘制位图(drawBitmap)
a. 绘制矢量图(drawPicture)
- 作用:绘制矢量图的内容,即绘制存储在矢量图里某个时刻Canvas绘制内容的操作
矢量图(Picture)的作用:存储(录制)某个时刻Canvas绘制内容的操作
- 应用场景:绘制之前绘制过的内容
1. 相比于再次调用各种绘图API,使用Picture能节省操作 & 时间
2. 如果不手动调用,录制的内容不会显示在屏幕上,只是存储起来
特别注意:使用绘制矢量图时前请关闭硬件加速,以免引起不必要的问题!
具体使用方法:
Picture.getWidth ();
Picture.getHeight ()
Picture.beginRecording(int width, int height)
Picture.endRecording ()
Picture.draw (Canvas canvas)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
一般使用的具体步骤
// 步骤1:创建Picture对象
Picture mPicture = new Picture();
// 步骤2:开始录制
mPicture.beginRecording(int width, int height);
// 步骤3:绘制内容 or 操作Canvas
canvas.drawCircle(500,500,400,mPaint);
...(一系列操作)
// 步骤4:结束录制
mPicture.endRecording ();
步骤5:某个时刻将存储在Picture的绘制内容绘制出来
mPicture.draw (Canvas canvas);
下面我将用一个实例去表示如何去使用: - 实例介绍 将坐标系移动到(450,650);绘制一个圆,将上述Canvas操作录制下来,并在某个时刻重新绘制出来。
步骤1:创建Picture对象
Picture mPicture = new Picture();
步骤2:开始录制
Canvas recordingCanvas = mPicture.beginRecording(500, 500);
// 注:要创建Canvas对象来接收beginRecording()返回的Canvas对象
步骤3:绘制内容 or 操作Canvas
// 位移
// 将坐标系的原点移动到(450,650)
recordingCanvas.translate(450,650)
// 记得先创建一个画笔
Paint paint = new Paint()
paint.setColor(Color.BLUE)
paint.setStyle(Paint.Style.FILL)
// 绘制一个圆
// 圆心为(0,0),半径为100
recordingCanvas.drawCircle(0,0,100,paint)
步骤4:结束录制
mPicture.endRecording();
步骤5:将存储在Picture的绘制内容绘制出来
有三种方法:
- Picture.draw (Canvas canvas)
- Canvas.drawPicture()
- PictureDrawable.draw()
将Picture包装成为PictureDrawable
主要区别如下:
![Paste_Image.png](https://i-blog.csdnimg.cn/blog_migrate/945c56416aa8529c16277c2ce826c8e8.webp?x-image-process=image/format,png)
方法1:Picture提供的draw()
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
mPicture.draw(canvas);
}
![效果图](https://i-blog.csdnimg.cn/blog_migrate/4d0b1ac4eaab49d7463abb7178b74824.webp?x-image-process=image/format,png)
方法2:Canvas提供的drawPicture()
不会影响Canvas状态
public void drawPicture (Picture picture)
// 方法2
// Rect dst代表显示的区域
// 若区域小于图形,绘制的内容根据选区进行缩放
public void drawPicture (Picture picture, Rect dst)
// 方法3
public void drawPicture (Picture picture, RectF dst)
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
canvas.drawPicture(mPicture, new RectF(0, 0, mPicture.getWidth(), mPicture.getHeight()));
canvas.drawPicture(mPicture, new RectF(0, 0, mPicture.getWidth(), 200));
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
![效果图](https://i-blog.csdnimg.cn/blog_migrate/85ab117cd400adf53d36f9abce84784e.webp?x-image-process=image/format,png)
方法3:使用PictureDrawable的draw方法绘制
将Picture包装成为PictureDrawable
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
PictureDrawable drawable = new PictureDrawable(mPicture);
drawable.setBounds(0, 0,mPicture.getWidth(), mPicture.getHeight());
drawable.draw(canvas);
drawable.setBounds(0, 0,250, mPicture.getHeight());
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
![效果图](https://i-blog.csdnimg.cn/blog_migrate/47a894fa5b25c4591a4296b4432900ab.webp?x-image-process=image/format,png)
b. 绘制位图(drawBitmap)
- 作用:将已有的图片转换为位图(Bitmap),最后再绘制到Canvas上
位图,即平时我们使用的图片资源
获取Bitmap对象的方式
要绘制Bitmap,就要先获取一个Bitmap对象,具体获取方式如下:
![获取Bitmap对象方式](https://i-blog.csdnimg.cn/blog_migrate/e1df3d9f1c597d2bd7b463b3fa51c51b.webp?x-image-process=image/format,png)
特别注意:绘制位图(Bitmap)是读取已有的图片转换为Bitmap,最后再绘制到Canvas。
所以:
- 对于第1种方式:排除
-
对于第2种方式:虽然满足需求,但一般不推荐使用
具体请自行了解关于Drawble的内容
-
对于第3种方式:满足需求,下面会着重讲解
通过BitmapFactory获取Bitmap (从不同位置获取):
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),R.raw.bitmap);
Bitmap bitmap=null;
try {
InputStream is = mContext.getAssets().open("bitmap.png");
bitmap = BitmapFactory.decodeStream(is);
is.close();
} catch (IOException e) {
e.printStackTrace();
}
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/bitmap.png");
Bitmap bitmap = BitmapFactory.decodeStream(is);
is.close();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
绘制Bitmap
绘制Bitmap共有四种方法:
public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)
// 方法2
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)
// 方法3
public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
// 方法4
public void drawBitmap (Bitmap bitmap, Rect src, RectF dst, Paint paint)
// 下面详细说
方法1
public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)
// 后两个参数matrix, paint是在绘制时对图片进行一些改变
// 后面会专门说matrix
// 如果只是将图片内容绘制出来只需将传入新建的matrix, paint对象即可:
canvas.drawBitmap(bitmap,new Matrix(),new Paint());
![效果图](https://i-blog.csdnimg.cn/blog_migrate/6a05b5c33a94c39e7ca3f475a4ac7b6a.webp?x-image-process=image/format,png)
方法2
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)
canvas.drawBitmap(bitmap,300,400,new Paint());
![Paste_Image.png](https://i-blog.csdnimg.cn/blog_migrate/584408ba90da2828f0c02b8c3a95e219.webp?x-image-process=image/format,png)
方法3
public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
// 参数(src,dst) = 两个矩形区域
Rect src = new Rect(0,0,bitmap.getWidth()/2,bitmap.getHeight());
Rect dst = new Rect(100,100,250,250);
canvas.drawBitmap(bitmap,src,dst,null);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
![分析](https://i-blog.csdnimg.cn/blog_migrate/e66ad0c116305b01a7b7fb74cee2e16a.webp?x-image-process=image/format,png)
特别注意的是:如果src规定绘制图片的区域大于dst指定显示的区域的话,那么图片的大小会被缩放。
方法3的应用场景:
![动态效果图](https://i-blog.csdnimg.cn/blog_migrate/5d2b5ae3efb3afe11f152b62c359c8a2.webp?x-image-process=image/format,png)
在绘制时,只需要一个资源文件,然后逐渐描绘就可以
![资源文件](https://i-blog.csdnimg.cn/blog_migrate/202a8ba6d4d243c4880f9141e66c3021.webp?x-image-process=image/format,png)
绘制过程如下:
![描绘过程](https://i-blog.csdnimg.cn/blog_migrate/336738e32449a0674e4b5c9ab7097cfa.webp?x-image-process=image/format,png)
4.2.5 绘制路径
canvas.drawPath(mPath, mPaint)
关于Path类,具体请看我写的文章: Path类的最全面详解 - 自定义View应用系列
4.2.6 画布操作
- 作用:改变画布的性质
改变之后,任何的后续操作都会受到影响
A. 画布变换
a. 平移(translate)
- 作用:移动画布(实际上是移动坐标系,如下图)
- 具体使用
canvas.translate(200, 100)
![效果图](https://i-blog.csdnimg.cn/blog_migrate/886ec742f86512205d9658cb87c2e432.webp?x-image-process=image/format,png)
b. 缩放(scale)
public final void scale(float sx, float sy)
public final void scale (float sx, float sy, float px, float py)
我将用下面的例子说明缩放的使用和缩放中心的意义。
RectF rect = new RectF(0,-200,200,0);
canvas.drawRect(rect, mPaint1);
canvas.scale(1.5f, 1.5f);
canvas.drawRect(rect,mPaint2);
RectF rect = new RectF(0,-200,200,0);
canvas.drawRect(rect, mPaint1);
canvas.scale(1.5f, 1.5f, 100,0);
canvas.drawRect(rect,mPaint2);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
![效果图](https://i-blog.csdnimg.cn/blog_migrate/57f7252a19df6fd329c8e3c5d51d9cc8.webp?x-image-process=image/format,png)
当缩放倍数为负数时,会先进行缩放,然后根据不同情况进行图形翻转:
(设缩放倍数为(a,b),旋转中心为(px,py)):
- a<0,b>0:以px为轴翻转
- a>0,b<0:以py为轴翻转
- a<0,b<0:以旋转中心翻转
具体如下图:(缩放倍数为1.5,旋转中心为(0,0)为例)
![Paste_Image.png](https://i-blog.csdnimg.cn/blog_migrate/51ef9ff0c51c829c6b84aef4fbc663be.webp?x-image-process=image/format,png)
c. 旋转(rotate)
注意:角度增加方向为顺时针(区别于数学坐标系)
![与数学坐标系对比](https://i-blog.csdnimg.cn/blog_migrate/225777a861dabdb1f1bfd1cd0c8c3b97.webp?x-image-process=image/format,png)
public final void rotate(float degrees)
// 以原点(0,0)为中心旋转 90 度
canvas.rotate(90);
public final void rotate(float degrees, float px, float py)
// 以(30,50)为中心旋转 90 度
canvas.rotate(90,30,50);
![效果图](https://i-blog.csdnimg.cn/blog_migrate/aaa9cfddfbdcfe01b330e3e1ee70107e.webp?x-image-process=image/format,png)
d. 错切(skew)
- 作用:将画布在x方向倾斜a角度、在y方向倾斜b角度
- 具体使用:
public void skew(float sx, float sy)
canvas.translate(300, 500);
canvas.drawRect(20, 20, 400, 200, mPaint2);
canvas.skew(1f, 0);
canvas.drawRect(20, 20, 400, 200, mPaint1);
canvas.skew(-1f, 0);
canvas.drawRect(20, 20, 400, 200, mPaint1);
canvas.skew(0, 1f);
canvas.drawRect(20, 20, 400, 200, mPaint1);
canvas.skew(0, -1f);
canvas.drawRect(20, 20, 400, 200, mPaint1);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
![效果图](https://i-blog.csdnimg.cn/blog_migrate/2a0c8ac3c292b93a616a41f978379dcf.webp?x-image-process=image/format,png)
B. 画布裁剪
即从画布上裁剪一块区域,之后仅能编辑该区域
特别注意:其余的区域只是不能编辑,但是并没有消失,如下图
![Paste_Image.png](https://i-blog.csdnimg.cn/blog_migrate/53017f8346e9b0c4ca506fec045953d5.webp?x-image-process=image/format,png)
裁剪共分为:裁剪路径、裁剪矩形、裁剪区域
public boolean clipPath(@NonNull Path path)
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op)
public boolean clipRect(int left, int top, int right, int bottom)
public boolean clipRect(float left, float top, float right, float bottom)
public boolean clipRect(float left, float top, float right, float bottom,
@NonNull Region.Op op)
public boolean clipRegion(@NonNull Region region)
public boolean clipRegion(@NonNull Region region, @NonNull Region.Op op)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
这里特别说明一下参数Region.Op op
作用:在剪下多个区域下来的情况,当这些区域有重叠的时候,这个参数决定重叠部分该如何处理,多次裁剪之后究竟获得了哪个区域,有以下几种参数:
![Paste_Image.png](https://i-blog.csdnimg.cn/blog_migrate/37675692e1d414bfe8e8f312c3cd31f0.webp?x-image-process=image/format,png)
以三个参数为例讲解:
Region.Op.DIFFERENCE:显示第一次裁剪与第二次裁剪不重叠的区域
![Paste_Image.png](https://i-blog.csdnimg.cn/blog_migrate/c88e83af37ffb6b0bf21fd9ff8a8dee7.webp?x-image-process=image/format,png)
canvas.translate(300, 500);
canvas.drawColor(Color.GRAY);
canvas.clipRect(0, 0, 600, 600);
canvas.drawColor(Color.RED);
canvas.clipRect(0, 200, 600, 400, Region.Op.DIFFERENCE);
canvas.drawColor(Color.BLACK);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
Region.Op.REPLACE:显示第二次裁剪的区域
![](https://i-blog.csdnimg.cn/blog_migrate/db356faa8e78dc5e250031b5ba38b4ba.webp?x-image-process=image/format,png)
//原来画布设置为灰色)
canvas.drawColor(Color.GRAY)
//第一次裁剪
canvas.clipRect(0, 0, 600, 600)
//将第一次裁剪后的区域设置为红色
canvas.drawColor(Color.RED)
//第二次裁剪,并显示第二次裁剪的区域
canvas.clipRect(0, 200, 600, 400, Region.Op.REPLACE)
//将第二次裁剪的区域设置为蓝色
canvas.drawColor(Color.BLUE)
Region.Op.INTERSECT:显示第二次与第一次的重叠区域
![Paste_Image.png](https://i-blog.csdnimg.cn/blog_migrate/f7ceb869bb82b958487ccc8aed96784b.webp?x-image-process=image/format,png)
//原来画布设置为灰色)
canvas.drawColor(Color.GRAY)
//第一次裁剪
canvas.clipRect(0, 0, 600, 600)
//将第一次裁剪后的区域设置为红色
canvas.drawColor(Color.RED)
//第二次裁剪,并显示第一次裁剪与第二次裁剪重叠的区域
canvas.clipRect(-100, 200, 600, 400, Region.Op.INTERSECT)
//将第一次裁剪与第二次裁剪重叠的区域设置为黑色
canvas.drawColor(Color.BLACK)
关于其他参数,较为简单,此处不作过多展示。
C. 画布快照
这里先理清几个概念
- 画布状态:当前画布经过的一系列操作
- 状态栈:存放画布状态和图层的栈(后进先出)
![状态栈](https://i-blog.csdnimg.cn/blog_migrate/a01e277692343e7e91aa7bb66b960f02.webp?x-image-process=image/format,png)
- 画布的构成:由多个图层构成,如下图
- 在画布上操作 = 在图层上操作
- 如无设置,绘制操作和画布操作是默认在默认图层上进行
- 在通常情况下,使用默认图层就可满足需求;若需要绘制复杂的内容(如地图),则需使用更多的图层
- 最终显示的结果 = 所有图层叠在一起的效果
![画布构成 - 图层](https://i-blog.csdnimg.cn/blog_migrate/c1cce2b1dcb682c20f5e4cacefc0cd40.webp?x-image-process=image/format,png)
a. 保存当前画布状态(save)
- 作用:保存画布状态(即保存画布的一系列操作)
- 应用场景:画布的操作是不可逆的,而且会影响后续的步骤,假如需要回到之前画布的状态去进行下一次操作,就需要对画布的状态进行保存和回滚
public int save ()
public int save (int saveFlags)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
![入栈](https://i-blog.csdnimg.cn/blog_migrate/a01e277692343e7e91aa7bb66b960f02.webp?x-image-process=image/format,png)
b. 保存某个图层状态(saveLayer)
- 作用:新建一个图层,并放入特定的栈中
- 具体使用
使用起来非常复杂,因为图层之间叠加会导致计算量成倍增长,营尽量避免使用。
public int saveLayer (RectF bounds, Paint paint)
public int saveLayer (RectF bounds, Paint paint, int saveFlags)
public int saveLayer (float left, float top, float right, float bottom, Paint paint)
public int saveLayer (float left, float top, float right, float bottom, Paint paint, int saveFlags)
// 有图层alpha(不透明度)通道
public int saveLayerAlpha (RectF bounds, int alpha)
public int saveLayerAlpha (RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha, int saveFlags)
c. 回滚上一次保存的状态(restore)
canvas.restore();
![效果图](https://i-blog.csdnimg.cn/blog_migrate/a01e277692343e7e91aa7bb66b960f02.webp?x-image-process=image/format,png)
d. 回滚指定保存的状态(restoreToCount)
- 作用:恢复指定状态;将指定位置以及以上所有状态出栈
- 具体使用:
canvas.restoreToCount(3) ;
![效果图](https://i-blog.csdnimg.cn/blog_migrate/0dbe7431a572c3e97d790958b3d58a0b.webp?x-image-process=image/format,png)
e. 获取保存的次数(getSaveCount)
- 作用:获取保存过图层的次数
即获取状态栈中保存状态的数量
canvas.getSaveCount();
总结
对于画布状态的保存和回滚的套路,一般如下:
// 步骤1:保存当前状态
// 把Canvas的当前状态信息入栈
save();
// 步骤2:对画布进行各种操作(旋转、平移Blabla)
...
// 步骤3:回滚到之前的画布状态
// 把栈里面的信息出栈,取代当前的Canvas信息
restore();
5. 总结
- 通过阅读本文,相信你已经全面了解Canvas类的使用;
- 如果希望继续了解自定义View的原理,请参考我写的文章:
自定义View基础 - 最易懂的自定义View原理系列(1)
自定义View Measure过程 - 最易懂的自定义View原理系列(2)
自定义View Layout过程 - 最易懂的自定义View原理系列(3)
自定义View Draw过程- 最易懂的自定义View原理系列(4)
请帮顶或评论点赞!因为你们的赞同/鼓励是我写作的最大动力!