前言:最近一直在学自定义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实现圆弧的渐变色效果。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持自由互联。