前言
Android自定义View是Android初中级开发工程师向高级工程师进阶所必须掌握的一块内容,其重要性不言而喻。接下来的一段时间,我会连续出几篇跟自定义View相关的文章,从易到难,跟大家一起学习Android自定义View。本文讲一个Android很简单的View——DashBoard(仪表盘),以这个例子带大家去学习自定义View的基本绘制,让大家学会自定义View,并最终掌握。
注:本文的Demo在文章的最后
必须要掌握的几个点
在开始我们的绘制DashBoard之前,有几个点是必须要掌握的,这些是绘制的基础,也是前提。
Paint
自定义View的过程就是一个绘制的过程,而绘制就好像我们画画一样,而画画就必须要会画笔,Paint就是我们的画笔。
- Paint 类的几个最常用的方法。具体是:
- Paint.setStyle(Style style) 设置绘制模式
- Paint.setColor(int color) 设置颜色
- Paint.setStrokeWidth(float width) 设置线条宽度
- Paint.setTextSize(float textSize) 设置文字大小
- Paint.setAntiAlias(boolean aa) 设置抗锯齿开关
这里重点讲一下Paint.setStyle(Style style)方法,这个方法设置的是绘制的 Style 。Style 具体来说有三种: FILL, STROKE 和 FILL_AND_STROKE 。FILL 是填充模式,STROKE 是画线模式(即勾边模式),FILL_AND_STROKE 是两种模式一并使用:既画线又填充。它的默认值是 FILL,填充模式。只有当Style是STROKE 和 FILL_AND_STROKE时,Paint.setStrokeWidth(float width)才有意义,你全是填充的就不涉及什么线条宽度了。
canvas
Paint是画笔,可画画光有画笔还不行,还必须得有画布,Canvas就是画布。Canvas这个类是绘制最重要的类,没有之一,几乎所有绘制的方法都出自于这个类。
坐标系
方法先不讲,先讲一下坐标系,在Android 里,每个View 都有一个自己的坐标系,彼此之间是不影响的。这个坐标系的原点是 View 左上角的那个点;水平方向是 x 轴,右正左负;竖直方向是 y 轴,下正上负(注意,是下正上负,不是上正下负,和上学时候学的坐标系方向不一样也就是下面这个样子。
这个坐标非常重要,因为我们所有的绘制都是在这个坐标系的基础上开展的,而关于坐标系还有这么个两个方法要特别注意:
- Canvas.rotate(float degrees)//旋转坐标系,正角度顺时针,负角度逆时针
- Canvas.translate(float dx, float dy)
注意:以上两个方法的操作的对象是坐标系,跟View本身没有关系,之所以使用是为了让我们更好、更方便地绘制View。
方法
Canvas最重要也最常用的方法都是drawXXX()方法,方法太多了,我不可能一一列举,写几个最常用的,余下的请自行google
- drawCircle(float centerX, float centerY, float radius, Paint paint) 画圆
基本看参数名字就能猜出来是啥意思了,前面讲了坐标系的概念,前两个参数就是圆心的X、Y坐标了,第三个是半径大小,最后一个是画笔。 - drawRect(float left, float top, float right, float bottom, Paint paint) 画矩形 (参数啥意思基本都能猜出来,不多讲,不行还是google)
- drawOval(float left, float top, float right, float bottom, Paint paint) 画椭圆
- drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 画线
- drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 绘制弧形或扇形
left, top, right, bottom 描述的是这个弧形所在的椭圆;startAngle 是弧形的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度),sweepAngle 是弧形划过的角度;useCenter 表示是否连接到圆心,如果不连接到圆心,就是弧形,如果连接到圆心,就是扇形。 - drawPath(Path path, Paint paint) 画自定义图形
这里Path对象要讲一下,Path.addXxx()——添加子图形,例如Path.addCircle(float x, float y, float radius, Direction dir) 添加圆;Path.xxxTo() ——画线(直线或曲线)lineTo(float x, float y) / rLineTo(float x, float y) 画直线.
小结
以上就是我们开始绘制DashBoard之前还需要掌握的基础,因为都用得到。Paint是画笔,主要就是设置画笔相关的属性,颜色、大小、风格等等;canvas是画布,坐标系的概念必须清楚,重要的几个方法也必须知道。
DashBoard
先上个图
看图其实很简单,基本上就分为三步,第一份画弧;第二步画刻度;第三步画指针。
画弧线
画弧线之前,我简单讲一下自定义View的流程,创建一个DashBoard的类型继承View,重写构造方法和onDraw(Canvas canvas)
public class DashBoard extends View {
public DashBoard(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();//初始化Paint
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
初始化Paint
private void initPaint(){
mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);//抗锯齿
mPaint.setStyle(Paint.Style.STROKE);//画线模式
mPaint.setStrokeWidth(Utils.px2dp(2));//线宽度
mPaint.setColor(Color.BLACK);
}
做好以上初始化工作,我们就开始第一步画弧线。调用Canvas.drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)这个方法上面有介绍过,这里就不详细讲了,前四个参数很好设置,我们定义圆心在View的中心位置,即(getWidth()/2,geHeight()/2),半径150dp,那么前四个参数就有了,第六个参数sweepAngle是划过的度数,这个我们定义为240度,第七个是否连接中心,false,我们不需要连接中心,最后一个放自己的Paint就行了,现在的关键就是第五个参数,开始角度的计算,下面我出张图帮助大家计算一下
图中画得应该比较清楚了,不过多解释,直接上代码
private void drawArc(Canvas canvas){
rectF = new RectF(getWidth() / 2 - RADIUS, getHeight() / 2 - RADIUS,
getWidth() / 2 + RADIUS, getHeight() / 2 + RADIUS);
canvas.drawArc(rectF,90+(360-SWEEPANGLE)/2,SWEEPANGLE,false,mPaint);
}
效果图
画刻度
关于画刻度,其实就是画线吗,那画线的方法拿过来看一下,Canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint) ,需要线的起始点和结束点的坐标,如果我要画21个刻度,那需要21个点的刻度都算一遍,我去,谁可能这么干啊。放心,我们当然不会这么干了,下面提供两种方式。
第一种方式
思路:坐标系的旋转+平移
之前在讲Canvas里提过,每一个Android的View都对应着有一个坐标系,坐标原点在View的左上角(可以看一下上面的那张图),现在如果我们把坐标原点平移到圆心的位置,并且再顺时针旋转30°,那么当前的坐标系就是下面这样的
那么在我们平移旋转以后,在画 右下角第一个刻度的时候,就相当于这个刻度的起始点和结束点的 纵坐标都是0,因为我们把原点移动到了圆心,而 结束点的横坐标就是圆的半径(RADIUS),而 起始点的横坐标就是(半径-刻度线的长度)。下面我还是用一张图来解释一下
这样第一个刻度线,我们就画出来了,现在假定我们要画21个刻度线,21刻度线对应 20个间隔,总角度是240度,每个刻度线间隔角度就是240/20即12度,所以其他的刻度线就可以让 坐标系每次逆时针旋转12度画一次,用代码表达一下会更清晰。
private void drawDegree(Canvas canvas){
canvas.translate(getWidth()/2,getHeight()/2);
canvas.rotate(30);
for (int i=0;i<20;i++){
//Utils.px2dp(10)是刻度线的长度,为10dp
canvas.drawLine(RADIUS-Utils.px2dp(10),0,RADIUS,0,mPaint);
canvas.rotate(-SWEEPANGLE/20);//逆时针选择 负值是逆时针
}
//最后一根线
canvas.drawLine(RADIUS-Utils.px2dp(10),0,RADIUS,0,mPaint);
canvas.rotate(240-30);//旋转回去的角度
canvas.translate(-getWidth()/2,-getHeight()/2);
}
画完以后的效果图
这个图我故意没有缩放,细心的你可能已经发现了第一个和最后一个刻度明显有点不自然,我再放大一下
这下很明显了吧,感觉好像是空了一半的宽度没有画在弧线上。这是为什么呢? 还得再上个图
看图我们可以知道,我们在画刻度的时候,Paint就是我们的画笔,默认是宽度的,我们画刻度的纵坐标是0,但是实际画的时候是把画笔的 中间位置放在0的坐标上,这就导致了好像空了一半Paint出来,知道了什么原因其实解决起来也很简单,就是把起始点和结束点的纵坐标 相应的提高 半个画笔的高度,直接看代码吧
private void drawDegree(Canvas canvas){
canvas.translate(getWidth()/2,getHeight()/2);
canvas.rotate(30);
for (int i=0;i<20;i++){
//纵坐标下正上负,向上提高,加负值,即-mPaint.getStrokeWidth()/2
canvas.drawLine(RADIUS-Utils.px2dp(10),-mPaint.getStrokeWidth()/2,RADIUS,-mPaint.getStrokeWidth()/2,mPaint);
canvas.rotate(-SWEEPANGLE/20);
}
//最后一个点,因坐标系已经旋转了240度,向上提高,加整值,即mPaint.getStrokeWidth()/2
canvas.drawLine(RADIUS-Utils.px2dp(10),mPaint.getStrokeWidth()/2,RADIUS,mPaint.getStrokeWidth()/2,mPaint);
canvas.rotate(240-30);//旋转回去的角度
canvas.translate(-getWidth()/2,-getHeight()/2);
}
看了注释,你会发现第一个点和最后一个点的提高方式不同,这也是为什么上面”相应的“三个字我要加粗了,再看一眼修改后的效果图
第二种方式
思路:使用PathMeasure测量弧线长度,利用PathDashPathEffect来画刻度
简单讲一下PathMeasure和PathDashPathEffect
- PathMeasure:用来测量路径的长度,public PathMeasure(Path path, boolean forceClosed),通过PathMeasure.getLength()
- PathDashPathEffect:Paint.setPathEffect(PathEffect effect)给图形的轮廓设置效果的,PathDashPathEffect是PathEffect的一个子类,它的构造方法 PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style) 中, shape 参数是用来绘制的 Path ; advance 是两个相邻的 shape 段之间的间隔,不过注意,这个间隔是两个 shape 段的起点的间隔,而不是前一个的终点和后一个的起点的距离; phase 和 DashPathEffect 中一样,是虚线的偏移;最后一个参数 style,是用来指定拐弯改变的时候 shape 的转换方式。style 的类型为 PathDashPathEffect.Style ,是一个 enum ,具体有三个值:TRANSLATE:位移,ROTATE:旋转,MORPH:变体
知道了这两个方法,我们就可以先用PathMeasure拿到弧线的长度,除以20获得每个间隔的长度,然后通过Paint.setPathEffect(new PathDashPathEffect())方法来画刻度就行了,直接上代码
private void drawDegree2(Canvas canvas){
//刻度的路径
dash=new Path();
//Path.Direction.CW顺时针方向 同时顺时针切线方向为X轴正向
dash.addRect(0,0,Utils.px2dp(2),Utils.px2dp(10), Path.Direction.CW);
//弧线长度的路径
Path length=new Path();
length.addArc(rectF,90+(360-SWEEPANGLE)/2,SWEEPANGLE);
//测量弧线长度
pathMeasure=new PathMeasure(length,false);
//这里(pathMeasure.getLength()-mPaint.getStrokeWidth())/20 弧线长度之所以减去Paint的宽度跟我第一种方式去掉宽度是一个意思
mPaint.setPathEffect(new PathDashPathEffect(dash,
(pathMeasure.getLength()-mPaint.getStrokeWidth())/20,0, PathDashPathEffect.Style.ROTATE));
canvas.drawArc(rectF,90+(360-SWEEPANGLE)/2,SWEEPANGLE,false,mPaint);
mPaint.setPathEffect(null);
}
这里我就不细讲了,注释还是比较清楚,效果图跟第一种方式是一样的就不贴图,个人还是更加推荐第一种的画刻度方式。
画指针
画指针呢,就比较简单了,其实就是调用画线的方法,先把坐标系平移动原点位置,设置一个当前的角度currentAngle还有指针长度INDICATOR,唯一有一点难度的就是计算结束点的横纵坐标,需要用到三角函数的知识
- 横坐标:Math.cos(Math.toRadians(currentAngle))*INDICATOR
- 纵坐标:Math.sin(Math.toRadians(currentAngle))*INDICATOR
很简单,上代码
private void drawIndicator(Canvas canvas){
canvas.translate(getWidth()/2,getHeight()/2);
canvas.drawLine(0,0,
(float) Math.cos(Math.toRadians(currentAngle))*INDICATOR,
(float)Math.sin(Math.toRadians(currentAngle))*INDICATOR,
mPaint);
canvas.translate(getWidth()/2,getHeight()/2);
}
最后
Android自定义View是Android比较难的一块内容,本文主要通过绘制DashBoard来讲基本的绘制,Paint和Canvas的基本用法,接下来的一段时间内,我会继续出自定义View相关的内容,下一篇文章会讲绘制文字。
最后放上文章的demo DashBoard,觉得还不错的请给个star哈