用CorePlot实现类似Air Quality的柱状图滚动效果(2/2)

我们接着来看剩下的3个难点:

3. 滚动是高亮的柱子的选择以及设置高亮标签

4. 顶端时间的显示

5. 数据刷新功能

先来说高亮标签的设置吧。

这里需要设置关于barplot的几个代理方法:

    /**
    *  @author KaKa, 15-06-24 14:06:57
    *
    *  BarPlot Delegate
    *  用于选中不同的柱子后显示出来label
    */
    func barPlot(plot: CPTBarPlot!, barWasSelectedAtRecordIndex idx: UInt, withEvent event: UIEvent!) {
        // 如果已经有了annotation,就清零
        graphView.hostedGraph.plotAreaFrame.plotArea.removeAllAnnotations()

        // Setup a style for the annotation
        let hitAnnotationTextStyle      = CPTMutableTextStyle.textStyle() as! CPTMutableTextStyle
        hitAnnotationTextStyle.color    = CPTColor.blackColor()
        hitAnnotationTextStyle.fontSize = 10.0;
        hitAnnotationTextStyle.fontName = FONT_HEITI;

        // Determine point of symbol in plot coordinates
        // 这里y坐标设置成0,x就是idx
        let anchorPoint = [Int(idx),0]
        // Add annotation
        // First make a string for the y value
        var timeStr = self.model.timesArray.objectAtIndex(Int(idx-1)) as! NSString
        if timeStr != "" {
            // 只有当timeStr中不是空时才继续执行
            switch self.model.type.integerValue {
            case 0:
                let range = NSMakeRange(8, 2)
                timeStr = timeStr.substringWithRange(range) + "日"
                break
            case 1,2:
                timeStr = timeStr.substringFromIndex(11)
                break
            default:
                break
            }

            let string = "\(timeStr) , \(self.model.valuesArray.objectAtIndex(Int(idx-1)))\(self.model.unit)"
            // Now add the annotation to the plot area
            let textLayer = CPTTextLayer(text: string, style: hitAnnotationTextStyle)

            textLayer.fill = CPTFill(image: CPTImage(CGImage: UIImage(named:"Popover.png")?.CGImage, scale: 2.0))
            textLayer.paddingBottom = 5
            textLayer.paddingTop = 5
            /* DXPopover会在背景里面加上阴影,所以放弃使用了,不过研究清楚了点的坐标获取
            // 新建popview

            var popView = DXPopover()
            annotationLabel.text = string

            var pointers = [NSDecimal](count: 2, repeatedValue: CPTDecimalFromUnsignedInteger(0))
            let plotXvalue = self.numberForPlot(plot, field: UInt(CPTScatterPlotFieldX.value), recordIndex: idx)
            pointers[0] = CPTDecimalFromFloat(plotXvalue.floatValue)
            println("\(CPTDecimalFromUnsignedInteger(idx))")

            let plotspace = graphView.hostedGraph.defaultPlotSpace
            println("\(plotspace.numberOfCoordinates)")

            var popPoint  = plotspace.plotAreaViewPointForPlotPoint(&pointers, numberOfCoordinates: plotspace.numberOfCoordinates)
            popView.showAtPoint(popPoint, popoverPostion: DXPopoverPosition.Down, withContentView: self.annotationView, inView: graphView)
            */

            selectedBarAnnotation = CPTPlotSpaceAnnotation(plotSpace: graphView.hostedGraph.defaultPlotSpace, anchorPlotPoint: anchorPoint)
            selectedBarAnnotation!.contentLayer = textLayer
            selectedBarAnnotation!.displacement = CGPointMake(0.0, -15.0)
            graphView.hostedGraph.plotAreaFrame.plotArea.addAnnotation(selectedBarAnnotation)
        }
    }

我注释掉的部分内容是之前测试的使用DXPopover在iPhone上做出来pop view的效果,虽然成功了,但是因为DxPopover会在后面加上阴影,导致效果其实比较难看,于是就放弃了,不过通过那个研究掌握了如何把plot中的坐标和外层的view的坐标转换过来。

当然这个只是完成了第一步,我们可以找到指定的柱子,让后将其设置高亮,并且在柱子下方放上一个textLayer用来显示图片和文字内容;考虑到我们的整个plotview是承载在scrollview里面,所以让scorllview进行滚动时,我们要能判断出来去高亮显示哪个柱子。

所以就需要进一步设置scrollview的代理方法:

    /**
    *  @author KaKa, 15-06-29 16:06:51
    *
    *  Scroll_view的delegate
    */
    func scrollViewDidScroll(scrollView: UIScrollView) {
        // 判断屏幕向左还是向右滑动
        var scrollDirection : ScrollDirection?
        if self.lastContentOffset > scrollView.contentOffset.x {
            scrollDirection = ScrollDirection.ScrollDirectionLeft
        }else if self.lastContentOffset < scrollView.contentOffset.x{
            scrollDirection = ScrollDirection.ScrollDirectionRight
        }else{
            scrollDirection = ScrollDirection.ScrollDirectionNone
        }
        self.lastContentOffset = scrollView.contentOffset.x

        // 当屏幕滑动时选中不同的bar,每个柱子是10px,我们知道起始的contentoffset的originalX是多少,拿originalX-currentX/10 就可以得出需要显示的是第几个数据
        // 因为我们的柱子是从最后一个移动到第一个,总数是知道的: count,然后总共的移动的长度是知道的:originalContentOffset_X,那么每个柱子实际的 δcontentoffset  = originalContrentOffset_X/count
        // 那么,我们用(originalContentOffset_x - currentOffset_x)/δcontentoffset 就是需要移动的柱子个数

        var singleOffset: CGFloat? = (OriginalContentOffSet_x/CGFloat(num))
        if self.scrollType == 0 {
            reduceIndex = Int((OriginalContentOffSet_x - scrollView.contentOffset.x)/singleOffset!)
        }else{
            // 如果当前的条目很少,就不能采用上面的方法来移动了,要采用每次移动多少个格子的办法
            singleOffset = 10.0
            if abs(lastHightLighBarOffset! - scrollView.contentOffset.x) > singleOffset {
                var reduce = Int((lastHightLighBarOffset! - scrollView.contentOffset.x) / singleOffset!)
                if reduce > 0 && scrollDirection == ScrollDirection.ScrollDirectionLeft {
                    reduceIndex = (reduceIndex! + reduce > (num - 1)) ? (num - 1) : reduceIndex!+reduce
                }else if reduce < 0 && scrollDirection == ScrollDirection.ScrollDirectionRight {
                    reduceIndex = (reduceIndex! + reduce < 0 ) ? 0 : reduceIndex! + reduce
                }
                if scrollView.contentOffset.x > 0 && scrollView.contentOffset.x < OriginalContentOffSet_x{
                    lastHightLighBarOffset = scrollView.contentOffset.x
                }
            }
        }

        if (reduceIndex >= 0 && reduceIndex < num)  {
            barPlot(theBarPlot, barWasSelectedAtRecordIndex: UInt(num-reduceIndex!), withEvent: nil)
        }else if scrollView.contentOffset.x < 0 {
            barPlot(theBarPlot, barWasSelectedAtRecordIndex: UInt(1), withEvent: nil)
        }else if scrollView.contentOffset.x > scrollView.contentSize.width {
            barPlot(theBarPlot, barWasSelectedAtRecordIndex: UInt(num), withEvent: nil)
        }

其实这里面也考虑的两种不同的方式去移动,一种是当我们的当前屏幕的柱子比较多时,可以采用根据contentoffset变化多少的来直接设置高亮对应的柱子;第二种情况是当前的柱子数目不是很多,我们如果还用之前的方法,就会出现向左滑动一点,柱子直接从最后一个跳到了第一个的情况,所以我们要采用第2种方法来移动柱子。

接下来我们来看顶端时间的移动:

这里也是要分成两步:

先来把x轴设置成我们自定义的style

        // 设置x轴label,对不同类型的项目,x轴的label显示的内容不一样,所以要一个labeArray数组来存放处理之后的时间数据
        // timeArray中的格式为  yyyy.mm.dd hh:mi
        var labelArray = NSMutableArray()
        var newLabel = CPTAxisLabel()

        let timesDictionary : NSDictionary = self.setxAxisValues(self.model.timesArray, type: self.model.type.integerValue)
        for (k,v) in  Array(timesDictionary).sorted({($0.0 as! Int) < ($1.0 as! Int)}) {
            newLabel = CPTAxisLabel(text: "| \(v)", textStyle: labelStyle)
            newLabel.tickLocation =  NSNumber(integer: k as! Int).decimalValue
            (newLabel.contentLayer as! CPTTextLayer).fill = CPTFill(color: CPTColor.clearColor())
            // 将所有的label的长度固定,然后text左对齐,这样就能解决当text字数不一样时出错的问题的了
            newLabel.contentLayer.frame = CGRectMake(0, 0, 100, 20)
            newLabel.offset = -10.0
            newLabel.rotation = CGFloat(Double(M_PI)/6)*0;
            newLabel.alignment = CPTAlignmentLeft
            labelArray.addObject(newLabel)

//            println("\(k) : \(v)。。。 TickLocation is : \(k as! Int)")
        }

这个步骤只在之前的initPlotGraphView方法中实现,

接下来完成一个函数:

    // 输入所有的时间数组和当前维度(0:天,1:小时,2:分钟),输出处理过的字典,key是label的location,起始是4;value是x轴label的内容
    func setxAxisValues(timesArray: NSArray, type: Int) -> (NSDictionary){
        var result = [Int: NSString]()
        var location = Int(0)       // 起始label的location设定是4
        var lastIndex = 0        // 记录上一个添加到数组中的index,用于记录index的间隔来判断location的显示位置
        for var i = 0; i < timesArray.count-1; i++ {
            // 需要遍历一遍数组,找出每个时间段起始的时间点,并写到dictionary中,因为每个月的天数不一样,所以不能简单的用+30来计算。
            // 对于第一个日期比较敏感,需要计算好是否需要显示,比如如果是小时维度的,那么第一个数是23点的就不用显示了,这样会挤到一起
            var timeStr = timesArray.objectAtIndex(i) as! NSString
            if i == 0 {
            // 第一个数据比较敏感,要单独计算是否需要添加到数组中
                switch type{
                case 0:
                    let range = NSMakeRange(8, 2)
                    if timeStr.substringWithRange(range).toInt() < 24 {
                        // 小于24号的时候再显示label,否则就不显示第一个
                        timeStr = timeStr.substringToIndex(7)
                        result[location] = timeStr
                        lastIndex = i
                    }
                    break
                case 1:
                    let range = NSMakeRange(11, 2)
                    if timeStr.substringWithRange(range).toInt() < 15 {
                        // 小于15点的时候再显示label
                        timeStr = timeStr.substringToIndex(10)
                        result[location] = timeStr
                        lastIndex = i
                    }
                    break
                case 2:
                    let range = NSMakeRange(14, 2)
                    if (timeStr.substringWithRange(range).toInt())!%30 < 15 {
                        // 分钟数要对30求余
                        timeStr = timeStr.substringFromIndex(11) + "   " + timeStr.substringToIndex(11)
                        result[location] = timeStr
                        lastIndex = i
                    }
                    break
                default:
                    break
                }
            }else{
                // 剩余的数据按照是不是整点来添加到字典中,需要记录上一个location的位置,每次加入到字典中时,记录下当前的index
                switch type{
                case 0:
                    let range = NSMakeRange(8, 2)
                    if timeStr.substringWithRange(range).toInt() == 1 {
                        // 计算当前的index和上一个index之间的差距
                        let timeInteval = i - lastIndex
                        location += timeInteval
                        // 每月的1号添加到字典中
                        timeStr = timeStr.substringToIndex(7)
                        result[location] = timeStr as String
                        lastIndex = i
                    }
                    break
                case 1:
                    let range = NSMakeRange(11, 2)
                    let time = timeStr.substringWithRange(range).toInt()
                    if timeStr.substringWithRange(range).toInt() == 0 {
                        // 计算当前的index和上一个index之间的差距
                        let timeInteval = i - lastIndex
                        location += timeInteval
                        // 整点的时候再显示label
                        timeStr = timeStr.substringToIndex(10)
                        result[location] = timeStr as String
                        lastIndex = i
                    }
                    break
                case 2:
                    let range = NSMakeRange(14, 2)
                    if (timeStr.substringWithRange(range).toInt())!%30 == 0 {
                        // 计算当前的index和上一个index之间的差距
                        let timeInteval = i - lastIndex
                        location += timeInteval
                        // 分钟数要对30求余,30分或者60分的时候显示
                        timeStr = timeStr.substringFromIndex(11) + "   " + timeStr.substringToIndex(11)
                        result[location] = timeStr as String
                        lastIndex = i
                    }
                    break
                default:
                    break
                }
            }
        }
        return result
    }

因为我们的项目中时间分为3个维度,按天、小时和分钟进行不同的划分,所以分类处理的情况也稍微有点复杂,还要考虑到第一个label是否显示的问题,因为很可能出线第一个label和第二个label重叠的情况。

最后的屏幕滚动刷新的操作在以前的blog中有讲过,相对于之前的内容来说比较简单:

    func scrollViewWillBeginDecelerating(scrollView: UIScrollView) {
        if scrollView.contentOffset.x < -50 || (scrollView.contentOffset.x + scrollView.frame.width) > scrollView.contentSize.width+50{
            UIView.animateWithDuration(1.0, animations:
                { () -> Void in
                    // frame发生偏移,距离左侧50px
                    if scrollView.contentOffset.x < -50 {
                        self.isForward = true
                        scrollView.contentInset = UIEdgeInsetsMake(0,50,0,0)
                    }else{
                        self.isForward = false
                        scrollView.contentInset = UIEdgeInsetsMake(0,0,0,50)
                    }
                    self.indicatorView!.startAnimating()
                    self.pullRefreshLabel!.hidden = true
                    // 发起网路请求
                    self.singleBarPlotHTTPRequest(self.isForward)
                }, completion:
                { (Bool finished) -> Void in
                    self.indicatorView!.stopAnimating()
                    self.pullRefreshLabel!.hidden = false
                    scrollView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
            })
        }
    }

至此,所有的关键点我们都已经搞定了。那最后就把全套的代码都上传一下。这个会比较长。。。

//
//  Created by KaKa on 15/6/18.
//  Copyright (c) 2015年 . All rights reserved.
//

import UIKit

class SingleItemDataView: UIView,CPTBarPlotDataSource,CPTBarPlotDelegate,UIScrollViewDelegate {

    var model: SingleItemDataModel!
    @IBOutlet var vContent: UIView!
    @IBOutlet weak var scroll_view: UIScrollView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var graphView: CPTGraphHostingView!

    @IBOutlet weak var label_1: UILabel!
    @IBOutlet weak var label_2: UILabel!
    @IBOutlet weak var label_3: UILabel!
    @IBOutlet weak var label_4: UILabel!
    @IBOutlet weak var label_5: UILabel!

    var selectedBarAnnotation : CPTPlotSpaceAnnotation?
    var num: Int!

    var theBarPlot: CPTBarPlot!
    var annotationView: UIView!
    var annotationLabel: UILabel!

    var OriginalContentOffSet_x: CGFloat!
    var lastContentOffset: CGFloat?

    var indicatorView: UIActivityIndicatorView?
    var pullRefreshLabel: UILabel?

    var isForward = false
    var scrollType : Int? = 0   //scroll type 用来表示当条目不够时的滚动方式。0表示默认,1表示条目不足的滚动方式
    var lastHightLighBarOffset: CGFloat? = 0.0
    var reduceIndex : Int? = 0

    //*------------------------------------------------*//
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        NSBundle.mainBundle().loadNibNamed("SingleItemDataView", owner: self, options: nil)
        self.vContent.frame = CGRectMake(0, 0, frame.size.width, frame.size.height)
        self.userInteractionEnabled = true
        self.addSubview(vContent)

        if self.model != nil {
            num = self.model.valuesArray.count
        }else {
            num = 0
        }

        selectedBarAnnotation = nil
        // 初始化选中bar的annotation
        annotationView            = UIView(frame: CGRectMake(0, 0, 100, 80))
        annotationLabel           = UILabel(frame: CGRectMake(0, 0, 100, 80))
        annotationLabel.textColor = UIColor.whiteColor()
        annotationLabel.font      = UIFont(name: FONT_HEITI, size: 10)
        annotationView.addSubview(annotationLabel)
        // add the 	process and label for pullToRefresh
        indicatorView = UIActivityIndicatorView(frame: CGRectMake(-50, scroll_view.frame.height/2 - 30, 20, 20))
        indicatorView!.color = UIColor.whiteColor()

        pullRefreshLabel = UILabel(frame: CGRectMake(-100, scroll_view.frame.height/2-30, 60, 30))
        pullRefreshLabel!.font = UIFont(name: "heiti SC", size: 12)
        pullRefreshLabel!.textColor = UIColor.whiteColor()
        pullRefreshLabel!.text = "加载更多"

        scroll_view.addSubview(indicatorView!)
        scroll_view.addSubview(pullRefreshLabel!)
        scroll_view.bringSubviewToFront(indicatorView!)
        scroll_view.bringSubviewToFront(pullRefreshLabel!)
    }

    // 写入标题和数组数据
    func refresh(){
        // 设置滚动方式
        scrollType = 0
        // 设置多少个条目
        num = self.model.valuesArray.count
        // 设置当前reduce 条目数为0
        reduceIndex = 0
        // 构建界面
        initPlotGraphView()
        // 写入标题
        self.titleLabel!.text = self.model.name as String
        // 写入y轴各刻度
        self.setyAxisValues()
        // 刷新数据
        self.graphView.reloadInputViews()
    }

    /**
    关于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

        // 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))

        // 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,对不同类型的项目,x轴的label显示的内容不一样,所以要一个labeArray数组来存放处理之后的时间数据
        // timeArray中的格式为  yyyy.mm.dd hh:mi
        var labelArray = NSMutableArray()
        var newLabel = CPTAxisLabel()

        let timesDictionary : NSDictionary = self.setxAxisValues(self.model.timesArray, type: self.model.type.integerValue)
        for (k,v) in  Array(timesDictionary).sorted({($0.0 as! Int) < ($1.0 as! Int)}) {
            newLabel = CPTAxisLabel(text: "| \(v)", textStyle: labelStyle)
            newLabel.tickLocation =  NSNumber(integer: k as! Int).decimalValue
            (newLabel.contentLayer as! CPTTextLayer).fill = CPTFill(color: CPTColor.clearColor())
            // 将所有的label的长度固定,然后text左对齐,这样就能解决当text字数不一样时出错的问题的了
            newLabel.contentLayer.frame = CGRectMake(0, 0, 100, 20)
            newLabel.offset = -10.0
            newLabel.rotation = CGFloat(Double(M_PI)/6)*0;
            newLabel.alignment = CPTAlignmentLeft
            labelArray.addObject(newLabel)

//            println("\(k) : \(v)。。。 TickLocation is : \(k as! Int)")
        }
        xAxis.axisLabels = NSSet(array: labelArray as [AnyObject]) as Set<NSObject>

        // 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
    }

    // 输入所有的时间数组和当前维度(0:天,1:小时,2:分钟),输出处理过的字典,key是label的location,起始是4;value是x轴label的内容
    func setxAxisValues(timesArray: NSArray, type: Int) -> (NSDictionary){
        var result = [Int: NSString]()
        var location = Int(0)       // 起始label的location设定是4
        var lastIndex = 0        // 记录上一个添加到数组中的index,用于记录index的间隔来判断location的显示位置
        for var i = 0; i < timesArray.count-1; i++ {
            // 需要遍历一遍数组,找出每个时间段起始的时间点,并写到dictionary中,因为每个月的天数不一样,所以不能简单的用+30来计算。
            // 对于第一个日期比较敏感,需要计算好是否需要显示,比如如果是小时维度的,那么第一个数是23点的就不用显示了,这样会挤到一起
            var timeStr = timesArray.objectAtIndex(i) as! NSString
            if i == 0 {
            // 第一个数据比较敏感,要单独计算是否需要添加到数组中
                switch type{
                case 0:
                    let range = NSMakeRange(8, 2)
                    if timeStr.substringWithRange(range).toInt() < 24 {
                        // 小于24号的时候再显示label,否则就不显示第一个
                        timeStr = timeStr.substringToIndex(7)
                        result[location] = timeStr
                        lastIndex = i
                    }
                    break
                case 1:
                    let range = NSMakeRange(11, 2)
                    if timeStr.substringWithRange(range).toInt() < 15 {
                        // 小于15点的时候再显示label
                        timeStr = timeStr.substringToIndex(10)
                        result[location] = timeStr
                        lastIndex = i
                    }
                    break
                case 2:
                    let range = NSMakeRange(14, 2)
                    if (timeStr.substringWithRange(range).toInt())!%30 < 15 {
                        // 分钟数要对30求余
                        timeStr = timeStr.substringFromIndex(11) + "   " + timeStr.substringToIndex(11)
                        result[location] = timeStr
                        lastIndex = i
                    }
                    break
                default:
                    break
                }
            }else{
                // 剩余的数据按照是不是整点来添加到字典中,需要记录上一个location的位置,每次加入到字典中时,记录下当前的index
                switch type{
                case 0:
                    let range = NSMakeRange(8, 2)
                    if timeStr.substringWithRange(range).toInt() == 1 {
                        // 计算当前的index和上一个index之间的差距
                        let timeInteval = i - lastIndex
                        location += timeInteval
                        // 每月的1号添加到字典中
                        timeStr = timeStr.substringToIndex(7)
                        result[location] = timeStr as String
                        lastIndex = i
                    }
                    break
                case 1:
                    let range = NSMakeRange(11, 2)
                    let time = timeStr.substringWithRange(range).toInt()
                    if timeStr.substringWithRange(range).toInt() == 0 {
                        // 计算当前的index和上一个index之间的差距
                        let timeInteval = i - lastIndex
                        location += timeInteval
                        // 整点的时候再显示label
                        timeStr = timeStr.substringToIndex(10)
                        result[location] = timeStr as String
                        lastIndex = i
                    }
                    break
                case 2:
                    let range = NSMakeRange(14, 2)
                    if (timeStr.substringWithRange(range).toInt())!%30 == 0 {
                        // 计算当前的index和上一个index之间的差距
                        let timeInteval = i - lastIndex
                        location += timeInteval
                        // 分钟数要对30求余,30分或者60分的时候显示
                        timeStr = timeStr.substringFromIndex(11) + "   " + timeStr.substringToIndex(11)
                        result[location] = timeStr as String
                        lastIndex = i
                    }
                    break
                default:
                    break
                }
            }
        }
        return result
    }

    // 显示各个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
        }
    }

    /**
    *  @author KaKa, 15-06-19 14:06:52
    *
    *  CPTBarPlotDataSource
    */
    //pragma mark CPTBarPlotDataSource

    func numberOfRecordsForPlot(plot: CPTPlot!) -> UInt {
        return UInt(self.model.valuesArray.count)
    }

    func numberForPlot(plot: CPTPlot!, field fieldEnum: UInt, recordIndex idx: UInt) -> NSNumber! {
        var nums : NSNumber?
        if(plot.isKindOfClass(CPTPlot)){
            switch(fieldEnum){
            case UInt(CPTBarPlotFieldBarLocation.value):
                nums = NSNumber(unsignedLong: idx+1)
                break
            case UInt(CPTBarPlotFieldBarTip.value):
                var temp = self.model.valuesArray.objectAtIndex(Int(idx)) as! String
                nums = NSDecimalNumber(string: temp)
                break
            default:
                break
            }
        }
        return nums;
    }

    // 柱子上显示对应的值
//    func dataLabelForPlot(plot: CPTPlot!, recordIndex idx: UInt) -> CPTLayer! {
//        var textLineStyle = CPTMutableTextStyle()
//        textLineStyle.fontSize = 12
//        textLineStyle.color = CPTColor.whiteColor()
//        var label = CPTTextLayer(text: mArray.objectAtIndex(Int(idx)) as! String, style: textLineStyle)
//        return label
//    }

    // 填充不同的颜色
    func barFillForBarPlot(barPlot: CPTBarPlot!, recordIndex idx: UInt) -> CPTFill! {
        var areaColor : CPTColor!
        let percentNum = (self.model.valuesArray.objectAtIndex(Int(idx)) as! NSString).floatValue / self.model.maxValue.floatValue
        // 根据标识位来填充不同的颜色
        if percentNum <= 0.3 {
            areaColor = CPTColor.greenColor()
        }else if percentNum <= 0.7 {
            areaColor = CPTColor.orangeColor()
        }else if percentNum <= 1.0 {
            areaColor = CPTColor.redColor()
        }else{
            areaColor = CPTColor.purpleColor()
        }
        var barFill = CPTFill(color: areaColor)
        return barFill
    }

    /**
    *  @author KaKa, 15-06-24 14:06:57
    *
    *  BarPlot Delegate
    *  用于选中不同的柱子后显示出来label
    */
    func barPlot(plot: CPTBarPlot!, barWasSelectedAtRecordIndex idx: UInt, withEvent event: UIEvent!) {
        // 如果已经有了annotation,就清零
        graphView.hostedGraph.plotAreaFrame.plotArea.removeAllAnnotations()

        // Setup a style for the annotation
        let hitAnnotationTextStyle      = CPTMutableTextStyle.textStyle() as! CPTMutableTextStyle
        hitAnnotationTextStyle.color    = CPTColor.blackColor()
        hitAnnotationTextStyle.fontSize = 10.0;
        hitAnnotationTextStyle.fontName = FONT_HEITI;

        // Determine point of symbol in plot coordinates
        // 这里y坐标设置成0,x就是idx
        let anchorPoint = [Int(idx),0]
        // Add annotation
        // First make a string for the y value
        var timeStr = self.model.timesArray.objectAtIndex(Int(idx-1)) as! NSString
        if timeStr != "" {
            // 只有当timeStr中不是空时才继续执行
            switch self.model.type.integerValue {
            case 0:
                let range = NSMakeRange(8, 2)
                timeStr = timeStr.substringWithRange(range) + "日"
                break
            case 1,2:
                timeStr = timeStr.substringFromIndex(11)
                break
            default:
                break
            }

            let string = "\(timeStr) , \(self.model.valuesArray.objectAtIndex(Int(idx-1)))\(self.model.unit)"
            // Now add the annotation to the plot area
            let textLayer = CPTTextLayer(text: string, style: hitAnnotationTextStyle)

            textLayer.fill = CPTFill(image: CPTImage(CGImage: UIImage(named:"Popover.png")?.CGImage, scale: 2.0))
            textLayer.paddingBottom = 5
            textLayer.paddingTop = 5
            /* DXPopover会在背景里面加上阴影,所以放弃使用了,不过研究清楚了点的坐标获取
            // 新建popview

            var popView = DXPopover()
            annotationLabel.text = string

            var pointers = [NSDecimal](count: 2, repeatedValue: CPTDecimalFromUnsignedInteger(0))
            let plotXvalue = self.numberForPlot(plot, field: UInt(CPTScatterPlotFieldX.value), recordIndex: idx)
            pointers[0] = CPTDecimalFromFloat(plotXvalue.floatValue)
            println("\(CPTDecimalFromUnsignedInteger(idx))")

            let plotspace = graphView.hostedGraph.defaultPlotSpace
            println("\(plotspace.numberOfCoordinates)")

            var popPoint  = plotspace.plotAreaViewPointForPlotPoint(&pointers, numberOfCoordinates: plotspace.numberOfCoordinates)
            popView.showAtPoint(popPoint, popoverPostion: DXPopoverPosition.Down, withContentView: self.annotationView, inView: graphView)
            */

            selectedBarAnnotation = CPTPlotSpaceAnnotation(plotSpace: graphView.hostedGraph.defaultPlotSpace, anchorPlotPoint: anchorPoint)
            selectedBarAnnotation!.contentLayer = textLayer
            selectedBarAnnotation!.displacement = CGPointMake(0.0, -15.0)
            graphView.hostedGraph.plotAreaFrame.plotArea.addAnnotation(selectedBarAnnotation)
        }
    }

    /**
    *  @author KaKa, 15-06-29 16:06:51
    *
    *  Scroll_view的delegate
    */
    func scrollViewDidScroll(scrollView: UIScrollView) {
        // 判断屏幕向左还是向右滑动
        var scrollDirection : ScrollDirection?
        if self.lastContentOffset > scrollView.contentOffset.x {
            scrollDirection = ScrollDirection.ScrollDirectionLeft
        }else if self.lastContentOffset < scrollView.contentOffset.x{
            scrollDirection = ScrollDirection.ScrollDirectionRight
        }else{
            scrollDirection = ScrollDirection.ScrollDirectionNone
        }
        self.lastContentOffset = scrollView.contentOffset.x

        // 当屏幕滑动时选中不同的bar,每个柱子是10px,我们知道起始的contentoffset的originalX是多少,拿originalX-currentX/10 就可以得出需要显示的是第几个数据
        // 因为我们的柱子是从最后一个移动到第一个,总数是知道的: count,然后总共的移动的长度是知道的:originalContentOffset_X,那么每个柱子实际的 δcontentoffset  = originalContrentOffset_X/count
        // 那么,我们用(originalContentOffset_x - currentOffset_x)/δcontentoffset 就是需要移动的柱子个数

        var singleOffset: CGFloat? = (OriginalContentOffSet_x/CGFloat(num))
        if self.scrollType == 0 {
            reduceIndex = Int((OriginalContentOffSet_x - scrollView.contentOffset.x)/singleOffset!)
        }else{
            // 如果当前的条目很少,就不能采用上面的方法来移动了,要采用每次移动多少个格子的办法
            singleOffset = 10.0
            if abs(lastHightLighBarOffset! - scrollView.contentOffset.x) > singleOffset {
                var reduce = Int((lastHightLighBarOffset! - scrollView.contentOffset.x) / singleOffset!)
                if reduce > 0 && scrollDirection == ScrollDirection.ScrollDirectionLeft {
                    reduceIndex = (reduceIndex! + reduce > (num - 1)) ? (num - 1) : reduceIndex!+reduce
                }else if reduce < 0 && scrollDirection == ScrollDirection.ScrollDirectionRight {
                    reduceIndex = (reduceIndex! + reduce < 0 ) ? 0 : reduceIndex! + reduce
                }
                if scrollView.contentOffset.x > 0 && scrollView.contentOffset.x < OriginalContentOffSet_x{
                    lastHightLighBarOffset = scrollView.contentOffset.x
                }
            }
        }

        if (reduceIndex >= 0 && reduceIndex < num)  {
            barPlot(theBarPlot, barWasSelectedAtRecordIndex: UInt(num-reduceIndex!), withEvent: nil)
        }else if scrollView.contentOffset.x < 0 {
            barPlot(theBarPlot, barWasSelectedAtRecordIndex: UInt(1), withEvent: nil)
        }else if scrollView.contentOffset.x > scrollView.contentSize.width {
            barPlot(theBarPlot, barWasSelectedAtRecordIndex: UInt(num), withEvent: nil)
        }

        // 当刷新数据时的处理
        // 保持indecator和label的位置一直在左右两端
        if( scrollView.contentOffset.x < -50){
            self.indicatorView!.frame = CGRectMake(scrollView.contentOffset.x+50, scrollView.frame.height/2-25,20,20)
            self.pullRefreshLabel!.frame = CGRectMake(scrollView.contentOffset.x+40, scrollView.frame.height/2-20,60,30)
        }else if (scrollView.contentOffset.x + scrollView.frame.width) > scrollView.contentSize.width+50{
            self.indicatorView!.frame = CGRectMake(scrollView.contentOffset.x + scrollView.frame.width - 60, scrollView.frame.height/2-25,20,20)
            self.pullRefreshLabel!.frame = CGRectMake(scrollView.contentOffset.x + scrollView.frame.width - 80, scrollView.frame.height/2-20,60,30)
        }else{
            if self.isForward{
                self.indicatorView!.frame = CGRectMake(-10, scrollView.frame.height/2-30,20,20)
            }else{
                // 这里屏幕已经弹回最前端了,所以contentOffset.x是0
                self.indicatorView!.frame = CGRectMake(scrollView.contentSize.width + scrollView.frame.width, scrollView.frame.height/2-30,20,20)
            }
            self.pullRefreshLabel!.frame = CGRectMake(-100, scrollView.frame.height/2-30,60,30)
        }
    }

    func scrollViewWillBeginDecelerating(scrollView: UIScrollView) {
        if scrollView.contentOffset.x < -50 || (scrollView.contentOffset.x + scrollView.frame.width) > scrollView.contentSize.width+50{
            UIView.animateWithDuration(1.0, animations:
                { () -> Void in
                    // frame发生偏移,距离左侧50px
                    if scrollView.contentOffset.x < -50 {
                        self.isForward = true
                        scrollView.contentInset = UIEdgeInsetsMake(0,50,0,0)
                    }else{
                        self.isForward = false
                        scrollView.contentInset = UIEdgeInsetsMake(0,0,0,50)
                    }
                    self.indicatorView!.startAnimating()
                    self.pullRefreshLabel!.hidden = true
                    // 发起网路请求
                    self.singleBarPlotHTTPRequest(self.isForward)
                }, completion:
                { (Bool finished) -> Void in
                    self.indicatorView!.stopAnimating()
                    self.pullRefreshLabel!.hidden = false
                    scrollView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
            })
        }
    }

    /**
    单个barPlot的刷新数据
    */
    func singleBarPlotHTTPRequest(isForward: Bool){
        // 设置新请求数据的终止时间
        var endTime = ""
        var startTime = ""
        if isForward{
            endTime = (self.model.timesArray.objectAtIndex(0) as! String)
            startTime = GlobalVariables.getProcessedTime(endTime, model: self.model.type.integerValue, direction: ScrollDirection.ScrollDirectionLeft).removeWhitespace()
            endTime = endTime.removeWhitespace()
        }else{
            startTime = (self.model.timesArray.objectAtIndex(self.num-1) as! String)
            endTime = GlobalVariables.getProcessedTime(startTime, model: self.model.type.integerValue, direction: ScrollDirection.ScrollDirectionRight).removeWhitespace()
            startTime = startTime.removeWhitespace()
        }
        // 设置字典
        let user = GlobalVariables.getUserName()
        let pass = GlobalVariables.getUserPass()
        let sysId = GlobalVariables.getSystemId()
        let itemId = self.model.itemId
        let dic = ["user":user,"pass":pass,"sysId":sysId,"ItemId":itemId,"startTime":startTime,"endTime":endTime] as NSDictionary

        var tempMode : HTTPRequestModel! = nil
        var tempArray : NSMutableArray! = nil
        HTTPRequestManager.HTTPRequestStart(HTTPType.HTTPItemData, parmas: dic, success:{
            () -> () in
            // request Success
            println("ItemData success:  \(GlobalVariables.getCurrentTime())")
            tempMode = HTTPRequestManager.arrayForRequestModel.objectAtIndex(HTTPType.HTTPItemData.rawValue) as! HTTPRequestModel
            tempArray = tempMode!.requestArray as! NSMutableArray
            if (tempArray.objectAtIndex(0) as! NSString) != "" {
                self.model.timesArray = nil
                self.model.valuesArray = nil
                self.model.timesArray = (tempArray.objectAtIndex(1) as! NSString).componentsSeparatedByString(",")
                self.model.valuesArray = (tempArray.objectAtIndex(0) as! NSString).componentsSeparatedByString(",")
                self.refresh()
                if !isForward {
                    self.scroll_view.contentOffset.x = 0
                    self.barPlot(self.theBarPlot, barWasSelectedAtRecordIndex: UInt(1), withEvent: nil)
                    self.lastHightLighBarOffset = self.scroll_view.contentOffset.x
                }
            }
            }, fail: { () -> () in
            // request failed
                println("Refresh failed")
        })
    }
}

/**
*  @author KaKa, 15-07-16 14:07:43
*
*  ScrollView's scroll direction
*/

enum ScrollDirection: Int{
    case
    ScrollDirectionNone = 0,
    ScrollDirectionLeft,
    ScrollDirectionRight,
    ScrollDirectionUp,
    ScrollDirectionDown
}

写的比较乱,主要是为了我个人查找方面。如果有朋友想一起讨论的话可以留言。

这里面的内容必须得经过自己去尝试,去不断地研究才能理解透彻,我基本前后花了1个月才把这里面的方方面面研究清楚。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-11-06 10:09:45

用CorePlot实现类似Air Quality的柱状图滚动效果(2/2)的相关文章

用CorePlot实现类似Air Quality的柱状图滚动效果(1/2)

也是最近1个月在项目中不断研究CorePlot实现柱状图的效果. 先来看一下Air Quality的柱状图效果: 经过1个月的研究,现在基本上已经重现了这个柱状图的功能(99%),而且还加上了刷新数据的功能. 计划通过两篇blog来记录下开发中的难点,在后一篇blog中会把所有的源码挂上去. 难点有以下几个: 1. Coreplot自带的滚动机制在我这边做起来有一些卡顿,用户体验很不好,这里需要替换掉它自带的滚动功能 2. 不用coreplot的滚动以后,需要新建一个view用于承载左侧固定的坐

Windows Phone中使用Storyboard做类似 IOS 屏幕小白点的效果

windows phone中做动画其实很方便的,可以使用Blend拖来拖去就做出一个简单的动画,下面做了一个 ios屏幕小白点的拖动效果,包括速度判断移动 使用Blend生成以下代码 <Storyboard x:Name="HandFunGTLSb"><!-- 向左滑动时动画 --> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransfo

实现类似微信语音播放动画的效果

要求就是点击播放按钮,小喇叭开始动,当语音结束后,停止动画.如图: 这需要用到UIImageView的帧动画,该动画可以让一系列图片在特定的时间内按顺序显示出来.需要的素材如下: audio_icon_1 *****分割线***** audio_icon_2 *******分割线*******audio_icon_3 代码如下: // 添加播放时候的动画图片 [_audioBtn addSubview:self.animationview]; [_audioBtn setImage:[UIIma

如何实现SP文档库类似百度文档库的效果 (副标题:如何在SP2013文档库的SWF文件用FlexPager显示)

如何实现SP文档库类似百度文档库的效果 (副标题:如何在SP2013文档库的SWF文件用FlexPager显示) 1. 编辑文档库列表显示页面,如下图: 2. 添加内容编辑器,如下图: 3. 添加如下在[内容编辑器中]-[编辑源],添加如下JS代码,如下图: ?  代码如下: <scrip type="text/javascript" src="/SiteAssets/jquery-1.4.1.js"></script> <script

请教如何用 peewee 实现类似 django ORM 的这种查询效果。

本人新入坑的小白,如有不对的地方请包涵~~~! 在 django 中代码如下:模型定义: class Friends(models.Model): first_id = models.IntegerField() second_id = models.IntegerField() class Meta: unique_together=('first_id', 'second_id',) 查询语句如下: friend_list_info = [] friend_list1 = Friends.o

【Android】类似QQ风格的popupwindow弹窗效果

[Android]类似QQ风格的popupwindow弹窗效果 该源码主要是实现类似QQ风格的popupwindow弹出窗效果,出现时有遮挡图层,消失时无遮挡图层. 下载地址:http://www.devstore.cn/code/info/273.html

UWP:使用Composition实现类似安卓的水波纹Ripple效果

原文:UWP:使用Composition实现类似安卓的水波纹Ripple效果 先放效果图: 首先,建立一个RippleHelper.cs文件,然后建立以下附加属性: IsFillEnable:是否扩大到整个控件 RippleDuration:持续时间 RippleRadius:不扩大到整个控件时的最大半径 RippleColor:波纹的颜色 public static bool GetIsFillEnable(DependencyObject obj) { return (bool)obj.Ge

尝试自己的Perl语言的包 TCP协议的再包装起到类似python语言装饰器的效果

#!/usr/bin/perl # Filename: BuildSocketTCP.pm # #   Copyright 2012 Axxeo GmbH #   Licensed under the Apache License, Version 2.0 (the "License"); #   you may not use this file except in compliance with the License. #   You may obtain a copy of t

尝试自己的Perl语言的包 UDP协议的再包装起到类似python语言装饰器的效果

#!/usr/bin/perl # Filename: BuildSocketUDP.pm # #   Copyright 2012 Axxeo GmbH #   Licensed under the Apache License, Version 2.0 (the "License"); #   you may not use this file except in compliance with the License. #   You may obtain a copy of t