目录
-
坐标系以及view的位置信息
自定义view在平时有很多应用,我们知道自定义view,其实就是通过Canvas进行绘制,但是在绘制之前,一些基本的知识要明确,安卓的坐标系和我们实际在数学中用的坐标系还有一些区别, 在安卓中初始化以屏幕的左上角为原点,
这是关于坐标的解释,还有我们再绘画view的时候。经常用到view的长宽,left,top,right,bottom的坐标。view获取左,上,右下这些坐标都是基于parent的基础,是指相对parent的距离。不是相对于整个屏幕的。
通过procession画的,还处于摸索阶段,如果哪位同学知道画数学函数之类好用的软件可以推荐一下。
-
API简介
作用 | Api | 说明 |
---|---|---|
绘制颜色 | drawColor, drawRGB, drawARGB | 通过ARGB设置画布颜色 |
绘制基本形状 | drawPoints, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc,drawPath | 绘制点,线,长方形,圆形,椭圆,弧形,以及路径 |
绘制图片 | drawBitmap, drawPicture | 绘制位图 |
绘制文本 | drawText, drawPosText, drawTextOnPath | 绘制各式文本,其中根据路径绘制比较常用 |
画布裁剪 | clipPath, clipRect | 可以设置画布的展示区域 |
画布状态 | save, restore, saveLayerXxx, restoreToCount, getSaveCount | 此部分api就是保存图层状态、 回滚到指定状态、 获取保存次数,可以看成类似git保存版本状态 |
画布变换 | translate, scale, rotate, skew | 位移、缩放、 旋转、错切 |
Matrix | getMatrix, setMatrix, concat | 实际上画布的位移,缩放等操作的都是图像矩阵Matrix,通过C++源码可以看出。在这里google已经把matrix封装好,但是如果需要特殊效果,需要自己使用matrix操作,比如窗口抖动等效果没有封装好的api可用 |
-
Canvas基本操作
要画东西,你需要4个基本组件:一个用来容纳像素的位图,一个用来承载画布的画布绘制调用(写入位图),一个绘制素材(例如Rect,
路径,文本,位图),和画笔(描述颜色和样式画)。其中关于绘制基本形状没有什么可说的。其中绘制弧度:
这里是画了一个圆弧,其实画圆弧就是截取一个长方形内切椭圆的一段弧度,代码如下:
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawArc(canvas);
}
private void drawArc(Canvas canvas){
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(5);
mPaint.setAntiAlias(true);
//表示空心,这样画出的是线,否则就是实心的图形
mPaint.setStyle(Paint.Style.STROKE);
canvas.translate(500,500);
RectF rf = new RectF(0,0,300,300);
canvas.drawArc(rf,0,-80,false,mPaint);
canvas.drawRect(rf,mPaint);
}
画出这个长方形是为了更好的理解弧度的由来。这里需要注意的有几点,1 Rect和RectF都是表示长方形,只是RectF参数是float,Rect参数是int。 2 画长方形他的四个参数分别是,左,上,右,下,要注意底部的坐标必须大于顶部,右部必须大于左部。 3 在ondraw千万不允许用new函数,因为ondraw会频繁调用,如果使用new,分配大量内存,会造成内存抖动,这里只是为了演示代码,所以使用了new。
对于path其实可以看出一连串的点连接而成。 其中还可以包括贝塞尔曲线之类的。我们可以根据曲线来画文字,比如
代码如下:
private void drawTexts(Canvas canvas){
Path paths = new Path();
canvas.translate(500,500);
RectF rf = new RectF(0,-400,400,0);
paths.addArc(rf, 60, 180);
canvas.drawPath(paths,mPaint);
mPaint.setTextSize(50);
canvas.drawTextOnPath("中国人民万岁", paths, 0, -20, mPaint);
}
drawTextOnPath中的3,4个参数是指文字相对path水平和竖直方向的位移。
-
Canvas变化
我们可以大致把canvas的变化分为四类,位移(translate),旋转(rotate),缩放(scale),倾斜(skew).
1 translate 位移比较简单。 就是将当前的原点一定到指定的x,y的位置。
比如:
private void drawTranslate(Canvas canvas){
//初始的时候原点为(0,0),画一个圆心为(200,200)半径为100的圆
mPaint.setColor(Color.GREEN);
canvas.drawCircle(200,200,100,mPaint);
//将原点移动到200,200
canvas.translate(200,200);
//移动之后(200,0)就相当于移动前,(400,200.)
mPaint.setColor(Color.parseColor("#ff00ff"));
canvas.drawCircle(200,0,100,mPaint);
}
移动是可以叠加的。 第二次移动是以第一次移动后的原点位置为标准。其他以此类推。
2 scale,缩放。 api提供了2中缩放的方法:
public void scale(float sx, float sy) {
if (sx == 1.0f && sy == 1.0f) return;
nScale(mNativeCanvasWrapper, sx, sy);
}
public final void scale(float sx, float sy, float px, float py) {
if (sx == 1.0f && sy == 1.0f) return;
translate(px, py);
scale(sx, sy);
translate(-px, -py);
}
可以看出两种缩放的方式,第一个就直接进行缩放,第二种是以px,py为原点进行缩放。区别可看如下代码:
我们做如下效果的缩放:
private void drawScale(Canvas canvas){
canvas.translate(400,800);
mPaint.setColor(Color.RED);
RectF rt = new RectF(0,-300,400,0);
//简单的 画2条线当做X,Y轴
mPaint.setColor(Color.BLACK);
canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
//首先画出黑色的基础矩形
mPaint.setColor(Color.BLACK);
canvas.drawRect(rt,mPaint);
//保存当前设置
canvas.save();
//将坐标轴按x轴,y轴分别扩大1.3倍,然后再用蓝色画这个矩形,
//会发现蓝色矩形长宽都是原来基础矩形的1.3被,当这里设置(0,1)时缩小,(1,+∞)时扩大
canvas.scale(1.3f,1.3f);
mPaint.setColor(Color.BLUE);
canvas.drawRect(rt,mPaint);
//恢复保存以前的状态。就是canvas没有放大前的状态,和save成对出现
canvas.restore();
canvas.save();
//以200,0为原点进行缩放,将x,y缩小为原来的0.5倍,然后再画。
//其实他是分为三步,第一个进行唯一,translate(200,0),然后缩放scal(0.5,0.5)
//然后在进行位移(-200,0);但是第二次位移在缩放的基础上了。所以再位移-200,并没有回到原来的缩放前的原点。
//因为是在(200,0)的基础上唯一,x轴缩小0.5,位移-200,变为-100.所以实际上如果没有缩放,他是在(200,0)
//的基础上进行为(-100,0)的位移。所以虽然看着(200,0)和(-200,0)正好互补,但是因为缩放的存在不能回到原来了。
//所以红色的矩形如图所示的位置
canvas.scale(0.5f,0.5f,200,0);
mPaint.setColor(Color.RED);
canvas.drawRect(rt,mPaint);
//再次回到save之前的状态,即canvas没有做任何变化的状态
canvas.restore();
canvas.save();
//(2,-0.5)x轴变为原来的2倍,y轴首先变为原来的0.5被,然后需要y轴的反转。
canvas.scale(2,-0.5f);
mPaint.setColor(Color.parseColor("#ff00ff"));
canvas.drawRect(rt,mPaint);
//画一个紫色的变化后的矩形。如图所示
canvas.restore();
}
我们看最初展示的android源码可以看出。scale(float sx, float sy, float px, float py)是先进行位移再旋转,然后再次位移。translate(px, py)移动的物理距离分别是px和py,经过scale(sx, sy)缩放后再通过translate(-px, -py)位移,移动的物理距离就是-px*sx和-py*sy。
所以我们可以看出缩放情况如下:
倍数(n) | 说明 |
---|---|
(-∞, 0) | 首先对x,y轴进行n倍伸缩,然后对应的坐标轴 |
(0,+∞) | 直接将对应的x,y轴进行n倍伸缩 |
n==1 | 根据代码可以看出不做任何变化 |
3 rotate(旋转),旋转同样提供了2中方法
public void rotate(float degrees) {
if (degrees == 0.0f) return;
nRotate(mNativeCanvasWrapper, degrees);
}
public final void rotate(float degrees, float px, float py) {
if (degrees == 0.0f) return;
translate(px, py);
rotate(degrees);
translate(-px, -py);
}
第一种是直接以原点为中心进行旋转degrees度, 第二种是以px,py为原点进行旋转。它也是分为3步,1是进行位移(px,py),然后旋转,之后再进行位移(-px,-py),同理,旋转之后,表面看两次位移正好互补,但是不能回到原来的点了。首先来看以原点为标准旋转
坐标轴变了,所有的坐标点也就变了,计算的时候。 你可以仍然以水平为X轴思考,无论是画线,还是图形。在原来X轴上的操作,旋转之后会和X轴一样旋转。比如下图:
private void drawRotate(Canvas canvas){
canvas.translate(400,800);
mPaint.setColor(Color.RED);
RectF rt = new RectF(0,-300,400,0);
//简单的 画2条线当做X,Y轴
mPaint.setColor(Color.BLACK);
canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
canvas.drawCircle(500,-100,100,mPaint);
mPaint.setStrokeWidth(30);
mPaint.setColor(Color.GREEN);
canvas.drawPoint(0,0,mPaint);
mPaint.setStrokeWidth(5);
canvas.save();
//旋转60度
canvas.rotate(60);
mPaint.setColor(Color.RED);
//画x,y轴还有圆形的代码和旋转一直一模一样
canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
canvas.drawCircle(500,-100,100,mPaint);
canvas.restore();
}
通过看代码我们会发现。 绘画黑色的坐标轴与圆形, 和旋转之后画红色的坐标与圆形的代码一模一样。 所以无论怎样旋转,任何图形与其坐标轴的相对位置不会发生改变。这就是网上很多圆形进度条的原理。如下效果:
这种气势就是画一条水平的线,然后不停的旋转。
4 倾斜skew
public void skew(float sx, float sy) {
if (sx == 0.0f && sy == 0.0f) return;
nSkew(mNativeCanvasWrapper, sx, sy);
}
参数sx,sy分别是x,y轴上倾斜角度的tan值。比如skew(1,1)则分别是切斜45度
这是x轴切斜45度的效果,代码如下:
private void drawSkew(Canvas canvas){
canvas.translate(400,800);
RectF rt = new RectF(0,-300,400,0);
//简单的 画2条线当做X,Y轴
mPaint.setColor(Color.BLACK);
canvas.drawLine(-400,0,getWidth()-400,0,mPaint);
canvas.drawLine(0,-800,0,getHeight()-800,mPaint);
canvas.drawRect(0,0,200,200,mPaint);
canvas.skew(1,0);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,200,200,mPaint);
}
其实可以把canvas的操作看成是画很多层,移动之后就属于在另一层重新画,无论怎样移动,都可以想象成再水平面操作。因为所有的绘画元素与原点的相对位置不变。
需要注意一点,绘画的移动,缩放,旋转,倾斜,都是可以连续多次进行的。多次操作效果叠加。
-
save和restore
实际开发中绘画的时候经常会有移动,缩放等操作。但是变换完成之后,还想恢复到原来的状态。再进行绘制。如果没有save,和restore,比如我们translate(100,20),操作完成之后,还要进行translate(-100,-20)进行复原,这还是一次操作。如果多次操作就会很麻烦。canvas提供save和restore解决了这个问题。save就相当于git中保存状态,restore就相当于回滚操作。回到save的状态。
private void drawSaveRestore(Canvas canvas){
//画一个以300,300位圆心,100位半径的圆,这个时候坐标原点为左上角(0,0)
mPaint.setColor(Color.RED);
canvas.drawCircle(300,300,100,mPaint);
mPaint.setStrokeWidth(15);
canvas.drawLine(0,0,getRight()-100,0,mPaint);
canvas.drawLine(0,0,0,getBottom()-100,mPaint);
mPaint.setStrokeWidth(8);
//保存这个时候的状态。即原点为(0,0)的状态
canvas.save();
//进行移动,将原点移动到(500,500)的位置
canvas.translate(500,500);
mPaint.setColor(Color.BLACK);
canvas.drawLine(0,0,500,0,mPaint);
canvas.drawLine(0,0,0,500,mPaint);
//画一个以(500,500)为原点长宽都是200的正方形
canvas.drawRect(0,0,200,200,mPaint);
//恢复到save的状态。这个时候,坐标原点又是(0,0)了,
canvas.restore();
mPaint.setColor(Color.parseColor("#ff00ff"));
canvas.drawRect(0,0,200,200,mPaint);
}
save和restore是成对出现的。是一对一的。canvas还提供了restoreToCount(int saveCount)方法,去恢复到指定的保存状态。我们可以查看源码。一切说的很清楚。
/**
* Efficient way to pop any calls to save() that happened after the save
* count reached saveCount. It is an error for saveCount to be less than 1.
*
* Example:
* int count = canvas.save();
* ... // more calls potentially to save()
* canvas.restoreToCount(count);
* // now the canvas is back in the same state it was before the initial
* // call to save().
*
* @param saveCount The save level to restore to.
*/
public void restoreToCount(int saveCount) {
if (saveCount < 1) {
if (!sCompatibilityRestore || !isHardwareAccelerated()) {
// do nothing and throw without restoring
throw new IllegalArgumentException(
"Underflow in restoreToCount - more restores than saves");
}
// compat behavior - restore as far as possible
saveCount = 1;
}
nRestoreToCount(mNativeCanvasWrapper, saveCount);
}
其实我们可以把save操作当做入栈操作,然后restore可以简单看做是出栈操作。但是save都有编号,也可以指定编号进行恢复。
-
分层的概念layer
如果做过地图的项目大家都清楚。在标志某个地点或者其他类似效果。会通过层的概念在指定地点添加覆盖物。canvas也有分层的概念,通过savelayer的方式保存,
代码如下:
private void drawLayer(Canvas canvas) {
canvas.translate(100, 100);
mPaint.setColor(Color.RED);
canvas.drawCircle(75, 75, 75, mPaint);
canvas.saveLayerAlpha(0, 0, 200, 200, 0x88);
mPaint.setColor(Color.BLUE);
canvas.drawCircle(125, 125, 75, mPaint);
canvas.restore();
}
在savexxx的函数中,存在flag的参数,在api28中已经全部改为:ALL_SAVE_FLAGS,其他已经无效:
/**
* Restore everything when restore() is called (standard save flags).
* <p class="note"><strong>Note:</strong> for performance reasons, it is
* strongly recommended to pass this - the complete set of flags - to any
* call to <code>saveLayer()</code> and <code>saveLayerAlpha()</code>
* variants.
*
* <p class="note"><strong>Note:</strong> all methods that accept this flag
* have flagless versions that are equivalent to passing this flag.
*/
public static final int ALL_SAVE_FLAG = 0x1F;
private static void checkValidSaveFlags(int saveFlags) {
if (sCompatiblityVersion >= Build.VERSION_CODES.P
&& saveFlags != ALL_SAVE_FLAG) {
throw new IllegalArgumentException(
"Invalid Layer Save Flag - only ALL_SAVE_FLAGS is allowed");
}
}
通过这里的源码可以看出。
-
总结
其实view的绘制原理是一个很复杂的过程。相信的大家都可以大致说出,measure,layout,draw这三个流程,此篇只是简单介绍canvas的应用,甚至不用去管他的流程,先明白怎样画的即可。所有复杂的view绘制,其实都可以拆分成简单的操作,绘制可以从简单做起。