MPAndroidChart是一个广泛使用的图表开源库,本文的重点在于介绍它的图表绘制深度定制方法及相关讨论,也有一些八卦信息和脑洞,交互部分了解不多不讨论,对于配置参数、接入方法因为已经有很多文章介绍,不再赘述。

简介

MPAndroidChart是Android最好和使用最广泛的图表开源库,它还有有一个iOS孪生兄弟Charts,也是最优秀的图表开源库之一。它们两个的关系确实很像孪生兄弟:Api接口对齐程度达到95%,而且同步开发,内部类结构甚至类名上也基本对齐,以至于Android和iOS两端任意一端的图表解决方案,都可以不加修改的拿到另外一端去执行。 下面是它们的Github:

https://github.com/PhilJay/MPAndroidChart

https://github.com/danielgindi/Charts

MPAndroidChart Api说明

MPAndoridChart详细文档

这个网站看起来非常山寨,它确实是MPChart的官方文档网站,对于如何上手以及各个Api的用法解释得非常准确详细。由于iOS的Charts和MPAndroidChart在Api层面几乎完全对齐,Charts也可以使用这个文档。就在Charts的GitHub上还提到“Study the Android version’s Documentation-Wiki”。

如何使用、配置、完成图表开发,参考上述官方文档即可,网上许多MPAndroidChart详解之类的文章,很少超过对对Api的翻译水平,并不如自己尝试一下了解的更快更详细。

如果是比较标准的图表需求用这个库可以很容易的做出来,一般只要使用一些配置就可以了,即便是很个性化的需求,用这个库实现起来也相对容易。

八卦

MPAndroidChart的作者Philipp Jahoda,是一名奥地利人,生活在林茨(Linz),可能现在生活在维也纳,他是一个重度骑行爱好者。技术栈非常广,从Java后端、数据库,到前端的Swift、Android/Kotlin、RN等都有很多经验。还是一名企业家,经常去学校做演讲,很可能在当地也是一位小有名气的人物。如果生活在中国大概可以加入市政协或者担任县工商联副主席级别的职务。

大概是2015年或者更早开始了MPAndroidChart的开发,大概同时间也在开发Butleroy应用,这个应用在iOS上也可以搜到。有可能是他们在做ButleroyApp的时候,需要图表功能于是就开发了这样一个库。

2018年作为四个联合创始人之一的Butleroy应用公司获得了数百万欧元的投资,他们公司名字也叫做Butleroy,是基于机器学习的数字助理,看起来是一个Outlook工具,帮助制定日程和提醒的。

2019年,ButleroyApp跻身进入iOS上的付费应用top榜,现在还能在AppleStore下载到,要花60块钱,今天我看了看似乎缺乏打理,目前的平均评分只有3,精选评论标题是“不推荐”,但是Mac版本的评分更好一些,也许这个应用更适合桌面使用。它的更新记录停止在2020年。看来这次创业,至少没有大获成功。

MPAndroidChart版本停留在2019年,之后没有进一步的更新,但是小的更新持续到现在,对应的iOS版本也在更新。iOS版本的MPAndroidChart,Charts似乎也是和MPAndroidChart同步开发的,他们的API对齐程度令人惊讶,我在一个规模更小的项目上遇到了对齐Api的需求,但是非常困难而且对齐程度远远不如MPAndroidChart&Charts。

现在供职于ahoikapptn,可能是联合创始人,这应该是一家外包公司,主要服务大型客户,提供从Ui设计到最终App上架的全套服务。但是也有自己的App产品,还出售服装,我也不清楚这些服装是周边产品还是他们本来就出售服装。

然而Charts的作者来自以色列,很难想象一个奥地利人和以色列人远隔万里是怎么做出来一个Api基本对齐的两套工具的?我们当时挤在一个办公室里面都没有把Api对齐到他们的程度。

这个开源项目持续好几年也总共只收到了捐赠共计 $2100,看来靠开源养活自己是不太可能的,程序员需为五斗米折腰。

这个问题当中,作者本人的回答没有被采纳,反而是其他人给出了不同的答案被采纳了。

定制

就我使用MPAndroidChart的经验来看,定制化的图表开发,并不需要修改MPAndroidChart内部的SDK,只要使用它的参数设定加上现有的扩展能力就足够了。如果你觉得自己的需求是必须修改SDK才能实现的,那么很可能是对代码了解不够。这意味着MPAndroidChart的作者对图表有非常深刻的认识,在隐藏细节的同时,也开放了足够的定制化空间。

在快手创作者版开发的过程中,我还是修改了SDK的代码,但是实际上后来对MPAndroidChart有更多了解以后,才发现是可以不必修改的。

举例

首先,我们看一个例子,怎么对MPAndroidChart进行扩展,实现配置项无法支持的需求开发。唯一的回答是需要修改SDK代码,实际上并不需要。

需求

让折线图展示成为渐变彩色,如下: JPG

解决方案

在现有的Api支持当中,修改折线颜色是没有问题的, 但是没有修改成彩色的,在原来的回答当中,是直接修改了SDK里面负责绘制折线的代码,设置为渐变色解决的。

如果不修改SDK,可以通过这样的方式修改:

  1. 从LineChart继承一个CustomLineChart;
  2. 从 LineChartRenderer继承一个CustomLineChartRenderer;
  3. CustomLineChart重写init方法,把mRenderer设置为CustomLineChartRenderer;
  4. CustomLineChartRenderer的drawLinear方法给Painter设置渐变色彩:
protected void drawLinear(Canvas c, ILineDataSet dataSet) {
   Shader shader = new LinearGradient(0, 0, 200, 200 , Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
   mRenderPaint.setShader(shader);
   super.drawLinear(c, dataSet);
}

就可以实现彩色折线效果,下图略鬼畜,不过调优以后应该会好看一些:

PNG

图表的组成部分

一个图表,大致可以划分成为下面三部分:

PNG

两个红框:坐标区域

蓝框:绘制区域

绿框:图例区域

还有标签和十字线、图表交互等,不影响讨论和结论,就从略了。

MPAndroidChart基本类图

PNG

Chart类是图表的基类,它的几个成员分别对应了图表的几个区域:

● mData -> 图表数据

● mRenderer -> 蓝框绘制区域

● mXaxis -> 红框坐标区域

● mLengendRenderer -> 绿框图例绘制区域

各种图表都是Chart的子类,分别在子类当中实现本图表的特有功能,并且可以通过set/get方法把mRenderer、mXaxis等成员替换为自己需要的实际类型。比如折线图的类就是LineChart,它的mRenderer是LineChartRenderer,其他还有柱状图是BarChat等,不在类图当中一一标记。

可见,MPAndroidChart的成员和类继承关系,和图表的区域划分基本一致。

定制开发图表

那么定制开发图表的方法就很清楚了,需要定制化哪部分,就找到对应的类,可以用组合方式替换实现就用组合方式,不能用组合方式,就用继承方式,把自己的定制代码从外部设置给Chart。

例子里面,就是要定制折线绘制部分,那么对应的类是Renderer,通过继承-组合方式替换以后就解决,类图如下(橙色是新继承出来的子类):

PNG

比如说希望定制化坐标轴:

● 首先从现有的方法和属性里面找能否满足需求;

● 如果还不可以,就要在对应的Chart类(比如BarChart)当中找到坐标轴的实现类(比如XAxis);

● 然后通过继承一个子类的方式,在子类里面修改出需要的效果;

● 把这个子类替换为Chart类实际使用的坐标轴实现类

通过上述4个步骤可以完成高度定制化的图表需求。

细节定制修改

注意到我们前面的代码是很简单的:

protected void drawLinear(Canvas c, ILineDataSet dataSet) {
   Shader shader = new LinearGradient(0, 0, 200, 200 , Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
   mRenderPaint.setShader(shader);
   super.drawLinear(c, dataSet);
}

所有的修改都会这样简单吗?并不会的,就是这个渐变线折线图的例子,实际上mRenderPaint的使用是需要满足一定条件的,所以现在的做法可能会造成性能问题。如果确实有一大段代码的复杂逻辑当中的一两行需要修改,还可以做到不修改SDK吗?

CustomLineChartRenderer.drawLinear确实是一大段代码,选择设置颜色相关逻辑的流程图如下:

PNG

题目要求的彩色折线效果,需要把黄色流程框里面的“设置画笔颜色”替换成“设置画笔为彩色渐变”。对于这种非常定制化的修改,我以前认为,只能通过SDK对外暴露接口来满足需求,否则就只能修改SDK代码了。然而设置画笔颜色的用接口暴露了出去,绘制折线图要不要暴露?判断坐标是否位于范围以内要不要暴露?是不是每一步逻辑都需要作为接口暴露出去?

任何一个SDK都不可能通过接口穷尽所有的定制化需求,这个思路其实和设置属性是没有区别的,在维护上、使用上都存在无法解决的成本问题,只可能选择典型重要的属性和接口暴露。

但是在MPAndroidChart当中,数据往往定义为protected类型,那么就提供了一个新的思路是:子类把整个一段函数复制过来修改定制。

也有private的数据影响我们复制代码,比如LineChartRenderer.drawLinear就使用了一个private类型的mLineBuffer,导致我们不能复制代码。仔细观察一下mLineBuffer的使用范围,会发现它只在drawLinear当中用到,那么它其实不影响,只要把这个数据结构一起复制过来就可以了。

作者的编程习惯是非常好的,这样写出来的代码本身就具备很高的重用性,确保我们可以在复杂情况下,可以通过复制父类代码并且修改的方式,完成自己需要的定制工作。

做到这一步,应该没什么定制化需求还是要修改SDK的了。

总结

MPAndroidChart的定制开发可以通过如下方式实现:

● 首先使用开放出来的属性和接口设置;

● 其次找到需要定制化的部分对应类,继承子类,通过重写函数在子类当中实现定制功能,然后设置回到Chart;

● 如果定制化内容复杂,那么可以把基类里面的函数复制到重写函数里面修改逻辑实现。

其中最后一条对我来说挺新鲜的,以前也没见到有什么书上或者其他类库用这种做法。

疑问

图表toB的业务,往往对定制话没有特殊要求,一般只要通过属性和设置就可以满足需求,但是toC的业务,例如健身App、个人账目等,会有各种各样的奇特图表需求从产品、设计师的脑洞里喷涌而出,这样的需求,MPAndroidChart都可以满足吗?

MPAndroidChart可以满足“未知”的图表需求吗?

我自己的经历是不用修改SDK就可以满足需求的,但是样本数量太少了。所以我去stackoverflow的MPAndoridChart提问区回答上面的问题,而且尽量不去修改SDK。刷了一百多分以后,确实这些问题都不需要修改SDK就可以解决,在这个观察样本量基础上说MPAndroidChart是可以满足未知的图表需求,是很可靠了。

MPAndroidChart是怎么“未卜先知”的?

我以前参加过SDK的开发,当时一个困惑就是,怎么满足“未知”的需求?会不会业务方一个需求提上来,发现还得改SDK才能满足就不好看了。

常用做法是:

首先,能力上划分需求边界,是我们SDK的能力范围以内才支持,能力范围以外本来就不应该支持,一个分享SDK,常见分享能力和扩展是要支持的,但是不能让它支持登录。

其次,能力以外的功能确实变化很多,比如分享面板是界面上的内容,设计师甚至可以要求做一个彩虹样式的面板,作为SDK怎么支持?这些东西就使用依赖倒置,交给业务方自己实现。

这个做法不算错,但是它实际上没有帮助业务方处理“未知”的需求,而是不接收“未知”的需求。

MPAndroidChart的做法是:

从前面“定制”部分可以知道,图表的各个功能做出了功能正交的划分,各个功能真正做到高内聚、低耦合。如果有未知的需求不能用SDK提供的设置方式满足,那么就可以通过替换功能组件的方式实现自己的需求即可。比如想改变图例的方式,可以用一个继承的类替换掉mLengendRenderer。代码习惯很好,比如很多数据结构定义为protected,确保子类可以访问,方便继承以后修改对应的方法和逻辑,给了业务方很大的灵活性。

图表也有这个基础,它反正就是在二维平面上绘制,划分成几个区域来解决也是很方便的,需要考虑的主要逻辑问题就是绘制顺序,不过在我回答的问题里面也没有出现绘制顺序导致的问题。

但是无论如何,对图表的深入理解和正确划分都是一切的基础。

为什么强调不修改SDK?

如果你的应用肯定只有一个图表,那么修改SDK是没问题的。

如果有第二个或者更多图表,就不要修改SDK了,一方面通过继承-组合方式是很容易避免修改SDK的,另外一方面,对SDK的修改可能影响到其他图表的绘制和开发。

MPAndroidChart和Charts是怎么对齐Api的?

已经询问两位作者,还没回复,有了回复再更新。

我怀疑是Charts直接复刻MPAndroidCharts的,这要Charts的开发人员对iOS和Android都很熟悉,而且很认可MPAndroidCharts的设计、实现理念才行。

脑洞

一个问题是使用者不知道自己的需求怎么才能在MPChart上满足,怎么设置,怎么修改,也许ChatGpt可能帮帮忙。

这样的问题不仅仅是MPAndroidChart存在,其实也普遍存在于任何SDK或者框架当中,怎么在设计框架的时候尽量简化,让使用者更方便的自己知道如何使用,怎么让使用者不需要关心内部实现细节,也是挺困难的,好像没有哪个SDK或者框架能做到。

他们提供的demo,如果能点一下就提示出来这是什么类什么方法实现的就好了,定位代码方便很多,现在需要自己断点调试还是不太方便。

MPAndroidChart里面大量定义protected级别的数据我还是第一次遇到,这种做法很方便通过继承定制化特别的函数,但是在其他的类库或者一些设计模式的书里面,也没有提到这样的用法,不知道是作者自己发明的,还是哪里参考的,或者这本来就是一种特别普通的做法不值得总结出来?

Android的FrameWork、古代的MFC/VC++、VCL/Delphi都是没有这种做法的,Swift、Qt应该也是没有的,对于子类修改父类逻辑没有特意提供支持。它们都是对界面库的封装,显然也不允许修改SDK,它们面对的问题比MPAndroidChart来说应该是有过之无不及,它们的设计都经受住了考验证明是成功有效的。所以,会不会还有一种可能是这种通过定义protected方便继承定制化函数内部逻辑的做法,其实只是无奈之举,并不算最好的做法?也就是我们有可能设计出来一个更好的图表库,它既可以不修改SDK就满足未知需求,也不用提供protect数据的方式要求使用者自己修改逻辑?

上面一众Ui库的架构师比如Anders Hejlsberg肯定比MPAndroidChart作者Philipp Jahoda更加高明,MPAndroidChart作者Philipp Jahoda又比我高明,那么我来思考这个问题明显在僭越,不思考了,上帝已经笑坏肚子。