业务分析
主要是实现一个类似汽车码表的功能,一根指针,指向设定的数值,当值发生变化时,指针会随着数据摆动,定位至设定的数值。也许文字表述有点不清楚,直接上效果图吧!
![](https://img.haomeiwen.com/i13752465/7420184b554af02f.gif)
实现思路
要实现这样一种效果,其实并不复杂,抽丝剥茧,一步一步来,首先第一步,为了以后复用,这一定是一个自定义View,然后根据所给画布的大小,先确定我们要画的仪表盘在哪个区域进行绘制,
-
绘制区域的确定
由于其他人要引用我们的View时,你不可能预先告诉他,我们的View有大小限制,你需要设置多少宽高,我们的自定义View才能正常显示,这显然是不现实的。因此为了预防这种情况的发生,我们需要先确定仪表盘的绘制区域:
两种极限情况
从上图中可以看到,不管你给定的宽高是多少,我们的绘制区域如图所示,先确定所能画的最大半径,由于并不是半圆,左右各超过了10°,所以在计算最大半径时,需要用到三角函数:
protected void onSizeChanged(int width, int height, int oldw, int oldh) {
super.onSizeChanged(width, height, oldw, oldh);
if (width > 0 && height > 0) {
mRightDownX = width;
mRightDownY = height;
// 根据被赋予的画布大小,计算半径
// 判断高度,高度的分界线为r+sinConner*r(此时的半径为width/2)
if (mRightDownY >= ((1 + Math.sin(CONNER * Math.PI / 180)) * (mRightDownX / 2))) {
mCircleRadius = mRightDownX / 2 - PADDING;
} else {
// 获取与要画圆的正切的正方形的边长
float minLength = Math.min(mRightDownX, mRightDownY) - PADDING;
// 获取圆的半径
// 由于不是半圆,要多画10度,需要算出这10度的y的高度(高度/(1+sin10))
mCircleRadius = (float) ((minLength / (1 + Math.sin(CONNER * Math.PI / 180))));
}
mCenterX = mStartX + mCircleRadius;
mCenterY = mStartY + mCircleRadius;
postInvalidate();
}
}
在onSizeChanged
回调中,可以获得设置的View的宽高,利用这个宽高,计算我们仪表盘的最大半径mCircleRadius
。
-
画布坐标系
由于画仪表盘时,会有很多涉及角度和弧度的绘制操作,因此这边讲一下在canvas中的角度对应的坐标系:
角度
- 绘制基础表盘
确定了半径之后,接下来的工作就容易很多了,首先画仪表盘的框架:阴影圆弧,最外层的弧线,中间的刻度和文字等。
private void drawOutArc(Canvas canvas) {
initPaint();
// 有阴影的大圆弧
float radiusWidth = mCircleRadius / 2;
mPaint.setStrokeWidth(radiusWidth);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(SysUtils.getColor(R.color.circle_board_bg));
canvas.drawArc(new RectF(mStartX + radiusWidth / 2, mStartY + radiusWidth / 2,
mCircleRadius * 2 + mStartX - radiusWidth / 2, mCircleRadius * 2 + mStartY - radiusWidth / 2),
180 - CONNER, 180 + CONNER * 2, false, mPaint);
// 最外面圆弧线条
float strokeWidth = SysUtils.convertDpToPixel(1);
mPaint.setStrokeWidth(strokeWidth);
mPaint.setColor(SysUtils.getColor(R.color.circle_board_default_radius));
canvas.drawArc(new RectF(mStartX, mStartY, mCircleRadius * 2 + mStartX,
mCircleRadius * 2 + mStartY), 180 - CONNER, 180 + CONNER * 2, false, mPaint);
// 中间的刻度
mPaint.setColor(SysUtils.getColor(R.color.circle_board_middle_line));
canvas.drawLine(mCenterX, mStartY + SysUtils.convertDpToPixel(2), mCenterX, mStartY + SysUtils.convertDpToPixel(7), mPaint);
//写文字
initPaint();
int spSize = 13;
float scaledSizeInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
spSize, getResources().getDisplayMetrics());
mPaint.setTextSize(scaledSizeInPixels);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setColor(SysUtils.getColor(R.color.circle_text_color));
// 7是刻度底端,13是行高
canvas.drawText("均值" + mText, mCenterX, mStartY + SysUtils.convertDpToPixel(7 + 13), mPaint);
}
- 画中间的点和指针
private void drawCenter(Canvas canvas) {
initPaint();
// 中间圆点的半径
int radius = SysUtils.convertDpToPixel(4);
mPaint.setStrokeWidth(SysUtils.convertDpToPixel(10));
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(SysUtils.getColor(R.color.circle_board_dot_red));
// 画指针
Path path = new Path();
// 设计到三个点,组成三角指针的形状(因指针头要圆头,需要四个点定位)
float fingureLength = mCircleRadius - SysUtils.convertDpToPixel(15);
float[] point1 = CustomerViewUtils.getCoordinatePoint(mCenterX, mCenterY, fingureLength, mCurrentAngle - 0.4f);
float[] point11 = CustomerViewUtils.getCoordinatePoint(mCenterX, mCenterY, fingureLength, mCurrentAngle + 0.4f);
path.lineTo(point1[0], point1[1]);
float[] point2 = CustomerViewUtils.getCoordinatePoint(mCenterX, mCenterY, radius / 2, mCurrentAngle - 90);
path.lineTo(point2[0], point2[1]);
float[] point3 = CustomerViewUtils.getCoordinatePoint(mCenterX, mCenterY, radius / 2, mCurrentAngle + 90);
path.lineTo(point3[0], point3[1]);
path.lineTo(point11[0], point11[1]);
path.lineTo(point1[0], point1[1]);
path.close();
canvas.drawPath(path, mPaint);
// 先画背景色的圆
mPaint.setColor(SysUtils.getColor(R.color.circle_board_dot_out));
canvas.drawCircle(mCenterX, mCenterY, radius + SysUtils.convertDpToPixel(2), mPaint);
// 画中心红色的圆
mPaint.setColor(SysUtils.getColor(R.color.circle_board_dot_red));
canvas.drawCircle(mCenterX, mCenterY, radius, mPaint);
mPaint.setStrokeCap(Paint.Cap.ROUND);
}
这里用到了一个计算方法,是通过角度来计算点在圆弧上的落点坐标的:
/**
* 依圆心坐标,半径,扇形角度,计算出扇形终射线与圆弧交叉点的xy坐标
* 0为水平 270是正上方
*
* @param radius 半径
* @param cirAngle 角度
* @return x,y
*/
public static float[] getCoordinatePoint(float centerX, float centerY, float radius, float cirAngle) {
float[] point = new float[2];
//将角度转换为弧度
double arcAngle = Math.toRadians(cirAngle);
if (cirAngle < 90) {
point[0] = (float) (centerX + Math.cos(arcAngle) * radius);
point[1] = (float) (centerY + Math.sin(arcAngle) * radius);
} else if (cirAngle == 90) {
point[0] = centerX;
point[1] = centerY + radius;
} else if (cirAngle > 90 && cirAngle < 180) {
arcAngle = Math.PI * (180 - cirAngle) / 180.0;
point[0] = (float) (centerX - Math.cos(arcAngle) * radius);
point[1] = (float) (centerY + Math.sin(arcAngle) * radius);
} else if (cirAngle == 180) {
point[0] = centerX - radius;
point[1] = centerY;
} else if (cirAngle > 180 && cirAngle < 270) {
arcAngle = Math.PI * (cirAngle - 180) / 180.0;
point[0] = (float) (centerX - Math.cos(arcAngle) * radius);
point[1] = (float) (centerY - Math.sin(arcAngle) * radius);
} else if (cirAngle == 270) {
point[0] = centerX;
point[1] = centerY - radius;
} else {
arcAngle = Math.PI * (360 - cirAngle) / 180.0;
point[0] = (float) (centerX + Math.cos(arcAngle) * radius);
point[1] = (float) (centerY - Math.sin(arcAngle) * radius);
}
return point;
}
- 画外层渐变色的圆弧
private void drawGradientArc(Canvas canvas) {
initPaint();
mPaint.setStrokeWidth(SysUtils.convertDpToPixel(1.5f));
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
int[] colors = {SysUtils.getColor(R.color.circle_start_color), SysUtils.getColor(R.color.circle_end_color)};
// SweepGradient默认是从3点钟位置开始渐变的,需要旋转一哈
SweepGradient gradient = new SweepGradient(mCenterX, mCenterY, colors, null);
Matrix matrix = new Matrix();
matrix.setRotate(180 - CONNER, mCenterX, mCenterY);
gradient.setLocalMatrix(matrix);
mPaint.setShader(gradient);
// 当前圆弧对应的展开角度,就是指针的角度-170度
canvas.drawArc(new RectF(mStartX, mStartY, mCircleRadius * 2 + mStartX,
mCircleRadius * 2 + mStartY), 180 - CONNER, mCurrentAngle - 170, false, mPaint);
}
- 设值和指针动画
仪表盘算是基本完成了,剩下的就是数值的设置和指针的摆动动画了:
/**
* 设置均值
*/
public void setMiddleValue(String deGree) {
if (TextUtils.isEmpty(deGree)) {
return;
}
float degree = SysUtils.parseFloat(deGree);
if (degree > 0.0f) {
mTotalValue = degree * 2;
mText = deGree + "%";
postInvalidate();
} else if ("0".equals(deGree)) {
mTotalValue = 0;
mText = "0%";
postInvalidate();
}
}
设置当前的数值:
public void setCurrentValue(String value) {
// 如果非法,直接设置为170度
if (TextUtils.isEmpty(value)) {
mFinalAngle = 180 - CONNER;
} else {
float v = SysUtils.parseFloat(value);
// 计算数值对应的角度
if (v <= 0.0f) {
// 0对应180度
mFinalAngle = 180;
} else if (v > mTotalValue) {
// 如果大于均值的两倍,直接拉满
mFinalAngle = CONNER + 360;
} else {
if (mTotalValue > 0.0f) {
float degree = 180 * (v / mTotalValue);
mFinalAngle = 180 + degree;
} else {
// 总值小于等于0,设置的值大于0,应该直接拉满
mFinalAngle = CONNER + 360;
}
}
}
ValueAnimator valueAnimator = ValueAnimator.ofFloat(mCurrentAngle, mFinalAngle);
valueAnimator.setDuration(500);
valueAnimator.setInterpolator(new Interpolator() {
@Override
public float getInterpolation(float v) {
return 1 - (1 - v) * (1 - v) * (1 - v);
}
});
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCurrentAngle = (float) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
valueAnimator.start();
}
总结
至此,一个完整的仪表盘功能就实现了,弄明白了基本思路,在此基础上加加减减,亦是一件很容易的事了~
网友评论