一、前言
本篇也是仪表自定义项目,其实仪表需要的无非是高中初中的一些数学基础知识,连矩阵都用不上,当然你可能会说Canvas.translate不就是矩阵转换么 ?
这么说其实也没啥问题,关键是你会自己写个矩阵去做乘法么?
二、实现
2.1 效果预览
2.2 实现逻辑
我们绘制的时候一定要注意角度,在Android Canvas坐标系中,坐标轴是向下为y轴正方向,因此,他的旋转是顺时针的,因此要在旋转为 135度的位置开始绘制。
另外文字的绘制我们使用的是旋转Canvas,其实还有方法就是drawTextOnPath,当然,这里简单的做法还是旋转Canvas。
三、 代码实现
3.1 初始化
这里我们自定义AutoMeterView要注意的一个问题是,整个圆弧的区域角度为270度。
private float sumDegree = (360 - 90.0f);
另一方面,我们这里为了方便绘制,将Canvas先逆时针旋转 (90 + 45) 度,使得X坐标系对齐开始绘制位置
canvas.rotate(90 + 45);
旋转Canvas 显然要简单的多,但其实还有可以利用Math#atan2实现
3.2 文字绘制
为了让文字绘制出现旋转效果,我们每次都需要旋转一定的角度,旋转角度计算公式如下
float perDegree = sumDegree / 100;
下面是文字绘制的核心代码
saveCount = canvas.save();
mPaint.setStyle(Paint.Style.FILL);
canvas.translate(width / 2F, height / 2F);
canvas.rotate(-90 - 45);
//该方法旋转的是坐标系,而不是图层
for (int i = 0; i <= 10; i++) {
if (i > 0) {
canvas.rotate(perDegree * 10);
//每次旋转坐标系一定的位置
}
mPaint.setColor(getPaintColor(i * 10));
String text = String.valueOf(i * 10);
mPaint.getTextBounds(text, 0, text.length(), bounds);
float textBaseline = innerRadius - strokeWidth - bounds.height() + getTextPaintBaseline(mPaint);
canvas.drawText(text, 0 - bounds.width() / 2F, -textBaseline, mPaint);
//从y轴负方向开始绘制
}
canvas.restoreToCount(saveCount);
3.3 指针绘制
制定绘制一定要放在后面做,这样做的原因防止被文字遮挡住指针
saveCount = canvas.save();
canvas.translate(width / 2F, height / 2F);
mPaint.setColor(getPaintColor(degreeOffset));
int tail = (int) dpTopx(outlineWidth);
double degreeRadianDelta = Math.toRadians(90 + 45 + degreeOffset * perDegree);
float startX = (float) (-tail * Math.cos(degreeRadianDelta));
float startY = (float) (-tail * Math.sin(degreeRadianDelta));
float endX = (float) ((innerRadius - tail) * Math.cos(degreeRadianDelta)); float endY = (float) ((innerRadius - tail) * Math.sin(degreeRadianDelta));
canvas.drawLine(startX, startY, endX, endY, mPaint);
canvas.drawCircle(0, 0, tail / 2, mPaint);
canvas.restoreToCount(saveCount);
3.4 动画
为了让指针偏移的过程中出现抖动回弹效果,我们这里利用动画来实现角度的更新
mDegreeAnimator = ValueAnimator.ofFloat(this.degreeOffset, bounceOffset, targetDegreeOffset);
mDegreeAnimator.setDuration(500);
mDegreeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
postUpdateDegree((Float) animation.getAnimatedValue());
}
});
mDegreeAnimator.start();
3.5 完整逻辑
下面是完整代码,测量和布局基本逻辑和之前的一样
public class AutoMeterView extends View {
private DisplayMetrics displayMetrics;
private TextPaint mPaint;
private float lineWidth = 10;
private float outlineWidth = 20;
private float textSize = 16;
private float sumDegree = (360 - 90.0f);
Rect bounds = new Rect();
RectF rectF = new RectF();
float degreeOffset = 0;
private ValueAnimator mDegreeAnimator;
public AutoMeterView(Context context) {
this(context, null);
}
public AutoMeterView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AutoMeterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
displayMetrics = context.getResources().getDisplayMetrics();
initPaint();
}
private void initPaint() {
// 实例化画笔并打开抗锯齿
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(true);
mPaint.setTextSize(dpTopx(textSize));
}
private float dpTopx(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
public void setSumDegree(float sumDegree) {
this.sumDegree = sumDegree;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = displayMetrics.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
heightSize = displayMetrics.widthPixels / 2;
}
//来个正方形大小
widthSize = heightSize = Math.min(widthSize, heightSize);
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(dpTopx(outlineWidth));
float strokeWidth = mPaint.getStrokeWidth();
float outRadius = (float) (Math.min(width / 2, height / 2));
float innerRadius = (outRadius - 3 * strokeWidth);
float innerDotRadius = (outRadius - 2 * strokeWidth);
float perDegree = sumDegree / 100;
int saveCount = canvas.save();
canvas.translate(width / 2F, height / 2F);
canvas.rotate(90 + 45);
mPaint.setColor(0xff54d68c);
rectF.set(-width / 2F + strokeWidth / 2, -height / 2F + strokeWidth / 2, width / 2F - strokeWidth / 2, height / 2F - strokeWidth / 2);
canvas.drawArc(rectF, 0, 20 * perDegree, false, mPaint);
mPaint.setColor(0xffff9922);
canvas.drawArc(rectF, 20 * perDegree, 60 * perDegree, false, mPaint);
mPaint.setColor(Color.RED);
canvas.drawArc(rectF, 80 * perDegree, 20 * perDegree, false, mPaint);
mPaint.setStrokeWidth(dpTopx(lineWidth));
strokeWidth = mPaint.getStrokeWidth();
outRadius = (int) (Math.min(width / 2, height / 2));
innerRadius = (int) (outRadius - 4 * strokeWidth);
innerDotRadius = (int) (outRadius - 3 * strokeWidth);
for (int i = 0; i <= 100; i++) {
float innerLineRadius = innerDotRadius;
if (i % 10 == 0) {
mPaint.setStrokeWidth(strokeWidth / 4);
innerLineRadius = innerRadius;
} else {
mPaint.setStrokeWidth(strokeWidth / 5);
}
float startX = (float) (innerLineRadius * Math.cos(Math.toRadians(i * perDegree)));
float startY = (float) (innerLineRadius * Math.sin(Math.toRadians(i * perDegree)));
float endX = (float) (outRadius * Math.cos(Math.toRadians(i * perDegree)));
float endY = (float) (outRadius * Math.sin(Math.toRadians(i * perDegree)));
mPaint.setColor(getPaintColor(i));
canvas.drawLine(startX, startY, endX, endY, mPaint);
}
canvas.restoreToCount(saveCount);
saveCount = canvas.save();
mPaint.setStyle(Paint.Style.FILL);
canvas.translate(width / 2F, height / 2F);
canvas.rotate(-90 - 45);
//该方法旋转的是坐标系,而不是图层
for (int i = 0; i <= 10; i++) {
if (i > 0) {
canvas.rotate(perDegree * 10);
//每次旋转坐标系一定的位置
}
mPaint.setColor(getPaintColor(i * 10));
String text = String.valueOf(i * 10);
mPaint.getTextBounds(text, 0, text.length(), bounds);
float textBaseline = innerRadius - strokeWidth - bounds.height() + getTextPaintBaseline(mPaint);
canvas.drawText(text, 0 - bounds.width() / 2F, -textBaseline, mPaint);
//从y轴负方向开始绘制
}
canvas.restoreToCount(saveCount);
saveCount = canvas.save();
canvas.translate(width / 2F, height / 2F);
mPaint.setColor(getPaintColor(degreeOffset));
int tail = (int) dpTopx(outlineWidth);
double degreeRadianDelta = Math.toRadians(90 + 45 + degreeOffset * perDegree);
float startX = (float) (-tail * Math.cos(degreeRadianDelta));
float startY = (float) (-tail * Math.sin(degreeRadianDelta));
float endX = (float) ((innerRadius - tail) * Math.cos(degreeRadianDelta)); float endY = (float) ((innerRadius - tail) * Math.sin(degreeRadianDelta));
canvas.drawLine(startX, startY, endX, endY, mPaint);
canvas.drawCircle(0, 0, tail / 2, mPaint);
canvas.restoreToCount(saveCount);
}
private int getPaintColor(float degreeOffset) {
if (degreeOffset <= 20) {
return 0xff54d68c;
} else if (degreeOffset > 20 && degreeOffset < 80) {
return 0xffff9922;
} else {
return Color.RED;
}
}
/**
* 基线到中线的距离=(Descent+Ascent)/2-Descent
* 注意,实际获取到的Ascent是负数。公式推导过程如下:
* 中线到BOTTOM的距离是(Descent+Ascent)/2,这个距离又等于Descent+中线到基线的距离,即(Descent+Ascent)/2=基线到中线的距离+Descent。
*/
public static float getTextPaintBaseline(Paint p) {
Paint.FontMetrics fontMetrics = p.getFontMetrics();
return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
}
public void setDegreeOffset(float targetDegreeOffset) {
if (targetDegreeOffset < 0) {
targetDegreeOffset = 0;
}
if (targetDegreeOffset > 100) {
targetDegreeOffset = 100;
}
if (this.degreeOffset == targetDegreeOffset) {
return;
}
if (getWidth() == 0 || getHeight() == 0) {
this.degreeOffset = targetDegreeOffset;
return;
}
if (mDegreeAnimator != null) {
mDegreeAnimator.cancel();
}
float perDegree = sumDegree / 100;
float bounceOffset = 0;
if (this.degreeOffset > targetDegreeOffset) {
bounceOffset = targetDegreeOffset - perDegree * 5;
if (bounceOffset < 0) {
bounceOffset = 0;
}
} else {
bounceOffset = targetDegreeOffset + perDegree * 5;
if (bounceOffset > 100) {
bounceOffset = 100;
}
}
mDegreeAnimator = ValueAnimator.ofFloat(this.degreeOffset, bounceOffset, targetDegreeOffset);
mDegreeAnimator.setDuration(500);
mDegreeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
postUpdateDegree((Float) animation.getAnimatedValue());
}
});
mDegreeAnimator.start();
}
private void postUpdateDegree(float animatedValue) {
this.degreeOffset = animatedValue;
postInvalidate();
}
}
以上是完整逻辑
四、总结
本篇到这里就结束了,其实仪表的画法比较简单,主要涉及到旋转、平移等操作,另外,对于文本的绘制,需要对文字的区域进行测量,找到相应的基线。
本篇是Animator + 刻度表,掌握动画与三角函数的结合运动,希望对你有所帮助。