MPAndroidChart绘制浅析

前言

一直在使用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);
    }
  /**在往下就是各种配置了 */
   略略略

}

它是所有图表的基类,里面是一些基础的方法,包括数据,高亮,动画,描述,空数据等公用方法的实现和抽象。保存当前图表信息等…
继承结构如下

  1. Chart(图表类基类)
    1. BarLineChartBase(柱状图折线图抽象类)
      1. BubbleChart(气泡图)
      2. CandleStickChart(烛状图)
      3. CombinedChart(复合图表)
      4. BarChart(柱状图)
      5. LineChart(折线图)
      6. ScatterChart(散点图)
    2. PieRadarChartBase(饼状图雷达图抽象类)
      1. PieChart(饼状图)
      2. RadarChart(雷达图)

根据不同的图表做了不同的实现,比如说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,继承结构大概如下:

  1. Render基类
    1. AxisRender(轴绘制)
    2. DataRender(图表绘制抽象类)
      1. CombinedChartRender(复合图表绘制,这个类是3.0版本添加的,可以展示折线图,柱状图,散点图等混合)
      2. BubbleChartRenderer(气泡图绘制)
      3. BarChartRender(柱状图绘制)
      4. LineScatterCandleRadarRenderer(折线图,散点图,烛状图,雷达图抽象类)
        1. LineRadarRenderer(折线图,雷达图抽象)
          1. LineChartRender(折线图绘制类)
          2. RadarChartRender(雷达图绘制类)
        2. ScatterChartRender(散点图绘制)
        3. CandleStickChartRenderer(烛状图绘制)
    3. LegendRender(图例绘制)

在不同的图表构造中初始化不同的将mRender对象初始化成不同的图表的Render对象,这里传入的参数有
1. 不同图表的DataProvider(DataProvider是一个接口,实现了获取Y轴方向(左或右),和获取数据的方法)。
2. ChartAnimator这是一个动画类,执行动画效果
3. ViewPortHandler 图表信息类,包括边距,大小,转换等级(缩放)
之后这些Render类就根据自己的实现在canvas上面绘制东西了。

其他补充

由于很好奇它的点击事件是怎么实现的,这边也看了一下它的点击事件。
每个图表写了自己的TouchListener
在构造中需要传入的参数有
1. 图表大小的矩阵(用来计算缩放等级,还有当前点击事件位置)
2. 图表对象

之后根据Touch事件判断相应的手势或者点击,触摸事件作出反应(点击,手势缩放,移动等)。调用View的postInvalidate 方法通知刷新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值