android 自定义View 仪表盘 DashboardView 的实现
2016-06-27 20:23
549 查看
有天上班,老板突然扔给我一张图,
说:这个东西能不能做一下。
我说应该可以。然后老板那就没有下文了,我想既然问了,那我就抽空做一下。
当我做出来的时候去找老板,我说上次你给我发的那个图,我已经做出来了,您要不要看一下。
老板说,不用了,不需要了。
不需要了。。 不需要了 。。 不需要了!!
听到这句话我的内心是几乎是崩溃的,哭哭。
好吧,既然这样,那么就开源出来吧(github地址),并且写下了这篇博客作为实现过程的记录,也希望能给一部分人带来一些帮助。
另:本人可能姿势水平不太高,如果有什么错误还请大家帮忙指正 , 蟹蟹。
好了 , 进入正题
首先放一张实现完成的gif
第一步,我们先在attrs文件下添加我们的自定义属性:
第二部,新建一个java类,来管理这些属性
注意,当使用
最后,创建我们的
至此,我们的自定义View就算是完成了,感谢大家的耐心观看,如果有什么错误或者不足之处,还请多多指点。
github地址
说:这个东西能不能做一下。
我说应该可以。然后老板那就没有下文了,我想既然问了,那我就抽空做一下。
当我做出来的时候去找老板,我说上次你给我发的那个图,我已经做出来了,您要不要看一下。
老板说,不用了,不需要了。
不需要了。。 不需要了 。。 不需要了!!
听到这句话我的内心是几乎是崩溃的,哭哭。
好吧,既然这样,那么就开源出来吧(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地址
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories