一.概述
MPAndroidChart是一款基于Android的开源图表库,MPAndroidChart不仅可以在Android设备上绘制各种统计图表,而且可以对图表进行拖动和缩放操作,应用起来非常灵活。MPAndroidChart同样拥有常用的图表类型:线型图、饼图、柱状图和散点图。
GitHub地址:
https://github.com/PhilJay/MPAndroidChart
二.实例讲解
下面先以BarChart为例讲解一下在Chart类的基础上,开发者为BarChart准备的实例化好的组成部分。
经过前两篇文章的分析,我们知道一个基本的Chart实例应该是是有这么几部分组成的:
(1)DataRenderer(数据渲染器)
(2)Legend(图例)
(3)Axis(坐标轴)
(4)Listener(一开始我们分析,Chart类的监听器有两个,但是经过源码分析,我们得知Chart的基本监听器有三个)
(5)Animator:动画显示
(6)Data:图表数据(根源的数据怎么能忘记呢)
(7)MarkerView
(8)HighLighter(高亮覆盖显示)
(9)ViewPortHandler(暂时理解为绘图区域)
下面呢,我将以BarChart为例讲解Chart的这些基本组成部分都是怎么被具像化,怎么样工作的:
昨天的blog中我们讲解chart类中最重要的DataRenderer 这个类的工作方式,并且讲解了如何使用Matrix(transformer和mViewPortHandler中的matrix)和Buffer类完成对Chartitem位置的确定。以及我们怎么确认高亮,并且绘制高亮。
今天我们把剩下的部分讲解一下,主要有坐标轴,图例,MarkerView,ViewPortHandler这四个部分。
首先我们讲AxisRenderer,这是关于坐标轴的渲染器,首先我们看一下BarChart的
XAxisRendererBarChart
public XAxisRendererBarChart(ViewPortHandler viewPortHandler, XAxis xAxis, Transformer trans, BarChart chart) { super(viewPortHandler, xAxis, trans); this.mChart = chart; } /** * draws the x-labels on the specified y-position * * @param pos */ @Override protected void drawLabels(Canvas c, float pos, PointF anchor) { final float labelRotationAngleDegrees = mXAxis.getLabelRotationAngle(); // pre allocate to save performance (dont allocate in loop) float[] position = new float[] { 0f, 0f }; BarData bd = mChart.getData(); int step = bd.getDataSetCount(); for (int i = mMinX; i <= mMaxX; i += mXAxis.mAxisLabelModulus) { position[0] = i * step + i * bd.getGroupSpace() + bd.getGroupSpace() / 2f;// // consider groups (center label for each group) if (step > 1) { position[0] += ((float) step - 1f) / 2f; } mTrans.pointValuesToPixel(position); if (mViewPortHandler.isInBoundsX(position[0]) && i >= 0 && i < mXAxis.getValues().size()) { String label = mXAxis.getValues().get(i);//得到标签数值 if (mXAxis.isAvoidFirstLastClippingEnabled()) { // avoid clipping of the last if (i == mXAxis.getValues().size() - 1) { float width = Utils.calcTextWidth(mAxisLabelPaint, label); if (position[0] + width / 2.f > mViewPortHandler.contentRight()) position[0] = mViewPortHandler.contentRight() - (width / 2.f); // avoid clipping of the first } else if (i == 0) { float width = Utils.calcTextWidth(mAxisLabelPaint, label); if (position[0] - width / 2.f < mViewPortHandler.contentLeft()) position[0] = mViewPortHandler.contentLeft() + (width / 2.f); } } drawLabel(c, label, i, position[0], pos, anchor, labelRotationAngleDegrees);//重要的方法在这 } } } @Override public void renderGridLines(Canvas c) { if (!mXAxis.isDrawGridLinesEnabled() || !mXAxis.isEnabled()) return; float[] position = new float[] { 0f, 0f }; mGridPaint.setColor(mXAxis.getGridColor()); mGridPaint.setStrokeWidth(mXAxis.getGridLineWidth()); BarData bd = mChart.getData(); int step = bd.getDataSetCount(); for (int i = mMinX; i < mMaxX; i += mXAxis.mAxisLabelModulus) { position[0] = i * step + i * bd.getGroupSpace() - 0.5f; mTrans.pointValuesToPixel(position); if (mViewPortHandler.isInBoundsX(position[0])) { c.drawLine(position[0], mViewPortHandler.offsetTop(), position[0], mViewPortHandler.contentBottom(), mGridPaint);//根据最大位置,绘制出网格线 } } }
然后我们再去看一下XAxisRendererBarChart的父类
XAxisRenderer
其实在父类中才是更基本的方法:
public XAxisRenderer(ViewPortHandler viewPortHandler, XAxis xAxis, Transformer trans) { super(viewPortHandler, trans); //在XAxisRenderer中我们声明了LabelPaint this.mXAxis = xAxis; mAxisLabelPaint.setColor(Color.BLACK); mAxisLabelPaint.setTextAlign(Align.CENTER); mAxisLabelPaint.setTextSize(Utils.convertDpToPixel(10f)); } public void computeAxis(float xValMaximumLength, List<String> xValues) { mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); StringBuilder widthText = new StringBuilder(); //传进来的xValMaximumLength值 int xValChars = Math.round(xValMaximumLength); //计算方式是这样的,我们在使用XAxisRenderer时,需要传入一个xValMaximumLength值,这个值经过Math.round方法的处理后成为一个类似于Char类型数值的size概念的这个一个数 for (int i = 0; i < xValChars; i++) { widthText.append('h'); } //然后将widthText装入和xValChars数值一样个数的h字符,所以得到的widthText的内容是全是h字符的这个一个字符串 final FSize labelSize = Utils.calcTextSize(mAxisLabelPaint, widthText.toString()); //之后我们使用这个字符串和得到的坐标轴绘制画笔得到标签的size final float labelWidth = labelSize.width; final float labelHeight = Utils.calcTextHeight(mAxisLabelPaint, "Q"); //标签的高度是大些的Q的高度来计算出标签的高度 final FSize labelRotatedSize = Utils.getSizeOfRotatedRectangleByDegrees( labelWidth, labelHeight, mXAxis.getLabelRotationAngle()); //然后接下来就是去计算坐标刻度之间的距离 StringBuilder space = new StringBuilder(); int xValSpaceChars = mXAxis.getSpaceBetweenLabels(); for (int i = 0; i < xValSpaceChars; i++) { space.append('h'); } final FSize spaceSize = Utils.calcTextSize(mAxisLabelPaint, space.toString()); //一开始的逻辑和前面计算刻度的逻辑是一样的 mXAxis.mLabelWidth = Math.round(labelWidth + spaceSize.width);//但是到了这里就不一样了,我们还加上了labelWidth mXAxis.mLabelHeight = Math.round(labelHeight); mXAxis.mLabelRotatedWidth = Math.round(labelRotatedSize.width + spaceSize.width); mXAxis.mLabelRotatedHeight = Math.round(labelRotatedSize.height); //就是这样我们计算出了坐标轴上的位置数据 mXAxis.setValues(xValues); } @Override public void renderAxisLabels(Canvas c) { if (!mXAxis.isEnabled() || !mXAxis.isDrawLabelsEnabled()) return; float yoffset = mXAxis.getYOffset(); mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); mAxisLabelPaint.setColor(mXAxis.getTextColor()); //设置画笔的各种属性 if (mXAxis.getPosition() == XAxisPosition.TOP) { drawLabels(c, mViewPortHandler.contentTop() - yoffset, new PointF(0.5f, 1.0f)); } else if (mXAxis.getPosition() == XAxisPosition.TOP_INSIDE) { drawLabels(c, mViewPortHandler.contentTop() + yoffset + mXAxis.mLabelRotatedHeight, new PointF(0.5f, 1.0f)); } else if (mXAxis.getPosition() == XAxisPosition.BOTTOM) { drawLabels(c, mViewPortHandler.contentBottom() + yoffset, new PointF(0.5f, 0.0f)); } else if (mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE) { drawLabels(c, mViewPortHandler.contentBottom() - yoffset - mXAxis.mLabelRotatedHeight, new PointF(0.5f, 0.0f)); } else { // BOTH SIDED drawLabels(c, mViewPortHandler.contentTop() - yoffset, new PointF(0.5f, 1.0f)); drawLabels(c, mViewPortHandler.contentBottom() + yoffset, new PointF(0.5f, 0.0f)); }//这里进行了各种判断来进行在不同情况下的标签绘制 } @Override public void renderAxisLine(Canvas c) { if (!mXAxis.isDrawAxisLineEnabled() || !mXAxis.isEnabled()) return; mAxisLinePaint.setColor(mXAxis.getAxisLineColor()); mAxisLinePaint.setStrokeWidth(mXAxis.getAxisLineWidth()); if (mXAxis.getPosition() == XAxisPosition.TOP || mXAxis.getPosition() == XAxisPosition.TOP_INSIDE || mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { c.drawLine(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop(), mViewPortHandler.contentRight(), mViewPortHandler.contentTop(), mAxisLinePaint); } if (mXAxis.getPosition() == XAxisPosition.BOTTOM || mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE || mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { c.drawLine(mViewPortHandler.contentLeft(), mViewPortHandler.contentBottom(), mViewPortHandler.contentRight(), mViewPortHandler.contentBottom(), mAxisLinePaint); } }
下面是最最最重要的方法
protected void drawLabel(Canvas c, String label, int xIndex, float x, float y, PointF anchor, float angleDegrees) {//这里是所有绘制标签的基本方法,传入的值包括标签内容,index还有位置心喜 String formattedLabel = mXAxis.getValueFormatter().getXValue(label, xIndex, mViewPortHandler); // TODO: 16/8/26 判断绘制换行文本 Float labelHeight = mXAxis.getTextSize(); Float labelInterval = 10f;//这下面是我自己修改的部分通过这样我可以绘制多行标签 int formattedLabelLength = formattedLabel.length(); if(formattedLabelLength > labelReqLength){ Utils.drawXAxisValue(c, formattedLabel.substring(0,labelReqLength), x, y, mAxisLabelPaint, anchor, angleDegrees); Utils.drawXAxisValue(c, formattedLabel.substring(labelReqLength,formattedLabelLength), x, y + labelHeight + labelInterval, mAxisLabelPaint, anchor, angleDegrees); }else{ Utils.drawXAxisValue(c, formattedLabel, x, y, mAxisLabelPaint, anchor, angleDegrees); } }
讲到这里我们就完成了对坐标轴绘制的讲解:
下面我们讲一下图例的绘制,图例Legend的绘制会使用到
LegendRenderer,在Chart基类中我们发现到
mLegendRenderer这个实例对象,但是在Bar Chart中并没有找到其实现的BarChart
LegendRenderer这个类,这说明BarChart在绘制时是通过Chart基类中的LegendRenderer进行绘制的。
mLegendRenderer = new LegendRenderer(mViewPortHandler, mLegend);
其实Legend的绘制最主要的是就是找到绘制图例的位置。Legend的位置确定是放在各个子类Chart之中的。 下面的方法实现的是为legend 准备好颜色,标签等
/** * Prepares the legend and calculates all needed forms, labels and colors. * * @param data */ public void computeLegend(ChartData<?> data) { if (!mLegend.isLegendCustom()) { List<String> labels = new ArrayList<String>(); List<Integer> colors = new ArrayList<Integer>(); // 遍历实体为图例准备好标签 for (int i = 0; i < data.getDataSetCount(); i++) { IDataSet dataSet = data.getDataSetByIndex(i); List<Integer> clrs = dataSet.getColors(); int entryCount = dataSet.getEntryCount(); // if we have a barchart with stacked bars if (dataSet instanceof IBarDataSet && ((IBarDataSet) dataSet).isStacked()) { IBarDataSet bds = (IBarDataSet) dataSet; String[] sLabels = bds.getStackLabels(); for (int j = 0; j < clrs.size() && j < bds.getStackSize(); j++) { labels.add(sLabels[j % sLabels.length]); colors.add(clrs.get(j)); } if (bds.getLabel() != null) { // add the legend description label colors.add(ColorTemplate.COLOR_SKIP); labels.add(bds.getLabel()); } } else if (dataSet instanceof IPieDataSet) { List<String> xVals = data.getXVals(); IPieDataSet pds = (IPieDataSet) dataSet; for (int j = 0; j < clrs.size() && j < entryCount && j < xVals.size(); j++) { labels.add(xVals.get(j)); colors.add(clrs.get(j)); } if (pds.getLabel() != null) { // add the legend description label colors.add(ColorTemplate.COLOR_SKIP); labels.add(pds.getLabel()); } } else if(dataSet instanceof ICandleDataSet && ((ICandleDataSet) dataSet).getDecreasingColor() != ColorTemplate.COLOR_NONE) { colors.add(((ICandleDataSet) dataSet).getDecreasingColor()); colors.add(((ICandleDataSet) dataSet).getIncreasingColor()); labels.add(null); labels.add(dataSet.getLabel()); } else { // 为图例里的标签对应上颜色 for (int j = 0; j < clrs.size() && j < entryCount; j++) { // if multiple colors are set for a DataSet, group them if (j < clrs.size() - 1 && j < entryCount - 1) { labels.add(null); } else { // add label to the last entry String label = data.getDataSetByIndex(i).getLabel(); labels.add(label); } colors.add(clrs.get(j)); } } } if (mLegend.getExtraColors() != null && mLegend.getExtraLabels() != null) { for (int color : mLegend.getExtraColors()) colors.add(color); Collections.addAll(labels, mLegend.getExtraLabels()); } mLegend.setComputedColors(colors); mLegend.setComputedLabels(labels); } Typeface tf = mLegend.getTypeface(); if (tf != null) mLegendLabelPaint.setTypeface(tf); mLegendLabelPaint.setTextSize(mLegend.getTextSize()); mLegendLabelPaint.setColor(mLegend.getTextColor()); // calculate all dimensions of the mLegend mLegend.calculateDimensions(mLegendLabelPaint, mViewPortHandler); }
如何根据图例的设定进行绘制在这里就不讲解了。
接下来讲解的是MarkerView,首先在Chart基类中有方法是为了计算MarkerView显示的位置drawMarkers
/** * draws all MarkerViews on the highlighted positions */ protected void drawMarkers(Canvas canvas) { // if there is no marker view or drawing marker is disabled if (mMarkerView == null || !mDrawMarkerViews || !valuesToHighlight()) return; for (int i = 0; i < mIndicesToHighlight.length; i++) { Highlight highlight = mIndicesToHighlight[i]; int xIndex = highlight.getXIndex(); int dataSetIndex = highlight.getDataSetIndex(); float deltaX = mXAxis != null ? mXAxis.mAxisRange : ((mData == null ? 0.f : mData.getXValCount()) - 1.f); if (xIndex <= deltaX && xIndex <= deltaX * mAnimator.getPhaseX()) { Entry e = mData.getEntryForHighlight(mIndicesToHighlight[i]); // make sure entry not null if (e == null || e.getXIndex() != mIndicesToHighlight[i].getXIndex()) continue; float[] pos = getMarkerPosition(e, highlight); // check bounds if (!mViewPortHandler.isInBounds(pos[0], pos[1])) continue; // callbacks to update the content mMarkerView.refreshContent(e, highlight); mMarkerView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));//计算Marker的大小(Measure) mMarkerView.layout(0, 0, mMarkerView.getMeasuredWidth(),//计算Marker的位置(layout) mMarkerView.getMeasuredHeight());// if (pos[1] - mMarkerView.getHeight() <= 0) { float y = mMarkerView.getHeight() - pos[1]; mMarkerView.draw(canvas, pos[0], pos[1] + y); } else { mMarkerView.draw(canvas, pos[0], pos[1]); }//将传过来的Marker实例绘制在指定的位置 } } }
这个方法是将Marker的位置计算出来,并且讲Marker的实例绘制在指定的位置,然后我们看一下MarkView类的方法,其中最主要的是
setupLayoutResource(),也就是我们可以通过XML资源文件完成对MarkerView样式的设置:
private void setupLayoutResource(int layoutResource) { View inflated = LayoutInflater.from(getContext()).inflate(layoutResource, this); inflated.setLayoutParams(new LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT)); inflated.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); // measure(getWidth(), getHeight()); inflated.layout(0, 0, inflated.getMeasuredWidth(), inflated.getMeasuredHeight()); }
通过这个方法,我们使我们的Marker不再单调 接下来我们讲一下关于ViewPortHandler,这是一个包含着一个实例名叫做
/** * this rectangle defines the area in which graph values can be drawn */ protected RectF mContentRect = new RectF();//如果我们想对绘图区域进行变化,我现在还是做不到,我也不知道为什么
这样的一个矩形区域,然后这个区域的作用主要是如注释所说的这个矩形区域是我们的图表区域被绘制的地方
然后它主要提供了两个类别的方法
第一个类别是设置和得到数据绘制区域的尺寸
public void setChartDimens(float width, float height) { float offsetLeft = this.offsetLeft(); float offsetTop = this.offsetTop(); float offsetRight = this.offsetRight(); float offsetBottom = this.offsetBottom(); mChartHeight = height; mChartWidth = width; restrainViewPort(offsetLeft, offsetTop, offsetRight, offsetBottom); }
public float offsetLeft() { } public float offsetRight() { } public float offsetTop() { } public float offsetBottom() { } public float contentTop() { } public float contentLeft() { } public float contentRight() { } public float contentBottom() { } public float contentWidth() { } public float contentHeight() { } public RectF getContentRect() { } public PointF getContentCenter() { } public float getChartHeight() { } public float getChartWidth() { }
还有一个是对矩形区域进行变换的方法:
public Matrix zoomIn(float x, float y) public Matrix zoomOut(float x, float y) { } public Matrix zoom(float scaleX, float scaleY) { } public Matrix zoom(float scaleX, float scaleY, float x, float y) { } public Matrix setZoom(float scaleX, float scaleY) { } public Matrix setZoom(float scaleX, float scaleY, float x, float y) { } public Matrix fitScreen(){ } public void centerViewPort(final float[] transformedPts, final View view) { }
下面的blog我们再去讲解动画和监听器部分