Android自定义View之巧用canvas实现仿雷达数据分布图

本文地址:https://blog.csdn.net/qq_40785165/article/details/115279537,转载需附上此地址

大家好,我是小黑,一个还没秃头的程序员~~~

宝剑锋从磨砺出,梅花香自苦寒来。今天工作不努力,明天努力找工作。

今天分享的内容是前一段时间做的一个仿雷达数据分布图,效果如下:

如效果所示,控件自动分成若干等分,并在分割线处标注文字,中间分布若干数据,分割的数量以及其他样式均可自定义配置

思路如下:

  1. 继承View实现构造方法
  2. 定义自定义属性并获取
  3. 实例化各个画笔工具
  4. 测量宽高,通过canvas的旋转进行线条以及数据点的绘制

代码拆解如下:

(一)继承View实现构造方法,由于篇幅原因,这个步骤就不贴代码了,具体看底下完整代码

(二)定义自定义属性并获取,定义了分割的等份、颜色、字体样式等,属性的用处都有注释,具体看代码,代码如下:

attr_radar_view.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RadarChartView">
        <attr name="padding" format="integer" />
        <attr name="line_count" format="integer" />
        <attr name="radar_background" format="color" />
        <attr name="text_size" format="integer" />
        <attr name="text_color" format="color" />
        <attr name="line_width" format="integer" />
    </declare-styleable>
</resources>
 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RadarChartView);
        //内间距
        mPadding = typedArray.getInt(R.styleable.RadarChartView_padding, DEFAULT_PADDING);
        //线条的数量,决定均分成多少部分
        mLineCount = typedArray.getInt(R.styleable.RadarChartView_line_count, DEFAULT_LINE_COUNT);
        DEFAULT_BACKGROUND = mContext.getResources().getColor(R.color.color_grey);
        DEFAULT_TEXT_COLOR = mContext.getResources().getColor(R.color.colorBlack);
        //背景颜色
        mBackground = typedArray.getColor(R.styleable.RadarChartView_radar_background, DEFAULT_BACKGROUND);
        //文字的颜色
        mTextColor = typedArray.getColor(R.styleable.RadarChartView_text_color, DEFAULT_TEXT_COLOR);
        //文字的大小
        mTextSize = typedArray.getColor(R.styleable.RadarChartView_text_size, DEFAULT_TEXT_SIZE);
        //分割线的宽度
        mLineWidth = typedArray.getColor(R.styleable.RadarChartView_line_width, DEFAULT_LINE_WIDTH);

(三)实例化各个画笔工具,代码如下:

    private void initPaint() {
        setBackgroundColor(Color.TRANSPARENT);
        mPaintLine = new Paint();
        mPaintLine.setStrokeWidth(mLineWidth);
        mPaintLine.setAntiAlias(true);
        mPaintLine.setStyle(Paint.Style.STROKE);
        mPaintLine.setColor(Color.WHITE);

        mPaintCircle = new Paint();
        mPaintCircle.setStyle(Paint.Style.FILL);
        mPaintCircle.setAntiAlias(true);
        mPaintCircle.setColor(mBackground);

        mPaintPoint = new Paint();
        mPaintPoint.setColor(Color.WHITE);
        mPaintPoint.setStyle(Paint.Style.FILL);

        mRect = new Rect();
        mPaintText = new Paint();
        mPaintText.setTextSize(mTextSize);
        mPaintText.setColor(mTextColor);

    }

(四)测量宽高,通过canvas的旋转进行线条以及数据点的绘制,测量的宽高使用父组件的宽高即可,代码如下:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
        setMeasuredDimension(mWidthSize, mWidthSize);
    }

绘制时根据旋转进行分割线、数据点的绘制,比起计算坐标绘制会显得更便捷与精确,思路为:restore->rotate->save->restore->rotate->save,第一次旋转是为了绘制线条或者数据点,第二次旋转是为了将画布旋转会原来的位置,所以旋转的角度为:平均角度*索引

  1. save():保存当前画布的状态
  2. restore():取出上一次画布的状态,调用前需要先调用save(),否则会报错

绘制数据的思路就是利用Bean类中的角度字段进行一定角度的旋转,旋转的起始位置为左水平线,绘制分割线以及数据的代码如下所示:

        //画一个带有填充色的圆
        canvas.drawCircle(mWidthSize / 2, mWidthSize / 2, mWidthSize / 2 - mPadding, mPaintCircle);
        //最外层画一个有颜色的圆弧
        canvas.drawCircle(mWidthSize / 2, mWidthSize / 2, mWidthSize / 2 - mPadding, mPaintLine);
        canvas.save();
        //根据份数求出度数
        float angle = 360f / (mLineCount * 2);
        //根据旋转绘制分割线
        for (int i = 0; i < mLineCount; i++) {
            canvas.restore();
            canvas.rotate(angle * i, mWidthSize / 2, mWidthSize / 2);
            canvas.drawLine(mPadding, mWidthSize / 2, mWidthSize - mPadding, mWidthSize / 2, mPaintLine);
            canvas.save();
            canvas.restore();
            canvas.rotate(-angle * i, mWidthSize / 2, mWidthSize / 2);
            canvas.save();
        }
        //插入标点数据,从左边的水平线开始-即180度的线开始旋转分布
        for (int i = 0; i < mList.size(); i++) {
            DataBean item = mList.get(i);
            if (item.getStatus() == 2) {
                mPaintPoint.setColor(mContext.getResources().getColor(R.color.colorAccent));
            } else {
                mPaintPoint.setColor(mContext.getResources().getColor(R.color.colorPrimary));
            }
            canvas.restore();
            //根据数据传进来的度数字段进行旋转
            float angle1 = item.getAngle();
            Log.e(TAG, "onDraw: " + angle1 + "," + mList.size());
            canvas.rotate(angle1, mWidthSize / 2, mWidthSize / 2);
            canvas.drawCircle(mWidthSize / 2 - (float) Math.random() * (mWidthSize / 2 - mPadding - 80) - 50, mWidthSize / 2, 5, mPaintPoint);
            canvas.save();
            canvas.restore();
            canvas.rotate(-angle1, mWidthSize / 2, mWidthSize / 2);
            canvas.save();
        }

由于文字需要保持水平角度,所以不能使用旋转,这里只能在相应位置绘制了,不过所幸位置都是有规律性的,配合Math.cos()以及Math.sin()函数就可以算出位置,如图:

代码如下:

//绘制分隔位置的文字
        for (int i = 0; i < mLabels.size(); i++) {
            String label = mLabels.get(i);
            mPaintText.getTextBounds(label, 0, label.length(), mRect);
            int radius = mWidthSize / 2 - mPadding;
            float cos = (float) Math.abs(Math.cos(Math.toRadians(angle * i)));
            float sin = (float) Math.abs(Math.sin(Math.toRadians(angle * i)));
            float b = radius * cos;
            float a = radius * sin;
            float x = 0;
            float y = 0;
            if (angle * i == 90) {
                x = mWidthSize / 2 - b - mRect.width() / 2;
                y = mWidthSize / 2 - a;
            } else if (angle * i > 90 && angle * i < 180) {
                x = mWidthSize / 2 + b;
                y = mWidthSize / 2 + mRect.height() / 2 - a;
            } else if (angle * i == 180) {
                x = mWidthSize / 2 + b;
                y = mWidthSize / 2 + mRect.height() / 2 - a;
            } else if (angle * i > 180 && angle * i < 270) {
                x = mWidthSize / 2 + b;
                y = mWidthSize / 2 + mRect.height() / 2 + a;
            } else if (angle * i == 270) {
                x = mWidthSize / 2 - b - mRect.width() / 2;
                y = mWidthSize / 2 + a + mRect.height();
            } else if (angle * i > 270 && angle * i < 360) {
                x = mWidthSize / 2 - b - mRect.width();
                y = mWidthSize / 2 + a + mRect.height() / 2;
            } else {
                x = mWidthSize / 2 - b - mRect.width();
                y = mWidthSize / 2 + mRect.height() / 2 - a;
            }
            canvas.drawText(label, x, y, mPaintText);
        }

        canvas.save();

最后,完整的代码如下,自定义属性上方有贴出来代码:

public class RadarChartView extends View {
    private Context mContext;
    private Paint mPaintLine;//绘制线的画笔
    private Paint mPaintCircle;//绘制外部圆的画笔
    private Paint mPaintPoint;//绘制中间点的画笔
    private Rect mRect;
    private Paint mPaintText;//绘制文字的画笔

    public int DEFAULT_BACKGROUND;
    public int DEFAULT_TEXT_SIZE = 22;
    public int DEFAULT_TEXT_COLOR;
    public int DEFAULT_LINE_WIDTH = 5;
    public int DEFAULT_LINE_COUNT = 4;
    public int DEFAULT_PADDING = 0;

    private List<DataBean> mList = new ArrayList<>();
    private int mWidthSize;
    private List<String> mLabels = new ArrayList<>();
    private int mPadding;
    private int mLineCount;
    private int mBackground;
    private int mTextSize;
    private int mTextColor;
    private int mLineWidth;


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

    public RadarChartView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RadarChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RadarChartView);
        //内间距
        mPadding = typedArray.getInt(R.styleable.RadarChartView_padding, DEFAULT_PADDING);
        //线条的数量,决定均分成多少部分
        mLineCount = typedArray.getInt(R.styleable.RadarChartView_line_count, DEFAULT_LINE_COUNT);
        DEFAULT_BACKGROUND = mContext.getResources().getColor(R.color.color_grey);
        DEFAULT_TEXT_COLOR = mContext.getResources().getColor(R.color.colorBlack);
        //背景颜色
        mBackground = typedArray.getColor(R.styleable.RadarChartView_radar_background, DEFAULT_BACKGROUND);
        //文字的颜色
        mTextColor = typedArray.getColor(R.styleable.RadarChartView_text_color, DEFAULT_TEXT_COLOR);
        //文字的大小
        mTextSize = typedArray.getColor(R.styleable.RadarChartView_text_size, DEFAULT_TEXT_SIZE);
        //分割线的宽度
        mLineWidth = typedArray.getColor(R.styleable.RadarChartView_line_width, DEFAULT_LINE_WIDTH);
        initPaint();
    }

    public void setList(List<DataBean> list) {
        mList = list;
    }

    public void setLabels(List<String> labels) {
        mLabels = labels;
    }

    private void initPaint() {
        setBackgroundColor(Color.TRANSPARENT);
        mPaintLine = new Paint();
        mPaintLine.setStrokeWidth(mLineWidth);
        mPaintLine.setAntiAlias(true);
        mPaintLine.setStyle(Paint.Style.STROKE);
        mPaintLine.setColor(Color.WHITE);

        mPaintCircle = new Paint();
        mPaintCircle.setStyle(Paint.Style.FILL);
        mPaintCircle.setAntiAlias(true);
        mPaintCircle.setColor(mBackground);

        mPaintPoint = new Paint();
        mPaintPoint.setColor(Color.WHITE);
        mPaintPoint.setStyle(Paint.Style.FILL);

        mRect = new Rect();
        mPaintText = new Paint();
        mPaintText.setTextSize(mTextSize);
        mPaintText.setColor(mTextColor);

    }

    private static final String TAG = "RadarChartView";

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
        setMeasuredDimension(mWidthSize, mWidthSize);
    }


    @Override
    protected void onDraw(Canvas canvas) {

        //画一个带有填充色的圆
        canvas.drawCircle(mWidthSize / 2, mWidthSize / 2, mWidthSize / 2 - mPadding, mPaintCircle);
        //最外层画一个有颜色的圆弧
        canvas.drawCircle(mWidthSize / 2, mWidthSize / 2, mWidthSize / 2 - mPadding, mPaintLine);
        canvas.save();
        //根据份数求出度数
        float angle = 360f / (mLineCount * 2);
        //根据旋转绘制分割线
        for (int i = 0; i < mLineCount; i++) {
            canvas.restore();
            canvas.rotate(angle * i, mWidthSize / 2, mWidthSize / 2);
            canvas.drawLine(mPadding, mWidthSize / 2, mWidthSize - mPadding, mWidthSize / 2, mPaintLine);
            canvas.save();
            canvas.restore();
            canvas.rotate(-angle * i, mWidthSize / 2, mWidthSize / 2);
            canvas.save();
        }
        //绘制分隔位置的文字
        for (int i = 0; i < mLabels.size(); i++) {
            String label = mLabels.get(i);
            mPaintText.getTextBounds(label, 0, label.length(), mRect);
            int radius = mWidthSize / 2 - mPadding;
            float cos = (float) Math.abs(Math.cos(Math.toRadians(angle * i)));
            float sin = (float) Math.abs(Math.sin(Math.toRadians(angle * i)));
            float b = radius * cos;
            float a = radius * sin;
            float x = 0;
            float y = 0;
            if (angle * i == 90) {
                x = mWidthSize / 2 - b - mRect.width() / 2;
                y = mWidthSize / 2 - a;
            } else if (angle * i > 90 && angle * i < 180) {
                x = mWidthSize / 2 + b;
                y = mWidthSize / 2 + mRect.height() / 2 - a;
            } else if (angle * i == 180) {
                x = mWidthSize / 2 + b;
                y = mWidthSize / 2 + mRect.height() / 2 - a;
            } else if (angle * i > 180 && angle * i < 270) {
                x = mWidthSize / 2 + b;
                y = mWidthSize / 2 + mRect.height() / 2 + a;
            } else if (angle * i == 270) {
                x = mWidthSize / 2 - b - mRect.width() / 2;
                y = mWidthSize / 2 + a + mRect.height();
            } else if (angle * i > 270 && angle * i < 360) {
                x = mWidthSize / 2 - b - mRect.width();
                y = mWidthSize / 2 + a + mRect.height() / 2;
            } else {
                x = mWidthSize / 2 - b - mRect.width();
                y = mWidthSize / 2 + mRect.height() / 2 - a;
            }
            canvas.drawText(label, x, y, mPaintText);
        }

        canvas.save();
        //插入标点数据,从左边的水平线开始-即180度的线开始旋转分布
        for (int i = 0; i < mList.size(); i++) {
            DataBean item = mList.get(i);
            if (item.getStatus() == 2) {
                mPaintPoint.setColor(mContext.getResources().getColor(R.color.colorAccent));
            } else {
                mPaintPoint.setColor(mContext.getResources().getColor(R.color.colorPrimary));
            }
            canvas.restore();
            //根据数据传进来的度数字段进行旋转
            float angle1 = item.getAngle();
            canvas.rotate(angle1, mWidthSize / 2, mWidthSize / 2);
            canvas.drawCircle(mWidthSize / 2 - (float) Math.random() * (mWidthSize / 2 - mPadding - 80) - 50, mWidthSize / 2, 5, mPaintPoint);
            canvas.save();
            canvas.restore();
            canvas.rotate(-angle1, mWidthSize / 2, mWidthSize / 2);
            canvas.save();
        }
        super.onDraw(canvas);
    }
}

实体类如下:

public class DataBean {
    private String c_time;//用来计算度数的字段
    private int status;//状态
    private float angle;//求出的相应的度数

    public DataBean(String c_time, int status) {
        this.c_time = c_time;
        this.status = status;
    }

    public float getAngle() {
        //计算度数,根据自己项目的逻辑进行计算
        return TimeUtils.getAngle(c_time.split(" ")[1]);
    }

    public String getC_time() {
        return c_time;
    }

    public void setC_time(String c_time) {
        this.c_time = c_time;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }
}

使用的时候往控件中传入相应的文字标签以及数据即可

mRadarChartView.setLabels(Arrays.asList("00:00", "03:00", "06:00", "09:00", "12:00", "15:00", "18:00", "21:00"));
mRadarChartView.setList(mList);

这样一来,一个可以均分的数据分布图就完成了,最后,希望喜欢我文章的朋友们可以帮忙点赞、收藏、评论,也可以关注一下,如果有问题可以在评论区提出,我后面会继续写关于自己自定义控件的经验的博客,与大家分享,谢谢大家的支持与阅读!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值