控件开发实战-自绘折线图

目前图标开源项目很多,也很优秀,如MpAndroidChart。这些开源项目都提供了丰富的功能,能够满足大多数的开发需求。然而实际开发中,需求是不会适应框架的,总有些需求是开源项目所不能满足的,那么这时候就需要我们自己搞。同时也告诉自己,我们不是代码的搬运工。

提到折线图,我们日常生活中的坐标系与Android屏幕的坐标系是不同的,至今没明白为什么屏幕坐标系原点要在右上角。首先需要图坐标系与Android坐标系的关系,或者说给定一组图中点的坐标,如何把它转化成Android坐标系内的坐标。

话说得有点绕,上图:



屏幕坐标系左上角是原点,直角坐标系原点在显示区域内。

blankSize你可以理解为任何引起内边距的可能,它可能是直接的padding,也可能是单纯为了显示效果而设置的固定值,在这里是padding和固定值的累加。上几篇博文介绍到view应当支持padding,那么用户如果就不想设置padding,坐标线的显示就会出现问题,所以,这里给他默认一个小小的留白。

那么,在padding和留白的共同作用下,显示区域的边界是什么呢?

_left=paddingLeft+blankLeft;

_right=width-paddingRight-blankRight;

_top=paddingTop+blankTop;

_buttom=width-paddingBottom-blankBottom;

也可以把上下左右的留白都设计成一样的 ,那样也可以。

我们在纸上画折线图啊,柱状图等的时候,总喜欢先画原点,然后x,y两条轴,然后是单位,单位1代表多少。这样做的一个问题就是容易造成纸张大部分的留白,或者单位设的不好,纸不够长了。Android屏幕可没有这么长,所以,为了好看和实用,任意时候,让折线或者曲线在竖直方向上顶天立地。

最大值:maxValue 显示在图表的上边界。

最小值minValue 显示在图表的下边界。

数据数量,valueSize

x轴单位长度xScale:默认图表显示八个点,用图表宽度除以8得到。如果valueSize>defaultNum,则宽度除以valueSize。有人问为什么默认八个点,如果数据一共两个点,那显示出来是多么尴尬的效果,太长了不好看。

y轴单位长度yScale,height/(maxValue-minValue)

(x0,y0)在屏幕上应该绘制的Y坐标:

y=height-blankSizeB-(y0-minValue)*yScale

x的值是随着数组的循环在变化的

x=_left+(i-1)*xScale 

这样,直角坐标系中的坐标就被我们转化成了安卓坐标系的坐标。

拿到了坐标,我们就可以遍历数据,一条line一条line的画图了。This is level 1!


这种方式得到的是简单折线图,没有数据怎么办?人家就想看一张空的图表怎么办?难道让用户面对黑屏尴尬吗?N0,不论有没有数据,都要显示图表的边界,而且没数据的时候要用文字展示出来。

理想的文字显示位置是图表的中心。下面是在全屏情况下在中心写Text时的坐标

Paint.FontMetrics fm = paint.getFontMetrics();
文字高度 float textHeight = fm.descent - fm.ascent;
文字长度 = paint.measureText()
横向居中:X = (布局width - 文字长度)/2
纵向居中:Y(baseLine ) = (布局height + 文字高度)/2 - descent



有了无数据时的操作,你的图表更加健壮,然而,它的显示样式不可更改,而且折线图怎么看怎么别扭,太难看。那么上一些二年级水平的内容,如何让你的折线图变平滑的方式:运用贝赛尔曲线。

贝赛尔曲线是像这样:

图片来自百度百科

的一段曲线,关于贝赛尔曲线的详情请查看百度百科,总之很牛叉就对了。运用它就可以使得折线图变成光滑的曲线图,前提是你需要设置控制点。

这里我们用三阶:,(x1,y1)(x2,y2)之间的控制点我选取((x1+x2)/2,y1),((x1+x2)/2,y2)

Android为我们提供了画贝赛尔曲线的方法,Path.cubicTo

然后,就得到了有点优雅的曲线:


最后,很多需求都需要显示一个均值,或者一个标准,我们绘制标准线,并且将标准线上方画上颜色以区分:

这是效果图。



编码中用到的颜色,线宽,文字样式,需要提供xml直接设置的方式,具体操作前几篇讲过,就不再赘述,

接下来附上代码:

public class MySimpleChart extends View {

    private DecimalFormat decimalFormat = new DecimalFormat("0.0");

    //没数据时候的 显示样式
    private int noData_BackColor = Color.WHITE;//backgroundColor
    private float noData_TextSize = DensityUtils.dp(getContext(), 15);//提示信息字体大小
    private int noData_TextColor = Color.GRAY;//提示信息字体大小
    private String noData_Text = "No Data"; //提示信息
    private int noData_BorderLineColor = Color.BLUE;//没数据时边界颜色。


    //有数据的时候,显示样式 颜色部分
    private int hasData_Color_Back = Color.WHITE;//背景色
    private int hasData_Color_Point = Color.RED;//如需描点,点的颜色。
    private int hasData_Color_Line = Color.RED;//线的颜色。
    private int hasData_BorderLineColor = Color.BLACK;//有数据时候边界颜色
    //有数据的时候,显示样式 大小部分。
    private float hasData_Width_Line = DensityUtils.dp(getContext(), 8);//线宽


    //有数据 显示标准线时 的颜色
    private int hasData_Color_StanLineUpBack = 0xfffef1ed;//标准线,标准线上方的背景颜色
    private int hasData_Color_StanLine = Color.RED;//标准线颜色
    private int hasData_Color_StanLineText = Color.RED;//标准线数值文本的颜色


    //有数据 显示标准线时 大小
    private float hasData_Width_StanLine = DensityUtils.dp(getContext(), 3);//标准线宽
    private float hasData_Size_StanLineText = DensityUtils.dp(getContext(), 13);//标准线数值文本的大小


    /***********
     * 显示区域的边界
     **************************/
    private int _left;
    private int _top;
    private int _right;
    private int _bottom;

    private int _height;
    private int _width;
    private Paint mPaint;
    private Paint mPaint_DrawPoint;

    private int blankSize = 8;
    private List<Float> values = new ArrayList<>();
    private String title = "title";
    private boolean needDrawPoint;
    private boolean needDrawStanLine;


    public MySimpleChart(Context context) {
        this(context, null, 0);
    }

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

    public MySimpleChart(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint_DrawPoint = new Paint();
        //测试时在这里初始化数据,实际使用时候注释掉      initValues();
        initSets(context, attrs);
    }

    private void initSets(Context context, AttributeSet attrs) {
        //获取xml中的配置信息
        final TypedArray typedArray = context.obtainStyledAttributes(attrs,
                R.styleable.MyView_ArcScale);
        this.noData_BackColor = typedArray.getColor(R.styleable.MySimpleChart_noData_BackColor, noData_BackColor);
        this.noData_BorderLineColor = typedArray.getColor(R.styleable.MySimpleChart_noData_BorderLineColor, noData_BorderLineColor);
        this.noData_TextColor = typedArray.getColor(R.styleable.MySimpleChart_noData_TextColor, noData_TextColor);
        this.noData_Text = typedArray.getString(R.styleable.MySimpleChart_noData_Text);
        this.noData_TextSize = typedArray.getDimension(R.styleable.MySimpleChart_noData_TextSize, noData_TextSize);
        this.hasData_BorderLineColor = typedArray.getColor(R.styleable.MySimpleChart_hasData_BorderLineColor, hasData_BorderLineColor);
        this.hasData_Color_Back = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_Back, hasData_Color_Back);
        this.hasData_Color_Line = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_Line, hasData_Color_Line);
        this.hasData_Color_Point = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_Point, hasData_Color_Point);
        this.hasData_Color_StanLine = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_StanLine, hasData_Color_StanLine);
        this.hasData_Color_StanLineUpBack = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_StanLineUpBack, hasData_Color_StanLineUpBack);
        this.hasData_Color_StanLineText = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_StanLineText, hasData_Color_StanLineText);
        this.hasData_Size_StanLineText = typedArray.getDimension(R.styleable.MySimpleChart_hasData_Size_StanLineText, hasData_Size_StanLineText);
        this.hasData_Width_Line = typedArray.getDimension(R.styleable.MySimpleChart_hasData_Width_Line, hasData_Width_Line);
        this.hasData_Width_StanLine = typedArray.getDimension(R.styleable.MySimpleChart_hasData_Width_StanLine, hasData_Width_StanLine);
        this.needDrawPoint = typedArray.getBoolean(R.styleable.MySimpleChart_needDrawPoint, needDrawPoint);
        this.needDrawStanLine = typedArray.getBoolean(R.styleable.MySimpleChart_needDrawStanLine, needDrawStanLine);
        this.stanLineValue = typedArray.getFloat(R.styleable.MySimpleChart_stanLineValue, stanLineValue);
        typedArray.recycle();//回收资源


    }


    int paddingLeft;
    int paddingRight;
    int paddingTop;
    int paddingButtom;


    float xScale;// x每一个单位代表多长
    float yScale;// y每单位代表多长

    int defCount = 8;//默认图标上显示八个节点。

    private float stanLineValue;

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paddingButtom = getPaddingBottom();
        paddingLeft = getPaddingLeft();
        paddingRight = getPaddingRight();
        paddingTop = getPaddingTop();
        _left = paddingLeft + blankSize;
        _top = paddingTop + blankSize;
        _right = getWidth() - paddingRight - blankSize;
        _bottom = getHeight() - paddingButtom - blankSize;
        _width = _right - _left;
        _height = _bottom - _top;


        if (values.isEmpty()) drawNoDataView(mPaint, canvas);
        else {

            drawDataView(mPaint, canvas);

        }
    }


    private void drawDataView(Paint paint, Canvas canvas) {

        //边界线
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(hasData_BorderLineColor);
        paint.setPathEffect(new DashPathEffect(new float[]{5, 5, 5, 5}, 1));
        canvas.drawRect(_left, _top, _right, _bottom, paint);
        paint.reset();

        initXScale();
        initYScale();
        if (needDrawStanLine) drawStanLine(paint, canvas, stanLineValue);
        //直角坐标系原点
        int point_startX = _left;
        int point_startY = _bottom;
        float startX;
        float startY;
        float endX = 0;
        float endY = 0;
        float ctrlX;
        Path path = new Path();
        for (int i = 1; i < values.size(); i++) {
            startX = point_startX + (i - 1) * xScale;
            startY = point_startY - (values.get(i - 1) - minValue) * yScale;
            endX = point_startX + i * xScale;
            endY = point_startY - (values.get(i) - minValue) * yScale;
            ctrlX = (startX + endX) / 2;
            path.moveTo(startX, startY);
            path.cubicTo(ctrlX, startY, ctrlX, endY, endX, endY);
            if (needDrawPoint) {

                drawPoint(endX, endY, canvas, mPaint_DrawPoint);
            }
        }

        if (needDrawPoint) drawPoint(endX, endY, canvas, mPaint_DrawPoint);
        paint.setStrokeWidth(hasData_Width_Line);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(hasData_Color_Line);
        canvas.drawPath(path, paint);

    }

    private void drawPoint(float endX, float endY, Canvas canvas, Paint paint) {
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(hasData_Color_Point);
        canvas.drawPoint(endX, endY, paint);
    }


    private void drawStanLine(Paint paint, Canvas canvas, float stanLineValue) {
        float yValue = _bottom - (stanLineValue - minValue) * yScale;
        //标准线上的部分绘制深色
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(hasData_Color_StanLineUpBack);
        canvas.drawRect(_left, _top, _right, yValue, paint);
        //绘制标准线
        paint.setStrokeCap(Paint.Cap.BUTT);
        paint.setStrokeWidth(hasData_Width_StanLine);
        Paint.FontMetrics fm = paint.getFontMetrics();
        float baseline = fm.descent;
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(hasData_Color_StanLine);
        paint.setPathEffect(new DashPathEffect(new float[]{5, 5, 5, 5}, 1));
        Path standardLinePath = new Path();
        standardLinePath.moveTo(_left, yValue);
        standardLinePath.lineTo(_right, yValue);
        canvas.drawPath(standardLinePath, paint);
        paint.reset();

        //绘制右侧边界线和标准值
        paint.setPathEffect(null);
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(hasData_Size_StanLineText);
        String standard = decimalFormat.format(stanLineValue);
        canvas.drawText(standard, _right + paddingRight / 2, yValue + baseline, paint);
        paint.setColor(Color.parseColor("#d9d9d9"));
        canvas.drawLine(_right, _top, _right, _bottom, paint);

    }


    float maxValue;
    float minValue;

    private void initYScale() {
        maxValue = values.get(0);
        minValue = values.get(0);

        for (Float f : values) {
            if (f < minValue) minValue = f;
            if (f > maxValue) maxValue = f;
        }
        yScale = _height / (maxValue - minValue);
//        L.d("yScale is"+yScale);
    }

    private void initXScale() {
        int size = values.size();
        int count = size > defCount ? size : defCount;
        xScale = _width / count;
//        L.d(size+"----xScale is " +
//                ""+xScale);
    }

    private void drawNoDataView(Paint paint, Canvas canvas) {
        //画无数据时显示内容
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(noData_BorderLineColor);
        paint.setPathEffect(new DashPathEffect(new float[]{5, 5, 5, 5}, 1));
        canvas.drawRect(_left, _top, _right, _bottom, paint);

        paint.setColor(Color.GRAY);
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(DensityUtils.dp(getContext(), 20));
        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        paint.setTextAlign(Paint.Align.CENTER);
        canvas.drawText(noData_Text, (_left + _right) / 2, (_top + _bottom) / 2 + fontMetrics.descent, paint);


    }

    public void setValue(List<Float> values ){
        this.values.addAll(values);
        invalidate();
    }

    public void addValue(List<Float> values){
        this.values.addAll(values);
        invalidate();
    }
    public void addValue(Float value){
        this.values.add(value);
        invalidate();
    }
    public void resetValue(){
        this.values.clear();
        invalidate();
    }

//    private void initValues() {
//        values.add(4f);
//        values.add(2f);
//        values.add(3f);
//        values.add(4f);
//        values.add(5f);
//        values.add(6f);
//        values.add(7f);
//        values.add(8f);
//        values.add(9f);
//        values.add(4f);
//        values.add(5f);
//        values.add(6f);
//        values.add(7f);
//
//    }
}


下一篇准备开发柱状图 ,让其动态显示


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值