一.概述
MPAndroidChart是一款基于Android的开源图表库,MPAndroidChart不仅可以在Android设备上绘制各种统计图表,而且可以对图表进行拖动和缩放操作,应用起来非常灵活。MPAndroidChart同样拥有常用的图表类型:线型图、饼图、柱状图和散点图。
GitHub地址:
https://github.com/PhilJay/MPAndroidChart
二.以BarChart为例进行源码讲解
MPAndroidChart中集合了特别多的Chart类型,当然了我们常用的几个图表仅仅只有那么几种对吧,比如说像线性图,就像你高中时候的函数图。比如说饼状图,这个你也应该知道吧,很常见的一种图表。还有一种图表雷达图,知道不?就是这个样子的:
见识过吧,这种图表。然后还有一种就是我们今天让讲解的BarChart,柱状图。
我们先看一下BarChart是怎么在XML里面怎么得到的:
(1)首先我们在我的XML资源文件里添加上BarChart,这个在上一篇博客中也提到了,很简单。
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/chart1"
android:layout_width="match_parent"
android:layout_height="360dp" /></span>
(2)然后我们在View中得到这个对象。
mChart = (BarChart) findViewById(R.id.chart1);
(3)然后我们找到BarChart这个对象的类,我们看看他里面到底有什么呢,之前我们有分析过大体是有什么呢,会不会和我们的猜测差不多呢。
public class BarChart extends BarLineChartBase<BarData> implements BarDataProvider {
/** flag that enables or disables the highlighting arrow */
private boolean mDrawHighlightArrow = false;
/**
* if set to true, all values are drawn above their bars, instead of below their top
*/
private boolean mDrawValueAboveBar = true;
/**
* if set to true, a grey area is drawn behind each bar that indicates the maximum value
*/
private boolean mDrawBarShadow = false;
/**
* if set to true, the width of bar will scale by counts of bars
*/
private boolean mAutoScale = true;
/**
* bar's width without bar space
*/
private float barWidthSize;
/**
* bar first onLayout if first this is true
*/
private boolean isFirstOnLayout = true;
public BarChart(Context context) {
super(context);
}
public BarChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BarChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
先看代码的第一部分,我们看到了BarChart继承自 BarLineChartBase<BarData>,然后实现了一个BarDataProvider的这个借口,我们在去看一下BarLineChartBase<BarData>这个类里是什么样的吧。
public abstract class BarLineChartBase<T extends BarLineScatterCandleBubbleData<? extends IBarLineScatterCandleBubbleDataSet<? extends Entry>>>
extends Chart<T> implements BarLineScatterCandleBubbleDataProvider {
/**
* 图表中可以包含最多的实体值的数量,如果实体值的数量超过这个数量之后,该实体值就不会被现实出来
*/
protected int mMaxVisibleCount = 100;
/**
* flag that indicates if auto scaling on the y axis is enabled
*/
private boolean mAutoScaleMinMaxEnabled = false;
private Integer mAutoScaleLastLowestVisibleXIndex = null;
private Integer mAutoScaleLastHighestVisibleXIndex = null;
/**
*是否可以支持双指手势
*/
protected boolean mPinchZoomEnabled = false;
/**
* 两次点击可以放大柱状图的显示
*/
protected boolean mDoubleTapToZoomEnabled = true;
/**
* flag that indicates if highlighting per dragging over a fully zoomed out
* chart is enabled
*/
protected boolean mHighlightPerDragEnabled = true;
/**
* flag that indicates whether the highlight should be full-bar oriented, or single-value?
*/
protected boolean mHighlightFullBarEnabled = false;
/**
* 是否可以缩放
*/
private boolean mDragEnabled = true;
private boolean mScaleXEnabled = true;
private boolean mScaleYEnabled = true;</span>
//前面这些都是相对一些柱状图的属性的值,在后面的BarChart绘制中,会使用到这些值将BarChart绘制成不同的属性
/**
* paint object for the BarChart
*/
protected Paint mGridBackgroundPaint;
protected Paint mBorderPaint;
protected boolean mDrawGridBackground = false;
protected boolean mDrawBorders = false;
/**
* Sets the minimum offset (padding) around the chart, defaults to 15
*/
protected float mMinOffset = 15.f;
/**
* flag indicating if the chart should stay at the same position after a rotation. Default is false.
*/
protected boolean mKeepPositionOnRotation = false;
//好了下面就是验证我们猜测的重点
/**
* the listener for user drawing on the chart
*/
protected OnDrawListener mDrawListener;//这是监听器
/**
* the object representing the labels on the left y-axis
*/
protected YAxis mAxisLeft;//这是左侧的Y轴
/**
* the object representing the labels on the right y-axis
*/
protected YAxis mAxisRight;//这是右侧的Y轴,可能会疑惑为什么有两个轴,等到实际应用的时候你就知道为什么饿
protected YAxisRenderer mAxisRendererLeft;
protected YAxisRenderer mAxisRendererRight;
protected Transformer mLeftAxisTransformer;
protected Transformer mRightAxisTransformer;//我们的Transformer,这个之后会拿出一篇来讲
protected XAxisRenderer mXAxisRenderer;//X轴的渲染器
// /** the approximator object used for data filtering */
// private Approximator mApproximator;
public BarLineChartBase(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public BarLineChartBase(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BarLineChartBase(Context context) {
super(context);
}
//在init中,我们看到讲X轴Y轴进行了初始化,并且初始化了transformer
//初始化chartTouchListener
//初始化了背景和描边的画笔
@Override
protected void init() {
super.init();
mAxisLeft = new YAxis(AxisDependency.LEFT);
mAxisRight = new YAxis(AxisDependency.RIGHT);
mLeftAxisTransformer = new Transformer(mViewPortHandler);
mRightAxisTransformer = new Transformer(mViewPortHandler);
mAxisRendererLeft = new YAxisRenderer(mViewPortHandler, mAxisLeft, mLeftAxisTransformer);
mAxisRendererRight = new YAxisRenderer(mViewPortHandler, mAxisRight, mRightAxisTransformer);
mXAxisRenderer = new XAxisRenderer(mViewPortHandler, mXAxis, mLeftAxisTransformer);
setHighlighter(new ChartHighlighter(this));
mChartTouchListener = new BarLineChartTouchListener(this, mViewPortHandler.getMatrixTouch());
mGridBackgroundPaint = new Paint();
mGridBackgroundPaint.setStyle(Style.FILL);
// mGridBackgroundPaint.setColor(Color.WHITE);
mGridBackgroundPaint.setColor(Color.rgb(240, 240, 240)); // light
// grey
mBorderPaint = new Paint();
mBorderPaint.setStyle(Style.STROKE);
mBorderPaint.setColor(Color.BLACK);
mBorderPaint.setStrokeWidth(Utils.convertDpToPixel(1f));
}
// for performance tracking
private long totalTime = 0;
private long drawCycles = 0;
//然后这里是LineBase的OnDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mData == null)
return;
long starttime = System.currentTimeMillis();
calcModulus();
mXAxisRenderer.calcXBounds(this, mXAxis.mAxisLabelModulus);
mRenderer.calcXBounds(this, mXAxis.mAxisLabelModulus);//在X轴的区域内进行绘制
// execute all drawing commands
drawGridBackground(canvas);//绘制图表的背景
if (mAxisLeft.isEnabled())
mAxisRendererLeft.computeAxis(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisMaximum);
if (mAxisRight.isEnabled())
mAxisRendererRight.computeAxis(mAxisRight.mAxisMinimum, mAxisRight.mAxisMaximum);//计算XY轴的范围
mXAxisRenderer.renderAxisLine(canvas);
mAxisRendererLeft.renderAxisLine(canvas);
mAxisRendererRight.renderAxisLine(canvas);//绘制XY轴的线
if (mAutoScaleMinMaxEnabled) {
final int lowestVisibleXIndex = getLowestVisibleXIndex();
final int highestVisibleXIndex = getHighestVisibleXIndex();
if (mAutoScaleLastLowestVisibleXIndex == null ||
mAutoScaleLastLowestVisibleXIndex != lowestVisibleXIndex ||
mAutoScaleLastHighestVisibleXIndex == null ||
mAutoScaleLastHighestVisibleXIndex != highestVisibleXIndex) {
calcMinMax();
calculateOffsets();
mAutoScaleLastLowestVisibleXIndex = lowestVisibleXIndex;
mAutoScaleLastHighestVisibleXIndex = highestVisibleXIndex;
}
}
// make sure the graph values and grid cannot be drawn outside the
// content-rect
mRenderer.drawData(canvas);
// if highlighting is enabled
if (valuesToHighlight())
mRenderer.drawHighlighted(canvas, mIndicesToHighlight);//绘制上初始状态的高亮
// Removes clipping rectangle
canvas.restoreToCount(clipRestoreCount);
mRenderer.drawExtras(canvas);
clipRestoreCount = canvas.save();
canvas.clipRect(mViewPortHandler.getContentRect());
if (!mXAxis.isDrawLimitLinesBehindDataEnabled())
mXAxisRenderer.renderLimitLines(canvas);
if (!mAxisLeft.isDrawLimitLinesBehindDataEnabled())
mAxisRendererLeft.renderLimitLines(canvas);
if (!mAxisRight.isDrawLimitLinesBehindDataEnabled())
mAxisRendererRight.renderLimitLines(canvas);//什么叫LimitLine啊?你想啊X轴和Y轴载屏幕上的显示有没有尽头
canvas.restoreToCount(clipRestoreCount);
mXAxisRenderer.renderAxisLabels(canvas);
mAxisRendererLeft.renderAxisLabels(canvas);
mAxisRendererRight.renderAxisLabels(canvas);//将X轴和Y轴的标签值绘制上
mRenderer.drawValues(canvas);//通过传入的数据使用渲染器讲数据进行了会址
mLegendRenderer.renderLegend(canvas);//绘制了图表的图例
drawMarkers(canvas);//绘制了MarkerView
drawDescription(canvas);//绘制了图表的描述
if (mLogEnabled) {
long drawtime = (System.currentTimeMillis() - starttime);
totalTime += drawtime;
drawCycles += 1;
long average = totalTime / drawCycles;
Log.i(LOG_TAG, "Drawtime: " + drawtime + " ms, average: " + average + " ms, cycles: "
+ drawCycles);
}
}
首先BarLineDataBase<BarData>是一个抽象类,在这个类中有很多基本的属性,这些属性用来控制图表的一些基本属性,比如说是否显示XY轴,是否支持缩放以及图表的数据限制等,在图表进行绘制的时候会根据这些属性进行绘制,然后绘制出不同属性的图表。
然后在Init方法中,BarLineDataBase<BarData>这个类将绘制图表所需要的几个重要的组成部分进行了初始化,包括XY轴,包括监听器,还有Transformer和Paint。
初始化之后,BarLineDataBase<BarData>复写了onDraw方法,在onDraw方法中绘制了XY轴,并且使用在Chart类中继承来的mRenderer执行了drawData(),drawValues()和drawExtras()这三个方法,根据装入chart中的数据进行了绘制。然后有绘制了Legend图例,MarkerView标记和Description描述。这样你会发现我们将图表里的东西都进行了绘制。并且还装入了监听器,转化器(transformer)等,在BarLineDataBase<BarData>类中的方法执行完之后,我们将BarChart的基本组成进行了组装。
BarLineDataBase<BarData>类是一个继承自Chart抽象类的抽象类,在BarLineDataBase<BarData>中持有了mRender,mLegend等对象,所以他可以对Chart抽象类中的基本组成对象进行操作。
分析到这里,我们答题知道BarChart的继承关系,BarChart这个类是BarLineDataBase<BarData>这个抽象类的一个实现类,而BarLineDataBase<BarData>是Chart抽象类的一个抽象类,那么我们去看一下Chart抽象类里面有什么对象组成,有执行了什么方法吧,根据名字我们可以知道Chart在这个项目是一个顶层的抽象类,分析到他进算是分析到头了,当然了她也继承自ViewGroup但是ViewGroup我们并没有分析的必要。
public abstract class Chart<T extends ChartData<? extends IDataSet<? extends Entry>>> extends
ViewGroup
implements ChartInterface {
public static final String LOG_TAG = "MPAndroidChart";
/**
* object that holds all data that was originally set for the chart, before
* it was modified or any filtering algorithms had been applied
*/
<span style="color:#ff0000;">protected T mData = null;</span>
<span style="color:#ff0000;"> /**
* 默认的格式转换器
*/
protected ValueFormatter mDefaultFormatter;
/**
* 绘制描述的Paint
*/
protected Paint mDescPaint;
/**
* 绘制详情的Paint
*/
protected Paint mInfoPaint;
/**
* 绘制在图表右下角的描述
*/
protected String mDescription = "Description";
/**
* the object representing the labels on the x-axis
*/
protected XAxis maxis;//X轴
protected boolean mTouchEnabled = true;
protected Legend mLegend;//图例</span>
<span style="color:#ff0000;"> protected OnChartValueSelectedListener mSelectionListener;//item选中时触发的监听器
protected ChartTouchListener mChartTouchListener;//图表被点击时触发的监听器
private OnChartGestureListener mGestureListener;//图表中的手势监听器
protected LegendRenderer mLegendRenderer;//图例的渲染器
protected DataRenderer mRenderer;//数据的渲染器
protected ChartHighlighter mHighlighter;//高亮
protected ViewPortHandler mViewPortHandler;//item绘制区域
protected ChartAnimator mAnimator;//动画</span>
//根据上面的这些声明的对象,我们基本印证了自己的猜测,图表的组成部分就是这些。
private float mExtraTopOffset = 0.f,
mExtraRightOffset = 0.f,
mExtraBottomOffset = 0.f,
mExtraLeftOffset = 0.f;
/**
* default constructor for initialization in code
*/
public Chart(Context context) {
super(context);
init();
}
/**
* constructor for initialization in xml
*/
public Chart(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* even more awesome constructor
*/
public Chart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* initialize all paints and stuff
*<span style="font-family: Arial, Helvetica, sans-serif;">//在init中,对图表的基本组成进行了初始化</span>
<pre name="code" class="java">
setWillNotDraw(false);
// setLayerType(View.LAYER_TYPE_HARDWARE, null);
if (android.os.Build.VERSION.SDK_INT < 11)
mAnimator = new ChartAnimator();
else
mAnimator = new ChartAnimator(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// ViewCompat.postInvalidateOnAnimation(Chart.this);
postInvalidate();
}
});
// initialize the utils
Utils.init(getContext());
mDefaultFormatter = new DefaultValueFormatter(1);
mViewPortHandler = new ViewPortHandler();
mLegend = new Legend();
mLegendRenderer = new LegendRenderer(mViewPortHandler, mLegend);
mXAxis = new XAxis();
mDescPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDescPaint.setColor(Color.BLACK);
mDescPaint.setTextAlign(Align.RIGHT);
mDescPaint.setTextSize(Utils.convertDpToPixel(9f));
mInfoPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mInfoPaint.setColor(Color.rgb(247, 189, 51)); // orange
mInfoPaint.setTextAlign(Align.CENTER);
mInfoPaint.setTextSize(Utils.convertDpToPixel(12f));
mDrawPaint = new Paint(Paint.DITHER_FLAG);
if (mLogEnabled)
Log.i("", "Chart.init()");
}
然后在初始化之后,我们来看一下,Chart抽象类的几个重要的方法,这些方法写出来大部分需要被实现Chart抽象类的子类复写。
<@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
if (mData == null) {
boolean hasText = !TextUtils.isEmpty(mNoDataText);
boolean hasDescription = !TextUtils.isEmpty(mNoDataTextDescription);
float line1height = hasText ? Utils.calcTextHeight(mInfoPaint, mNoDataText) : 0.f;
float line2height = hasDescription ? Utils.calcTextHeight(mInfoPaint, mNoDataTextDescription) : 0.f;
float lineSpacing = (hasText && hasDescription) ?
(mInfoPaint.getFontSpacing() - line1height) : 0.f;
// if no data, inform the user
float y = (getHeight() -
(line1height + lineSpacing + line2height)) / 2.f
+ line1height;
if (hasText) {
canvas.drawText(mNoDataText, getWidth() / 2, y, mInfoPaint);
if (hasDescription) {
y = y + line1height + lineSpacing;
}
}
if (hasDescription) {
canvas.drawText(mNoDataTextDescription, getWidth() / 2, y, mInfoPaint);
}
return;
}
if (!mOffsetsCalculated) {
calculateOffsets();
mOffsetsCalculated = true;
}
}</span>
onDraw()作者写的实在太聪明了,因为他知道,在对数据进行绘制的时候不同的图表有着完全不同的绘制方式,所以她在最基本的Chart类的onDraw方法中写了当数据为空的时候的绘制原则,因为有数据的时候各有各的展示方式,而没有数据的时候处理的方式都一样,假设你真的是很奇怪,那么你复写也就行了。
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]);
</span>//得到该实体,进行非空判断后,拿到该实体的数据
// make sure entry not null
if (e == null || e.getXIndex() != mIndicesToHighlight[i].getXIndex())
continue;
float[] pos = getMarkerPosition(e, highlight);
//计算出MarkerView的显示位置
// 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));
// mMarkerView.layout(0, 0, mMarkerView.getMeasuredWidth(),
// mMarkerView.getMeasuredHeight());
// mMarkerView.draw(mDrawCanvas, pos[0], pos[1]);
//执行mMarkerView的Measure,layout 和Draw方法绘制出MarkerView
mMarkerView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mMarkerView.layout(0, 0, mMarkerView.getMeasuredWidth(),
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]);
}</span>
//至于是怎么绘制的,你应该去看是谁实现了MarkerView的这三个方法,然后根据实现的方法了解绘制的方式
}
}
}
绘制MarkerView的方法。
其他还有很多基本方法,诸如得到Chart的基本组成部分的实体对象,对Item进行高亮的HighLightValue方法等(当然HighLighte r这个类也是需要详细讲解的)。其他的杂暗示不赘述。
分析到这里,我们完成了有下面的类往他的父类进行溯源的这个过程,我们找到了最基本的Chart类,然后又有BarLineDataBase<BarData>这个抽象类对BarChart进行了进一步的抽象,然后我们Bar Chart类继承自BarLineDataBase<BarData>类,实现了一个具体的可以实例话的类。我们弄明白了Chart类的基本继承关系,并且我们在网上溯源的过程中了解了一个Chart的基本组成部分,这些基本组成是Chart的共性。
所以我们接下来要做的事情就行对这些基本组成部分进行分析,在我们对源码进行修改的时候,假设我们需要修改柱状图的item显示的宽度,那么我们就应该去找DataRenderer 这个类对吧,假设我们不要要图例前面的标签就仅仅是简单的正方形的色块,需求要求我们讲起实现为一个圆形的色块,那么我们就应该对Legend这个对象和mLegendRenderer 进行修改对吧。
当然了对应我们的BarChart我们理所当然的能想到,我们在做修改的时候(假设就是上面的那些需求),我们修改的类应该是DataRenderer 的对应BarChart的子类BarChartDataRenderer,和Legend对应的BarChart的Legend类(在后面的分析中,我发现我的这个猜测是错的,图例类在图表中的变化并不大,所以开发者并没有编写对应不同图表的Legend类。)
接下来的博客我们将分析图表的这些基本组成部分。