Android Canvas 绘制图形 — 拾音器
最近在网上看到一个拾音器的设计图,看起来挺美观的,于是我就想把它实现出来,不多废话,先看图,左边是设计图,右边是实现图
除了指针不同,其他的大体上一样
Android Canvas介绍
Canvas作为绘制图形的直接对象,提供了以下几个非常有用的方法
- Canvas.save()
- Canvas.restore()
- Canvas.translate()
- Canvas.roate()
Canvas.save()
这个方法,从字面上理解就是保存的意思,而它的作用也正是将之前的画布保存起来,让后续的操作能像在新的画布上一样操作,这个PhotoShop的图层是一个概念Canvas.restore()
这个方法,可以理解为合并图层,就是将之前保存下来的图层合并为一个图层Canvas.translate()
这个方法,可以理解为移动坐标系,很多人在用时,理解为移动画布,所以在计算坐标时会出一些问题,在这里把他理解为移动坐标系更加恰当。画布的初始坐标系是左上角,如果我们调用translate(x, y)之后,则表示将原点(0, 0)移动到了(x, y),后面的坐标计算都是在这一点上进行的Canvas.roate()
可以理解为旋转坐标系,用法和Canvas.translate()相似,但多了个角度的参数,调用roate(degree, x, y)之后,则表示将以(x, y)为原点的坐标系旋转degree个角度
Paint理解为画笔,在Canvas上所有的图形,都需要这个对象,以下有几个常用方法
- paint.setAntiAlias()
- paint.setStyle()
- paint.setStrokeWidth()
- paint.setColor()
paint.setAntiAlias()
是给画笔设置是否抗锯齿,参数为布尔类型paint.setStyle()
是给画笔设置画笔样式paint.setStrokeWidth()
给画笔设置画笔宽度paint.setColor()
给画笔设置画笔颜色
实现
有了上面的理论基础,现在我们可以来实现上面的效果了,先来分析一下上面效果图中有什么
- 刻度盘——外面的半圆刻度盘
- 刻度线——整数的深色粗线和其余德灰色细线
- 圆形底盘——共三个圆形底盘,最上面的深蓝色圆形要在指针上面
- 指针
新建一个类,继承View
public class TunerView extends View {
private int mWidth;
private int mHeight;
public TunerView(Context context){
super(context);
//获取屏幕的宽高
WindowManager windowManager =
(WindowManager) getContext().Context.WINDOW_SERVICE);
mWidth = windowManager.getDefaultDisplay().getWidth();
mHeight = windowManager.getDefaultDisplay().getHeight();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
}
}
第一步(Step 1):画刻度盘
由于画布一开始绘制文字时,文字是垂直的,而在设计图中刻度盘是从左往右,文字是水平的,所以需要先对画布坐标系进行旋转
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int xc = mWidth / 2; //屏幕中心X坐标
int yc = mHeight / 2; //屏幕中心Y坐标
int radius = xc - 30; //圆半径
/**
* 正常情况下,一开始写文字,都是垂直的,但我们的仪表盘
* 要求从左边开始画,文字是水平的,所以需要先对画布坐标系进行逆时针旋转90°
*/
canvas.rotate(-90, xc, yc);
//画刻度盘
Paint paintDegree = new Paint();
paintDegree.setStrokeWidth(3);
//画刻度线条
for (int i = -50; i <= 50; i++){
//区别整点和非整点的线条样式
if (i % 10 == 0){
paintDegree.setStrokeWidth(5);
paintDegree.setTextSize(30);
paintDegree.setAntiAlias(true);
paintDegree.setColor(getResources().getColor(R.color.colorAccent));
//这里画线的坐标计算,需要自行去理解,这里我就不多加解释了
canvas.drawLine(xc, yc - radius, xc, yc - radius + 40, paintDegree);
String degree = String.valueOf(i);
canvas.drawText(degree,
//这里使用了Paint对象的measureText()方法,
//该方法是传入一个String类型的参数,经过计算之后返回该String对象中
//文字所占用的宽度
xc - paintDegree.measureText(degree) / 2,
yc - radius + 70,
paintDegree);
/**
* 由于本案中两个整点之间只被分为了5分,所以每份的间隔就是2
*/
} else if (i % 2 == 0){
paintDegree.setStrokeWidth(3);
paintDegree.setTextSize(15);
paintDegree.setAntiAlias(true);
paintDegree.setColor(getResources().getColor(R.color.grey));
canvas.drawLine(xc, yc - radius + 10, xc, yc - radius + 30, paintDegree);
}
//每画完一条刻度线条,就需要对画布坐标系进行旋转,通过旋转简化坐标运算
//旋转角度为:总度数 / 总份数
//本案例中,总度数为180°, 分值区间为(-50 ~ 50),所以总份数为100份
canvas.rotate((float)(180 / 100), xc, yc);
}
//画圆形底盘
...
}
运行效果:
第二步(Step 2):画园形底盘
画实心圆形底盘,通过调用paint.setColor()
来设置颜色,调用paint.drawCircle()
方法来画出圆形
//画大圆盘
Paint bigCircle = new Paint();
bigCircle.setColor(getResources().getColor(R.color.colorPrimary));
bigCircle.setAntiAlias(true);
canvas.drawCircle(xc, yc, xc - 180, bigCircle);
//画中等圆盘
Paint midCircle = new Paint();
midCircle.setColor(getResources().getColor(R.color.colorPrimaryDark));
midCircle.setAntiAlias(true);
canvas.drawCircle(xc, yc, xc - 280, midCircle);
//画小圆盘
Paint smallCircle = new Paint();
smallCircle.setColor(getResources().getColor(R.color.colorAccent));
smallCircle.setAntiAlias(true);
canvas.drawCircle(xc, yc, xc - 320, smallCircle);
drawCircle()
方法有四个参数,
- cx:圆心的x坐标,float类型
- cy:圆心的y坐标,float类型
- radius:圆形的半径,float类型
- paint:画笔对象,Paint类型
运行效果:
第三步(Step 3):画指针
画指针其实和画线是一个道理,所以我们直接调用drawLine()
方法就可以了
//画指针
Paint paintCursor = new Paint();
paintCursor.setStrokeWidth(3);
paintCursor.setAntiAlias(true);
paintCursor.setColor(getResources().getColor(R.color.colorAccent));
canvas.drawLine(xc, yc, xc - radius + 80, yc + 10, paintCursor);
drawLine()
方法有5个参数
- startX:线条的起始点x坐标,类型float
- startY:线条的起始点y坐标,类型float
- stopX:线条的结束点x坐标,类型float
- stopY:线条的结束点y坐标,类型float
- paint:画笔对象,Paint类型
运行效果:
这里注意一下,由于我们的指针使用的颜色和最小的那个圆形的颜色是一样的,所以运行效果里看不出问题,如果换一个颜色,就会发现,指针在最小的圆形上面,而设计图上的指针是在最小的圆形下面,那么这要怎么解决呢?不要方,只需要把绘制最小那个圆的canvas.drawCircle()
方法放到绘制指针的drawLine()
方法之后就可以了
第四步(Step 4):画正下方的文字
画正下方的文字,只需要调用canvas.drawText()
方法就可以了
Paint pitchPaint = new Paint();
pitchPaint.setAntiAlias(true); //设置抗锯齿
pitchPaint.setTextSize(40);
pitchPaint.setColor(getResources().getColor(R.color.colorAccent));
String pitch = "Pitch";
canvas.drawText(pitch,
xc - pitchPaint.measureText(pitch) / 2,
//这里加上180,是因为要加上最外面那个大的圆形的半径
//再加上80,是为了让文字和最大的那个圆产生间隙
yc + 180 + 80,
pitchPaint);
运行效果:
可以看到文字的方向不对,为什么文字的方向不对呢?这是因为我们在绘制刻度盘的时候,我们对画布坐标系进行了旋转,画完刻度盘之后,坐标系被旋转了180°,但由于在绘制刻度盘之前,我们对整个画布坐标系进行逆时针旋转了90°,所以实际上只旋转了90°,因此,我们看到绘制的文字不是在正下方,而是在左边,被旋转了90°,那么这要怎么解决呢?最简单的方法当然是在绘制文字之前,把画布坐标系旋转回来。所以需要添加一行代码:
canvas.rotate(-90, xc, yc);
运行效果
最后
由于上面的案例代码里用到了很多写死的数字代码(Hard Code),这不利于代码的灵活性,而且,既然有指针,就应该让指针根据一个提供的输入值,来让指针指向对应的数值位置,所以,我对代码进行了改良,代码如下:
/**
* Created by zhongzilu on 2016/5/25 0025.
*/
public class DialView extends View {
private float mWidth;
private float mHeight;
private float mAngle = 0; //指针旋转角度,值为0时指针垂直显示
private static int mMaxValue = 50; //分值区间的最大值
private static int mMinValue = -50; //分值区间的最小值
/**输入值,通过输入值来计算指针的旋转角度,即{@mAngle}的值,
* 最终在界面上呈现的效果是指针指向输入值的刻度上*/
private float mValue = 0;
/**刻度盘呈现的总弧度,本案例中总弧度为180,呈半圆形*/
private float mArc = 180;
public DialView(Context context, AttributeSet attrs) {
super(context, attrs);
//获取屏幕的宽高
WindowManager windowManager = (WindowManager) getContext().getSystemService(
Context.WINDOW_SERVICE);
mWidth = windowManager.getDefaultDisplay().getWidth();
mHeight = windowManager.getDefaultDisplay().getHeight();
}
/**
* 注意:
* 下方的所以坐标计算和长度计算都是依据在屏幕宽度为621px情况下的
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float xc = mWidth / 2; //圆中心X坐标
float yc = mHeight / 2; //圆中心Y坐标
float radius = (float)(xc - xc * 0.05314); //圆半径
//画刻度盘
Paint paintDegree = new Paint();
/**
* 正常情况下,一开始画线或写文字,都是垂直的,但我们的仪表盘
* 要求从左边开始画,文字是水平的,所以需要先进行画布旋转90°
*/
canvas.rotate((-mArc / 2), xc, yc);
System.out.println("before rotate==>" + (-mArc / 2));
/**
* 本案例中,设置的分值区间为(-50 ~ 50)
*/
for (int i = mMinValue; i <= mMaxValue; i++){
//区别整点和非整点
if (i % 10 == 0){
//在屏幕宽度为621下,大小为5
paintDegree.setStrokeWidth((float)(mWidth * 0.008051));
//刻度字体大小是依据是:在屏幕宽度为621的分辨率下,刚好为30
paintDegree.setTextSize((float)(mWidth * 0.048309));
paintDegree.setAntiAlias(true);
paintDegree.setColor(getResources().getColor(R.color.colorAccent));
canvas.drawLine(xc, yc - radius, xc, (float)(yc - radius + xc * 0.101449), paintDegree);
String degree = String.valueOf(i);
canvas.drawText(degree,
//这里使用了Paint对象的measureText()方法,
//该方法是传入一个String类型的参数,经过计算之后返回该String对象中
//文字所占用的宽度
xc - paintDegree.measureText(degree) / 2,
//下方mWidth * 0.048309 = 在屏幕宽度为621下,长度为30
yc - radius + (float)(xc * 0.101449 + mWidth * 0.048309),
paintDegree);
/**
* 由于两个整点之间只被分为了5分,所以每份的间隔就是2
*/
} else if (i % 2 == 0){
paintDegree.setStrokeWidth((float)(mWidth * 0.004830));
//在屏幕宽度为621下,字体大小为15
paintDegree.setTextSize((float)(mWidth * 0.024154));
paintDegree.setAntiAlias(true);
paintDegree.setColor(getResources().getColor(R.color.grey));
/**
* 画灰色短横线,坐标计算依据是灰色短横向长度为深色长横线的一半,
* 并且,两种刻度线的中点在同一个圆的圆弧上
* 由于深色刻度线的长度 = 正中深色小圆的半径 = 屏幕宽度一半的0.101449倍
*/
canvas.drawLine(xc,
(float)(yc - radius + xc * 0.101449 / 4),
xc,
(float)(yc - radius + xc * 0.101449 / 4 * 3),
paintDegree);
}
//通过旋转画布简化坐标运算
canvas.rotate(mArc / (float)(mMaxValue - mMinValue), xc, yc);
}
/**
* 三个圆盘的半径和屏幕宽度的一半的比例为
* 屏幕宽度一半 :大圆 :中圆 :小圆 =
* 1 :0.481481 :0.201288 :0.101449
*/
//画大圆盘
Paint bigCircle = new Paint();
bigCircle.setColor(getResources().getColor(R.color.colorPrimary));
bigCircle.setAntiAlias(true);
canvas.drawCircle(xc, yc, (float)(xc * 0.481481), bigCircle);
//画中等圆盘
Paint midCircle = new Paint();
midCircle.setColor(getResources().getColor(R.color.colorPrimaryDark));
midCircle.setAntiAlias(true);
canvas.drawCircle(xc, yc, (float)(xc * 0.201288), midCircle);
//画小圆盘
Paint smallCircle = new Paint();
smallCircle.setColor(getResources().getColor(R.color.colorAccent));
smallCircle.setAntiAlias(true);
canvas.drawCircle(xc, yc, (float)(xc * 0.101449), smallCircle);
canvas.save();
//画指针
Paint paintCursor = new Paint();
//在屏幕宽度为621下,大小为3
paintCursor.setStrokeWidth((float)(mWidth * 0.004830));
paintCursor.setAntiAlias(true);
paintCursor.setColor(getResources().getColor(R.color.colorAccent));
/**
* 思路:要想让指针以圆心为中心旋转一定角度,要么旋转画布,要么根据坐标来画,
* 由于旋转角度比根据坐标更简单,所以就用旋转角度的方式来实现
*
* 指针具体旋转多少度,得根据算法来计算
*
* 算法:角度 = 输入值 * ( 总弧度 / 分值总数)
*
* 举例:本案例中给出的分值区间为(-50 ~ 50),所以分值总数为100
* 假设现在输入值为30,那么角度就为54°
*/
mAngle = mValue * (mArc / (float) (mMaxValue - mMinValue));
System.out.println("mAngle ==>" + mAngle);
canvas.rotate(mAngle, xc, yc);
/**
* xc * 0.101449 = 深色刻度线条的长度
* mWidth * 0.064412 = 在屏幕宽度为621下,长度为40
* 所以下面第三个参数的值可以理解为距离深色刻度线下方有40倍数的间隙
*/
canvas.drawLine(xc, yc,
xc - radius + (float)(xc * 0.101449 + mWidth * 0.064412),
yc + 3, paintCursor);
//覆盖在指针上的圆
/**
* 由于之前的坐标系已经发生了旋转,所以要在正下方写上文字,就需要旋转回来
* 当然也可以通过去计算坐标来显示在正下方,但旋转画布的方式更加简单和更容易理解
*/
canvas.rotate(-(mAngle + mArc / 2), xc, yc);
Paint pitchPaint = new Paint();
pitchPaint.setAntiAlias(true);
//在屏幕宽度为621下,字体大小为40
pitchPaint.setTextSize((float)(mWidth * 0.048309));
pitchPaint.setColor(getResources().getColor(R.color.colorAccent));
String pitch = "Pitch";
canvas.drawText(pitch,
xc - pitchPaint.measureText(pitch) / 2,
//这里加上180,是因为要加上最外面那个大的圆形的半径
//再加上(mWidth * 0.123188),是为了让文字和最大的那个圆产生间隙
//在屏幕宽度为621下,间隙为80
yc + (float)(xc * 0.481481 + mWidth * 0.123188),
pitchPaint);
//值
Paint pitchValuePaint = new Paint();
pitchValuePaint.setAntiAlias(true);
pitchValuePaint.setTextSize((float)(mWidth * 0.080515));
pitchValuePaint.setColor(getResources().getColor(R.color.colorAccent));
String value = "- -";
canvas.drawText(value,
xc - pitchValuePaint.measureText(value) / 2,
yc + (float)(xc * 0.481481 + mWidth * 0.209339),
pitchValuePaint);
canvas.restore();
}
}
经过修改过后,只需要修改全局变量mValue
的值,就可以使指针指向界面上对应的值了,比如mValue = 30
,运行效果
总结
总的来说,没什么难度,关键点在于绘图时的坐标计算,以及对旋转画布的理解,旋转画布是旋转的画布坐标系。在本案例中,我没有对指针的旋转做动画处理,是因为本文重在讲解Canvas画图,各位也可以自行加上指针旋转动画代码。好了,以上就是今天的内容,各位可以发挥自己的想象力,绘制出更多更有趣的图形。我是钟子路,Thanks for watching!