我与那该死的设计师的曲线之战(iOS-Charts)

前言

先直接上图看设计师要求的图片吧

首页

看到这个首页效果的时候觉得微微有点蛋疼,但觉得应该是可以的,没多想
后来才知道是我太年轻了。。

找了各种第三方,留给自己时间不多,基本没时间自己做一个出来,所以只能借助第三方,最后决定用 Charts,毕竟是大神而且和安卓一起开发遇到问题也可以一起讨论,start 也破万。

正文

不说废话了,直接开始吧,首先由于项目决定使用的语言是 OC 语言,charts 是 swift 语言,所以只能混编,项目最低支持版本8.0

关于charts 怎么集成到 OC 代码里面,我看过简书已经写了很多了,我就不重复了,而且也没什么技术难点,无非就是拖进去就好。我没有使用 pods 毕竟需要修改源码。。想到就略蛋疼。。。

基础集成

把 charts 集成好之后就可以使用了,先编译一遍确认能够正常编译通过,通过后就可以开始使用了。先说下基本的设置吧。我是用的曲线,也就是折线的分类,charts 有很多个模块,其他模块我没有去研究,毕竟没那么多时间,如果你用storyboard 创建的,那就是新建一个 view,类的名字直接为LineChartView,
如果是代码创建,也是直接用这个类创建一个 view 然后设置 frame 添加到父类 view 上就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// chartview 本身的属性设置
self.chartView.delegate = self;// 代理
self.chartView.backgroundColor = [UIColor clearColor];//设置背景颜色
self.chartView.descriptionText = @"";//隐藏描述文字
self.chartView.noDataText = @"暂时没有体重数据";// 设置没有数据的显示内容
self.chartView.legend.enabled = NO;//不显示图例说明
self.chartView.scaleYEnabled = NO;//取消Y轴缩放
self.chartView.scaleXEnabled = NO;//取消X轴缩放
self.chartView.scaleEnabled = NO;// 缩放
self.chartView.doubleTapToZoomEnabled = NO;//取消双击缩放
self.chartView.dragDecelerationEnabled = NO;//拖拽后是否有惯性效果
self.chartView.dragDecelerationFrictionCoef = 0;//拖拽后惯性效果的摩擦系数(0~1),数值越小,惯性越不明显
self.chartView.rightAxis.enabled = NO;//不绘制右边轴的信息
self.chartView.leftAxis.enabled = NO;//不绘制左边轴的信息
self.chartView.xAxis.enabled = NO;
self.chartView.xAxis.drawGridLinesEnabled = NO;//不绘制网络线
self.chartView.xAxis.axisLineColor = [UIColor whiteColor];// x 轴的网络线颜色
self.chartView.xAxis.labelPosition = XAxisLabelPositionTopInside;// X轴的位置
self.chartView.xAxis.granularity = 1;// 间隔为1

基本上看得懂英文的都懂得这些的意思
看效果图,需要点击的时候出现一个 view,跟随点的位置,charts 本身已经实现了这个功能,名字叫BalloonMarker,但是集成的时候没有集成过来,需要自己写,或者直接在 demo 里面搜索这个类拷贝到自己的项目中去修改

1
2
3
4
5
6
7
8
9
// 显示气泡效果
BalloonMarker *marker = [[BalloonMarker alloc]
initWithColor: [UIColor colorWithRed:0.384 green:0.800 blue:0.980 alpha:1.000]
font: [UIFont systemFontOfSize:12.0]
textColor: UIColor.whiteColor
insets: UIEdgeInsetsMake(8.0, 8.0, 20.0, 8.0)];
marker.img = [UIImage imageNamed:@"marker"];
marker.chartView = self.chartView;
self.chartView.marker = marker;

基础设置都设置好了,接下来就可以填充数据了,charts 你大致是可以理解为这样的工作模式:

设置 chartview 的属性 -> 设置LineChartData数据 -> 最后设置各种数据显示属性

这是我第一个坑,有一些属性是需要等后面数据填充了才可以进行设置,否者会遇到各种奇怪的问题。
我们继续进行数据的填充吧,chartview 可以设置多条线,每一条线就是一个LineChartDataSet,每一条线上面的点就是ChartDataEntry,想在一条线上显示多少个点就需要多少个ChartDataEntry。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
NSMutableArray *arr = [NSMutableArray array];
for (int i = 0; i < self.weights.count; i++) {
ChartDataEntry *entry = [[ChartDataEntry alloc] initWithX:i y:ww];
entry.flag = model.weightflag;//自己增加的属性
entry.realValue = model.weight;//也是自己增加的属性
[arr addObject:entry];
}
LineChartDataSet *set = [[LineChartDataSet alloc] initWithValues:arr label:@""];
//对于线的各种设置
set.drawValuesEnabled = NO;//不显示文字
set.highlightEnabled = YES;//选中拐点,是否开启高亮效果(显示十字线)
set.highlightColor = [UIColor clearColor];// 十字线颜色
set.drawCirclesEnabled = YES;//是否绘制拐点
set.cubicIntensity = 0.2;// 曲线弧度
set.circleRadius = 5.0f;//拐点半径
set.drawCircleHoleEnabled = NO;//是否绘制中间的空心
set.circleHoleRadius = 4.0f;//空心的半径
set.circleHoleColor = [UIColor whiteColor];//空心的颜色
set.circleColors = @[[UIColor colorWithRed:0.114 green:0.812 blue:1.000 alpha:1.000]];
set.mode = LineChartModeCubicBezier;// 模式为曲线模式
set.drawFilledEnabled = YES;//是否填充颜色
// 设置渐变效果
[set setColor:[UIColor colorWithRed:0.114 green:0.812 blue:1.000 alpha:1.000]];//折线颜色
NSArray *gradientColors = @[(id)[ChartColorTemplates colorFromString:@"#FFFFFFFF"].CGColor,
(id)[ChartColorTemplates colorFromString:@"#C4F3FF"].CGColor];
CGGradientRef gradientRef = CGGradientCreateWithColors(nil, (CFArrayRef)gradientColors, nil);
set.fillAlpha = 1.0f;//透明度
set.fill = [ChartFill fillWithLinearGradient:gradientRef angle:90.0f];//赋值填充颜色对象
CGGradientRelease(gradientRef);//释放gradientRef
// 把线放到LineChartData里面,因为只有一条线,所以集合里面放一个就好了,多条线就需要不同的 set 啦
LineChartData *data = [[LineChartData alloc] initWithDataSets:@[set]];

把线都设置好了,放在 data 里面后,还需要把 data 放在 chartview 的 data 里面,这样才算是填充了数据

self.chartView.data = [self setData]; // 加载数据

之后就进行最后的设置了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//设置最大显示值
NSMutableArray *arr = [NSMutableArray array];
for (NSUInteger i = 0; i < self.weights.count; i++) {
FFWeightModel *model = self.weights[i];
[arr addObject:@(model.weight)];
}
double max = [[arr valueForKeyPath:@"@max.doubleValue"] doubleValue];
self.chartView.leftAxis.axisMaxValue = max;
//设置最小显示值
double min = [[arr valueForKeyPath:@"@min.doubleValue"] doubleValue];
self.chartView.leftAxis.axisMinValue = min;
// 设置区域显示,要显示7个数据,所以设置最大显示和最小显示
[_chartView setVisibleXRangeMaximum:6];// 最大显示
[_chartView setVisibleXRangeMinimum:6];// 最小显示

//设计师说要数据加载的时候前面留空白3个,拖到后面也是留空白3个,保证点都是在中间为准,所以又设置了留白数据
self.chartView.xAxis.axisMinimum = -3;//最前面留空白3个区域
self.chartView.xAxis.axisMaximum = self.weights.count+2.1;//后面留空3个区域,别问我为什么是2.1

//添加添加限制的线
ChartLimitLine *limitLine = [[ChartLimitLine alloc] initWithLimit:[self.userweight[@"weight_first"] doubleValue] label:[NSString stringWithFormat:@"初始%@kg",self.userweight[@"weight_first"]]];
limitLine.lineWidth = 1;
limitLine.lineDashLengths = @[@(5.0),@(5.0)];
limitLine.lineColor = [UIColor colorWithRed:1.000 green:0.671 blue:0.671 alpha:1.000];
limitLine.labelPosition = ChartLimitLabelPositionCenter;//位置
limitLine.valueTextColor = [UIColor colorWithWhite:0.502 alpha:1.000];//label文字颜色
limitLine.valueFont = [UIFont systemFontOfSize:12];//label字体

ChartLimitLine *limitLine2 = [[ChartLimitLine alloc] initWithLimit:[self.userweight[@"weight_target"] doubleValue] label:[NSString stringWithFormat:@"目标%@kg",self.userweight[@"weight_target"]]];
limitLine2.lineWidth = 1;
limitLine2.lineDashLengths = @[@(5.0),@(5.0)];
limitLine2.lineColor = [UIColor colorWithRed:0.682 green:0.925 blue:1.000 alpha:1.000];
limitLine2.labelPosition = ChartLimitLabelPositionCenter;//位置
limitLine2.valueTextColor = [UIColor colorWithWhite:0.502 alpha:1.000];//label文字颜色
limitLine2.valueFont = [UIFont systemFontOfSize:12];//label字体

//把限制线添加到 chartview 里面
[self.chartView.leftAxis addLimitLine:limitLine];
[self.chartView.leftAxis addLimitLine:limitLine2];

//最后每次打开都自动高亮最后一个数据,所以需要设置
FFWeightModel *model = self.weights.lastObject;
[self.chartView highlightValueWithX:self.weights.count-1 y:model.realWeight dataSetIndex:0 callDelegate:NO];
//由于每次打开都是显示在最前面的点,所以需要移动显示的位置
[_chartView moveViewToX:self.weights.count];// 移动到那个点
//最后显示出动画
[self.chartView animateWithYAxisDuration:1];//动画

其实以上都是简单的设置,只要花时间就能设置出来,但是难点永远不是这些,是那些坑爹的需要,是那些不知道技术实现起来很难以为只是很简单的,所以需求总是天天有,只能拼命改改改T_T

##说一些 charts 的一些坑吧

###集成BalloonMarker的坑
我是直接拿 demo 的过来用的,如果不熟悉 swift 的同学就要注意了,你把 swift 的文件集成过来后,需要编译一次,然后你是不是想说应该怎么声明这个BalloonMarker,因为你在 import 里面没有看到这个,混编的 swift 其实全部都在一个不可见的 h 文件里面,你只需要 #import “你项目的名字-swift.h”这个文件就可以了,所有混编的 swift 都会在这里声明,不过你需要先编译一次才会在里面可以找到
对于BalloonMarker修改 view 的背景,他是直接画出来的,但设计师的那个是切图出来,所以你需要在BalloonMarker里面找到open override func draw(context: CGContext, point: CGPoint)这个方法,然后删除或者注释掉那个很长的属性if let color = color{},新建了一个属性叫open var img: UIImage?然后直接在被你删掉或者注释掉的地方添加

1
2
3
if let img = img{
img.draw(at: CGPoint(x:rect.origin.x-2,y:rect.origin.y))
}

编译一次后你就可以直接在外部调用 img 这个属性并且赋值了

###数据源的坑
如果你需要显示中文,恭喜来这个坑,目前我没找到什么好的解决办法
比如想显示 x 轴的文字,就需要self.chartView.xAxis.valueFormatter = self;并且实现它的数据源代理就可以用他的方法了,- (NSString * _Nonnull)stringForValue:(double)value axis:(ChartAxisBase * _Nullable)axis;目前没有很好的利用这个方法,觉得不好用

###代理的坑
charts 主要用到有三个代理

1
2
3
4
5
6
7
@objc optional func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight)

// Called when nothing has been selected or an "un-select" has been made.
@objc optional func chartValueNothingSelected(_ chartView: ChartViewBase)

// Callbacks when the chart is moved / translated via drag gesture.
@objc optional func chartTranslated(_ chartView: ChartViewBase, dX: CGFloat, dY: CGFloat)

第一个是点击后响应的方法,第二个是点击同一个点响应的方法,第三个是拖动就会响应的方法

先说第一个吧,点击后根据你点击的 x 的位置定位到你点击的那个点,并且高亮显示,如果你有用 markerview 就会显示出来,包括高亮线,由于我用不到,所以就设置高亮线为透明

我这边的要求是,不管点击哪个点,都要自动移动到中间,所以需要取出你点击的点,然后移动到中间
[self.chartView moveViewToAnimatedWithXValue:entry.x-3 yValue:0 axis:AxisDependencyLeft duration:.3];

相对来说这个是很容易实现的,毕竟你点击后能够得到对于的ChartDataEntry的值,所以算出你点击的位置然后设置在中间都是很容易的,但是滑动就难了,滑动的时候需要保证每次滑动都是在中间,随便说下,滑动方法里面的 dx 和 dy 是在你每次手指滑动的时候重新开始计算的,拿 dx 来说,你滑动左边数值是从0开始加上去的,右边滑动是从0到负数的叠加,还好 chartview 有两个属性可以用来做判断,分别是
self.chartView.highestVisibleX
self.chartView.lowestVisibleX
NSInteger index = self.chartView.highestVisibleX-3;拿到 index
然后显示高亮状态[self.chartView highlightValueWithX:index y:model.realWeight dataSetIndex:0 callDelegate:NO];
但是这样还是不行,毕竟滑动不会自动居中,所以就导致显示的点可能会不在中间,所以自己改源码增加了一个滑动结束的代理

###高亮的坑
安卓那边高亮显示可以直接用
- (void)highlightValueWithX:(double)x dataSetIndex:(NSInteger)dataSetIndex;
但是 iOS 这边直接用这个方法是不行的,在 github 上也有人问过此类问题,最后只能用另一个属性方法
- (void)highlightValueWithX:(double)x y:(double)y dataSetIndex:(NSInteger)dataSetIndex;
但由于我在滑动代理里面增加了高亮的方法,高亮方法默认是打开代理的状态,这样会导致滑动出错,所以需要用另一个高亮显示的方法
- (void)highlightValueWithX:(double)x y:(double)y dataSetIndex:(NSInteger)dataSetIndex callDelegate:(BOOL)callDelegate;
设置代理为 NO 就可以了

#总结
其实到了最后跟设计图还是有很多不同的,放出实际图,其实多少辛酸泪不是简单两语能够说得清道得明,排版有点乱,大家请将就下哈~这是第一版,希望以后再努力的去改一改吧!

打开 APP 后显示的效果

拖动到中间的效果