前言:最近一直在学自定义View的相关知识,感觉这在Android中还是挺难的一块,当然这也是每个程序员必经之路,正好公司项目要求实现类似仪表盘的效果用于直观的显示公司数据,于是就简单的写了个demo,记录实现的过程。上篇《Android自定义View实现圆弧进度效果》简单记录了圆弧及文字的绘制,渐变色的仪表盘效果将更加升入的介绍canvas及paint的使用(如画布旋转,paint的渐变色设置等)。
知识梳理
1.圆弧渐变色(SweepGradient)
2.圆弧上刻度绘制
3.指针指示当前数据位置(Bitmap)
4.数据文本跟随弧度显示(drawTextOnPath)
效果图:
1.继承自View
(1)重写构造方法,初始化Paint
1
2
3
4
5
6
7
8
9
10
11
12
|
public DashBoardView(Context context) { this (context, null ); } public DashBoardView(Context context, AttributeSet attrs) { this (context, attrs, 0 ); } public DashBoardView(Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); init(); } |
初始化相关Paint
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
/** * 初始化Paint */ private void init() { //设置默认宽高值 defaultSize = dp2px( 260 ); //设置图片线条的抗锯齿 mPaintFlagsDrawFilter = new PaintFlagsDrawFilter ( 0 , Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); //最外层圆环渐变画笔设置 mOuterGradientPaint = new Paint(Paint.ANTI_ALIAS_FLAG); //设置圆环渐变色渲染 mOuterGradientPaint.setXfermode( new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP)); float position[] = { 0 .1f, 0 .3f, 0 .8f}; Shader mShader = new SweepGradient(width / 2 , radius, mColors, position); mOuterGradientPaint.setShader(mShader); mOuterGradientPaint.setStrokeCap(Paint.Cap.ROUND); mOuterGradientPaint.setStyle(Paint.Style.STROKE); mOuterGradientPaint.setStrokeWidth( 30 ); //最外层圆环刻度画笔设置 mCalibrationPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCalibrationPaint.setColor(Color.WHITE); mCalibrationPaint.setStyle(Paint.Style.STROKE); //中间圆环画笔设置 mMiddlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mMiddlePaint.setStyle(Paint.Style.STROKE); mMiddlePaint.setStrokeCap(Paint.Cap.ROUND); mMiddlePaint.setStrokeWidth( 5 ); mMiddlePaint.setColor(GRAY_COLOR); //内层圆环画笔设置 mInnerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mInnerPaint.setStyle(Paint.Style.STROKE); mInnerPaint.setStrokeCap(Paint.Cap.ROUND); mInnerPaint.setStrokeWidth( 4 ); mInnerPaint.setColor(GRAY_COLOR); PathEffect mPathEffect = new DashPathEffect( new float []{ 5 , 5 , 5 , 5 }, 1 ); mInnerPaint.setPathEffect(mPathEffect); //外层圆环文本画笔设置 mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(GRAY_COLOR); mTextPaint.setTextSize(dp2px( 12 )); //中间文字画笔设置 mCenterTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCenterTextPaint.setTextAlign(Paint.Align.CENTER); //中间圆环进度画笔设置 mMiddleProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mMiddleProgressPaint.setColor(GREEN_COLOR); mMiddleProgressPaint.setStrokeCap(Paint.Cap.ROUND); mMiddleProgressPaint.setStrokeWidth( 5 ); mMiddleProgressPaint.setStyle(Paint.Style.STROKE); //指针图片画笔 mPointerBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPointerBitmapPaint.setColor(GREEN_COLOR); //获取指针图片及宽高 mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pointer); mBitmapHeight = mBitmap.getHeight(); mBitmapWidth = mBitmap.getWidth(); } |
注:
A、最外层圆弧的渐变色使用的是SweepGradient类实现的,SweepGradient继承自Shader;
B、注意渐变色的开始角度问题,如果跟圆弧起始角度不一致,记得使用矩阵转换进行旋转,再让paint去设置shader;
C、SweepGradient的第3个参数int[] colors必须包含两个及以上颜色值,不然会报错;
D、SweepGradient的第四个参数的数组大小必须和第三个参数的数组大小一样,也可以填入null。
(2)重写onMeasure,用于测量view宽高
onMeasure方法:
1
2
3
4
5
|
@Override protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(remeasure(widthMeasureSpec, defaultSize), remeasure(heightMeasureSpec, defaultSize)); } |
remeasure方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/** * 根据传入的值进行重新测量 */ public int remeasure( int measureSpec, int defaultSize) { int result; int specSize = MeasureSpec.getSize(measureSpec); switch (MeasureSpec.getMode(measureSpec)) { case MeasureSpec.UNSPECIFIED: //未指定 result = defaultSize; break ; case MeasureSpec.AT_MOST: //设置warp_content时设置默认值 result = Math.min(specSize, defaultSize); break ; case MeasureSpec.EXACTLY: //设置math_parent 和设置了固定宽高值 result=specSize; break ; default : result = defaultSize; } return result; } |
(3)重写onChange,用于获取view宽高
在onChange方法中获取当前View的宽高及获取圆弧的半径,初始化圆弧的RectF等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
@Override protected void onSizeChanged( int w, int h, int oldw, int oldh) { super .onSizeChanged(w, h, oldw, oldh); //确定View宽高 width = w; height = h; //圆环半径 radius = width / 2 ; //外层圆环 float oval1 = radius - mOuterGradientPaint.getStrokeWidth() * 0 .5f; mOuterRectF = new RectF(-oval1, -oval1, oval1, oval1); //中间和内层圆环 float oval2 = radius * 5 / 8 ; float oval3 = radius * 3 / 4 ; mInnerRectF = new RectF(-oval2 + dp2px( 5 ), -oval2 + dp2px( 5 ), oval2 - dp2px( 5 ), oval2 - dp2px( 5 )); mMiddleRectF = new RectF(-oval3 + dp2px( 10 ), -oval3 + dp2px( 10 ), oval3 - dp2px( 10 ), oval3 - dp2px( 10 )); //中间进度圆环 oval4 = radius * 6 / 8 ; mMiddleProgressRectF = new RectF(-oval4+ dp2px( 10 ), -oval4+ dp2px( 10 ), oval4- dp2px( 10 ), oval4- dp2px( 10 )); } |
(4)重写onDraw方法,用于绘制view
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@SuppressLint ( "DrawAllocation" ) @Override protected void onDraw(Canvas canvas) { //设置画布绘图无锯齿 canvas.setDrawFilter(mPaintFlagsDrawFilter); //绘制圆弧 drawArc(canvas); //绘制圆弧上的刻度 drawCalibration(canvas); //绘制跟随圆弧path的文字 drawArcText(canvas); //绘制圆弧中心文字 drawCenterText(canvas); //绘制当前bitmap指针指示进度 drawBitmapProgress(canvas); } |
2.Canvas绘制view
mStartAngle=105f,mEndAngle=250f
(1)绘制圆弧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/** * 分别绘制外层 中间 内层圆环 */ private void drawArc(Canvas canvas) { canvas.save(); canvas.translate(width / 2 , height / 2 ); //画布旋转140° canvas.rotate( 140 ); //最外层的渐变圆环 canvas.drawArc(mOuterRectF, -mStartAngle, -mEndAngle, false , mOuterGradientPaint); //绘制内层虚线圆弧 canvas.drawArc(mInnerRectF, -mStartAngle, -mEndAngle, false , mInnerPaint); //绘制中间圆弧 canvas.drawArc(mMiddleRectF, -mStartAngle, -mEndAngle, false , mMiddlePaint); canvas.restore(); } |
(2)绘制渐变色圆弧上的大小刻度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/** * 绘制外层渐变色圆弧上的大小刻度线 */ private void drawCalibration(Canvas canvas) { int dst = ( int ) ( 2 * radius - mOuterGradientPaint.getStrokeWidth()); for ( int i = 0 ; i <= 40 ; i++) { canvas.save(); canvas.rotate(-(- 30 + 6 * i), radius, radius); if (i % 10 == 0 ) { mCalibrationPaint.setStrokeWidth( 4 ); //绘制大刻度 canvas.drawLine(dst, radius, 2 * radius, radius, mCalibrationPaint); } else { //小刻度 mCalibrationPaint.setStrokeWidth( 1 ); canvas.drawLine(dst, radius, 2 * radius, radius, mCalibrationPaint); } canvas.restore(); } } |
注:
A、圆弧的总弧度为240f,循环40次
B、小刻度每次旋转6弧度,每绘制10次小刻度就会绘制一次大刻度,即大刻度每次旋转60弧度
(3)绘制跟随圆弧弧度描述文字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/** * 绘制跟随圆弧弧度的文本 */ private void drawArcText(Canvas canvas) { canvas.save(); //每次旋转角度 int rotateAngle = 30 ; //旋转画布 canvas.rotate(- 118 , radius - dp2px( 26 ), radius-dp2px( 103 )); for ( int i = 0 ; i < valueList.size(); i++) { //计算起始角度 int startAngle = 30 * i - 108 ; //设置数据跟着圆弧绘制 Path paths = new Path(); paths.addArc(mInnerRectF, startAngle, rotateAngle); float textLen = mTextPaint.measureText(valueList.get(i)); canvas.drawTextOnPath(valueList.get(i), paths, -textLen / 2 + dp2px( 20 ), -dp2px( 22 ), mTextPaint); //canvas.drawText(text[i], radius - 10, radius * 3 / 16+dp2px(10), mTextPaint); } canvas.restore(); } |
注:
A、drawTextOnPath为文字随path路径显示,drawTextOnPath的第3个参数hOffset为文字水平方向的偏移量,第4个参数vOffset为文字垂直方向的偏移量;
B、重点是画布开始时的旋转角度及不同文字的起始角度
(4)绘制圆弧中心的数据及描述信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/** * 绘制圆弧中间的文本内容 */ private void drawCenterText(Canvas canvas) { //绘制当前数据值 mCenterTextPaint.setColor(GREEN_COLOR); mCenterTextPaint.setTextSize(dp2px( 25 )); mCenterTextPaint.setStyle(Paint.Style.STROKE); canvas.drawText(String.valueOf(mAnimatorValue), radius, radius, mCenterTextPaint); //绘制当前数据描述 mCenterTextPaint.setTextSize(dp2px( 20 )); canvas.drawText(mCurrentDes, radius, radius + dp2px( 25 ), mCenterTextPaint); } |
(5)绘制当前数值对应的圆弧及指针图片指示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/** * 绘制当前进度和指示图片 */ private void drawBitmapProgress(Canvas canvas) { //如果当前角度为0,则不绘制指示图片 if (mCurrentAngle==0f){ return ; } canvas.save(); canvas.translate(radius, radius); canvas.rotate( 270 ); //绘制对应的圆弧 canvas.drawArc(mMiddleProgressRectF, -mStartAngle- 20 , mCurrentAngle+ 5 , false , mMiddleProgressPaint); canvas.rotate( 60 + mCurrentAngle); //利用矩阵平移使图片指针方向始终指向刻度 Matrix matrix = new Matrix(); matrix.preTranslate(-oval4 - mBitmapWidth * 3 / 8 + 10 , -mBitmapHeight / 2 ); canvas.drawBitmap(mBitmap, matrix, mPointerBitmapPaint); canvas.restore(); } |
注:为了使指针图片的指针一直指向刻度盘上的刻度,这里使用了矩阵的平移。
3.添加动画及数据
(1)动画效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
/** * 当前数据对应弧度旋转及当前数据自增动画 */ public void startRotateAnim() { ValueAnimator mAngleAnim = ValueAnimator.ofFloat(mCurrentAngle, mTotalAngle); mAngleAnim.setInterpolator( new AccelerateDecelerateInterpolator()); mAngleAnim.setDuration( 2500 ); mAngleAnim.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mCurrentAngle = ( float ) valueAnimator.getAnimatedValue(); postInvalidate(); } }); mAngleAnim.start(); ValueAnimator mNumAnim = ValueAnimator.ofInt(mAnimatorValue, mCurrentValue); mNumAnim.setDuration( 2500 ); mNumAnim.setInterpolator( new LinearInterpolator()); mNumAnim.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mAnimatorValue = ( int ) valueAnimator.getAnimatedValue(); postInvalidate(); } }); mNumAnim.start(); } |
(2)设置数据及描述信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
/** * 设置数据 */ public void setValues( int values, List<String> valueList) { this .valueList=valueList; if (values <= 0 ) { mCurrentValue = values; mTotalAngle = 0f; mCurrentDes = "" ; } else if (values <= 14000 ) { mCurrentValue = values; mTotalAngle = values / 14000f * 60 - 2 ; Log.e( "rcw" , "mTotalAngle=" +mTotalAngle); mCurrentDes = "基础目标" ; } else if (values> 14000 &&values <= 17000 ) { mCurrentValue = values; mCurrentDes = "测试目标" ; mTotalAngle = values / 17000f * 120 - 2 ; } else if (values> 17000 &&values <= 21000 ) { mCurrentValue = values; mTotalAngle = values / 21000f * 180 - 2 ; mCurrentDes = "保底目标" ; } else { mCurrentValue=values; float ratio=values / 21000f; if (ratio< 20 ){ mTotalAngle = ratio+ 180 ; } else { mTotalAngle = ( float ) (ratio* 0.2 + 200 ); } mCurrentDes = "冲刺目标" ; } startRotateAnim(); } |
总结:自定义View实现仪表盘效果用到了canvas的旋转及矩阵平移;drawTextOnpath使的文字跟随path绘制;SweepGradient实现圆弧的渐变色效果。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://blog.csdn.net/ruancw/article/details/80746698