Android折线图绘制

需求功能描述

折线图是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 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 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 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 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绘制的图表,还能做出各种各样的数据变化的动态效果~

彩蛋

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


十字线功能
  • Ps.完整代码:
    Android折线图和十字线

你可能感兴趣的:(Android折线图绘制)