您的位置:首页 > 移动开发 > Android开发

android 自定义View 仪表盘 DashboardView 的实现

2016-06-27 20:23 549 查看
有天上班,老板突然扔给我一张图,



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

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

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

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

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

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

好吧,既然这样,那么就开源出来吧(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);
}


可以看到,这三个方法比上面的多了个float 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
ac0a
.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是一个很合理的值。


至此,我们的自定义View就算是完成了,感谢大家的耐心观看,如果有什么错误或者不足之处,还请多多指点。

github地址
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息