美学原理K线三正库__ZXKline

Animation.gif

github__ZXKline

1.简介篇

  • 蜡烛图和山形图绘制切换
  • 5栽指标绘制切换
  • 加上论蜡烛和指标线详情展示
  • 触底加载重多
  • 实时蜡烛绘制实现
  • 二级横屏和炬三级横屏

fullScreen1.png

fullScreen2.png

  • 适配两种布局

UI1.png

UI2.png

2.原理篇

2.1 tableView作为画布依耐

缘何选了tableView

  • 品味是否能针对绘制出candle的Cell进行复用;
  • 转换个考虑去轮子;

待缓解之题目:变纵向滚动为纵向滚动

旋转.png

  • 如图所示:在转时,是绕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 实时绘制

考虑如下情况:

实时绘制.png

代码大概是这般的 :

- (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 历史数据转模型

(详见ReformerZXCandleDataReformer)
地方历史数据格式为:

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

对应的型转换格式为:

- (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

约束

布局.png

  • 其间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. 超链:

  7. github地址ZXKline

  8. Json转模型Mac版ESJsonFormatForMac