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

前言:最近一直在学自定义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<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://www.aliyun.com/jiaocheng/32462.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容