前言
一直在使用MPAndroidChart但对其内部机制却没有做多少了解,自己之前还修改过MPAndroidChart的源码,某次面试被问到,MPAndroidChart是怎样进行绘制的,瞬间一脸懵逼,回答了个大概,但是被看出其实不是很了解。算亡羊补牢吧,今天抽了点时间看了MPAndroidChart 3.0的源码部分。
直接进入正题吧。
Chart基类
这边顺便讲解一下Chart这个类,它是所有图表类的抽象基类,继承自ViewGroup,实现了图表接口ChartInterface(这个接口用来实现图表的大小,边界,和范围的获取。),Chart里面存放了一些公共的配置和一些共有的抽象方法,数据等。
/**
* 这个类是图表类的基类,继承自ViewGroup,它可以让图表像View一样被使用。
* @author Philipp Jahoda
*/
@SuppressLint("NewApi")
public abstract class Chart<T extends ChartData<? extends IDataSet<? extends Entry>>> extends
ViewGroup
implements ChartInterface {
public static final String LOG_TAG = "MPAndroidChart";
/**
* 声明log是否开启(调试用)
*/
protected boolean mLogEnabled = false;
/**
* 图表数据
*/
protected T mData = null;
/**
* 触摸区域高亮
*/
protected boolean mHighLightPerTapEnabled = true;
/**
* 触摸事件完成后是否继续滚动(可以试试pieChart的旋转功能)
*/
private boolean mDragDecelerationEnabled = true;
/**
* 这个就是滚动的减慢速度,算摩擦系数,0就直接停了,1会一直转,所以他回自动变成0.999,
*/
private float mDragDecelerationFrictionCoef = 0.9f;
/**
* 数据格式化
*/
protected ValueFormatter mDefaultFormatter;
/**
* 图表描述画笔
*/
protected Paint mDescPaint;
/**
* 这个画笔是用来画无数据的情况
*/
protected Paint mInfoPaint;
/**
* 描述描述!!
*/
protected String mDescription = "Description";
/**
* X轴的label可以看折现图的横轴label
*/
protected XAxis mXAxis;
/**
* 手势开关
*/
protected boolean mTouchEnabled = true;
/**
* Legend是一个图例描述,里面是这个图例,位置等其他配置的信息。
*/
protected Legend mLegend;
/**
* 数据选中监听
*/
protected OnChartValueSelectedListener mSelectionListener;
/**
* 图表点击监听
*/
protected ChartTouchListener mChartTouchListener;
/**
* 空数据文本
*/
private String mNoDataText = "No chart data available.";
/**
* 手势监听
*/
private OnChartGestureListener mGestureListener;
/**
* 无数据描述
*/
private String mNoDataTextDescription;
/**
* 这个是图例绘制类
*/
protected LegendRenderer mLegendRenderer;
/**
* 数据绘制类基类实现
*/
protected DataRenderer mRenderer;
/**
* 区域高亮辅助基类。用来计算高亮区域并返回
*/
protected ChartHighlighter mHighlighter;
/**
* 边界约束管理
*/
protected ViewPortHandler mViewPortHandler;
/**
* 动画
*/
protected ChartAnimator mAnimator;
/**
* 边距
*/
private float mExtraTopOffset = 0.f,
mExtraRightOffset = 0.f,
mExtraBottomOffset = 0.f,
mExtraLeftOffset = 0.f;
/**
* 默认构造
*/
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
* 初始化函数,负责一些对象的初始化
*/
protected void init() {
...略
}
/**
* 设置新数据,在这里有个notify,在数据添加完之后刷新图表
*/
public void setData(T data) {
// let the chart know there is new data
notifyDataSetChanged();
}
/**
* 清空所有数据并刷新
*/
public void clear() {
mData = null;
mIndicesToHighlight = null;
invalidate();
}
/**
* 上面是直接置空。可能是想回收,这里是清除值后刷新
*/
public void clearValues() {
mData.clearValues();
invalidate();
}
/**
* 判断data是否为空
*/
public boolean isEmpty() {
if (mData == null)
return true;
else {
if (mData.getYValCount() <= 0)
return true;
else
return false;
}
}
/**
* 这个方法可以让图表知道自己掌握的数据,并显示出来。
* 需要重新进行计算
*/
public abstract void notifyDataSetChanged();
/**
* 边距计算
*/
protected abstract void calculateOffsets();
/**
* 最大最小y值计算抽象方法
*/
protected abstract void calcMinMax();
/**
* 计算单位
*/
protected void calculateFormatter(float min, float max) {
...略
}
/**
* 边距计算
*/
private boolean mOffsetsCalculated = false;
protected Paint mDrawPaint;
/**
* 主要的绘制方法
*/
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
...略
}
/**
* 描述位置
*/
private PointF mDescriptionPosition;
/**
* 绘制描述
*/
protected void drawDescription(Canvas c) {
...略
}
/**高亮模块 支持点击高亮等各种效果。这边处理绘制还有计算*/
..略
/**下面是MarkView */
..略
/** 下面是动画处理 */
..略
/** 动画开放方法 */
public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easingX,
EasingFunction easingY) {
mAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY);
}
public void animateX(int durationMillis, EasingFunction easing) {
mAnimator.animateX(durationMillis, easing);
}
public void animateY(int durationMillis, EasingFunction easing) {
mAnimator.animateY(durationMillis, easing);
}
/** 动画开放配置 */
public void animateXY(int durationMillisX, int durationMillisY, Easing.EasingOption easingX,
Easing.EasingOption easingY) {
mAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY);
}
public void animateX(int durationMillis, Easing.EasingOption easing) {
mAnimator.animateX(durationMillis, easing);
}
public void animateY(int durationMillis, Easing.EasingOption easing) {
mAnimator.animateY(durationMillis, easing);
}
public void animateX(int durationMillis) {
mAnimator.animateX(durationMillis);
}
public void animateXY(int durationMillisX, int durationMillisY) {
mAnimator.animateXY(durationMillisX, durationMillisY);
}
/**在往下就是各种配置了 */
略略略
}
它是所有图表的基类,里面是一些基础的方法,包括数据,高亮,动画,描述,空数据等公用方法的实现和抽象。保存当前图表信息等…
继承结构如下
- Chart(图表类基类)
- BarLineChartBase(柱状图折线图抽象类)
- BubbleChart(气泡图)
- CandleStickChart(烛状图)
- CombinedChart(复合图表)
- BarChart(柱状图)
- LineChart(折线图)
- ScatterChart(散点图)
- PieRadarChartBase(饼状图雷达图抽象类)
- PieChart(饼状图)
- RadarChart(雷达图)
- BarLineChartBase(柱状图折线图抽象类)
根据不同的图表做了不同的实现,比如说BarLineChartBase都有一个共同的属性是XY轴,在onDraw方法中对XY轴做了绘制,BarLineChartBase还支持缩放操作。PieRadarBase是没有XY轴且不支持缩放操作,但支持旋转。所以将这两个图单独抽象了一层。
onDraw算是共有的主要方法。因为数据绘制都是在onDraw方法中的canvas上面。
下面来讲解一下MPAndroidChart的绘制过程
MPAndroidChart绘制过程
所有的绘制都做了抽象在这边基本能重用的方法就做一层抽象。
我们来看一下最底层的抽象Renderer类
Chart的绘制是经由Renderer类之手,看一下Renderer类的实现。
Render
/**
* Abstract baseclass of all Renderers.
*
* @author Philipp Jahoda
*/
public abstract class Renderer {
/**
* 这个变量用来存放绘制区域还有偏移量等设置
*/
protected ViewPortHandler mViewPortHandler;
/**
* X轴需要绘制的最小值
*/
protected int mMinX = 0;
/**
* X轴需要绘制的最大值
*/
protected int mMaxX = 0;
/**
* 这边通过构造传入ViewPortHandler
*/
public Renderer(ViewPortHandler viewPortHandler) {
this.mViewPortHandler = viewPortHandler;
}
/**
* 这个方法用来计算当前的值是否在X的最小和最大之间
*/
protected boolean fitsBounds(float val, float min, float max) {
if (val < min || val > max)
return false;
else
return true;
}
/**
* 这个方法用来计算当前可以显示的X的大小,
*/
public void calcXBounds(BarLineScatterCandleBubbleDataProvider dataProvider, int xAxisModulus) {
int low = dataProvider.getLowestVisibleXIndex();
int high = dataProvider.getHighestVisibleXIndex();
int subLow = (low % xAxisModulus == 0) ? xAxisModulus : 0;
mMinX = Math.max((low / xAxisModulus) * (xAxisModulus) - subLow, 0);
mMaxX = Math.min((high / xAxisModulus) * (xAxisModulus) + xAxisModulus, (int) dataProvider.getXChartMax());
}
}
这个类里面的方法用来确定当前视图可显示的大小,所有的Render类都继承自它。包括AxisRenderer(轴和轴值绘制),LegendRender(图例绘制),DataRender(图表图形绘制)。
AxisRenderer,和LegendRender的实现都大同小异,DataRender属于图表绘制的抽象,因为图表的样式比较多,它扩展了一些可以供图表使用的方法,接下来主要拿DataRender来讲。
DataRender
/**
* 这个类是Renderer的子类,用来提供一些抽象的绘制方法。
*/
public abstract class DataRenderer extends Renderer {
/**
* 这个对象是用来设置图表动画的
*/
protected ChartAnimator mAnimator;
/**
* 初始化图表item绘制的画笔
*/
protected Paint mRenderPaint;
/**
* 绘制高亮区域
*/
protected Paint mHighlightPaint;
/**
* 这个没见用到
*/
protected Paint mDrawPaint;
/**
* 这个用来绘制图表的文本信息
*/
protected Paint mValuePaint;
/**
* 构造函数传入了动画对象ChartAnimator,控制绘制区域和偏移量的对象ViewPortHandler,里面做的是变量初始化操作
*/
public DataRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) {
super(viewPortHandler);
略..
}
/**
* 这个方法返回了文本绘制的画笔对象
*/
public Paint getPaintValues() {
return mValuePaint;
}
/**
* 高亮画笔
*/
public Paint getPaintHighlight() {
return mHighlightPaint;
}
/**
* 绘制画笔
*/
public Paint getPaintRender() {
return mRenderPaint;
}
/**
* Applies the required styling (provided by the DataSet) to the value-paint
* object.
*
* @param set
*/
protected void applyValueTextStyle(IDataSet set) {
mValuePaint.setTypeface(set.getValueTypeface());
mValuePaint.setTextSize(set.getValueTextSize());
}
/**
* 初始化Buffers,buffer是用来进行尺寸变换的一个类,他和transformer类配合生成实际的尺寸
*/
public abstract void initBuffers();
/**
* 数据绘制抽象类
*/
public abstract void drawData(Canvas c);
/**
*数值绘制抽象类
*/
public abstract void drawValues(Canvas c);
/**
* 数值绘制
*/
public void drawValue(Canvas c, ValueFormatter formatter, float value, Entry entry, int dataSetIndex, float x, float y, int color) {
mValuePaint.setColor(color);
c.drawText(formatter.getFormattedValue(value, entry, dataSetIndex, mViewPortHandler), x, y, mValuePaint);
}
/**
* 这个是为LineChart 或者PieChart等设计的附加绘制。因为lineChart需要为每个点进行绘制,PieChart可能需要绘制中间圆形等。
* @param c
*/
public abstract void drawExtras(Canvas c);
/**
* 绘制高亮数据
*/
public abstract void drawHighlighted(Canvas c, Highlight[] indices);
}
DataRender里面的方法大致如下
1. 图表的绘制抽象方法 (drawData)
2. 数值绘制的抽象方法 (drawValues)
3. 数值绘制方法(drawValue)
4. 图表高亮抽象方法(drawHighlighted)
5. 动画对象和画笔对象的初始化(构造函数)
看一下BarChartRender的实现
BarChartRender
public class BarChartRenderer extends DataRenderer {
/**
* BarDataProvider这个类中存放了所有的barData,还有一些类似于阴影,数值位置,高亮箭头等。
*/
protected BarDataProvider mChart;
/** the rect object that is used for drawing the bars
* 这个是用来设置每条bar的大小。主要是用做高亮绘制
*/
protected RectF mBarRect = new RectF();
/**
* 声明buffer的数组
*/
protected BarBuffer[] mBarBuffers;
/**
* 阴影画笔
*/
protected Paint mShadowPaint;
/**
* 边框画笔
*/
protected Paint mBarBorderPaint;
/**
* 构造函数传入了BarDataProvider,动画类,显示位置控制类,初始化了绘制所需的画笔
*/
public BarChartRenderer(BarDataProvider chart, ChartAnimator animator,
ViewPortHandler viewPortHandler) {
super(animator, viewPortHandler);
this.mChart = chart;
mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mHighlightPaint.setStyle(Paint.Style.FILL);
mHighlightPaint.setColor(Color.rgb(0, 0, 0));
// set alpha after color
mHighlightPaint.setAlpha(120);
mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mShadowPaint.setStyle(Paint.Style.FILL);
mBarBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBarBorderPaint.setStyle(Paint.Style.STROKE);
}
/**
* 初始化Buffer
*/
@Override
public void initBuffers() {
BarData barData = mChart.getBarData();
mBarBuffers = new BarBuffer[barData.getDataSetCount()];
for (int i = 0; i < mBarBuffers.length; i++) {
IBarDataSet set = barData.getDataSetByIndex(i);
mBarBuffers[i] = new BarBuffer(set.getEntryCount() * 4 * (set.isStacked() ? set.getStackSize() : 1),
barData.getGroupSpace(),
barData.getDataSetCount(), set.isStacked());
}
}
/**
* 绘制图表
* @param c
*/
@Override
public void drawData(Canvas c) {
BarData barData = mChart.getBarData();
for (int i = 0; i < barData.getDataSetCount(); i++) {
IBarDataSet set = barData.getDataSetByIndex(i);
if (set.isVisible() && set.getEntryCount() > 0) {
drawDataSet(c, set, i);
}
}
}
/**
* 根据每个dataset绘制图表
*/
protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) {
...略
}
/**
* 准备高亮的区域
*/
protected void prepareBarHighlight(float x, float y1, float y2, float barspaceHalf,
Transformer trans) {
float barWidth = 0.5f;
float left = x - barWidth + barspaceHalf;
float right = x + barWidth - barspaceHalf;
float top = y1;
float bottom = y2;
mBarRect.set(left, top, right, bottom);
trans.rectValueToPixel(mBarRect, mAnimator.getPhaseY());
}
/**
* 绘制数值 一系列的计算
*/
@Override
public void drawValues(Canvas c) {
略... }
/**
* 绘制高亮 一系列的计算
*/
@Override
public void drawHighlighted(Canvas c, Highlight[] indices) {
略...
}
public float[] getTransformedValues(Transformer trans, IBarDataSet data,
int dataSetIndex) {
return trans.generateTransformedValuesBarChart(data, dataSetIndex,
mChart.getBarData(),
mAnimator.getPhaseY());
}
/**
* 检查是否为可显示的值
* @return
*/
protected boolean passesCheck() {
return mChart.getBarData().getYValCount() < mChart.getMaxVisibleCount()
* mViewPortHandler.getScaleX();
}
/**
* 绘制其他
* @param c
*/
@Override
public void drawExtras(Canvas c) { }
}
可以看到里面就是图表项和文本绘制的具体实现了,主要方法是drawvalues方法,大致就是一些通过一些方法计算每条bar的值,然后进行绘制。
总结
在这边简单讲解一下设计的方法。因为所有的Chart都继承了ViewGroup,实现了View的onMeasure,onDraw,onLayout,onSizeChanged方法,所以它是可以像自定义控件一样来使用。
View的绘制都再Render中实现,不同图表实现了不同的Render,继承结构大概如下:
- Render基类
- AxisRender(轴绘制)
- DataRender(图表绘制抽象类)
- CombinedChartRender(复合图表绘制,这个类是3.0版本添加的,可以展示折线图,柱状图,散点图等混合)
- BubbleChartRenderer(气泡图绘制)
- BarChartRender(柱状图绘制)
- LineScatterCandleRadarRenderer(折线图,散点图,烛状图,雷达图抽象类)
- LineRadarRenderer(折线图,雷达图抽象)
- LineChartRender(折线图绘制类)
- RadarChartRender(雷达图绘制类)
- ScatterChartRender(散点图绘制)
- CandleStickChartRenderer(烛状图绘制)
- LineRadarRenderer(折线图,雷达图抽象)
- LegendRender(图例绘制)
在不同的图表构造中初始化不同的将mRender对象初始化成不同的图表的Render对象,这里传入的参数有
1. 不同图表的DataProvider(DataProvider是一个接口,实现了获取Y轴方向(左或右),和获取数据的方法)。
2. ChartAnimator这是一个动画类,执行动画效果
3. ViewPortHandler 图表信息类,包括边距,大小,转换等级(缩放)
之后这些Render类就根据自己的实现在canvas上面绘制东西了。
其他补充
由于很好奇它的点击事件是怎么实现的,这边也看了一下它的点击事件。
每个图表写了自己的TouchListener
在构造中需要传入的参数有
1. 图表大小的矩阵(用来计算缩放等级,还有当前点击事件位置)
2. 图表对象
之后根据Touch事件判断相应的手势或者点击,触摸事件作出反应(点击,手势缩放,移动等)。调用View的postInvalidate 方法通知刷新。