Android 图表开发开源库 MPAndroidChart 使用总结

文摘   科技   2024-05-30 14:51   山东  

2024年 第11篇


MPAndroidChart 集成使用过程详解


  • 性能

  • 集成

  • 使用总结

  • 踩坑及解决


01 引言



电视项目中需要一个折线图表示节电数据变化情况,类比 H5 来说,Android 中也应该有比较成熟的控件,经过调研后,发现 MPAndroidChart 功能比较强大,网上也有人说可能是目前 Android 开发最好用的一个三方库了,功能非常强大,集成简单。

这里把我的集成使用过程及使用中发现的强大惊喜记录下来,留作团队财富,可供后续参考。

首先,简单的介绍下强大的 MPAndroidChart,它支持常用的各种图:柱状图(横向,竖向)、线状图(多种效果)、饼状图、点状图,属性也很简单,我们使用的时候只需要熟悉控件的 11 各种属性即可。核心功能如下:

  • 支持 x,y 轴缩放

  • 支持拖拽

  • 支持手指滑动

  • 支持高亮显示

  • 支持保存图表到文件中

  • 支持从文件(txt)中读取数据

  • 预先定义颜色模板

  • 自动生成标注

  • 支持自定义 x,y 轴的显示标签

  • 支持 x,y 轴动画

  • 支持 x,y 轴设置最大值和附加信息

  • 支持自定义字体,颜色,背景,手势,虚线等


02 集成



集成很简单,直接导入作为依赖就可以,以下写了一个最小集成的例子,主要为了说明步骤:

「 引入依赖到工程中 」

// 项目工程的 build.gradle
repositories {
maven { url "https://jitpack.io" }
}

// Module的 build.gradle
dependencies {
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
}

「 Layout 中添加控件 」

<!--节电折线图-->
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/chart1"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="@dimen/tvcommon_px131"
android:layout_marginBottom="@dimen/tvcommon_px60"
android:layout_marginStart="@dimen/tvcommon_px44"
android:layout_marginEnd="@dimen/tvcommon_px30" />

「 Activity 中使用 」

// 3.4 节电详情页
mChart = findViewById(R.id.chart1)
initChat()
setChatData()

初始化

private fun initChat() {
mChart.setBackgroundColor(resources.getColor(R.color.color_00000000))
mChart.description.isEnabled = false

// 2. X 轴样式
val xAxis: XAxis = mChart.getXAxis()
xAxis.setDrawGridLines(false)
// xAxis.enableGridDashedLine(10f, 10f, 0f);
xAxis.position = XAxis.XAxisPosition.BOTTOM

// 3. Y轴样式
mChart.axisRight.isEnabled = false// disable dual axis (only use LEFT axis)
var yAxis: YAxis = mChart.axisLeft
yAxis.axisMaximum = 200f
yAxis.axisMinimum = 0f
}
Activity 中设置数据
public void setChatData(){
List<Entry> entries=new ArrayList<>();
List<Entry> entries1=new ArrayList<>();
entries.add(new Entry(3f,20));
entries1.add(new Entry(5f,30));

LineDataSet dataSet=new LineDataSet(entries,"数据一");
LineDataSet dataSet1=new LineDataSet(entries1,"数据二");
List<ILineDataSet> list=new ArrayList<>();
list.add(dataSet);
list.add(dataSet1);
LineData lineData=new LineData(list);

mChat.setData(lineData); //将模拟数据用于线形图,在线形图显示
}
大功告成!

03 使用总结



MPAndroidChart 的强大之处在于它的很多功能都可定制,只要你有想法,大部分都有解决方法,哪怕一时没有,只要肯找,说不准就能发现。

电视项目中要求的是一个折线图,所以这里的使用总结大多集中在折线及我们要实现的效果上,其它未涉及的图和属性暂时不写,后续使用的时候再作探索及总结。

「 整体功能及专业术语一览(网上找的一张图,镇楼,哈哈)」

「 基础设置 (非数据类型,在初始化时设置) 」

首先,在 initChat()函数中,进行了一些基础设置,把一些用不到的功能关闭:

private fun initChat() {
mChart.setBackgroundColor(resources.getColor(R.color.color_00000000))
mChart.description.isEnabled = false
mChart.setTouchEnabled(false)
mChart.setDrawGridBackground(false)
mChart.isDragEnabled = false
mChart.setScaleEnabled(false)
mChart.setPinchZoom(false)
mChart.legend.isEnabled = false
}


「 x、y 轴设置 (非数据类型,在初始化时设置) 」

这个也不属于数据设置,所以在初始化时进行,如下,分别进行了线宽,线颜色,Label 的字体大小、颜色、还有网格线:

private fun initChat() {
...
// 2. X 轴样式
val xAxis: XAxis = mChart.getXAxis()
xAxis.setDrawGridLines(false)
// xAxis.enableGridDashedLine(10f, 10f, 0f);
xAxis.position = XAxis.XAxisPosition.BOTTOM
xAxis.axisLineWidth = resources.getDimension(R.dimen.tvcommon_px1)
xAxis.axisLineColor = resources.getColor(R.color.color_979797)
xAxis.textSize = resources.getDimension(R.dimen.tvcommon_sp16)
xAxis.textColor = resources.getColor(R.color.white_60alpha)
xAxis.valueFormatter = object : ValueFormatter() {
override fun getAxisLabel(value: Float, axis: AxisBase?): String {
// 自定义 X 轴显示内容
returnsuper.getAxisLabel(value, axis) + "月"
}
}

// 3. Y轴样式
mChart.axisRight.isEnabled = false// disable dual axis (only use LEFT axis)
var yAxis: YAxis = mChart.axisLeft
yAxis.axisLineWidth = resources.getDimension(R.dimen.tvcommon_px1)
yAxis.axisLineColor = resources.getColor(R.color.color_979797)
yAxis.textSize = resources.getDimension(R.dimen.tvcommon_sp16)
yAxis.textColor = resources.getColor(R.color.white_60alpha)
yAxis.setDrawGridLines(true) // horizontal grid lines
yAxis.enableGridDashedLine(resources.getDimension(R.dimen.tvcommon_px6),
resources.getDimension(R.dimen.tvcommon_px6),
0f)
yAxis.axisMaximum = 200f
yAxis.axisMinimum = 0f
}

「 折线类型设置 」

需要在 填充数据时,给 LineDataSet 设置一个 mode,如下


private fun setChatData() {
...
val set1 = LineDataSet(values, "DataSet 1")
set1.mode = LineDataSet.Mode.HORIZONTAL_BEZIER // 折线类型
}

setMode(LineDataSet.Mode mode),设置模式有四种:

  1. CUBIC_BEZIER   立方曲线  

  2. LINEAR   直线

  3. STEPPED 阶梯

  4. HORIZONTAL_BEZIER  水平曲线

电视项目这里需要的是一个曲线,所以设置为 HORIZONTAL_BEZIER

「 顶点设置 」

顶点可进行 icon 绘制、小圆点(实心/空心)、自定义值显示

private fun setChatData() {
...
val set1 = LineDataSet(values, "DataSet 1")
set1.mode = LineDataSet.Mode.HORIZONTAL_BEZIER // 折线类型
// 顶点
set1.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return""// 这里返回空,即顶点不显示值,否则不太好看
}
})
set1.valueTextSize = resources.getDimension(R.dimen.tvcommon_sp12)
set1.setDrawIcons(false) // 不显示端点的icon
set1.setDrawCircles(false) // 显示顶点圆quan
set1.setDrawCircleHole(true) // 空心还是实心
}

「 折线及填充设置 」

private fun setChatData() {
...
// 折线
set1.color = resources.getColor(R.color.blue_gray_32B5E6)
set1.setCircleColor(resources.getColor(R.color.blue_gray_32B5E6))
set1.lineWidth = resources.getDimension(R.dimen.tvcommon_px2)
set1.circleRadius = resources.getDimension(R.dimen.tvcommon_px4)

// 折线包裹起来的区域
set1.fillFormatter = IFillFormatter { dataSet, dataProvider -> mChart.getAxisLeft().getAxisMinimum() }
if (Utils.getSDKInt() >= 18) {
// drawables only supported on api level 18 and above
val drawable = ContextCompat.getDrawable(this, R.drawable.shape_chat_fill_blue)
set1.fillDrawable = drawable
} else {
set1.fillColor = R.color.blue_gray_32B5E6
}
set1.setDrawFilled(true)
}

「 X 轴自定义显示 (非数据类型,在初始化时设置) 」

再回到 X 轴,由于项目要求,X 轴要显示节电的日期,但是构造数据时只是一个数组,默认显示的是数组的下标,所以这里需要自定义。

private fun initChat() {
...
// 2. X 轴样式
val xAxis: XAxis = mChart.getXAxis()
...
xAxis.valueFormatter = object : ValueFormatter() {
override fun getAxisLabel(value: Float, axis: AxisBase?): String {
// 自定义 X 轴显示内容,这里可以通过 axis 中的 postion 从数据中进行一个映射,找到它对应的日期
returnsuper.getAxisLabel(value, axis) + "月"
}
}
}



04 踩坑及解决



「 宽度或 margin 跟自己设置的不一样 」

1. 效果出来后,发现最右侧不是我设置的,比我预计的靠左了,离边比较远,达不到 UI 设计的效果,如下:

2. 这里的设置应该不是通过简单的 XML 位置属性就能修改的了,因为这属于 MPAndroidChat 控件的内部了,所以得深入 MPAndroidChat,找到产生间隙的原因,于是开始翻源码

3. 经过翻阅源码得知,它内部有一个 minOffset

@Override
public void calculateOffsets() {
...
float minOffset = Utils.convertDpToPixel(mMinOffset);
mViewPortHandler.restrainViewPort(
Math.max(minOffset, offsetLeft),
Math.max(minOffset, offsetTop),
Math.max(minOffset, offsetRight),
Math.max(minOffset, offsetBottom));
...
}
public void setMinOffset(float minOffset) {
mMinOffset = minOffset;
}

再往下查,原来 mMinOffset,是可能通过 setMinOffset()设置进去的,所以在我们的代码 initChat()中添加上一行,再看效果,OK 啦~

private fun initChat() {
...
mChart.minOffset = 0f
...
}

「 设置 x 轴的 Label 后,发现最底下一层有一点显示不全,被截断了,如下 」


1. 分析原因

    1. 这个应该也是 MPAndroidChat 内部实现导致的,修改外部 Layout 中的 margin, padding 应该不生效,果然,试过之后,没有生效,问题依旧

    2. 16sp 时显示是这个效果,而缩小字号后,比如 12sp, 就没有问题,能够显示,猜测,是它内部写死了一个距离,但看源码内的相关说明,它的高度是自动计算的,如下


/**
* Class representing the x-axis labels settings. Only use the setter methods to
* modify it. Do not access public variables directly. Be aware that not all
* features the XLabels class provides are suitable for the RadarChart.
*
* @author Philipp Jahoda
*/

publicclass XAxis extends AxisBase {

/**
* width of the x-axis labels in pixels - this is automatically
* calculated by the computeSize() methods in the renderers
*/

publicint mLabelWidth = 1;

/**
* height of the x-axis labels in pixels - this is automatically
* calculated by the computeSize() methods in the renderers
*/

publicint mLabelHeight = 1;

    c. 按理说不应该出现这种情况,难道是 MPAndroidChat 的 Bug,是不是高版本就好了呢,于是上网查了一下,https://gitee.com/jiangsongbai/MPAndroidChart, Github 打不开,到这里的一个 fork 上看一下,发现我用的已经是最新版了 v3.1.0 (吐槽:版本号竟然带个 v,看来不专业啊~)

d. 小插曲:由于项目中使用的是 dimen 中定义的 tvcommon_sp16,在不同分辨率下,可能不一样,我原先是写死的,抱着一点小希望,在代码中使用 dimen 试一下,期望字体能够变小一点,不触发此问题,结果又失望了

xAxis.textSize = resources.getDimension(R.dimen.tvcommon_sp16)

2. extraBottomOffset

   a. 再翻阅源码,还是在 BarLineChatBase.java 中的 calculateOffsets() 函数,发现了端倪,如下:

@Override
public void calculateOffsets() {
....
offsetTop += getExtraTopOffset();
offsetRight += getExtraRightOffset();
offsetBottom += getExtraBottomOffset();
offsetLeft += getExtraLeftOffset();

float minOffset = Utils.convertDpToPixel(mMinOffset);

mViewPortHandler.restrainViewPort(
Math.max(minOffset, offsetLeft),
Math.max(minOffset, offsetTop),
Math.max(minOffset, offsetRight),
Math.max(minOffset, offsetBottom));
....
}

b. 这里有一个 getExtraBottomOffset(),再往下看,它是在基类 chat.java 中,有一个 mExtraBottomOffset,

/**
* @return the extra offset to be appended to the viewport's bottom
*/

public float getExtraBottomOffset() {
return mExtraBottomOffset;
}

c. 看注释,像是有点用,尝试一下

private fun initChat() {
....
mChart.minOffset = 0f
mChart.extraBottomOffset = resources.getDimension(R.dimen.tvcommon_px2) // 因为X轴的Label字体设为 16sp 后,最底下有一点显示不全,需要在这里打上一个小补丁。
....
}

d. 大功告成~

「 顶点的数据显示想要隔位置显示效果 」

  1. 跟产品讨论的时候,把 UI 原先设计的选中点的值显示出来的效果去掉了(因为电视上没有触摸,通过遥控器交互暂时先不做选中效果),如果显示出来所有的数据,会显得比较凌乱,所以想进行隔几个显示或什么效果,研究后,发现重截函数中没有位置信息,无法进行计算是哪一个 postion,暂时无法做到不同的效果,只能统一显示或不显示,后续再进行深入研究,看是否能够做到

private fun setChatData() {
...
val set1 = LineDataSet(values, "DataSet 1")
set1.mode = LineDataSet.Mode.HORIZONTAL_BEZIER // 折线类型
// 顶点
set1.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return""// 这里返回空,即顶点不显示值,否则不太好看
}
})
...
}

05 小结



这里记录了在电视项目中使用 MPAndroidChat 的一些使用心得,重点集中在折线上,通过对它的各种属性进行修改自定义了自己的 UI 界面,达到与 UI 设计图一样的效果,也发掘出了 minOffset、extraBottomOffset 这样的小众属性的使用场景及效果,希望能够引起大家的一些共鸣,发现问题时,快速找到解决方法。


06 团队介绍


「三翼鸟数字化技术平台-场景设计交互平台」主要负责设计工具的研发,包括营销设计工具、家电VR设计和展示、水电暖通前置设计能力,研发并沉淀素材库,构建家居家装素材库,集成户型库、全品类产品库、设计方案库、生产工艺模型,打造基于户型和风格的AI设计能力,快速生成算量和报价;同时研发了门店设计师中心和项目中心,包括设计师管理能力和项目经理管理能力。实现了场景全生命周期管理,同时为水,空气,厨房等产业提供商机管理工具,从而实现了以场景贯穿的B端C端全流程系统。


    _________________ END__________________

三翼鸟数字化科技
三翼鸟数字化技术团队官方订阅号,提供技术前沿洞察、技术实践分享、最佳实践整合、技术规范发布、团队文化输出。