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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容