也是最近1个月在项目中不断研究CorePlot实现柱状图的效果。
先来看一下Air Quality的柱状图效果:
经过1个月的研究,现在基本上已经重现了这个柱状图的功能(99%),而且还加上了刷新数据的功能。
计划通过两篇blog来记录下开发中的难点,在后一篇blog中会把所有的源码挂上去。
难点有以下几个:
1. Coreplot自带的滚动机制在我这边做起来有一些卡顿,用户体验很不好,这里需要替换掉它自带的滚动功能
2. 不用coreplot的滚动以后,需要新建一个view用于承载左侧固定的坐标轴
3. 滚动是高亮的柱子的选择以及设置高亮标签
4. 顶端时间的显示
5. 数据刷新功能
如果不清楚如何用swift添加coreplot,参见我之前的这篇blog:http://blog.csdn.net/u011156012/article/details/44061411
本篇blog首先讲解下前两个问题。进入正题:
先上一下storyboard的构建:
二级页模块bg是一个背景的ImageView;Label就是中间上部的标题label;line1px是美工切出来的1个px的分割线,放在label下面;然后ScrollView用来承载我们的Coreplot,可以看到里面还是放了一个我们的Graph hosting view;最后的view是用来承载左侧的坐标系。
接下来看如何在scrollview中搭建hosting view
/** 关于graphView的长度计算,每个项目占10个px的长度是合适的,也就是说 150个柱子,graph的width,也就是scroll_view的contentsize的width 是150*10= 1500 比较合适;而plot space的xMax的值 设置为 柱子个数+10 */ func initPlotGraphView(){ // 禁止缩放 graphView.allowPinchScaling = false // Create graph // 设置graph的宽,num*10 如果 width 最小值为frame.width var graph_width = CGFloat(num * 10) if graph_width < self.frame.size.width { // 多+1的目的是为了让scrollview能够滚动 graph_width = self.frame.size.width + 51 self.scrollType = 1 } var graph = CPTXYGraph(frame: CGRectMake(0, 0, graph_width, graphView.frame.size.height)) println("There are \(num) bars, graph's width : \(graph.frame.size.width),height is : \(graph.frame.size.height)") // Set ScrollView self.scroll_view.contentSize = CGSizeMake(graph_width - 50, graph.frame.size.height) OriginalContentOffSet_x = self.scroll_view.contentSize.width - self.frame.size.width self.scroll_view.contentOffset = CGPointMake(OriginalContentOffSet_x, 0) // 设置起始的contentoffset self.lastContentOffset = self.scroll_view.contentOffset.x println("当前的contentOffset是:\(OriginalContentOffSet_x)") self.scroll_view.delegate = self graphView.frame = graph.bounds graph.plotAreaFrame.masksToBorder = false graphView.hostedGraph = graph // Configure the graph graph.backgroundColor = UIColor.clearColor().CGColor // Graph 在hostedGraph中的偏移 graph.paddingBottom = 20.0 graph.paddingLeft = 50.0 graph.paddingRight = 5.0 graph.paddingTop = 15.0 graph.plotAreaFrame.borderLineStyle = nil graph.plotAreaFrame.cornerRadius = 0.0
这里我用一个函数initPlotGraphView来把所有的代码写进去,因为这部分代码确实有点长。
我基本上加上了足够多的注释。有一些特别重要的部分单独拿出来强调以下。
正常情况下,我们的graphview的长度肯定会大于scrollview的长度的,因为数据足够多的话,这个长度一定是够的;当然一定会出现特殊的情况,比如我们不断向左边划屏,刷新历史的数据,总会遇到历史数据刷新过来只有几条或者十几条的情况,这样整个scrollview的contentsize的width就会太小,而导致scrollview不能滑动了,所以这里面如果graphview的width小于屏幕的宽度,就要把它+1,这样确保scrollview中的数目不多的情况下依旧可以滑屏。
这里补充以下前提:因为coreplot的数据是画上去的,在我们不适用它自带的滑屏效果后,我们需要把所有的柱子都画到scrollview里面,这样会占用不小的内存,实际测试中,画上600个柱子大概要消耗20M左右的内存,所以我们要采用滚动刷新的机制,确保每个scrollview中的柱子数目不能太多。项目中我使用的阈值是120个。
接下来设置bar plot
// set up bar plot theBarPlot = CPTBarPlot.tubularBarPlotWithColor(CPTColor.clearColor(), horizontalBars: false) // 去除每个柱子周围的黑线 var linestyle = CPTMutableLineStyle() linestyle.lineWidth = 0.1 linestyle.lineColor = CPTColor.lightGrayColor() theBarPlot.lineStyle = linestyle // setup line style var barLineStyle = CPTMutableLineStyle() barLineStyle.lineColor = CPTColor.whiteColor() barLineStyle.lineWidth = 1 // set up text style var textLineStyle = CPTMutableTextStyle() textLineStyle.color = CPTColor.whiteColor() // set up plot space var xMin = Float(0) var xMax = Float((num > 40 ? num : 40)+10) var yMin = Float(0) var yMax = self.model.maxValue.floatValue var plotSpace = graph.defaultPlotSpace as! CPTXYPlotSpace // 允许滚动 plotSpace.allowsUserInteraction = false // 设置滚动时的动画效果,这里采用默认的就好 // plotSpace.momentumAnimationCurve = CPTAnimationCurveExponentialIn // plotSpace.bounceAnimationCurve = CPTAnimationCurveExponentialIn // 设置x,y在视图显示中大小,也就是点的个数,通过这样设置可以达到放大缩小的效果,来达到我们想要的合理视图显示 // 这里因为我们外层添加了scrollview,来取代它自身的比较卡的滚动,所以,是1:1的关系 // 如果想用它自己的滚动,这里的x的length应该是xMax的1/4或者1/8这样子的,因为这里的长度是一屏之内显示的数量 plotSpace.xRange = CPTPlotRange(location: CPTDecimalFromFloat(xMin), length: CPTDecimalFromFloat(xMax)) plotSpace.yRange = CPTPlotRange(location: CPTDecimalFromFloat(yMin), length: CPTDecimalFromFloat(yMax)) //设置x、y轴的滚动范围,如果不设置,默认是无线长的 plotSpace.globalXRange = CPTPlotRange(location: CPTDecimalFromFloat(xMin), length: CPTDecimalFromFloat(xMax)) plotSpace.globalYRange = CPTPlotRange(location: CPTDecimalFromFloat(yMin), length: CPTDecimalFromFloat(yMax))
这里其实把每个柱子的边线宽度设置成了0.1,因为开始设置成0了以后,或者把theBarPlot.linestyle =nil ,会有一种情况,就是我们传回来的数据是0.x,就是大于0但是小于1时,柱子会不显示出来,所以这把边界设置成0.1可以保证当传进来的数字大于0小于1时也能够显示出来一个很小的线。
这里关于plotSpace的x和y range我弄成了1:1的关系,就是把所有的内容都显示出来,在注释里面也些了,如果要使plotspace滚动的话,xRange的长度应该是1/4或者1/8,意思就是分几个屏幕来显示所有的数据。
接下来绘制x轴和y轴
// add plot to graph theBarPlot.dataSource = self theBarPlot.delegate = self // 设定基值,大于该值的从此点向上画,小于该值的反向绘制,即向下画 theBarPlot.baseValue = CPTDecimalFromInt(0) // 设定柱状图的宽度(0.0~1.0)这里柱子的宽度还是上面的plotSpace的xRange和GlobalXRange有关,这里是个百分比,是在那两个值决定之后的柱子宽度为基准的一个百分比 theBarPlot.barWidth = CPTDecimalFromDouble(0.9) // 柱状图每个柱子开始绘制的偏移位置,我们让它绘制在刻度线中间,所以不偏移 theBarPlot.barOffset = CPTDecimalFromDouble(0.0) // set Axis and styles var axisSet = graph.axisSet as! CPTXYAxisSet var xLineStyle = CPTMutableLineStyle() xLineStyle.lineColor = CPTColor.whiteColor() xLineStyle.lineWidth = 1.0 var minorLineStyle = CPTMutableLineStyle() minorLineStyle.lineColor = CPTColor.blueColor() minorLineStyle.lineWidth = 0.5 var labelStyle = CPTMutableTextStyle() labelStyle.fontName = FONT_HEITI labelStyle.fontSize = 10 labelStyle.color = CPTColor.whiteColor() // xAxis var xAxis = axisSet.xAxis xAxis.axisLineStyle = nil // 加上这句才能显示label,如果去掉这两句,会显示1.0,2.0 而不是用户自定义的值。。。 // CPTAxisLabelingPolicyNone就是不使用系统自定义的label而用户来自定义位置和内容 xAxis.labelingPolicy = CPTAxisLabelingPolicyNone // 让x轴设置顶端的offset xAxis.axisConstraints = CPTConstraints.constraintWithUpperOffset(-5.0) // x轴大刻度线,线形设置 xAxis.majorTickLineStyle = nil // 刻度线的长度 xAxis.majorTickLength = 10 // 间隔单位,和xMin-xMax对应 xAxis.majorIntervalLength = CPTDecimalFromDouble(10) // 小刻度线 xAxis.minorTickLineStyle = nil // 小刻度线间隔距离 xAxis.minorTicksPerInterval = 1 // 设置y轴在x轴上的重合点,貌似没啥作用,起作用的是axisConstraints // xAxis.orthogonalCoordinateDecimal = CPTDecimalFromInt(0)
关于自定义的x轴label的显示我们放到下一个blog去介绍,这里面先暂时不多介绍。
// yAxis 这里其实是一系列让Y轴消失的动作 var yAxis = axisSet.yAxis yAxis.axisLineStyle = nil yAxis.majorTickLineStyle = nil yAxis.majorTickLength = 0 yAxis.majorIntervalLength = CPTDecimalFromInt(500) yAxis.minorTickLineStyle = nil yAxis.minorTicksPerInterval = 0 yAxis.labelTextStyle = nil yAxis.orthogonalCoordinateDecimal = CPTDecimalFromInt(0) // 固定Y轴坐标轴,就是在X轴横移的时候,y坐标轴不动 yAxis.axisConstraints = CPTConstraints(lowerOffset: CGFloat(1.0)) // 将bar plot添加到默认的空间中 graph.addPlot(theBarPlot, toPlotSpace: graph.defaultPlotSpace) // 选中最新的数据 barPlot(theBarPlot, barWasSelectedAtRecordIndex: UInt(num), withEvent: nil) lastHightLighBarOffset = scroll_view.contentOffset.x
}
这样整个initPlotGraphView就完成了,当然函数的最后选中特定的bar以及高亮的offset的用法我们都在下一个blog中介绍。
接下来显示左侧的纵轴的刻度是个很简单的函数,因为我们在之前把Y轴设定了消失的动作,所以这里面的Y轴其实就是我们手动设置上去的几个label
// 显示各个label的刻度值 func setyAxisValues(){ let max = self.model.maxValue.integerValue label_1.text = "\(max)" label_2.text = "\(max*3/4)" label_3.text = "\(max/2)" label_4.text = "\(max/4)" label_5.text = "0" if max == 1 { label_2.hidden = true label_3.hidden = true label_4.hidden = true label_5.hidden = false }else{ label_2.hidden = false label_3.hidden = false label_4.hidden = false label_5.hidden = false } }
这里面把传进来的最大值做相关的计算并且显示在label上。
基本上前两个难点就解决了。基本上我们已经搭建好了大的框架,下来的工作就是具体解决一些细节的东西,这里我们放到下一篇blog中详细讲解。