有天上班,老板突然扔给我一张图,

android MPAndroidChart 仪表 android虚拟仪表盘_android

说:这个东西能不能做一下。

我说可以。然后老板那就没有下文了,我想既然问了,那我就抽空做一下。

当我做出来的时候去找老板,我说上次你给我发的那个图,我已经做出来了,您要不要看一下。

老板说,不用了,不需要了。

不需要了。。 不需要了 。。 不需要了!!

听到这句话我的内心是几乎是崩溃的,哭哭。

好吧,既然这样,那么就开源出来吧(github地址),并且写下了这篇博客作为实现过程的记录,也希望能给一部分人带来一些帮助。

另:本人可能姿势水平不太高,如果有什么错误还请大家帮忙指正 , 蟹蟹。

好了 , 进入正题

首先放一张实现完成的gif

android MPAndroidChart 仪表 android虚拟仪表盘_开源_02

第一步,在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();

    }

在这里我用到了ValueAnimatorValuAnimator本质上就是通过设置一个起始值和结束值,来取到一个从起始值到结束值的一个逐渐增长的Animation值。在draw方法中使用这个值并且不断的重绘,就能达到一种动画效果。

通过ofFloat来设置起始值和结束值,并且动态的设置了动画时长。通过addUpdateListener(AnimatorUpdateListener)来获取不断变化的Animation值,并且重绘。

最后,调用valueAnimator.start()我们的动画效果就这么完成了,但是做完这步你会发现,动画是有了,但是这指针是匀速转动的,不太理想。

这时候,就要用到Interpolator也就是插值器了,安卓自带的插值器能够使Animation值的变化产生加速增长、减速增长、先加速后减速、回弹等效果。但是安卓自带的几个插值器用在这里也不太理想,不太符合真正的仪表盘的变化效果。

所以在这里需要自定义一个插值器来满足我们的需求。

首先确定插值器的曲线图

android MPAndroidChart 仪表 android虚拟仪表盘_插值器_03

数学表达式为

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是一个很合理的值。