折线图可以说是绘制最频繁的了,我们从官方的demo中看看LineChart可以怎么玩?
不管画什么,我们总得有个数据集合吧。后端的同志们给我们的是一个List集合,那LineChart是如何封装数据集合的呢?我们来瞧瞧代码:
private void setData(int count, float range) {
ArrayList values = new ArrayList();
for (int i = 0; i < count; i++) {
float val = (float) (Math.random() * range) + 3;
values.add(new Entry(i, val));
}
LineDataSet set1 = new LineDataSet(values, "DataSet 1");
set1.setDrawIcons(false);
// set the line to be drawn like this "- - - - - -"
set1.enableDashedLine(10f, 5f, 0f);
set1.enableDashedHighlightLine(10f, 5f, 0f);
// 设置线的颜色
set1.setColor(Color.BLACK);
//设置数据点圆的颜色
set1.setCircleColor(Color.BLACK);
//设置线的宽度
set1.setLineWidth(1f);
//设置数据点圆的半径
set1.setCircleRadius(3f);
//设置数据点圆是否空心
set1.setDrawCircleHole(true);
//设置数据点值得字的大小
set1.setValueTextSize(9f);
//设置是否要折线下面部分铺满颜色
set1.setDrawFilled(true);
//设置图例线的宽度
set1.setFormLineWidth(1f);
//设置图例线的样式
set1.setFormLineDashEffect(new DashPathEffect(new float[]{10f, 5f}, 0f));
//设置图例的文字大小
set1.setFormSize(15.f);
ArrayList dataSets = new ArrayList();
dataSets.add(set1); // add the datasets
// create a data object with the datasets
LineData data = new LineData(dataSets);
// set data
mChart.setData(data);
}
}
从这段代码中可以看到一个for循环,循环中做的一件事就是在 new Entry(i, val)。可以知道这是一个一个的点,通过这些点,就可以连成一条线,即 LineDataSet(values, “DataSet 1”);那LineData就包含很多条线了。LineDataSet有很多的set方法,这些作用看注释,或者亲自动手改改代码,看效果,就知道是干啥的了,应该很容易理解的。数据是有了,可以由点成线了。那把点画到哪个地方去呢?那就是要先有个坐标轴了。
找了下整个项目,发现了两个类,横坐标轴:XAxis,纵坐标轴:YAxis。他们共有的一个父类AxisBase。AxisBase中定义了很多属性,我列举一些常用的属性:
/**
* custom formatter that is used instead of the auto-formatter if set
*/
protected IAxisValueFormatter mAxisValueFormatter;
protected IAxisEntryFormatter mAxisEntryFormatter;
//坐标轴内的网格颜色
private int mGridColor = Color.GRAY;
//坐标轴内的网格宽度
private float mGridLineWidth = 1f;
//坐标轴颜色
private int mAxisLineColor = Color.GRAY;
//坐标轴宽度
private float mAxisLineWidth = 1f;
/**
* the number of entries the legend contains
*/
public int mEntryCount;
/**
* if true, the set number of y-labels will be forced
*/
protected boolean mForceLabels = false;
/**
* flag indicating if the grid lines for this axis should be drawn
*/
protected boolean mDrawGridLines = true;
/**
* flag that indicates if the line alongside the axis is drawn or not
*/
protected boolean mDrawAxisLine = true;
/**
* flag that indicates of the labels of this axis should be drawn or not
*/
protected boolean mDrawLabels = true;
/**
* array of limit lines that can be set for the axis
*/
protected List mLimitLines;
/**
* flag indicating the limit lines layer depth
*/
protected boolean mDrawLimitLineBehindData = false;
/**
* Extra spacing for `axisMinimum` to be added to automatically calculated `axisMinimum`
*/
protected float mSpaceMin = 0.f;
/**
* Extra spacing for `axisMaximum` to be added to automatically calculated `axisMaximum`
*/
protected float mSpaceMax = 0.f;
/**
* don't touch this direclty, use setter
* 最大的那个点是第几个
*/
public float mAxisMaximum = 0f;
/**
* don't touch this directly, use setter
*/
public float mAxisMinimum = 0f;
/**
* the total range of values this axis covers
*/
public float mAxisRange = 0f;
LimitLine又是什么呢?这个字面意思上来理解就是限制线,是一条警戒线,这条线一画,就知道哪些数据点超过了这条线。这条线可以是横向的也可以是竖向的,因为它是写在父类中。再来看看XAxis有哪些属性吧:
/**
* width of the x-axis labels in pixels - this is automatically
* calculated by the computeSize() methods in the renderers
* 坐标轴对应的坐标值的宽度,由computeSize()方法根据字体大小计算
*/
public int mLabelWidth = 1;
/**
* height of the x-axis labels in pixels - this is automatically
* calculated by the computeSize() methods in the renderers
* 坐标轴对应的坐标值的高度,由computeSize()方法根据字体大小计算
*/
public int mLabelHeight = 1;
/**
* width of the (rotated) x-axis labels in pixels - this is automatically
* calculated by the computeSize() methods in the renderers
* 旋转坐标值后的宽度,并不会旋转坐标轴
*/
public int mLabelRotatedWidth = 1;
/**
* height of the (rotated) x-axis labels in pixels - this is automatically
* calculated by the computeSize() methods in the renderers
* 旋转坐标值后的高度,并不会旋转坐标轴
*/
public int mLabelRotatedHeight = 1;
/**
* This is the angle for drawing the X axis labels (in degrees)
* 旋转坐标值的角度
*/
protected float mLabelRotationAngle = 0f;
/**
* if set to true, the chart will avoid that the first and last label entry
* in the chart "clip" off the edge of the chart
*/
private boolean mAvoidFirstLastClipping = false;
/**
* the position of the x-labels relative to the chart
*/
private XAxisPosition mPosition = XAxisPosition.TOP;
/**
* enum for the position of the x-labels relative to the chart
*/
public enum XAxisPosition {
TOP, BOTTOM, BOTH_SIDED, TOP_INSIDE, BOTTOM_INSIDE
}
上面有个枚举,XAxisPosition,有TOP, BOTTOM, BOTH_SIDED, TOP_INSIDE, BOTTOM_INSIDE这么多种类型。我设置运行了一下。原来这个是表示横坐标轴的位置。
再来看看YAxis:
/**
* indicates if the bottom y-label entry is drawn or not
* 是否要画Y轴的最下面的那个坐标值
*/
private boolean mDrawBottomYLabelEntry = true;
/**
* indicates if the top y-label entry is drawn or not
* 是否要画Y轴的最上面的那个坐标值
*/
private boolean mDrawTopYLabelEntry = true;
/**
* flag that indicates if the axis is inverted or not
* Y轴是否反向颠倒
*/
protected boolean mInverted = false;
/**
* flag that indicates if the zero-line should be drawn regardless of other grid lines
* 是否要画出数据为0的那条线
*/
protected boolean mDrawZeroLine = false;
/**
* Color of the zero line
*/
protected int mZeroLineColor = Color.GRAY;
/**
* Width of the zero line in pixels
*/
protected float mZeroLineWidth = 1f;
/**
* axis space from the largest value to the top in percent of the total axis range
* 留一部分空白的空间,让最大值可以不贴近屏幕最上方,方便用户操作。
*/
protected float mSpacePercentTop = 10f;
/**
* axis space from the smallest value to the bottom in percent of the total axis range
* 留一部分空白的空间,让最小值可以不贴近屏幕最下方,方便用户操作。
*/
protected float mSpacePercentBottom = 10f;
/**
* the position of the y-labels relative to the chart
*/
private YAxisLabelPosition mPosition = YAxisLabelPosition.OUTSIDE_CHART;
/**
* enum for the position of the y-labels relative to the chart
* 这个容易理解,坐标值在坐标轴的外部还是内部
*/
public enum YAxisLabelPosition {
OUTSIDE_CHART, INSIDE_CHART
}
/**
* the side this axis object represents
*/
private AxisDependency mAxisDependency;
/**
* the minimum width that the axis should take (in dp).
*
* default: 0.0
*/
protected float mMinWidth = 0.f;
/**
* the maximum width that the axis can take (in dp).
* use Inifinity for disabling the maximum
* default: Float.POSITIVE_INFINITY (no maximum specified)
*/
protected float mMaxWidth = Float.POSITIVE_INFINITY;
/**
* Enum that specifies the axis a DataSet should be plotted against, either LEFT or RIGHT.
*
* @author Philipp Jahoda
*/
public enum AxisDependency {
LEFT, RIGHT
}
上面代码的最后几行有一个枚举AxisDependency,即坐标依赖。居然还分两个左右坐标轴哈。平常嘛,一个坐标轴就搞定了啊。啥时候会用到两个Y坐标轴呢?我在画股票分时图的时候用到过。数据,坐标都有了,如果没有其他特殊需求,设置好数据和坐标的属性,就可以调用方法画图,完事了。
XAxis xAxis = mChart.getXAxis();
xAxis.enableGridDashedLine(10f, 10f, 0f);
LimitLine ll1 = new LimitLine(150f, "Upper Limit");
ll1.setLineWidth(4f);
ll1.enableDashedLine(10f, 10f, 0f);
ll1.setLabelPosition(LimitLabelPosition.RIGHT_TOP);
ll1.setTextSize(10f);
LimitLine ll2 = new LimitLine(-30f, "Lower Limit");
ll2.setLineWidth(4f);
ll2.enableDashedLine(10f, 10f, 0f);
ll2.setLabelPosition(LimitLabelPosition.RIGHT_BOTTOM);
ll2.setTextSize(10f);
YAxis leftAxis = mChart.getAxisLeft();
// reset all limit lines to avoid overlapping lines
leftAxis.removeAllLimitLines();
leftAxis.addLimitLine(ll1);
leftAxis.addLimitLine(ll2);
leftAxis.setAxisMaximum(200f);
leftAxis.setAxisMinimum(-50f);
leftAxis.enableGridDashedLine(10f, 10f, 0f);
leftAxis.setDrawZeroLine(false);
// limit lines are drawn behind data (and not on top)
leftAxis.setDrawLimitLinesBehindData(true);
mChart.getAxisRight().setEnabled(false);
// add data
setData(45, 100);
设置数据刷新就好了,可作者是怎么去画的呢?我们先想想,如果是我们自己来绘制的话,我们应该先绘制什么,后绘制什么?我们先绘制背景,再绘制坐标轴,再绘制数据点,连线,然后绘制坐标值,图例,等等。因为后面绘制的东西会覆盖在已经绘制好的上面。我们来仔细瞧瞧。这个画图的代码在BarLineChartBase中的OnDraw()方法中,我把计算时间的,无效代码去掉了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mData == null)
return;
//绘制背景
drawGridBackground(canvas);
//根据设置好的数据调整间距Chart大小
if (mAutoScaleMinMaxEnabled) {
autoScale();
}
//根据设置好的数据调整坐标轴
if (mAxisLeft.isEnabled())
mAxisRendererLeft.computeAxis(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisMaximum, mAxisLeft.isInverted());
if (mAxisRight.isEnabled())
mAxisRendererRight.computeAxis(mAxisRight.mAxisMinimum, mAxisRight.mAxisMaximum, mAxisRight.isInverted());
if (mXAxis.isEnabled())
mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false);
//画坐标轴
mXAxisRenderer.renderAxisLine(canvas);
mAxisRendererLeft.renderAxisLine(canvas);
mAxisRendererRight.renderAxisLine(canvas);
//画坐标轴内的网格线
mXAxisRenderer.renderGridLines(canvas);
mAxisRendererLeft.renderGridLines(canvas);
mAxisRendererRight.renderGridLines(canvas);
//满足条件后画限制线或者说是警戒线
if (mXAxis.isEnabled() && mXAxis.isDrawLimitLinesBehindDataEnabled())
mXAxisRenderer.renderLimitLines(canvas);
if (mAxisLeft.isEnabled() && mAxisLeft.isDrawLimitLinesBehindDataEnabled())
mAxisRendererLeft.renderLimitLines(canvas);
if (mAxisRight.isEnabled() && mAxisRight.isDrawLimitLinesBehindDataEnabled())
mAxisRendererRight.renderLimitLines(canvas);
// make sure the data cannot be drawn outside the content-rect
int clipRestoreCount = canvas.save();
canvas.clipRect(mViewPortHandler.getContentRect());
//画数据
mRenderer.drawData(canvas);
// if highlighting is enabled 画高亮线
if (valuesToHighlight())
mRenderer.drawHighlighted(canvas, mIndicesToHighlight);
// Removes clipping rectangle
canvas.restoreToCount(clipRestoreCount);
//画其他额外的
mRenderer.drawExtras(canvas);
//满足条件后画限制线或者说是警戒线
if (mXAxis.isEnabled() && !mXAxis.isDrawLimitLinesBehindDataEnabled())
mXAxisRenderer.renderLimitLines(canvas);
if (mAxisLeft.isEnabled() && !mAxisLeft.isDrawLimitLinesBehindDataEnabled())
mAxisRendererLeft.renderLimitLines(canvas);
if (mAxisRight.isEnabled() && !mAxisRight.isDrawLimitLinesBehindDataEnabled())
mAxisRendererRight.renderLimitLines(canvas);
//画坐标值
mXAxisRenderer.renderAxisLabels(canvas);
mAxisRendererLeft.renderAxisLabels(canvas);
mAxisRendererRight.renderAxisLabels(canvas);
//画数据点的值
if (isClipValuesToContentEnabled()) {
clipRestoreCount = canvas.save();
canvas.clipRect(mViewPortHandler.getContentRect());
mRenderer.drawValues(canvas);
canvas.restoreToCount(clipRestoreCount);
} else {
mRenderer.drawValues(canvas);
}
//画图例
mLegendRenderer.renderLegend(canvas);
//画描述
drawDescription(canvas);
//画标记值
drawMarkers(canvas);
}
果然和我们想的差不多哈。但我们看到绘制限制线或者说是警戒线的代码写了两次,看了if判断,就应该知道,这个限制线可以绘制在数据点的下面,也可以绘制在数据点的上面。看你喜好了。你可能不能理解drawHighlighted(画高亮线),drawMarkers(画标记值)是在绘制什么呢?啥时候需要绘制他们呢?
当你们的产品继续给你提需求的时候,你可能就慢慢的理解了。你以为把数据展示出来就好了,他们却和你说,数据太多了,看不到每一个数据的值,能不能在手指触摸到哪个数据的时候,在这个数据旁边展示出数据的值呢?所以呢,高亮线,就是触摸屏幕的时候,你手指触摸的那个点为中心,画出的十字交叉线,数据旁边展示出数据的值,就是标记值。MPAndroidChart已经为你封装好了这部分的需求,你只需要设置监听,自定义自己的Marker,实现起来,就没有太大问题。
这一片博客要接近尾声了,写太长了,大家注意力会慢慢下降的。在下一篇博客中,我再和大家分享drawHighlighted和drawMarkers吧。