折線圖的繪製
折線圖可以說是繪製最頻繁的了,我們從官方的demo中看看LineChart可以怎麼玩?
構建數據
不管畫什麼,我們總得有個數據集合吧。後端的同志們給我們的是一個List集合,那LineChart是如何封裝數據集合的呢?我們來瞧瞧代碼:
private void setData(int count, float range) {
ArrayList<Entry> values = new ArrayList<Entry>();
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<ILineDataSet> dataSets = new ArrayList<ILineDataSet>();
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<LimitLine> 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這麼多種類型。我設置運行了一下。原來這個是表示橫座標軸的位置。
- TOP - - - - - - - - - - - - 座標軸位於chart的頂端,座標值位於座標的外部。
- TOP_INSIDE - - - - - -座標軸位於chart的頂端,但座標值位於座標的內(下)部。
- BOTTOM - - - - - - - - -座標軸位於chart的底部,座標值位於座標的外部。
- BOTTOM_INSIDE - - 座標軸位於chart的底部,座標值位於座標的內(上)部。
- BOTH_SIDED, - - - - -座標軸上下兩端都有,座標值位於座標軸的外部。
再來看看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).
* <p/>
* 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吧。