前言:最近一直在学自定义View的相关知识,感觉这在Android中还是挺难的一块,当然这也是每个程序员必经之路,正好公司项目要求实现类似仪表盘的效果用于直观的显示公司数据,于是就简单的写了个demo,记录实现的过程。上篇《Android自定义View实现圆弧进度效果》简单记录了圆弧及文字的绘制,渐变色的仪表盘效果将更加升入的介绍canvas及paint的使用(如画布旋转,paint的渐变色设置等)。
知识梳理
1.圆弧渐变色(SweepGradient)
2.圆弧上刻度绘制
3.指针指示当前数据位置(Bitmap)
4.数据文本跟随弧度显示(drawTextOnPath)
效果图:
1.继承自View
(1)重写构造方法,初始化Paint
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
/**
* 初始化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方法:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(remeasure(widthMeasureSpec, defaultSize),
remeasure(heightMeasureSpec, defaultSize));
}
remeasure方法:
/**
* 根据传入的值进行重新测量
*/
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等
@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
@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)绘制圆弧
/**
* 分别绘制外层 中间 内层圆环
*/
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)绘制渐变色圆弧上的大小刻度
/**
* 绘制外层渐变色圆弧上的大小刻度线
*/
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)绘制跟随圆弧弧度描述文字
/**
*绘制跟随圆弧弧度的文本
*/
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)绘制圆弧中心的数据及描述信息
/**
* 绘制圆弧中间的文本内容
*/
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)绘制当前数值对应的圆弧及指针图片指示
/**
* 绘制当前进度和指示图片
*/
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)动画效果
/**
*当前数据对应弧度旋转及当前数据自增动画
*/
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)设置数据及描述信息
/**
*设置数据
*/
public void setValues(int values, List 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://www.aliyun.com/jiaocheng/32462.html