Android自定义View实现渐变色仪表盘

前言:最近一直在学自定义View的相关知识,感觉这在Android中还是挺难的一块,当然这也是每个程序员必经之路,正好公司项目要求实现类似仪表盘的效果用于直观的显示公司数据,于是就简单的写了个demo,记录实现的过程。上篇《Android自定义View实现圆弧进度效果》简单记录了圆弧及文字的绘制,渐变色的仪表盘效果将更加升入的介绍canvas及paint的使用(如画布旋转,paint的渐变色设置等)。

知识梳理

1.圆弧渐变色(SweepGradient)

2.圆弧上刻度绘制

3.指针指示当前数据位置(Bitmap)

4.数据文本跟随弧度显示(drawTextOnPath)

效果图:

Android自定义View实现渐变色仪表盘_第1张图片

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实现圆弧的渐变色效果。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

你可能感兴趣的:(Android自定义View实现渐变色仪表盘)