需求功能描述
折线图是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方块,也就是上面一段代码中看到的mChartRect
和mVolRect
,你可以给这些框框设置各种边距和间隔等。如,上面代码中,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
的原点在左上角,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
绘制的图表,还能做出各种各样的数据变化的动态效果~
彩蛋
这个自定义折线图中,还有一个功能,就是按下时出现十字线,手指移动时能够带动十字线的滑动,交叉点就是当前选中的数值:
十字线功能
- Ps.完整代码:
Android折线图和十字线
网友评论