有天上班,老板突然扔给我一张图,
说:这个东西能不能做一下。
我说可以。然后老板那就没有下文了,我想既然问了,那我就抽空做一下。
当我做出来的时候去找老板,我说上次你给我发的那个图,我已经做出来了,您要不要看一下。
老板说,不用了,不需要了。
不需要了。。 不需要了 。。 不需要了!!
听到这句话我的内心是几乎是崩溃的,哭哭。
好吧,既然这样,那么就开源出来吧(github地址),并且写下了这篇博客作为实现过程的记录,也希望能给一部分人带来一些帮助。
另:本人可能姿势水平不太高,如果有什么错误还请大家帮忙指正 , 蟹蟹。
好了 , 进入正题
首先放一张实现完成的gif
第一步,在attrs文件下添加自定义属性:
<declare-styleable name="DashboardView">
<attr name="arcColor" format="color"/>
<attr name="padding" format="dimension"/>
<attr name="android:text"/>
<attr name="tikeCount" format="integer"/>
<attr name="Unit" format="string"/>
<attr name="android:textSize"/>
<attr name="backgroundColor" format="color" />
<attr name="textColor" format="color"/>
<attr name="startProgressColor" format="color" />
<attr name="endProgressColor" format="color" />
<attr name="startNumber" format="integer" />
<attr name="maxNumber" format="integer" />
<attr name="progressColor" format="color"/>
</declare-styleable>
第二步,新建一个java类,来管理这些属性
public class DashboardViewAttr {
private int mTikeCount;
...
public DashboardViewAttr(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DashboardView, defStyleAttr, 0);
mTikeCount = ta.getInt(R.styleable.DashboardView_tikeCount, 48);
...
ta.recycle();
}
public int getmTikeCount() {
return mTikeCount;
}
...
}
注意,当使用 TypedArray
进行加载属性的时候,最后记得要回收一下,即调用TypedArray.recycle()
。
最后,创建DashboardView
,使之继承View
实现继承自View的三个构造方法,并且添加初始化方法,在带有三个参数的构造方法下面实例化刚刚创建的属性管理类。
public DashboardView(Context context) {
this(context, null);
init(context);
}
public DashboardView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
init(context);
}
public DashboardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
dashboardViewattr = new DashboardViewAttr(context, attrs, defStyleAttr);
init(context);
}
init方法中又分成两个方法,1、初始化自定义属性,2、初始化各个画笔:
//初始化自定义属性
mTikeCount = dashboardViewattr.getmTikeCount();
mTextSize = dashboardViewattr.getmTextSize();
mTextColor = dashboardViewattr.getTextColor();
mText = dashboardViewattr.getmText();
unit = dashboardViewattr.getUnit();
backgroundColor = dashboardViewattr.getBackground();
startColor = dashboardViewattr.getStartColor();
endColor = dashboardViewattr.getEndColor();
startNum = dashboardViewattr.getStartNumber();
maxNum = dashboardViewattr.getMaxNumber();
progressColor = dashboardViewattr.getProgressColor();
//初始化画笔
paintProgress.setAntiAlias(true);//设置抗锯齿
paintProgress.setStrokeWidth(progressHeight);//设置画笔宽度
paintProgress.setStyle(Paint.Style.STROKE);//设置画笔为空心
paintProgress.setStrokeCap(Paint.Cap.ROUND);//设置画笔笔触为圆形
paintProgress.setColor(progressColor);//设置画笔颜色
paintProgress.setDither(true);//设置防抖动
...
重写 onMeasure 方法, 目的是之在非EXACTLY模式下,也就是空间宽高指定为wrap_centent的时候,给他规定一个最大值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int realWidth = startMeasure(widthMeasureSpec);
int realHeight = startMeasure(heightMeasureSpec);
setMeasuredDimension(realWidth, realHeight);
}
private int startMeasure(int msSpec) {
int result = 0;
int mode = MeasureSpec.getMode(msSpec);
int size = MeasureSpec.getSize(msSpec);
if (mode == MeasureSpec.EXACTLY) {
result = size;
} else {
result = PxUtils.dpToPx(200, mContext);
}
return result;
}
终于到了最重要的环节之一了。也就是绘制环节,现在我们来重写onDraw方法,获取Canvas。
首先,先把坐标原点移动到中心,以方便绘制
canvas.translate(mWidth/2,mHight/2);
然后,绘制表盘,也就是该View上不会动的东西。
//绘制最外层的圆和背景色(如果设置了背景色的话)
private void drawBackground(Canvas canvas) {
//最外阴影线
canvas.drawCircle(0, 0, mWidth / 2 - 2, paintOutCircle);
canvas.save();
//背景
if (backgroundColor != 0) {
paintBackground.setColor(backgroundColor);
canvas.drawCircle(0, 0,mWidth / 2 -4, paintBackground);
}
}
根据设置的刻度数,绘制刻度(默认48)
private void drawerNum(Canvas canvas) {
canvas.save(); //记录画布状态
canvas.rotate(-(180 - START_ARC + 90), 0, 0);
int numY = -mHight / 2 + OFFSET + progressHeight;
float rAngle = DURING_ARC / mTikeCount;
for (int i = 0; i < mTikeCount + 1; i++) {
canvas.save(); //记录画布状态
canvas.rotate(rAngle * i,0, 0);
if (i == 0 || i % 3 ==0){
canvas.drawLine(0 , numY + 5, 0, numY + 25, paintNum);//画长刻度线
}else {
canvas.drawLine(0 , numY + 5, 0, numY + 15, paintNum);//画短刻度线
}
canvas.restore();
}
canvas.restore();
}
save()
为保存画布,restore()
为恢复上次保存的画布,retate()
为旋转画布。
利用这三个方法,可以很轻松的绘制刻度
然后绘制中心的小圆和小环。
private void drawInPoint(Canvas canvas) {
mMinCircleRadius = mWidth / 15 ;
mMinRingRadius = mMinCircleRadius *2 + mMinCircleRadius / 20;
paintCenterRingPointer.setStrokeWidth(mMinCircleRadius);
canvas.drawCircle(0, 0, mMinCircleRadius, paintCenterCirclePointer);//中心圆点
canvas.drawCircle(0, 0, mMinRingRadius, paintCenterRingPointer);//中心小圆环
}
接下来我们来绘制能动的部分
首先,弧形progressbar
private void drawProgress(Canvas canvas, float percent) {
rectF2 = new RectF( -mWidth/2 + OFFSET, - mHight /2 + OFFSET, mWidth/2 - OFFSET, mHight/2 - OFFSET);
canvas.drawArc(rectF2, START_ARC, DURING_ARC, false, paintProgressBackground);
if (percent > 1.0f) {
percent = 1.0f; //限制进度条在弹性的作用下不会超出
}
if (!(percent <= 0.0f )) {
canvas.drawArc(rectF2, START_ARC, percent * DURING_ARC, false, paintProgress);
}
}
绘制表针
private void drawerPointer(Canvas canvas, float percent) {
mMinCircleRadius = mWidth / 15 ;
rectF1 = new RectF( - mMinCircleRadius / 2, - mMinCircleRadius / 2, mMinCircleRadius / 2 , mMinCircleRadius / 2);
canvas.save();
float angel = DURING_ARC * (percent - 0.5f) - 180 ;
canvas.rotate(angel, 0, 0);//指针与外弧边缘持平
Path pathPointerRight = new Path();
pathPointerRight.moveTo(0, mMinCircleRadius / 2);
pathPointerRight.arcTo(rectF1,270,-90);
pathPointerRight.lineTo(0, mHight / 2 - OFFSET- progressHeight);
pathPointerRight.lineTo(0, mMinCircleRadius / 2);
pathPointerRight.close();
Path pathPointerLeft = new Path();
pathPointerLeft.moveTo( 0, mMinCircleRadius / 2);
pathPointerLeft.arcTo(rectF1,270,90);
pathPointerLeft.lineTo(0, mHight/2 - OFFSET- progressHeight);
pathPointerLeft.lineTo(0, mMinCircleRadius / 2);
pathPointerLeft.close();
Path pathCircle = new Path();
pathCircle.addCircle(0, 0, mMinCircleRadius / 4, Path.Direction.CW);
canvas.drawPath(pathPointerLeft,paintPointerLeft);
canvas.drawPath(pathPointerRight, paintPointerRight);
canvas.drawPath(pathCircle, paintPinterCircle);
canvas.restore();
}
表针分为三个部分:1、指针左半部分,2、指针右半部分,3、指针上的圆点。
考虑到兼容性的问题,在这里绘制指针并没有采用Path()
的布尔运算 ,而是使用的Path
基础的lineTo()
, arcTo()
,moveTo()
等方法。一样能实现同样的效果。
最后,绘制文字。
private void drawText(Canvas canvas, float percent) {
float length ;
paintText.setTextSize(mTextSize);
length = paintText.measureText(mText);
canvas.drawText(mText,-length /2, mMinRingRadius*2.0F, paintText);
paintText.setTextSize(mTextSize * 1.2f);
speed = StringUtil.floatFormat(startNum + (maxNum - startNum) * percent) + unit;
length = paintText.measureText(speed);
canvas.drawText(speed, -length /2 , mMinRingRadius*2.5F, paintText);
}
可以看到,这三个方法比上面的多了个percent
参数,我们将根据这个参数来改变指针的角度,progress
的进度以及数字的变化。
这个参数其实就是seekbar
传进来的progress
值。其实到现在可以算是结束了。但是指针和progress
的变化都特别生硬,所以我们要给他加上一个动画效果。
public void setPercent(int percent) {
setAnimator(percent);
}
private void setAnimator(final float percent) {
//根据变化的幅度来调整动画时长
animatorDuration = (long) Math.abs(percent - oldPercent) * 20;
valueAnimator = ValueAnimator.ofFloat(oldPercent,percent).setDuration(animatorDuration);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//把获取到的
DashboardView.this.percent = (float) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
oldPercent = percent;
}
});
valueAnimator.start();
}
在这里我用到了ValueAnimator
,ValuAnimator
本质上就是通过设置一个起始值和结束值,来取到一个从起始值到结束值的一个逐渐增长的Animation
值。在draw
方法中使用这个值并且不断的重绘,就能达到一种动画效果。
通过ofFloat来设置起始值和结束值,并且动态的设置了动画时长。通过addUpdateListener(AnimatorUpdateListener)
来获取不断变化的Animation
值,并且重绘。
最后,调用valueAnimator.start()
我们的动画效果就这么完成了,但是做完这步你会发现,动画是有了,但是这指针是匀速转动的,不太理想。
这时候,就要用到Interpolator
也就是插值器了,安卓自带的插值器能够使Animation
值的变化产生加速增长、减速增长、先加速后减速、回弹等效果。但是安卓自带的几个插值器用在这里也不太理想,不太符合真正的仪表盘的变化效果。
所以在这里需要自定义一个插值器来满足我们的需求。
首先确定插值器的曲线图
数学表达式为
pow(2, -10 * x) * sin((x - factor / 4) * (2 * PI) / factor) + 1
factor = 0.4 这个我们在构造函数的时候指定
新建类SpringInterpolator
,继承自BaseInterpolator
,将上面的数学表达式转化成代码,完整的代码为:
public class SpringInterpolator implements Interpolator {
private final float mTension;
public SpringInterpolator() {
mTension = 0.4f;
}
public SpringInterpolator(float tension) {
mTension = tension;
}
@Override
public float getInterpolation(float input) {
float result = (float) (Math.pow(2,-10 * input) *
Math.sin((input - mTension / 4) * (2 * Math.PI)/mTension) + 1);
return result;
}
}
好了,现在把这个自定义插值器设置给VuleAnimation
就能达到预期的效果了。
也可以通过改变tension
的值来改变指针的摆动效果,不过我认为0.4是一个很合理的值。