画布,在我们android的View或者自定义View中占有举足轻重的地位,在android中它的意义也是字面的意思,我们可以通过画笔绘制几何图形,文本,路径或者位图。我们可以将Canvas的API主要分为三类,一类是绘制相关的,例如drawText,drawLine...,一类是变换相关的,canvas.translate、canvas.rotate....,还有一类是状态保存和恢复相关的如canvas.save、canvas.restore....下面开始分别讲解。
和画布绘制相关的也就是我们在Canvas类中定义的类似drawXXXX以draw开头的方法:
上图截取了一部分Canvas中的drawXXX的方法。
我们平时使用TextView的时候都是调用setText方法将我们需要显示的文本内容显示在TextView上面,其实最终也是调用Canvas的drawText方法。先看下面的方法,特地将text的初始坐标放在(0,15)这个位置上,看一下效果:
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.GREEN);
mPaint.setTextSize(30);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.RED);
canvas.drawText("测试DrawText的功能的Demo",0,15,mPaint);
}
效果就是绿色的文本只显示了一部分,这是为什么呢。其实在android中当我们使用canvas.drawText的时候,设置的坐标的位置其实是设置的文字的左下角那边的位置。为什么这么说呢?大家小学的时候,都用过那种作业本,汉语拼音,四线三格,我们都是以第三条线位标准写拼音的,Android的标准也是差不多的。看下图:
与字体相关的在上图标注的几个基准线:
baseline:字符基准线(其实就是我们上面drawText标注的(0,15)这个15的值)
ascent:字符最高点到baseLine的推荐距离。
top:字符最高点到baseline之间的距离。
descent:字符最低点到baseline之间的推荐距离
bottom:字符最低点到baseline之间的最大距离
leading:行间距,即前一行的descent到当前行的ascent之间的距离。
与绘制文本相关的还有FontMetrics,可以获取上面我们标记解释的几个值:
public static class FontMetrics {
/**
* The maximum distance above the baseline for the tallest glyph in
* the font at a given text size.
*/
public float top;
/**
* The recommended distance above the baseline for singled spaced text.
*/
public float ascent;
/**
* The recommended distance below the baseline for singled spaced text.
*/
public float descent;
/**
* The maximum distance below the baseline for the lowest glyph in
* the font at a given text size.
*/
public float bottom;
/**
* The recommended additional space to add between lines of text.
*/
public float leading;
}
我们可以通过 下面方法来获得,并读取上面的属性:
Paint.FontMetrics metrics = mPaint.getFontMetrics();
paint中提供给了测量两种测量文字的方法,但是有一些细小的差别:分别位measureText()和getTextBounds()方法,他们都属于Paint的方法,两个函数的区别在于:
如果你用代码分别使用getTextBounds() 和 measureText() 来测量文字的宽度,你会发现 measureText() 测出来的宽度总是要比getTextBounds()d的 大一点点。这是因为这两个方法其实测量的是两个不一样的东西。
getTextBounds(): 它测量的是文字的显示范围(关键词:显示)。就是能够包裹住这段文字的嘴角的矩形,就是这段文字的 bounds。
measureText() : 它测量的是文字绘制时所占用的宽度。绘制文字的时候,往往需要占用比他的实际显示宽度更多一点的宽度,以此来让文字和文字之间保留一些间距,不会显得过于拥挤,也就是设置我们不同行文字之间的间距,导致了 measureText() 比getTextBounds()测量出的宽度要大一些
drawRect,drawCicle,drawBitmap,drawArc...等等都是比较简单的,直接使用API就ok了。
path即路径,可以用来绘制直线,曲线,以及这些直线或曲线构成的集合图形,还可用于根据路径绘制文字。常用的API有移动,连线,闭合,添加图形等....
private void init() {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(6);
mPaint.setColor(Color.RED);
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.moveTo(100,70); //路径起始点移动到(100,70)的位置
mPath.lineTo(140,800); //从(100,70)向(140,800)画一条线
mPath.lineTo(250,600); //从(140,800)向(250,600)画一条线
mPath.close(); //将线路闭合
canvas.drawPath(mPath,mPaint);
}
上图的效果是这样的,其实我们可以将mPath.lineTo(140,800)修改为mPath.rLineTo(40,730),rLineTo的意思其中r就是“相对位置”的意思,就是相对于path的上一个所画路径的结束点的相对位置也即是140-100 = 40,800-70=730;上图我们看到红色的图像是闭合的,有人可以以为使是调用mPath.close的结果,其实上例中如果价格mPath.close去掉,也是这样的结果,主要问题出在Paint的style上面,上例中将style设置为FILL,如果修改为STROKE的话
上面2图体现的结果不一样,图1未封闭,图2成为一个闭合的图像:
private void init() {
mPaint = new Paint();
//修改画笔的样式 为Style
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(6);
mPaint.setColor(Color.RED);
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.moveTo(100,70);
mPath.lineTo(140,800);
mPath.lineTo(250,600);
//图一需要去掉下面一句代码
mPath.close();
canvas.drawPath(mPath,mPaint);
}
上图中设置画笔都为STROKE,左图1注释掉mPath.close(), 右图2加上mPath.close()方法,结果就不一样了。path.close() 方法的意思就是,将路径闭合,如果路径本来就是闭合的就不用管,如果不是闭合的就将首尾两点连线闭合。
添加一个弧形。最终调用的都是含有6个参数的addArc方法,left,top表示的是弧形所在矩形的左上方的位置坐标,right,bottom表示弧形所在矩形的右下方的位置坐标,startAngle就是弧形开始的位置,sweepAngel表示弧形扫过的角度:
public void addArc(RectF oval, float startAngle, float sweepAngle) {
addArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle);
}
public void addArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle) {
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.addArc(200,200,400,400,-225,225);
mPath.close();
canvas.drawPath(mPath,mPaint);
}
addRect添加一个矩形,addRect方法中的参数除了矩形的left,top,right,bottom的坐标外,还有一个参数表示绘制的顺序:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.addRect(500,500,900,900,Path.Direction.CW);
canvas.drawPath(mPath,mPaint);
}
Path.Direction.CW 顺时针方向绘制
Path.direction.CCW 逆时针方向绘制
其他的mPath.addCircle,mPath.addOval 都需要这个参数
例如下面一段代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.addArc(200,200,400,400,-225,225);
mPath.arcTo(400,200,600,400,-180,225,false);
canvas.drawPath(mPath,mPaint);
}
上例中首先调mPath.addArc画一个弧形,然后调用mPath.arcTo在原来的图像上面追加一个弧形,类似前面的mPath.lineTo方法,在arcTo方法中最后一个参数为false,
boolean forceMoveTo
也就是forceMoveTo = false; 这个参数的意思是绘制的时候,是否移动起点的位置。当forceMoveTo = true,表示绘制的时候将起点移动到所要绘制的图像的起点位置,当forceMoveTo = false的时候,意思就是不移动起点,但是必须绘制一条从前一个图像的终点 指向 即将绘制的图像的起点位置的直线:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.moveTo(0,0);
mPath.lineTo(100,100);
mPath.arcTo(400,200,600,400,0,270,true);
canvas.drawPath(mPath,mPaint);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.lineTo(100,100);
mPath.arcTo(400,200,600,400,0,270,false);
canvas.drawPath(mPath,mPaint);
}
左图为 forceMoveTo = true 的效果,右图为 forceMove = false的效果。
还可以在一条路径上面,通过addPath将另外一条路径添加到当前的Path中
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//添加一个路径
mPath.moveTo(100, 70);
mPath.lineTo(140, 180);
mPath.lineTo(250, 330);
mPath.lineTo(400, 630);
mPath.lineTo(100, 830);
Path newPath = new Path();
newPath.moveTo(100, 1000);
newPath.lineTo(600, 1300);
newPath.lineTo(400, 1700);
mPath.addPath(newPath);
canvas.drawPath(mPath,mPaint);
}
贝塞尔曲线是用一系列点来控制曲线状态的,将这些点分为两类,一类是数据点,一类是控制点。
在很多绘图或者动画效果中需要用到贝塞尔曲线,在API中为我们提供了一阶,二阶,三阶贝塞尔曲线的API。其实一节贝塞尔曲线就是一条直线用lineTo就可以绘制,二阶贝塞尔曲线用quadTo或者rQuadTo(相对位置)方法绘制,三阶贝塞尔曲线由cubicTo 或者rCubicTo方法进行绘制,例如下面是绘制二阶贝塞尔曲线的一般使用方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画二阶贝塞尔曲线
mPath.moveTo(300, 500);
mPath.quadTo(500, 100, 800, 500);
//参数表示相对位置,等同于上面一行代码
// mPath.rQuadTo(200, -400, 500, 0);
canvas.drawPath(mPath,mPaint);
}
一阶曲线是没有控制点的,仅有两个数据点(A 和 B),最终效果一个线段。
一阶公式如下:
二阶曲线由两个数据点(A 和 C),一个控制点(B)来描述曲线状态,大致如下:
连接DE,取点F,使得: ,这样获取到的点F就是贝塞尔曲线上的一个点,动态图如下:
二阶公式如下:
二阶贝塞尔曲线公式推倒方法如下:
三阶贝塞尔曲线去下:
公式如下:
高阶贝塞尔曲线的通用公式如下,可以认为是一个递归的操作过程
Canvas中有许多与变换相关的API,可以帮助我们事先许多意想不到的效果。
canvas.translate使画布平移,先看下面的代码,画两个矩形,一个红色矩形坐标为(50,50),(400,400) 改变画笔颜色之后,画绿色矩形,坐标还为(50,50),(400,400),看效果:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(50,50,400,400,mPaint);
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(50,50,400,400,mPaint);
}
看上面的图像,绿色矩形在红色矩形的下方,再看下方代码和效果基本上可以看出平移的一些套路了
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(50,50,400,400,mPaint);
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(50,50,400,400,mPaint);
canvas.translate(-50,-50);
mPaint.setColor(Color.BLUE);
canvas.drawRect(50,50,400,400,mPaint);
}
画红色矩形的坐标为(50,50),(400,400),但是canvas.translate(100,100)之后画绿色矩形,我们看出同样是(50,50),(400,400)的坐标,但是绿色矩形和红色矩形没有重合,归结原因是画布平移之后,Canvas坐标原点平移到(100,100),然后再画绿色的矩形,实际上画的绿色的矩形"相对于"原来红色矩形的坐标系应该为(100+50,100+50),(100+400,100+400)。蓝色矩形同理。可以得出一个结论,画笔平移方法translate(x,y)方法传递x,y的值为 正数 画布坐标原点向下平移,传递的值为负数,画布坐标原点想上平移。
canvas.scale可以对画布进行缩放,有2个重载的方法:
public void scale(float sx, float sy) {
if (sx == 1.0f && sy == 1.0f) return;
nScale(mNativeCanvasWrapper, sx, sy);
}
/**
* Preconcat the current matrix with the specified scale.
*
* @param sx The amount to scale in X
* @param sy The amount to scale in Y
* @param px The x-coord for the pivot point (unchanged by the scale)
* @param py The y-coord for the pivot point (unchanged by the scale)
*/
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);
}
先看第一个函数,传入float sx,float sy是缩放比率:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,700,700,mPaint);
canvas.scale(0.5f,0.5f);
mPaint.setColor(Color.GREEN);
canvas.drawRect(200,200,700,700,mPaint);
}
我们看到调用scale(0.5f,0.5f)之后,同样坐标的红色矩形和绿色矩形,为啥绿色矩形会出现那样的位置呢。我个人对这个的理解是:其实我们画同样大小的红色和绿色大小的矩形,但是显示出的大小结果不一样了,就像我们查看百度地图或者高德地图一样,当我们缩小地图的时候,我们看到的范围其实更大了,其实是我们显示地图的比率变了而已。类比上面的效果,当我们画(200,200),(700,700)的红色矩形,我们的坐标系,可以理解为一个像素点为1cm,那么红色矩形就是宽高为500cm的矩形,但是当我们将画布缩小之后,我们一个单位像素点就变成了1/0.5 = 2cm,所以我们的绿色图形的宽高就为原来的一半,起始坐标相对于原来的画布变为(100,100)。所以按照这个理解,我们可以推测出canvas.scale(2,2)的效果为绿的图形放大了。起始坐标为(400,400).
第二个方法 public final void scale(float sx, float sy, float px, float py) 传递的是四个参数:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,700,700,mPaint);
canvas.scale(0.5f,0.5f,200,200);
mPaint.setColor(Color.GREEN);
canvas.drawRect(200,200,700,700,mPaint);
}
看到最后的效果是这样的。起始上面的代码的四个参数的scale方法等同于下面的代码
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,700,700,mPaint);
canvas.translate(200,200);
canvas.scale(0.5f,0.5f);
canvas.translate(200,200);
mPaint.setColor(Color.GREEN);
canvas.drawRect(200,200,700,700,mPaint);
}
那么 public final void scale(float sx, float sy, float px, float py) 的意思就是画布先平移translate(px,py)然后,scale(0.5f,0.5f),最后 translate(-px,-py).
canvas.rotate(float degress),传递的参数为度数:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//旋转操作
canvas.drawRect(0,0,300,300,mPaint);
canvas.rotate(45);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,300,300,mPaint);
}
看到上面的绿色矩形旋转了45°,有一部分旋转到屏幕之外了。。当canvas.rotate(degree)传入的 度数为正数的时候是绕顺时针旋转,当传入的度数为负数的时候,绕逆时针旋转,默认是绕画布的坐标原点旋转,上例就是绕(0,0)顺时针旋转45°。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//旋转操作
canvas.drawRect(200,200,700,700,mPaint);
canvas.rotate(-45,450,450);
mPaint.setColor(Color.GREEN);
canvas.drawRect(200,200,700,700,mPaint);
}
上例中canvas.rotate(-45,450,450) 后面两个参数是旋转的中心450,450,画布绕(450,450)逆时针旋转45度。
canvas.skew(float sx,float sy)
画布的错切(倾斜),sx,sy值,分别表示将画布在x方向和y方向上倾斜角度的tan 值。
当sx=1时,即将画布在x方向上旋转45度,其实就是x轴保持方向不变,y轴逆时针旋转45度。
当sy=1时,即将画布在y方向上旋转45度,其实就是y轴保持方向不变,x轴顺时针旋转45度。
当sx、sy都改变时,两者都进行相应的移动。
例如下面的代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//倾斜 错切操作
canvas.drawRect(0,0,200,200,mPaint);
canvas.skew(1,0);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,200,200,mPaint);
}
红色矩形为错切之前的矩形,将画布向x方向倾斜45°之后,然后再绘制绿色矩形,可能不是很好理解,看下面一幅图:
红色线为原先的x轴和y轴,当调用canvas.skew(1,0)之后,向x方向错切45°,其实就是将y轴以x为轴线旋转45°,然后再画矩形.如果上面图中不画红色矩形直接错切之后的效果:一切只是视角不一样而已。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//倾斜 错切操作
canvas.skew(1,0);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,200,200,mPaint);
}
canvas.clipXXX系列方法可以对画布进行裁剪,使裁剪之后额图形只能在裁剪的区域内绘制,否则 “无效”。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GRAY); //绘制灰色背景
canvas.drawRect(0,0,500,500,mPaint); //画一个矩形区域为(0,0) (500,500)
canvas.clipRect(new Rect(0,0,200,200)); //裁剪(0,0) (200,200)的区域
mPaint.setColor(Color.GREEN); //画笔设置为绿色
canvas.drawRect(0,0,200,200,mPaint); //绘制(0,0) (200,200)的绿色矩形
canvas.drawRect(250,250,400,400,mPaint); //绘制(250,250) (400,400)的绿色矩形
}
效果图为:
看到(250,250),(400,400)的绿色矩形并没有显示出来,是因为我们裁剪的大小区域为(0,0),(200,200) 而我们的(250,250),(400,400)的矩形区域不在上面的矩形区域中,所以并没有绘制出来。
canvas还有其他的裁剪方法,例如canvas.clipPath,和clipRect类似,只不过clipPath能够裁剪的图形多一些。clipOutRect反向裁剪。也就是说裁剪之后绘制的图像,只能显示在裁剪区域之外,在裁剪区域之内的不能显示出来。
setMatrix其实可以做上面的平移 旋转,缩放 等等操作。使用Matrix进行变换的一般操作步骤为:
- 创建
Matrix
对象;- 调用
Matrix
的pre/postTranslate/Rotate/Scale/Skew()
方法来设置几何变换;- 使用
Canvas.setMatrix(matrix)
或Canvas.concat(matrix)
来把几何变换应用到Canvas
。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//matrix变换
canvas.drawRect(0,0,400,400,mPaint);
Matrix matrix = new Matrix();
matrix.setTranslate(200,200);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,400,400,mPaint);
}
Canvas.setMatrix(matrix)
:用 Matrix
直接替换 Canvas
当前的变换矩阵,即抛弃 Canvas
当前的变换,改用 Matrix
的变换。Canvas.concat(matrix)
:用 Canvas
当前的变换矩阵和 Matrix
相乘,即基于 Canvas
当前的变换,叠加上 Matrix
中的变换。Canvas调用translate,scale,rotate,skew,matrix等的变换之后,后续的操作都是基于变换之后的Canvas的,都会受到影响,对后续的操作很不方便,Canvas提供了save,saveLayer,saveLayerAlpha,restore,restorToCount等方法来保存和恢复状态。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,500,500,mPaint);
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,300,300,mPaint);
canvas.drawRect(200,200,500,500,mPaint);
}
两个矩形的大小虽然一致,但是明显画布平移之后,坐标零点变换位置了,变为原来画布的(100,100)位置,如果我们想回到原来的坐标零点,需要在调用translate(-100,-100)才能将绿色矩形调整至原来的坐标零点。并且后面的(200,200),(500,500)的绿色矩形也是在画布平移之后的画布的基础上绘制的。Android中Canvas提供一些保存状态的API,让后续的绘制可以不受这些变换的影响。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,500,500,mPaint);
canvas.save();
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,300,300,mPaint);
mPaint.setColor(Color.BLUE);
canvas.restore();
canvas.drawRect(0,0,300,300,mPaint);
}
上述代码中调用了canvas.store() 和 canvas.restore()方法,store的意思就是保存当前的画布状态,restore就是恢复之前的画布状态,这里也就是恢复到tanslate(100,100)之前的画布状态,然后再绘制蓝色的矩形,就是在坐标零点了。
Android中一般情况下store和restore是成对出现的,意思为保存当前状态,退回到上一步的画布状态。例如下面多次成对出现这个两个方法(restore方法总是退回到最近的store方法调用之前的画布的状态)。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,500,500,mPaint);
canvas.save();
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,300,300,mPaint);
canvas.save();
canvas.translate(100,100);
canvas.drawLine(0,0,500,500,mPaint);
canvas.restore();
canvas.restore();
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,300,300,mPaint);
}
先画红色矩形,调用store,然后平移画布绘制绿色矩形,然后再调用store然后再平移画布,绘制绿色直线,最后两次调用restore,回到画布最开始的状态,在坐标零点绘制蓝色矩形。有点类似一个 “入栈出栈” 的过程 。还有一种restoreToCount的方法可以一步到位。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(200,200,500,500,mPaint);
int saveState = canvas.save();
canvas.translate(100,100);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,300,300,mPaint);
canvas.translate(100,100);
canvas.drawLine(0,0,500,500,mPaint);
canvas.restoreToCount(saveState);
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,300,300,mPaint);
}
调用 int saveState = canvas.save() ; 保存当前的状态,然后调用canvas.restoreToCount(saveState) 直接回退到那个状态。出栈操作就是直接回退到指定的栈的那一层。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//创建离屏绘制图层
int layerId = canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint);
// 在图层之间进行绘制
//最后将创建的图层绘制到canvas中
canvas.restoreToCount(layerId);
}
下面看一个引导页的效果实现:
多看几遍,我们可以将效果图分解为几个阶段:
1.6个小球绕中心 旋转的阶段
2.6个小球 以球所在的中心 先扩大 后收缩的阶段。
3.类似水波纹扩散的阶段。
自定义View的成员变量
private Paint mPaint; //画小圆的画笔
private Paint mHolePaint; //最后白色的波浪的画笔
private int colors [] = null;
private int mCenterX,mCenterY;
private float mCircleRadius = 18; //旋转的小球的半径
private float mCurrentAngle = 0; //小球旋转的时候的初始角度,做动画使用
private float mDiatance;
private SplashState mState; //当前的状态
private ValueAnimator mValueAnimator;
private float mRotateRadius = 100; //旋转大圆的半径
//当前大圆的半径
private float mCurrentRotateRadius = mRotateRadius;
//扩散圆的半径
private float mCurrentHoleRadius = 0F;
定义上面分析的集中状态:
//抽象内部类 描述上面3种状态
private abstract class SplashState{
abstract void drawState(Canvas canvas);
}
//小球的旋转状态
private class RotateState extends SplashState{
.....
}
//小球的扩散聚合状态
private class MerginState extends SplashState{
.....
}
//扩散 水波纹状态
private class SpreadState extends SplashState{
.....
}
自定义View中最重要的onDraw方法,但是看起来却那么简单:第一个状态也就是旋转状态
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mState == null){
mState = new RotateState();
}
mState.drawState(canvas);
}
看旋转状态的RotateState的实现
private class RotateState extends SplashState{
public RotateState(){
mValueAnimator = ValueAnimator.ofFloat(0f, (float) (Math.PI*2));
// mValueAnimator.setRepeatCount(1);
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.setDuration(1500);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentAngle = (float) animation.getAnimatedValue();
invalidate();
}
});
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mState = new MerginState();
}
});
mValueAnimator.start();
}
@Override
void drawState(Canvas canvas) {
drawBackground(canvas);
drawCicle(canvas);
}
}
定义属性动画,使小球旋转起来 看drawCicle方法:
private void drawCicle(Canvas canvas){
int length = colors.length;
float angle = (float)((2*Math.PI)/length);
for(int i=0;i
drawBackground是在3个状态中都要绘制的:
private void drawBackground(Canvas canvas){
if(mCurrentHoleRadius>0){ //扩散半径>0的时候 就是最好一个状态
float strokeWidth = mDiatance - mCurrentHoleRadius; //线宽在减小
float radius = strokeWidth / 2 + mCurrentHoleRadius; //半径一直在增大
mHolePaint.setStrokeWidth(strokeWidth);
canvas.drawCircle(mCenterX,mCenterY, radius, mHolePaint);
}else{
canvas.drawColor(Color.WHITE); //否则 只绘制白色的背景
}
}
上面的float radius = strokeWidth / 2 + mCurrentHoleRadius; 需要加上线宽的一半 ,否则白色圆扩散的半径就接不上了,这里为啥,可以看之前的AndroidUi之Paint 在最开始讲解了线宽 对绘制圆的影响。
注意:在MerginState的时候,执行属性动画时候,调用了
//反转执行动画...
mValueAnimator.reverse();
public MerginState(){
mValueAnimator = ValueAnimator.ofFloat(0,mRotateRadius);
mValueAnimator.setDuration(1000);
mValueAnimator.setInterpolator(new OvershootInterpolator(5));
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentRotateRadius = (float) animation.getAnimatedValue();
invalidate();
}
});
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mState = new SpreadState();
}
});
//反转执行动画...
mValueAnimator.reverse();
}
本来原来的属性动画,是旋转半径从0-mRotateRadius 并且拦截器为 OvershootInterpolator。现在调用reverse效果之后,从最大半径mRotateRadius的位置到0。
Demo中还有2个自定义View的效果,一个是爆炸效果,一个是类似QQ消息气泡效果,可以自行查看。
Demo下载地址