ios 火币用的k线框架
2017-06-01 08:36:26 yunkai666 阅读数 2079

k线系列目录

查看目录请点击这儿


前言

当写完了所有需要使用的素材类后,我们开始搭建一个比较完整的K线Demo。

它包含以下功能:

1、可以展示蜡烛图
2、可以展示OHLC图
3、可以左右滑动
4、可以长按出现十字叉
5、有基本的价格和日期区间展示

GO

在上几篇文章中,我们已经知道如何绘制蜡烛、边框、OHLC等。所以在这里,可以直接使用已经写好的类。

绘制边框

首页,我们先绘制一个包含主副图的边框:

/**
 绘制边框
 */
- (void)drawBorder
{
    //设置主图、主图指标、副图、副图指标rect
    _mainIndexRect = CGRectMake(0, 0, CGRectGetWidth(self.frame), mainIndexH);
    _mainRect = CGRectMake(0, mainIndexH, CGRectGetWidth(self.frame), (CGRectGetHeight(self.frame) - (mainIndexH + accessoryIndexH + dateH)) * mainFrameScale);
    _accessoryIndexRect = CGRectMake(0, mainIndexH + CGRectGetHeight(_mainRect)+dateH, CGRectGetWidth(self.frame), accessoryIndexH);
    _accessoryRect = CGRectMake(0, mainIndexH + CGRectGetHeight(_mainRect)+dateH+accessoryIndexH, CGRectGetWidth(self.frame), (CGRectGetHeight(self.frame) - (mainIndexH + accessoryIndexH + dateH)) * (1-mainFrameScale));


    CAShapeLayer *borderLayer = [CAShapeLayer layer];
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.bounds];

    [path moveToPoint:CGPointMake(0, mainIndexH)];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), mainIndexH)];

    [path moveToPoint:CGPointMake(0, CGRectGetMaxY(_mainRect))];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMaxY(_mainRect))];

    [path moveToPoint:CGPointMake(0, CGRectGetMinY(_accessoryIndexRect))];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMinY(_accessoryIndexRect))];

    [path moveToPoint:CGPointMake(0, CGRectGetMinY(_accessoryRect))];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMinY(_accessoryRect))];

    float mainUnitH = CGRectGetHeight(_mainRect) / 4.f;
    float mainUnitW = CGRectGetWidth(_mainRect) / 4.f;

    for (int idx = 1; idx <= 3; idx++)
    {
        //画3条横线
        [path moveToPoint:CGPointMake(0, mainIndexH + mainUnitH * idx)];
        [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), mainIndexH + mainUnitH * idx)];

        //画3条竖线
        [path moveToPoint:CGPointMake(idx * mainUnitW, mainIndexH)];
        [path addLineToPoint:CGPointMake(idx * mainUnitW, CGRectGetMaxY(_mainRect))];

        //画3条竖线
        [path moveToPoint:CGPointMake(idx * mainUnitW, CGRectGetMinY(_accessoryRect))];
        [path addLineToPoint:CGPointMake(idx * mainUnitW, CGRectGetMaxY(_accessoryRect))];
    }

    float accessoryUnitH = CGRectGetHeight(_accessoryRect) / 2.f;
    [path moveToPoint:CGPointMake(0, CGRectGetMaxY(_accessoryRect) - accessoryUnitH)];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMaxY(_accessoryRect) - accessoryUnitH)];

    borderLayer.path = path.CGPath;
    borderLayer.lineWidth = 0.5f;
    borderLayer.strokeColor = [UIColor colorWithRed:222.f/255.f green:222.f/255.f blue:222.f/255.f alpha:1.f].CGColor;
    borderLayer.fillColor = [UIColor clearColor].CGColor;

    [self.layer addSublayer:borderLayer];

}

效果如下:
边框

导入数据

边框绘制完以后,我们把k线数据转换为坐标点。在转换之前,还需要寻找当前在屏幕上展示数据的极限值,也就是最大最小值。

这里要注意,因为当前屏幕中一般最多显示60个左右的蜡烛数量,但是某一个周期K线的蜡烛数量一般是几百个,所以在K线数据转为模型数据后,需要有一个起始索引来标识当前展示的数据范围。

寻找极限值:

//求出最大最小值
    _min = (float)INT32_MAX;
    _max = (float)INT32_MIN;
    for (int idx=_startIndex; idx<_endIndex; idx++)
    {
        YKKLineModel *model = self.kLineModelArr[idx];
        if (_min > model.low)
        {
            _min = model.low;
        }
        if (_max < model.high)
        {
            _max = model.high;
        }
    }

把数据转换为坐标点:

    [self.displayPointArr removeAllObjects];
    //每根蜡烛的宽度
    float candleW = CGRectGetWidth(_mainRect) / candleCount;
    for (int idx = _startIndex; idx<_endIndex; idx++)
    {
        YKKLineModel *model = self.kLineModelArr[idx];
        float x = CGRectGetMinX(_mainRect) + candleW * (idx - (_startIndex - 0));

        CGPoint hPoint = CGPointMake(x + candleW/2,
                                     ABS(CGRectGetMaxY(_mainRect) - (model.high  - _min)/unitValue));
        CGPoint lPoint = CGPointMake(x + candleW/2,
                                     ABS(CGRectGetMaxY(_mainRect) - (model.low   - _min)/unitValue));
        CGPoint oPoint = CGPointMake(x + candleW/2,
                                     ABS(CGRectGetMaxY(_mainRect) - (model.open  - _min)/unitValue));
        CGPoint cPoint = CGPointMake(x + candleW/2,
                                     ABS(CGRectGetMaxY(_mainRect) - (model.close - _min)/unitValue));
        [_displayPointArr addObject:[YKCandlePointModel candlePointModelWithOpoint:oPoint
                                                                            Hpoint:hPoint
                                                                            Lpoint:lPoint
                                                                            Cpoint:cPoint]];
    }

绘制蜡烛/OHLC

导入数据并且转换为坐标点以后,接下来开始绘制。

绘制蜡烛:

    //每根蜡烛的宽度
    float candleW = CGRectGetWidth(_mainRect) / candleCount;

    for (int idx = 0; idx< candleCount; idx++)
    {
        YKCandlePointModel *model = pointModelArr[idx];
        CAShapeLayer *layer = [CAShapeLayer getCandleLayerWithPointModel:model candleW:candleW];

        [self.candleLayer addSublayer:layer];
    }

    [self.layer addSublayer:self.candleLayer];

绘制OHLC:

    //每根OHLC的宽度
    float candleW = CGRectGetWidth(_mainRect) / candleCount;

    for (int idx = 0; idx< candleCount; idx++)
    {
        YKCandlePointModel *model = pointModelArr[idx];
        CAShapeLayer *layer = [CAShapeLayer getOHLCLayerWithPointModel:model candleW:candleW];

        [self.ohlcLayer addSublayer:layer];
    }

    [self.layer addSublayer:self.ohlcLayer];

效果如下:

蜡烛

OHLC

绘制价格、日期区间

蜡烛绘制完以后,我们还剩下价格、日期。价格是指左侧的5个价格的区间标识,从上往下降序排列;日期为下方5个日期的区间标识,每一个标识都是距离当前点最近的点的日期。

绘制左侧价格:

    float unitPrice = (_max - _min) / 4.f;
    float unitH = CGRectGetHeight(_mainRect) / 4.f;

    //求得价格rect
    NSDictionary *attribute = @{NSFontAttributeName:[UIFont systemFontOfSize:9.f]};
    CGRect priceRect = [self rectOfNSString:[NSString stringWithFormat:@"%.2f", _max] attribute:attribute];

    for (int idx = 0; idx < 5; idx++)
    {
        float height = 0.f;
        if (idx == 4)
        {
            height = idx * unitH - CGRectGetHeight(priceRect);
        } else
        {
            height = idx * unitH;
        }
        CGRect rect = CGRectMake(CGRectGetMinX(_mainRect),
                                 CGRectGetMinY(_mainRect) + height,
                                 CGRectGetWidth(priceRect),
                                 CGRectGetHeight(priceRect));
        //计算价格
        NSString *str = [NSString stringWithFormat:@"%.2f", _max - idx * unitPrice];
        CATextLayer *layer = [CATextLayer getTextLayerWithString:str
                                                       textColor:[UIColor blackColor]
                                                        fontSize:9.f
                                                 backgroundColor:[UIColor clearColor]
                                                           frame:rect];

        [self.leftPriceLayer addSublayer:layer];
    }

    [self.layer addSublayer:self.leftPriceLayer];

绘制日期(这里要注意,因为使用的Demo数据周期为天,所以日期也具体到天):

    NSMutableArray *kLineDateArr = [NSMutableArray array];
    int unitCount = candleCount / 4;
    for (int idx=0; idx<5; idx++)
    {
        YKKLineModel *model = self.kLineModelArr[_startIndex + idx * unitCount];

        NSDate *detaildate = [NSDate dateWithTimeIntervalSince1970:model.timeStamp];
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"YYYY-MM-dd"];
        NSString *dateStr = [dateFormatter stringFromDate:detaildate];

        [kLineDateArr addObject:dateStr];
    }

    NSDictionary *attribute = @{NSFontAttributeName:[UIFont systemFontOfSize:9.f]};
    CGRect strRect = [self rectOfNSString:@"0000-00-00" attribute:attribute];
    float strW = CGRectGetWidth(strRect);
    float strH = CGRectGetHeight(strRect);

    float unitW = CGRectGetWidth(_mainRect) / 4;

    //循环绘制坐标点
    for (int idx = 0; idx < kLineDateArr.count; idx++)
    {
        CATextLayer *textLayer = nil;

        if (idx == kLineDateArr.count-1)
        {//最后一个
            CGRect rect = CGRectMake(idx * unitW - strW, CGRectGetMaxY(_mainRect), strW, strH);
            textLayer = [CATextLayer getTextLayerWithString:kLineDateArr[idx] textColor:[UIColor blackColor] fontSize:9.f backgroundColor:[UIColor clearColor] frame:rect];
        }else if(idx == 0)
        {//第一个
            CGRect rect = CGRectMake(idx * unitW, CGRectGetMaxY(_mainRect), strW, strH);
            textLayer = [CATextLayer getTextLayerWithString:kLineDateArr[idx] textColor:[UIColor blackColor] fontSize:9.f backgroundColor:[UIColor clearColor] frame:rect];
        }else
        {//中间
            CGRect rect = CGRectMake(idx * unitW - strW/2, CGRectGetMaxY(_mainRect), strW, strH);
            textLayer = [CATextLayer getTextLayerWithString:kLineDateArr[idx] textColor:[UIColor blackColor] fontSize:9.f backgroundColor:[UIColor clearColor] frame:rect];
        }

        [self.bottomDateLayer addSublayer:textLayer];
    }

    [self.layer addSublayer:self.bottomDateLayer];

效果如下:

区间标识

添加左右滑动

在以前我们讨论过滑动偏移量的获取方式,这里就暂且通过添加手势来获取偏移量。使用长按手势来做十字叉效果,使用拖动手势来做左右滑动效果。

当检测到用户长按时,获取坐标点然后转换为柱子索引,再绘制十字叉,左侧和下侧展示当前索引的数据。当用户抬起手指时,可以选择及时清除掉十字叉,也可以加一个延时清除。

当检测到用户拖动时,用偏移量的正负来判断用户是向左还是向右拖动。每次检测到拖动后,获取到偏移量,因为这个偏移量不是太线性,所以添加一个范围的判断。拿到偏移量以后,再更新展示数据的起始索引值,然后再更新视图。

添加手势:

    //添加左右拖动手势
    UIPanGestureRecognizer *panG = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)];
    [_kLineView addGestureRecognizer:panG];

    //添加长按手势
    UILongPressGestureRecognizer *longG = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(kLineLongGestureAction:)];
    longG.minimumPressDuration = 0.5f;
    longG.numberOfTouchesRequired = 1;
    [_kLineView addGestureRecognizer:longG];

响应手势:

/**
 K线响应长按手势

 @param longGesture 手势对象
 */
- (void)kLineLongGestureAction:(UILongPressGestureRecognizer *)longGesture
{
    if (longGesture.state == UIGestureRecognizerStateBegan || longGesture.state == UIGestureRecognizerStateChanged)
    {
        CGPoint point = [longGesture locationInView:_kLineView];

        float x = 0.f;
        if (point.x < 0.f)
        {
            x = 0.f;
        }else if (point.x > CGRectGetWidth(_kLineView.frame))
        {
            x = CGRectGetWidth(_kLineView.frame)-1;
        }else
        {
            x = point.x;
        }
        //当长按滑动时,每滑动一次话会重新刷新十字叉
        [_kLineView drawCrossViewWithX:x];
    }else
    {
        //当手指抬起时,及时把十字叉取消掉
        [_kLineView clearCrossViewLayer];
    }
}

/**
 响应拖动手势

 @param panGesture 手势对象
 */
- (void)panGestureAction:(UIPanGestureRecognizer *)panGesture
{
    CGPoint point = [panGesture translationInView:_kLineView];
    float offset =  point.x - kLineGlobalOffset;
    if (panGesture.state == UIGestureRecognizerStateChanged && ABS(offset) > 3)
    {
        if (offset > 0)
        {
            if (ABS(offset) > 20)
            {
                [_kLineView dragRightOffsetcount:5];

            } else if(ABS(offset) > 6)
            {
                [_kLineView dragRightOffsetcount:2];

            } else
            {
                [_kLineView dragRightOffsetcount:1];
            }
        }else
        {
            if (ABS(offset) > 20)
            {
                [_kLineView dragLeftOffsetcount:5];

            } else if(ABS(offset) > 6)
            {
                [_kLineView dragLeftOffsetcount:2];

            } else
            {
                [_kLineView dragLeftOffsetcount:1];
            }
        }
        kLineGlobalOffset = point.x;
    }

    if (panGesture.state == UIGestureRecognizerStateEnded ||
        panGesture.state == UIGestureRecognizerStateCancelled ||
        panGesture.state == UIGestureRecognizerStateFailed)
    {
        kLineGlobalOffset= 0.f;
    }
}

效果如下:

OHLC线

Demo源码下载

至此,我们已经把K线主图大部分的功能搭建完毕。Demo源码点击这里下载。下篇文章,会说到主副图指标的一些事,敬请期待。

2018-03-31 09:04:00 weixin_34409357 阅读数 27

Github地址 https://github.com/zhangxiaomeng1/XMLineChartView

采用继承的方法,写了个基类,,重写- (void)drawRect:(CGRect)rect 方法,setNeedsDisplay 调用drawRect方法;

  • (void)drawline:(CGContextRef)context

    startPoint:(CGPoint)startPoint
    
     stopPoint:(CGPoint)stopPoint
    
         color:(UIColor *)color
    
     lineWidth:(CGFloat)lineWitdth;
    
  • (void)drawLabelPrice:(CGContextRef)context;

//高亮状态滑动

  • (void)drawHighlighted:(CGContextRef)context

                point:(CGPoint)point
    
                 idex:(NSInteger)idex
    
                value:(id)value
    
                color:(UIColor *)color
    
            lineWidth:(CGFloat)lineWidth;
    
  • (void)drawLabel:(CGContextRef)context

    attributesText:(NSAttributedString *)attributesText

           rect:(CGRect)rect;
    

//画柱状图

  • (void)drawRect:(CGContextRef)context

          rect:(CGRect)rect
    
         color:(UIColor*)color;
    
  • (void)drawGridBackground:(CGContextRef)context

                    rect:(CGRect)rect;
2017-11-06 09:53:25 weixin_33882443 阅读数 11

github

1.简介篇

  • 蜡烛图和山形图绘制切换
  • 5种指标绘制切换
  • 长按蜡烛和指标线详情展示
  • 触底加载更多
  • 实时蜡烛绘制实现
  • 二级横屏和蜡烛三级横屏


  • 适配两种布局

2.原理篇

2.1 tableView作为画布依耐

为什么选择了tableView

  • 尝试是否能对绘制有candle的Cell进行复用;
  • 换个思维造轮子;

需要解决的问题:变纵向滚动为纵向滚动

  • 如图所示:在旋转时,是绕tableView中心进行旋转的,为了使旋转后的tableView的frame能够和superView的大小一致,那么就要使旋转前的tableView偏移一定距离;
    .
    .
    self.tableView.transform = CGAffineTransformMakeRotation(-M_PI/2);
    .
    .
    [self.view addSubview:self.tableView];
    .
    .
    [self.tableView mas_updateConstraints:^(MASConstraintMaker *make) {
              make.left.mas_equalTo((width-height)/2);
           make.top.mas_equalTo(-(width-height)/2);
           make.width.mas_equalTo(height);
             make.height.mas_equalTo(width);
    }];  复制代码
  • 优缺点:虽然进行到后面,蜡烛全是用CAShapeLayer+UIBeizerPath绘制的,cell的复用并没有起到多大的作用,并且旋转之后涉及到了tableView的x,y坐标在使用中的转换(这点大家注意下),但是能感到庆幸的是:使用了cell之后,在计算蜡烛横坐标的时候就是cell.indexPath.row*rowHeight;再者就是在缩放的时候,可以直接修改cell的高度就可以达到缩放的目的;

2.2 缩放

缩放有度

- (void)pinchAction:(UIPinchGestureRecognizer *)sender
{ 
    static CGFloat oldScale = 1.0f;
       CGFloat difValue = sender.scale - oldScale;
    NSLog(@"difValue=====%f",difValue);
       NSLog(@"oldScale=====%f",oldScale);
       if (ABS(difValue)>StockChartScaleBound) {

    CGFloat oldKlineWidth = self.candleWidth;
    // NSLog(@"原来的index%ld",oldNeedDrawStartIndex);
    self.candleWidth = oldKlineWidth * ((difValue > 0) ? (1+StockChartScaleFactor):(1-StockChartScaleFactor));
    oldScale = sender.scale;
    if (self.candleWidth < scale_MinValue) {

        self.candleWidth = scale_MinValue;
    }else if (self.candleWidth > scale_MaxValue)
    {
        self.candleWidth = scale_MaxValue;
    }
  }
}复制代码
  • 在每次缩放的时候,进行判断:
    1)只有触发的缩放大于某个预订值的时候才进行缩放
    2)控制每次缩放的比率;
    3)控制缩放的总体范围;

定点缩放

//这句话达到让tableview在缩放的时候能够保持缩放中心点不变;
//实现原理:在放大缩小的时候,计算出变化后和变化前中心点的距离,然后为了保持中心点的偏移值始终保持不变,就直接在原来的偏移上加减变换的距离
//ceil(centerPoint.y/oldKlineWidth)中心点前面的cell个数
//self.rowHeight-oldKlineWidth每个cell的高度的变化
CGFloat pinchOffsetY  = ceil(centerPoint.y/oldKlineWidth)*(self.candleWidth-oldKlineWidth)+oldNeedDrawStartPointY;
if (pinchOffsetY<0) {

    pinchOffsetY = 0;
}
if (pinchOffsetY+self.subViewWidth>self.kLineModelArr.count*self.candleWidth) {

    pinchOffsetY = self.kLineModelArr.count*self.candleWidth - self.subViewWidth;
}

[self.tableView setContentOffset:CGPointMake(0, pinchOffsetY)];复制代码

2.3 实现原理

宏观布局

两个关键参数:

  • 屏幕中显示的第一个蜡烛图的X坐标:

    NSUInteger leftArrCount = ABS(scrollViewOffsetX/self.candleWidth);
       _needDrawStartIndex = leftArrCount;      复制代码
  • 屏幕中能够显示的蜡烛个数:

     - (NSInteger)needDrawKlineCount
    {
        CGFloat width = self.subViewWidth;
        _needDrawKlineCount = ceil(width/self.candleWidth);
        return _needDrawKlineCount;
    }    复制代码

    根据这两个参数,起点和长度,就可以从数据源数组中准确的取出当前屏幕显示的蜡烛图的数据;然后滑动过程中实时计算并进行坐标转换

坐标相关换算

  • 极值:从当前屏幕显示的数据源数组获取的最大值和最小值
  • 单位价格所代表的像素值

      self.heightPerPoint = self.candleChartHeight/(self.maxAssert-self.minAssert);  复制代码
  • 开收高低值从价格转换成像素值

蜡烛绘制

CAShapeLayer+UIBeizerPath

2.4 Socket数据结算

详见ZXSocketDataReformer
针对服务器返回的数据格式:@"时间戳,实时价格";我们需要利用这一个个的数据自己构建蜡烛模型;

  • 第一模型构建:假如一分钟返回80个数据, 那么我们需要判断这一分钟开始的时候,并且取出这一分钟的第一个数据First,构建一个全新的模型A;模型A的开.收.高.低价都是第一数据的实时价格;
  • 模型替换:第一个模型构建之后,新的数据Second到来,那么我们比较得出高值和低值替换模型A的高低值,并且此时模型A的收盘价为数据Second的实时价格;
  • 模型结算(重点):
    结算:就是对个M1\M5\M15..中返回的所有数据自己结算出一个蜡烛模型,也就是四个值:开\收\高\低;
    结算的事件点判断方式:
    1)以socket返回数据的时间戳结算:这样结算在数据上不会有什么误差,但是时间上会有误差; eg:针对M1而言,假如在6'58''的时候返回此分蜡烛的最后一个值,如果用socket的时间作为结算的话,那么我们必须等到下一个socket返回值的时间戳到来才能结算,假如socket在7'00''-7'01''之间返回了数据的话,很好,我们可以直接结算上一个蜡烛,并且及时的创建一个新的蜡烛模型;但是数据并不是每次都会变化如此频繁,如果下一个数据的到来是7'16'';那么中间这18'',k线图会静止18'',那么相当于6'的那个蜡烛会延迟16''进行推进,便造成了时间上的误差;并且当数据涨停或者停牌的时候,socket数据没有变动,便不会返回数据,那么这个时间k线图也是不会有任何动作;
    2)以请求服务器时间戳结算:会导致数据上的误差;eg:在7'00''需要结算,但是这个时间socket在7'00''的时候返回了多个数据,但是结算的时候只会取到其中一个数据作为6'的收盘价,其他数据将遗留到下个蜡烛;
    解决:
    1)以socket和服务器的时间戳相结合的方式进行结算:我在ZXSocketDataReformer中也是这么做的,第一次请求服务器时间,然后本地安装定时器进行服务器时间同步; 由socket时间戳进行模型构造,到了整点,优先socket进行模型推进,如果整点的时候没有socket返回,就由服务器时间进行推进;
    2)定时器由服务器创建,最好就是在整点延迟1秒的时候,如果在00''-01''的时候已经有socket数据传送到移动端的话,那么就不需要推送假数据,如果没有socket数据产生,就推送一个假数据到移动端,告诉移动端,数据需要进行结算,移动端只需要用socket进行结算; (好吧,自己都绕晕了,如果要求不是那么高其实仅仅按照socket进行数据结算也够用了);

2.5 实时绘制

考虑如下情况:

代码大概是这样的 :

- (void)handleNewestCellWhenScrollToBottomWithNewKlineModel:(KlineModel *)klineModel

{

        //==0的时候需要插入一个新的cell;否则只需要刷新最后一个cell
    if (self.isNew) {

        KlineModel *newsDataModel =  [self calulatePositionWithKlineModel:klineModel];
        [self.kLineModelArr addObject:newsDataModel];

        double oldMax = self.maxAssert;
        double oldMin = self.minAssert;


        [self calculateNeedDrawKlineArr];
        [self calculateMaxAndMinValueWithNeedDrawArr:self.needDrawKlineArr];

        //不等的话就重绘
        if (oldMax<self.maxAssert||oldMin>self.minAssert) {


            dispatch_async(dispatch_get_main_queue(), ^{

                [self.tableView setContentOffset:CGPointMake(0, (self.kLineModelArr.count-self.needDrawKlineCount)*self.candleWidth+(self.needDrawKlineCount*self.candleWidth-self.subViewWidth))];
            });

            [self drawTopKline];

        }else{
            //否则就插入
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.kLineModelArr.count-1 inSection:0];
            dispatch_async(dispatch_get_main_queue(), ^{

                //先增加  再偏移
                [self.tableView beginUpdates];
                [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
                [self.tableView endUpdates];
                [self.tableView setContentOffset:CGPointMake(0, (self.kLineModelArr.count-self.needDrawKlineCount)*self.candleWidth+(self.needDrawKlineCount*self.candleWidth-self.subViewWidth))];
            });

            [self delegateToReturnKlieArr];
        }

    }else{


        KlineModel *newsDataModel =  [self calulatePositionWithKlineModel:klineModel];
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.kLineModelArr.count-1 inSection:0];

        [self.kLineModelArr replaceObjectAtIndex:self.kLineModelArr.count-1 withObject:newsDataModel];


        CGFloat oldMax = self.maxAssert;
        CGFloat oldMin = self.minAssert;


        [self calculateNeedDrawKlineArr];
        [self calculateMaxAndMinValueWithNeedDrawArr:self.needDrawKlineArr];
        //如果计算出来的最新的极值不在上一次计算的极值直接的话就重绘,否则就刷新最后一个即可
        if (oldMax<self.maxAssert||oldMin>self.minAssert) {

            [self drawTopKline];

        }else{

            dispatch_async(dispatch_get_main_queue(), ^{

                [self.tableView beginUpdates];
                [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
                [self.tableView endUpdates];
                [self delegateToReturnKlieArr];
            });

        }

    }

}复制代码

实际使用过程中在insert或者reloadrows的时候,偶尔会出现崩溃,暂时还没解决,索性改为了直接重绘全屏了(我内心也是拒绝的),若是你们也不甘心让它直接重绘,可到--ZXMainView.m--- (void)handleNewestCellWhenScrollToBottomWithNewKlineModel:(KlineModel *)klineModel;打开注释的方法,终结了它;

3.使用篇

3.1 基本使用

  • 基本的k线图的接入可以在demo中SecondStepViewController中看到,运行需在appDelegate中切换rootViewController;
  • JoinUpSocketViewController是接入socket实时绘制的demo,为了脱敏,控制器中的socket数据是随机产生的;
  • 具体的接入代码或者接口都可以在demo中看到,这里不做过多描述;

3.2 使用注意

3.2.1 历史数据转模型

(详见Reformer---ZXCandleDataReformer)
本地历史数据格式为:

/*
 @[@"时间戳,收盘价,开盘价,最高价,最低价,成交量",
 @"时间戳,收盘价,开盘价,最高价,最低价,成交量",
 @"时间戳,收盘价,开盘价,最高价,最低价,成交量",
 @"...",
 @"..."];
 */  复制代码

相应的模型转换格式为:

- (NSArray<KlineModel *>*)transformDataWithDataArr:(NSArray *)dataArr currentRequestType:(NSString *)currentRequestType
{
    self.currentRequestType = currentRequestType;
    //修改数据格式  →  ↓↓↓↓↓↓↓终点到啦↓↓↓↓↓↓↓↓↓  ←
    NSMutableArray *tempArr = [NSMutableArray array];
    __weak typeof(self) weakSelf = self;
    [dataArr enumerateObjectsUsingBlock:^(NSString *dataStr, NSUInteger idx, BOOL * _Nonnull stop) {

        NSArray *strArr = [dataStr componentsSeparatedByString:@","];
        KlineModel *model = [KlineModel new];
        model.timestamp  = [strArr[0] integerValue];
        model.timeStr = [weakSelf setTime:strArr[0]];
        model.closePrice = [strArr[1] doubleValue];
        model.openPrice = [strArr[2] doubleValue];
        model.highestPrice = [strArr[3] doubleValue];
        model.lowestPrice = [strArr[4] doubleValue];
        if (strArr.count>=6) {

            model.volumn = @([strArr[5] doubleValue]);
        }else{
            model.volumn = @(0);
        }

        model.x = idx;
        [tempArr addObject:model];
        model = nil;
    }];
    return tempArr;
}复制代码

历史数据模型转换需要使用者根据请求历史数据的实际格式进行转换;

3.2.2 Socket数据转模型

(详见ZXSocketDataReformer)
在socket结算的时候,若需要服务器时间结合socket返回的时间共同完成一个蜡烛的时候,这里需要改为获取服务器时间;

- (void)requestServiceTime:(void(^)(NSInteger timesamp))success
{

        //这里Demo使用的本地时间代替;正确的应该取下面的服务器时间
        NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0];
        NSTimeInterval timestamp = [date timeIntervalSince1970];
        success(timestamp);

        //获取服务器时间
    //    NSString *urlStr = @"服务器时间校对地址";
    //
    //    self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    //    self.manager.responseSerializer.acceptableContentTypes = [self.manager.responseSerializer.acceptableContentTypes setByAddingObject:@"text/html"];
    //    [self.manager GET:urlStr parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
    //
    //        NSString *time = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
    //        success([time integerValue]);
    //        //        NSLog(@"ServiceTime=%@",time);
    //
    //    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    //
    //    }];

}复制代码

3.2.3 布局修改

(详见ZXHeader.h)

整体布局修改的几个宏

/**
 * 价格坐标系在右边?YES->右边;NO->左边
 */
#define PriceCoordinateIsInRight YES     

/**
 * 蜡烛的信息配置的位置:YES->单独的view显示在view顶部;NO->弹框覆盖在蜡烛上
 */
#define IsDisplayCandelInfoInTop NO复制代码

约束

  • 其中CandleChartHeight、QuotaChartHeight、MiddleBlankSpace都是可变的,所以分了横竖屏分别定义;其他尺寸都是固定的。
  • 由于在内部就对各个控件的UI进行了组装,所以就预留了相关的尺寸约束或者颜色宏,可以在ZXHeader文件中进行修改,如若有不能修改之处,就只有去ZXAssemblyView.m文件中进行修改了;

从某种角度上来说,很多约束可以不改,但是宏中的TotalHeight必须根据项目需求进行修改

3.2.4 横竖屏适配

小技巧:因为我这里横屏之后是全屏并且隐藏了状态栏和导航栏的,为了旋转之后和竖屏的其他控件互不干扰,可以将assenblyView实例添加在self.view的最顶层,然后旋转过去之后就直接将其他控件覆盖在底层

4 其他问题

  1. 关于历史k线和socket衔接处暂未进行处理, 衔接还存在误差;
  2. 未知bug?待挖掘;
  3. k线图UI很简单,除了k线没有其他定制,但是接口都是完善的,主要是觉得关乎UI部分我做得越少,通用性就越高;
  4. 感谢Star;
  5. 有任何其他问题欢迎Issues或者简书留言;
  6. 超链:
2017-05-24 23:21:41 yunkai666 阅读数 2453

k线系列目录

查看目录请点击这儿


补充知识

K线虽然看上去复杂,但仔细观察就会知道几乎所有的k线无非由以下几种组成:

(1)蜡烛

蜡烛

(2)OHLC

OHLC

(3)线段

线段

(4)带状

带状

(5)圆形

圆形

除此之外还有其他的,都是比较少见的。

蜡烛绘制

补充了一点知识后,我们来进入主题。绘制上述所说的第一种,蜡烛。如果不懂什么是蜡烛,点这里补一下基础知识。

首先来分解蜡烛,其实它是由一个长方形加上两根线段(如果长方形是非空心的话,就是一根)。长方形是由开盘价坐标点、收盘价坐标点计算生成,线段是由最高价坐标点、最低价坐标点连接而成。

所以代码逻辑也就很简单了:

  1. 绘制一个长方形(实心)
  2. 然后再绘制一条线段

OK!上代码:

/**
 生成蜡烛Layer

 @param model 蜡烛坐标模型
 @return 返回layer
 */
+ (CAShapeLayer *)getCandleLayerWithPointModel:(YKCandlePointModel *)model
{
    //判断是否为涨跌
    BOOL isRed = model.oPoint.y >= model.cPoint.y ? YES : NO;

    //生成柱子的rect
    CGRect candleFrame = CGRectMake(isRed ? model.cPoint.x - 6 : model.oPoint.x-6,isRed ? model.cPoint.y : model.oPoint.y,12,ABS(model.oPoint.y - model.cPoint.y));

    UIBezierPath *path = [UIBezierPath bezierPathWithRect:candleFrame];

    //绘制上下影线
    [path moveToPoint:model.lPoint];
    [path addLineToPoint:model.hPoint];

    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.path = path.CGPath;

    //判断涨跌来设置颜色
    if (isRed)
    {
        //涨,设置红色
        layer.strokeColor = [UIColor redColor].CGColor;
        layer.fillColor = [UIColor redColor].CGColor;

    } else
    {
        //跌,设置绿色
        layer.strokeColor = [UIColor greenColor].CGColor;
        layer.fillColor = [UIColor greenColor].CGColor;
    }

    return layer;
}

代码效果如下:

蜡烛

剩下的几个图形,我们会再下一篇中进行介绍,点这里传送。

最后把Demo献上,点击下载。

2017-05-24 18:36:43 yunkai666 阅读数 1959

k线系列目录

查看目录请点击这儿


在分时线写完以后,我们开始接着学习如何写K线。其实k线并没有想象的那么复杂,还是像前几篇文章提供的思路一样,第一步、第二步、第三步…….把一个复杂的问题简单化,才是我们最需要做的事情。

首先看一下最终要完成的效果图:

K线

不管是现货还是股票类的k线,都是一样的。因为k线本质上是用来表示某个商品价格变动的情况(如果不了解k线基础知识,点击这儿)。上图的k线是由一根根蜡烛组成,分为主图、副图、主图指标、副图指标四部分,其中主图中还包含日期部分。

滑动的选择

Tip:如果读这一小节的内容感觉到云里雾里时,千万不要着急,其实完全可以略过这小节内容,跟着文章的思路往下走,等做完这部分内容时,可以再回顾一下。

在绘制之前,我们来讨论一个重要的问题,也是这篇文章说的一个重点,就是关于滑动的选择。

经过使用Reveal对市面上多个app的查看,以及自己在开发中踩了好多坑,在这里提供两种方式:

  1. 单个View
  2. 单个View + ScrollView

第一种指的是在主副图View上添加滑动手势,然后根据坐标产生的位移来实时刷新主副图View上的蜡烛。

第二种指的是在主副图View上方盖一个ScrollView,然后用户滑动ScrollView,根据ScrollView产生的偏移量来实时刷新主副图View上的蜡烛。

当选用第一种方式时,因为是添加滑动手势来获取的偏移量,所以这个偏移量不是非常线性,给用户的感觉是滑动起来不顺畅。解决办法是获取偏移量时,需要多次调试,每次获取的偏移量需要判断范围以及增加合适的倍数,尽量能保证View获得的偏移量线性。但使用这种方式的好处是不增加其他控件,在视图层次上很清晰。

当选用第二种方式时,用户能感知到的滑动体验很好,会感觉非常流畅。但有一个缺点不容忽视,那就是ScrollView的ContentSize是随着加载的蜡烛数量的增加而变大的,因为只有ContentSize和蜡烛数量相对应时,才可以滑动到最左或最右。所以,当一次性加载的蜡烛数量过高,会导致一个巨大的ScrollView存在。

边框的绘制

当明确了我们要达到的效果后,我们也可以仿照效果图把k线分为4部分:主图指标、主图、副图指标、副图。这里默认k线4部分是在同一个View上,并且是在这个view上面添加滑动手势。

绘制线段的方法在画分时线的文章中就已经讲过,这里不再重复。直接上代码:

//设置主图、主图指标、副图、副图指标rect
    _mainIndexRect = CGRectMake(0, 0, CGRectGetWidth(self.frame), mainIndexH);
    _mainRect = CGRectMake(0, mainIndexH, CGRectGetWidth(self.frame), (CGRectGetHeight(self.frame) - (mainIndexH + accessoryIndexH + dateH)) * mainFrameScale);
    _accessoryIndexRect = CGRectMake(0, mainIndexH + CGRectGetHeight(_mainRect)+dateH, CGRectGetWidth(self.frame), accessoryIndexH);
    _accessoryRect = CGRectMake(0, mainIndexH + CGRectGetHeight(_mainRect)+dateH+accessoryIndexH, CGRectGetWidth(self.frame), (CGRectGetHeight(self.frame) - (mainIndexH + accessoryIndexH + dateH)) * (1-mainFrameScale));


    CAShapeLayer *borderLayer = [CAShapeLayer layer];
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.bounds];

    [path moveToPoint:CGPointMake(0, mainIndexH)];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), mainIndexH)];

    [path moveToPoint:CGPointMake(0, CGRectGetMaxY(_mainRect))];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMaxY(_mainRect))];

    [path moveToPoint:CGPointMake(0, CGRectGetMinY(_accessoryIndexRect))];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMinY(_accessoryIndexRect))];

    [path moveToPoint:CGPointMake(0, CGRectGetMinY(_accessoryRect))];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMinY(_accessoryRect))];

    float mainUnitH = CGRectGetHeight(_mainRect) / 4.f;
    float mainUnitW = CGRectGetWidth(_mainRect) / 4.f;

    for (int idx = 1; idx <= 3; idx++)
    {
        //画3条横线
        [path moveToPoint:CGPointMake(0, mainIndexH + mainUnitH * idx)];
        [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), mainIndexH + mainUnitH * idx)];

        //画3条竖线
        [path moveToPoint:CGPointMake(idx * mainUnitW, mainIndexH)];
        [path addLineToPoint:CGPointMake(idx * mainUnitW, CGRectGetMaxY(_mainRect))];

        //画3条竖线
        [path moveToPoint:CGPointMake(idx * mainUnitW, CGRectGetMinY(_accessoryRect))];
        [path addLineToPoint:CGPointMake(idx * mainUnitW, CGRectGetMaxY(_accessoryRect))];
    }

    float accessoryUnitH = CGRectGetHeight(_accessoryRect) / 2.f;
    [path moveToPoint:CGPointMake(0, CGRectGetMaxY(_accessoryRect) - accessoryUnitH)];
    [path addLineToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetMaxY(_accessoryRect) - accessoryUnitH)];

    borderLayer.path = path.CGPath;
    borderLayer.lineWidth = 0.5f;
    borderLayer.strokeColor = [UIColor blackColor].CGColor;
    borderLayer.fillColor = [UIColor clearColor].CGColor;

    [self.layer addSublayer:borderLayer];

代码执行效果如下:

k线边框

需要源码的话,点这里

iOS股票k线组件源码

阅读数 3985

没有更多推荐了,返回首页