Android 自定义View之八等份仪表盘

效果图

这里写图片描述


实现思路

首先拆解这个View,可以分成四个部分来绘制

  • 外圆刻度部分,包含最外面的刻度圆和里面对应的数值,此外圆分为八个等份,每等份中包含五个小等份,所以总共需要40个刻度。这里不是一个完整的圆,此外圆部分占一个完整圆的240度

  • 内圆刻度部分,此处总共有100个刻度,与外圆刻度保持着对应关系,进度发生改变时需要改变对应部分的颜色,超过外圆刻度6部分的颜色需要变成红色

  • 指针部分,指针由一个中间的圆和一条线段组成,运动范围为0~8的刻度之间,因为此仪表占圆的240度,所以指针的运动范围也就是0~240度

  • 数值部分,这个数值需要根据外圆刻度来进行计算,当刻度数值大于6时则显示为红色


具体实现

对于View的宽高测量、View里的dppx数值适配等知识,在之前的一篇博客中都有讲解,需要了解的可以去看看

http://blog.csdn.net/zhuwentao2150/article/details/77511148

(1)跳过onMeasure方法的说明,直接进入onDraw绘制流程

    @Override
    protected void onDraw(Canvas canvas) {
        drawArcScale(canvas);
        drawArcInside(canvas);
        drawInsideSumText(canvas);
        drawPointer(canvas);
    }
  • 绘制外刻度圆及数值

View的最外圆刻度并不是一个完整的圆,而是一个扇形,而且这个扇形的起始点是在左下角的,如何来计算这个起始点和终点的位置呢

一个半圆是180度,然后在这个半圆里,刻度1~7这地方,总共有五等份,也就是说一等分占圆的30度,那么起点的度数就是180度,终点的度数为240度

canvas.drawArc(new RectF(scaleWidth, scaleWidth, mWidth - scaleWidth, mHeight - scaleWidth), 180, 240, false, mScalePaint);

通过以上的方法绘制出的扇形是以View最左边开始的,所以还需要让它移动到左下角
我们在绘制时,可以先调用canvas.rotate()方法把画布逆时针旋转30度

canvas.rotate(-30, mWidth / 2, mHeight / 2);

这样旋转30度后,我们以最左边180度的地方为起点绘制的扇形就会移动到左下角了,其实本可以直接通过指定度数为150~270的方式直接绘制出扇形,但是由于还要绘制那些刻度线,刻度线的起始坐标在左边时才好计算,而要移到刻度0那个位置则比较麻烦,所以这里直接把画布旋转到那个位置,这样就可以避免复杂的计算了。

canvas.drawLine(scaleWidth, mHeight / 2, DensityUtil.dip2px(mContext, 15), mHeight / 2, mScalePaint);

虽然移动了画布,但绘制线条的坐标还是以View的最左边为基准
绘制完刻度后接着就要把数字刻度画出来,这数字刻度的画法需要使用到canvas.rotate()方法和canvas.translate()方法的配合
首先需要调用canvas.translate()方法暂时把View的圆点移动到需要绘制数值刻度的地方

canvas.translate(DensityUtil.dip2px(mContext, 15) + textWidth + DensityUtil.dip2px(mContext, 5), mHeight / 2); 

然后就需要把之前被旋转的角度再次旋转回去,让文字竖直显示,这也是为什么要使用canvas.translate方法移动View圆点到数值刻度绘制的地方的原因,因为只需要旋转数值刻度这一块区域就可以了,不能影响到其它地方

绘制外刻度圆及数值的全部代码

    /**
     * 画外圆和文字刻度
     */
    private void drawArcScale(Canvas canvas) {
        canvas.save();

        canvas.rotate(-30, mWidth / 2, mHeight / 2);

        // 最外圆的线条宽度,避免线条粗时被遮蔽
        float scaleWidth = mScalePaint.getStrokeWidth();

        canvas.drawArc(new RectF(scaleWidth, scaleWidth, mWidth - scaleWidth, mHeight - scaleWidth), 180, 240, false, mScalePaint);

        // 定义文字旋转回的角度
        int rotateValue = 30;
        // 总八个等分,每等分5个刻度,所以总共需要40个刻度
        for (int i = 0; i <= 40; i++) {
            if (i % 5 == 0) {
                canvas.drawLine(scaleWidth, mHeight / 2, DensityUtil.dip2px(mContext, 15), mHeight / 2, mScalePaint);

                // 画文字
                String text = String.valueOf(i / 5);
                Rect textBound = new Rect();
                mScaleTextPaint.getTextBounds(text, 0, text.length(), textBound);   // 获取文字的矩形范围
                int textWidth = textBound.right - textBound.left;  // 获得文字宽度
                int textHeight = textBound.bottom - textBound.top;  // 获得文字高度

                canvas.save();
                canvas.translate(DensityUtil.dip2px(mContext, 15) + textWidth + DensityUtil.dip2px(mContext, 5), mHeight / 2);  // 移动画布的圆点

                if (i == 0) {
                    // 如果刻度为0,则旋转度数为30度
                    canvas.rotate(rotateValue);
                } else {
                    // 大于0的刻度,需要逐渐递减30度
                    canvas.rotate(rotateValue);
                }
                rotateValue = rotateValue - 30;

                canvas.drawText(text, -textWidth / 2, textHeight / 2, mScaleTextPaint);
                canvas.restore();

            } else {
                canvas.drawLine(scaleWidth, mHeight / 2, DensityUtil.dip2px(mContext, 10), mHeight / 2, mScalePaint);
            }
            canvas.rotate(6, mWidth / 2, mHeight / 2);
        }
        canvas.restore();
    }

这里每次画一个刻度都使用canvas.rotate(6, mWidth / 2, mHeight / 2)方法旋转了6度,那么为什么偏偏是旋转6度呢,因为我们的View占一个圆的240度,而刻度数为40个,所以240除以40得到6度

  • 绘制内刻度

内刻度总共需要100个刻度,通过计算可以得知,外刻度圆是180~240度,实际上就是跨越了240度,所以用100除以240得到旋转的角度2.4f,也就是每画一个刻度,就需要旋转2.4度

    /**
     * 画内圆刻度
     */
    private void drawArcInside(Canvas canvas) {
        canvas.save();

        canvas.rotate(-30, mWidth / 2, mHeight / 2);
        for (int i = 0; i < 100; i++) {
            if (mInsideProgress > i) {
                // 大于外圆刻度6时显示红色
                if (i <= 75) {
                    mInsidePaint.setColor(insideCircleColor);
                } else {
                    mInsidePaint.setColor(Color.RED);
                }
            } else {
                mInsidePaint.setColor(Color.LTGRAY);
            }
            canvas.drawLine(DensityUtil.dip2px(mContext, 40), mHeight / 2, DensityUtil.dip2px(mContext, 50), mHeight / 2, mInsidePaint);
            canvas.rotate(2.4f, mWidth / 2, mHeight / 2);
        }

        canvas.restore();
    }

当内圆刻度大于外圆刻度6的时候,也就是内圆大于75刻度值时,需要使用红色标识刻度,这个75是如何得来的呢

当进度到达外刻度6~8时,显示为红色,此时的计算方式为100/8=12.5, 也就是这八个等份中每等份12.5,通过6*12.5得75,得知需要在75开始时改变内刻度颜色

  • 绘制指针

指针就是一个直线加中间一个圆形组成

    /**
     * 画指针
     */
    private void drawPointer(Canvas canvas) {
        canvas.save();

        // 初始时旋转到0的位置
        canvas.rotate(-30, mWidth / 2, mHeight / 2);
        canvas.rotate(mProgress, mWidth / 2, mHeight / 2);
        canvas.drawLine(mWidth / 2 - DensityUtil.dip2px(mContext, 60), mHeight / 2, mWidth / 2, mHeight / 2, mPointerPaint);
        canvas.drawCircle(mWidth / 2, mHeight / 2, DensityUtil.dip2px(mContext, 5), mPointerPaint);

        canvas.restore();
    }
  • 绘制中间文字数值

文字数值在外圆刻度大于6、内圆刻度大于75时,需要变成红色来显示

    /**
     * 画内部数值
     */
    private void drawInsideSumText(Canvas canvas) {
        canvas.save();

        if (mInsideProgress > 75) {
            mTextPaint.setColor(Color.RED);
        } else {
            mTextPaint.setColor(Color.BLACK);
        }
        // 获取文字居中显示需要的参数
        String showValue = String.valueOf(value);
        Rect textBound = new Rect();
        mTextPaint.getTextBounds(showValue, 0, showValue.length(), textBound);    // 获取文字的矩形范围
        float textWidth = textBound.right - textBound.left;  // 获得文字宽
        float textHeight = textBound.bottom - textBound.top; // 获得文字高
        canvas.drawText(showValue, mWidth / 2 - textWidth / 2, mHeight / 2 + textHeight + DensityUtil.dip2px(mContext, 45), mTextPaint);

        canvas.restore();
    }

(2)设置进度
对外提供一个方法用于改变View的指针和内部刻度显示

    /**
     * 设置进度
     */
    public void setProgress(int progress) {

        // 内部刻度的进度
        this.mInsideProgress = progress;

        // 指针显示的进度
        this.mProgress = (float) progress * 2.4f;

        // 设置中间文字显示的数值
        this.value = (float) (progress * 0.08);

        invalidate();
    }

传递过来的参数progress的范围是0~100,所以有些需要进行一下处理才可以使用到View中
1. 内部刻度:总共有100个刻度标识,所以可以直接用progress
2. 指针进度:View总共的度数为240度,所以需要计算的方式就是240除以100,得2.4度
3. 文字刻度:需要用100来表示0~8,所以计算方式是8除以100,得0.08


源码

/**
 * 圆形仪表盘
 * Created by zhuwentao on 2017-08-26.
 */
public class CircleMeterView extends View {

    // 外圆刻度画笔
    private Paint mScalePaint;

    // 外圆刻度数值画笔
    private Paint mScaleTextPaint;

    // 内圆画笔
    private Paint mInsidePaint;

    // 中间数值画笔
    private Paint mTextPaint;

    // 指针画笔
    private Paint mPointerPaint;

    // View宽
    private float mWidth;

    // View高
    private float mHeight;

    // 外刻度圆进度
    private float mProgress = 0;

    // 内刻度圆进度
    private float mInsideProgress = 0;

    // 中间显示的数值
    private float value = 0;

    private int scaleColor;

    private int scaleTextColor;

    private int insideCircleColor;

    private int textSize;

    private int textColor;

    private int pointerColor;

    private Context mContext;

    public CircleMeterView(Context context) {
        this(context, null);
    }

    public CircleMeterView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleMeterView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 获取用户配置属性
        TypedArray tya = context.obtainStyledAttributes(attrs, R.styleable.CircleMeter);
        scaleColor = tya.getColor(R.styleable.CircleMeter_scaleColor, Color.BLUE);
        scaleTextColor = tya.getColor(R.styleable.CircleMeter_scaleTextColor, Color.BLACK);
        insideCircleColor = tya.getColor(R.styleable.CircleMeter_insideCircleColor, Color.BLUE);
        textSize = tya.getDimensionPixelSize(R.styleable.CircleMeter_textSize2, 36);
        textColor = tya.getColor(R.styleable.CircleMeter_textColor2, Color.BLACK);
        pointerColor = tya.getColor(R.styleable.CircleMeter_pointerColor, Color.RED);
        tya.recycle();

        initUI();
    }

    private void initUI() {
        mContext = getContext();

        // 刻度圆画笔
        mScalePaint = new Paint();
        mScalePaint.setAntiAlias(true);
        mScalePaint.setStrokeWidth(DensityUtil.dip2px(mContext, 2));
        mScalePaint.setColor(scaleColor);
        mScalePaint.setStrokeCap(Paint.Cap.ROUND);
        mScalePaint.setStyle(Paint.Style.STROKE);

        // 刻度文字画笔
        mScaleTextPaint = new Paint();
        mScaleTextPaint.setAntiAlias(true);
        mScaleTextPaint.setStrokeWidth(DensityUtil.dip2px(mContext, 2));
        mScaleTextPaint.setColor(scaleTextColor);
        mScaleTextPaint.setTextSize(24);
        mScaleTextPaint.setStyle(Paint.Style.FILL);

        // 中间值的画笔
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setStrokeWidth(DensityUtil.dip2px(mContext, 2));
        mTextPaint.setTextSize(36);
        mTextPaint.setColor(textColor);
        mTextPaint.setStrokeJoin(Paint.Join.ROUND);
        mTextPaint.setStyle(Paint.Style.FILL);

        // 内部扇形刻度画笔
        mInsidePaint = new Paint();
        mInsidePaint.setAntiAlias(true);
        mInsidePaint.setStrokeWidth(DensityUtil.dip2px(mContext, 1));
        mInsidePaint.setColor(insideCircleColor);
        mInsidePaint.setStyle(Paint.Style.FILL);

        // 指针画笔
        mPointerPaint = new Paint();
        mPointerPaint.setAntiAlias(true);
        mPointerPaint.setStrokeWidth(DensityUtil.dip2px(mContext, 5));
        mPointerPaint.setColor(pointerColor);
        mPointerPaint.setStrokeCap(Paint.Cap.ROUND);
        mPointerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        drawArcScale(canvas);
        drawArcInside(canvas);
        drawInsideSumText(canvas);
        drawPointer(canvas);
    }

    /**
     * 画外圆和文字刻度
     */
    private void drawArcScale(Canvas canvas) {
        canvas.save();

        canvas.rotate(-30, mWidth / 2, mHeight / 2);

        // 最外圆的线条宽度,避免线条粗时被遮蔽
        float scaleWidth = mScalePaint.getStrokeWidth();

        canvas.drawArc(new RectF(scaleWidth, scaleWidth, mWidth - scaleWidth, mHeight - scaleWidth), 180, 240, false, mScalePaint);

        // 定义文字旋转回的角度
        int rotateValue = 30;
        // 总八个等分,每等分5个刻度,所以总共需要40个刻度
        for (int i = 0; i <= 40; i++) {
            if (i % 5 == 0) {
                canvas.drawLine(scaleWidth, mHeight / 2, DensityUtil.dip2px(mContext, 15), mHeight / 2, mScalePaint);

                // 画文字
                String text = String.valueOf(i / 5);
                Rect textBound = new Rect();
                mScaleTextPaint.getTextBounds(text, 0, text.length(), textBound);   // 获取文字的矩形范围
                int textWidth = textBound.right - textBound.left;  // 获得文字宽度
                int textHeight = textBound.bottom - textBound.top;  // 获得文字高度

                canvas.save();
                canvas.translate(DensityUtil.dip2px(mContext, 15) + textWidth + DensityUtil.dip2px(mContext, 5), mHeight / 2);  // 移动画布的圆点

                if (i == 0) {
                    // 如果刻度为0,则旋转度数为30度
                    canvas.rotate(rotateValue);
                } else {
                    // 大于0的刻度,需要逐渐递减30度
                    canvas.rotate(rotateValue);
                }
                rotateValue = rotateValue - 30;

                canvas.drawText(text, -textWidth / 2, textHeight / 2, mScaleTextPaint);
                canvas.restore();

            } else {
                canvas.drawLine(scaleWidth, mHeight / 2, DensityUtil.dip2px(mContext, 10), mHeight / 2, mScalePaint);
            }
            canvas.rotate(6, mWidth / 2, mHeight / 2);
        }
        canvas.restore();
    }

    /**
     * 画内圆刻度
     */
    private void drawArcInside(Canvas canvas) {
        canvas.save();

        canvas.rotate(-30, mWidth / 2, mHeight / 2);
        for (int i = 0; i < 100; i++) {
            if (mInsideProgress > i) {
                // 大于外圆刻度6时显示红色
                if (i <= 75) {
                    mInsidePaint.setColor(insideCircleColor);
                } else {
                    mInsidePaint.setColor(Color.RED);
                }
            } else {
                mInsidePaint.setColor(Color.LTGRAY);
            }
            canvas.drawLine(DensityUtil.dip2px(mContext, 40), mHeight / 2, DensityUtil.dip2px(mContext, 50), mHeight / 2, mInsidePaint);
            canvas.rotate(2.4f, mWidth / 2, mHeight / 2);
        }

        canvas.restore();
    }

    /**
     * 画内部数值
     */
    private void drawInsideSumText(Canvas canvas) {
        canvas.save();

        if (mInsideProgress > 75) {
            mTextPaint.setColor(Color.RED);
        } else {
            mTextPaint.setColor(Color.BLACK);
        }
        // 获取文字居中显示需要的参数
        String showValue = String.valueOf(value);
        Rect textBound = new Rect();
        mTextPaint.getTextBounds(showValue, 0, showValue.length(), textBound);    // 获取文字的矩形范围
        float textWidth = textBound.right - textBound.left;  // 获得文字宽
        float textHeight = textBound.bottom - textBound.top; // 获得文字高
        canvas.drawText(showValue, mWidth / 2 - textWidth / 2, mHeight / 2 + textHeight + DensityUtil.dip2px(mContext, 45), mTextPaint);

        canvas.restore();
    }

    /**
     * 画指针
     */
    private void drawPointer(Canvas canvas) {
        canvas.save();

        // 旋转到0的位置
        canvas.rotate(-30, mWidth / 2, mHeight / 2);
        canvas.rotate(mProgress, mWidth / 2, mHeight / 2);
        canvas.drawLine(mWidth / 2 - DensityUtil.dip2px(mContext, 60), mHeight / 2, mWidth / 2, mHeight / 2, mPointerPaint);
        canvas.drawCircle(mWidth / 2, mHeight / 2, DensityUtil.dip2px(mContext, 5), mPointerPaint);

        canvas.restore();
    }


    /**
     * 设置进度
     */
    public void setProgress(int progress) {

        // 内部刻度的进度
        this.mInsideProgress = progress;

        // 指针显示的进度
        this.mProgress = (float) progress * 2.4f;

        // 设置中间文字显示的数值
        this.value = (float) (progress * 0.08);

        invalidate();
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int myWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int myWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int myHeightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int myHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        // 获取宽度
        if (myWidthSpecMode == MeasureSpec.EXACTLY) {
            mWidth = myWidthSpecSize;
        } else {
            // wrap_content
            mWidth = DensityUtil.dip2px(mContext, 120);
        }

        // 获取高度
        if (myHeightSpecMode == MeasureSpec.EXACTLY) {
            mHeight = myHeightSpecSize;
        } else {
            // wrap_content
            mHeight = DensityUtil.dip2px(mContext, 120);
        }

        // 设置该view的宽高
        setMeasuredDimension((int) mWidth, (int) mHeight);
    }
}

总结

  • 画布工具为我们提供的canvas.rotate()方法,指定的角度正数为顺时针旋转,负数为逆时针旋转,善用它可以让我们省去很多坐标位置计算,但要注意使用旋转后对之后绘制的影响,最好是使用canvas.save()canvas.restore()方法避免影响到之后的绘制。

  • 画布canvas.translate()方法,通过移动View原点坐标的方式来让画笔移动到想让它去的地方,移动后我们再通过canvas绘制上去的图形,就是以移动后的坐标做为View的原点,它和canvas.rotate()方法组合,可以减轻很多的计算负担,为了避免和之后绘制的模块有影响,通常需要使用canvas.save()canvas.restore()方法把需要进行移动修改的地方包裹起来。

  • 当以View的宽高做为圆直径时,要避免绘制最外围线条时圆与正方形相接的四个顶点被遮蔽,所以绘制前要通过Paint.getStrokeWidth()方法确定好外圆线条的宽度,再在这个基础上开始绘制。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值