美文网首页andnroid
Android折线图绘制

Android折线图绘制

作者: BigP | 来源:发表于2019-09-27 09:45 被阅读0次

    需求功能描述

    折线图是Android App中经常会使用到的一类图表,比如数据统计啊,数据分析啊,数据展示啊,股票分时图啊等等等等,都有折线图的身影。折线图也算是基础图表中比较易上手的一种,现在我们就来用Canvas实现折线图的效果吧!

    效果图:

    折线图

    实现思路

    这里其实画了两个图,上面一个折线图,下面一个柱状图,绘制过程并不困难,就一起讲了吧~首先这是个自定义View,继承View,通过重写onDraw()方法,来进行图表的绘制。

    绘制

    确定绘制区域
    由于有些时候,在界面展示过程中,各个View的大小和位置等可能会根据用户的操作而有所变化,比如突然某个控件变大了,导致我们的图表显示区域变小了。为了适应这种不可控的大小变化,个人建议在onSizeChanged()方法中,先确定需要绘制的区域的大小:

    @Override
    protected void onSizeChanged(int width, int height, int oldw, int oldh) {
        super.onSizeChanged(width, height, oldw, oldh);
        if (width > 0 && height > 0) {
            mChartRect = new RectF(LEFT_TEXT_WIDTH, TOP_MARGIN, width - RIGHT_TEXT_WIDTH, height * 0.7f);
            mVolRect = new RectF(LEFT_TEXT_WIDTH, height * 0.8f, width - RIGHT_TEXT_WIDTH, height - BOTTOM_MARGIN);
            mChartMiddleY = mChartRect.top + mChartRect.height() / 2;
            mWidth = width;
            mHeight = height;
        }
    }
    

    确定框架
    在绘制图表的过程中,我们可以把一个个的图标想象成一个个的方块,各种不同的图表,就画在我们界定的对应的方块中,这里有两个图表,那么我们就可以给予他两个Frame方块,也就是上面一段代码中看到的mChartRectmVolRect,你可以给这些框框设置各种边距和间隔等。如,上面代码中,LEFT_TEXT_WIDTH就是给左边文字预留的位置,还有各种Margin的设置等。
    绘制坐标系

    坐标系效果图
    基础图表一般都有坐标系,最简单的就是一条竖线一条横线组成的坐标系,这里其实也是x轴和y轴组成的坐标系,再添加一些基线,使效果和对照更明显一些。
    /**
     * 画基础框框
     */
    private void drawFrame(Canvas canvas) {
        initPaint();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(COLOR_BG);
        mPaint.setStrokeWidth(RECT_LINE_WIDTH);
        canvas.drawRect(0, 0, mWidth, mHeight, mPaint);
    
        // 画线条框
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(COLOR_LINE);
        canvas.drawRect(mChartRect, mPaint);
        canvas.drawRect(mVolRect, mPaint);
        // 画横线
        canvas.drawLine(mChartRect.left, (mChartRect.top + mChartMiddleY) / 2, mChartRect.right, (mChartRect.top + mChartMiddleY) / 2, mPaint);
        canvas.drawLine(mChartRect.left, mChartMiddleY, mChartRect.right, mChartMiddleY, mPaint);
        canvas.drawLine(mChartRect.left, (mChartRect.bottom + mChartMiddleY) / 2, mChartRect.right, (mChartRect.bottom + mChartMiddleY) / 2, mPaint);
        canvas.drawLine(mVolRect.left, mVolRect.top + mVolRect.height() / 2, mVolRect.right, mVolRect.top + mVolRect.height() / 2, mPaint);
        // 画竖线
        canvas.drawLine(mChartRect.left + mChartRect.width() / 4, mChartRect.top, mChartRect.left + mChartRect.width() / 4, mChartRect.bottom, mPaint);
        canvas.drawLine(mChartRect.left + mChartRect.width() / 2, mChartRect.top, mChartRect.left + mChartRect.width() / 2, mChartRect.bottom, mPaint);
        canvas.drawLine(mChartRect.right - mChartRect.width() / 4, mChartRect.top, mChartRect.right - mChartRect.width() / 4, mChartRect.bottom, mPaint);
        canvas.drawLine(mVolRect.left + mVolRect.width() / 4, mVolRect.top, mVolRect.left + mVolRect.width() / 4, mVolRect.bottom, mPaint);
        canvas.drawLine(mVolRect.left + mVolRect.width() / 2, mVolRect.top, mVolRect.left + mVolRect.width() / 2, mVolRect.bottom, mPaint);
        canvas.drawLine(mVolRect.right - mVolRect.width() / 4, mVolRect.top, mVolRect.right - mVolRect.width() / 4, mVolRect.bottom, mPaint);
    }
    

    绘制坐标系数值
    要画折线图,必须首先确认最大值和最小值,从而才能确认某个数据在图表中点的位置。首先我们定义一个实例来保存数据:

    public class TrendDataBean {
        public float newValue;
        public float vol;
    }
    

    很简明的数据结构,newValue是折线图中的数值,vol是下面柱状图的数值。
    再定义一个数组来保存最大最小值:private float[] mMaxMin = {Float.MIN_VALUE, Float.MAX_VALUE, Float.MIN_VALUE};分别记录折线图的最大值,最小值,和柱状图的最大值,由于柱状图数据都大于0,所以只要记录最大值就行。

    private float[] getMaxMin(List<TrendDataBean> trendDataBeans) {
        float[] maxMin = {Float.MIN_VALUE, Float.MAX_VALUE, Float.MIN_VALUE};
        if (trendDataBeans == null || trendDataBeans.size() < 1) {
            return maxMin;
        }
        for (int i = 0; i < trendDataBeans.size(); i++) {
            TrendDataBean bean = trendDataBeans.get(i);
            maxMin[0] = Math.max(maxMin[0], bean.newValue);
            maxMin[1] = Math.min(maxMin[1], bean.newValue);
            maxMin[2] = Math.max(maxMin[2], bean.vol);
        }
        return maxMin;
    }
    

    获得最大最小值之后,整个坐标系就确定了,接下来把这些数值以文字的形式绘制在图表中:


    坐标系
    private void drawText(Canvas canvas) {
        if (mMaxMin[0] != Float.MIN_VALUE && mMaxMin[1] != Float.MAX_VALUE) {
            initPaint();
            // 基准的最大值要比数据的最大值大才行,同理最小值也一样
            mPaint.setStyle(Paint.Style.FILL);
            int spSize = 9;
            float scaledSizeInPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spSize,
                    getResources().getDisplayMetrics());
            mPaint.setTextSize(scaledSizeInPx);
            mPaint.setTextAlign(Paint.Align.RIGHT);
            mPaint.setColor(COLOR_LABEL_TEXT);
    
            canvas.drawText(mMaxMin[1] + "", mChartRect.left - DP_5, mChartRect.bottom + DP_3, mPaint);
            canvas.drawText(mMaxMin[0] + "", mChartRect.left - DP_5, mChartRect.top + DP_3, mPaint);
            canvas.drawText((mMaxMin[0] + mMaxMin[1]) / 2f + "", mChartRect.left - DP_5, mChartRect.top + mChartRect.height() / 2f + DP_3, mPaint);
    
            // 画延x轴的几个数字,使用数据的数量作为x轴
            mPaint.setTextAlign(Paint.Align.CENTER);
            int size = mTrendDatas.size();
            canvas.drawText("0", mChartRect.left, mChartRect.bottom + DP_10, mPaint);
            canvas.drawText(size / 2 + "", mChartRect.left + mChartRect.width() / 2, mChartRect.bottom + DP_10, mPaint);
            canvas.drawText(size + "", mChartRect.right, mChartRect.bottom + DP_10, mPaint);
        }
    }
    

    确定步长
    顾名思义,步长就是两个点之间的距离,确定了步长之后,就能沿着x轴对数据点进行排布,这里是把所有数据填充满整个画布的x轴,因此直接使用宽度除以数据点数量-1,就是步长了:

    public void setData(List<TrendDataBean> data) {
        if (data != null && data.size() > 0) {
            this.mTrendDatas.clear();
            mTrendDatas.addAll(data);
    
            mMaxMin = getMaxMin(mTrendDatas);
    
            // 使用数据的数量作为x轴
            try {
                mStep = (float) mChartRect.width() / (data.size() - 1);
            } catch (Exception e) {
                mStep = mChartRect.width();
            }
    
            postInvalidate();
        }
    }
    

    绘制折线
    前期铺垫都已经做好啦,接下来画折线图,就是手到擒来了~首先计算某个数据点,到y轴的偏移量,通过简单的数学基础可以得出,用当前值除以最大值和最小值的差值,也就是当前数值所占最大差值的百分比,用这个百分比乘以图表的高度,就是y轴的偏移量了。

    private float getOffsetY(float value) {
        float result = 0f;
        // 确保差值在最大值之内
        float sub = mMaxMin[0] - mMaxMin[1];
        if (value <= mMaxMin[0] && mChartRect.height() != 0 && sub > 0) {
            return (value - mMaxMin[1]) / sub * (mChartRect.height());
        }
        return result;
    }
    

    在绘制之前,需要讲解一下画布的x和y的走向:

    canvas的xy排布
    可以看到,Canvas的原点在左上角,x向右递增,y向下递增,当然你可以通过旋转画布等操作来改变原点位置,这里说的是原点的默认位置~
    理解了这些,下面就是画线了,画线的思路就是:新建一个Path,循环数据源,一条数据就是一个点,将他们连成一条Path即可。
    Path path = new Path();
    ArrayList<TrendDataBean> tempBeans = new ArrayList<>(mTrendDatas);
    for (int i = 0; i < tempBeans.size(); i++) {
        TrendDataBean bean = tempBeans.get(i);
        if (i == 0) {
            // 移动到第一个点
            path.moveTo(mChartRect.left, mChartRect.bottom - getOffsetY(bean.newValue));
        } else {
            path.lineTo(mChartRect.left + mStep * i, mChartRect.bottom - getOffsetY(bean.newValue));
        }
    }
    canvas.drawPath(path, mPaint);
    

    绘制阴影
    阴影的绘制也很容易,可以接着使用上面画折线的path,然后将这条path移动到x轴,再移动到左下角,再移动到第一个点坐标,那就围成了阴影范围。渐变色直接使用LinearGradient来实现:

    // 绘制阴影
    if (tempBeans.size() > 1) {
        path.lineTo(mChartRect.left + (tempBeans.size() - 1) * mStep, mChartRect.bottom);
        path.lineTo(mChartRect.left, mChartRect.bottom);
        path.lineTo(mChartRect.left, mChartRect.bottom - getOffsetY(tempBeans.get(0).newValue));
        path.close();
        initPaint();
        mPaint.setStyle(Paint.Style.FILL);
        LinearGradient linearGradient = new LinearGradient(mChartRect.left, mHighestY, mChartRect.left, mChartRect.bottom,
                COLOR_GRADIENT_TOP, COLOR_GRADIENT_BOTTOM, Shader.TileMode.CLAMP);
        mPaint.setShader(linearGradient);
        canvas.drawPath(path, mPaint);
    }
    

    至此,我们上半部分的折线图表区域就绘制完毕了!


    接下来,我们来绘制下面的柱状图,有了折线图的基础,柱状图对于我们来说就是小菜一碟了~
    首先祭出计算偏移量的方法,和折线图的算法基本相同:

    private float getVolOffsetY(float vol) {
        float result = 0f;
        if (vol >= 0 && mMaxMin[2] > 0 && vol <= mMaxMin[2]) {
            return vol / mMaxMin[2] * mVolRect.height();
        }
        return result;
    }
    

    绘制柱子
    这里我们可以把柱子看做是一条线,而不是一个矩形,设置这条线的粗细,就能画出来我们想要的柱子的样子。顺便画个颜色,规则是和前一个点的数值比较,比前一个高就红色,不然就绿色。

    private void drawVol(Canvas canvas) {
        if (mStep <= 0 || mTrendDatas.size() < 1) {
            return;
        }
        initPaint();
        ArrayList<TrendDataBean> tempBeans = new ArrayList<>(mTrendDatas);
        for (int i = 0; i < tempBeans.size(); i++) {
            TrendDataBean bean = tempBeans.get(i);
            // 假设第一条是绿色的
            if (i == 0) {
                mPaint.setColor(ColorUtils.upPriceColor);
                // 两根成交量线之间距离0.5dp
                // 第一条线的粗细是正常的一半
                mPaint.setStrokeWidth((mStep - VOL_DISTANCES) / 2f);
                canvas.drawLine(mVolRect.left + mPaint.getStrokeWidth() / 2, mVolRect.bottom,
                        mVolRect.left + mPaint.getStrokeWidth() / 2, mVolRect.bottom - getVolOffsetY(bean.vol), mPaint);
    
            } else {
                // 和前一个价格比较,得出红绿
                TrendDataBean preBean = tempBeans.get(i - 1);
                if (preBean != null) {
                    mPaint.setColor(bean.newValue >= preBean.newValue ? ColorUtils.upPriceColor : ColorUtils.downPriceColor);
                    // 画线,
                    // 如果是最后一根线,与第一根画法类似
                    if (i == tempBeans.size() - 1) {
                        mPaint.setStrokeWidth((mStep - VOL_DISTANCES) / 2f);
                        canvas.drawLine(mVolRect.left + i * mStep - mPaint.getStrokeWidth() / 2, mVolRect.bottom,
                                mVolRect.left + i * mStep - mPaint.getStrokeWidth() / 2, mVolRect.bottom - getVolOffsetY(bean.vol), mPaint);
                    } else {
                        // 两根线之间的距离为VOL_DISTANCES
                        mPaint.setStrokeWidth(mStep - VOL_DISTANCES);
                        canvas.drawLine(mVolRect.left + i * mStep, mVolRect.bottom,
                                mVolRect.left + i * mStep, mVolRect.bottom - getVolOffsetY(bean.vol), mPaint);
                    }
                }
            }
        }
    }
    

    恭喜

    为能看到这里的盆友点个赞,您已经学会各种图表的基础了,之后点线相关的图表都不是啥难事了。当然,使用自定义View绘制的图表,还能做出各种各样的数据变化的动态效果~

    彩蛋

    这个自定义折线图中,还有一个功能,就是按下时出现十字线,手指移动时能够带动十字线的滑动,交叉点就是当前选中的数值:


    十字线功能

    相关文章

      网友评论

        本文标题:Android折线图绘制

        本文链接:https://www.haomeiwen.com/subject/netfuctx.html