美文网首页
一次k线图的实践

一次k线图的实践

作者: 逗留 | 来源:发表于2017-12-01 09:55 被阅读54次

    最近花了大概有一个月的时间做了一个k线图以及相关的功能,因为是第一次接触这类项目,鉴于里面碎碎的东西比较多,而且大部分这类项目都是这个样子,所以这里做个小结,防止过几天忘了。

    demo地址

    https://github.com/Zyj163/kLine_demo
    代码可以直接运行,里面包含测试数据,运行后查看效果。

    这里主要的难点在与计算方面,绘制部分简单介绍,具体可查看demo。
    分时图比较简单,也不做介绍,具体可查看demo。

    结构梳理
    image.png

    项目整体结构:
    resource/testJson -> 测试数据
    draw -> 核心实现
    connector -> 数据转画笔(大部分计算在这里)
    drawer -> 画笔(绘图实现的地方)
    model -> 数据模型
    view -> 视图展示
    stock -> 组装层
    thief -> 工具类


    image.png image.png
    整体实现思路

    在外部获取数据后,交给stockView,stockView会缓存并管理所有数据,并组装具体视图(如YJKLineView)与对应的connector,具体视图是画布与手势等视图的集合,该类会接收手势视图的事件回调,来决定需要处理哪些数据,将需要处理的数据交给自己的connector生产出画笔,然后将画笔交给对应的画布绘制。

    首先从基础开始

    drawer
    • YJDrawer
      所有画笔需要遵守的协议,用以暴露公共方法
    @protocol YJDrawer<NSObject>
    
    /**
     绘制到指定上下文
    
     @param ctx 指定上下文
     */
    - (void)drawInContext:(CGContextRef)ctx;
    
    
    - (void)resetLayers;
    @property (nonatomic, copy, readonly) NSArray<CALayer *> *layers;
    
    @end
    

    具体每个画笔的实现查看源码,都是CG框架的一些东西,代码中还实现了CALayer及其子类代替CG框架的方案,目前性能尚不稳定,暂不考虑。

    • YJDrawerView
      接口主要主要使用的方法:
    /**
     根据传入的画笔重新绘制
    
     @param drawer 画笔集合
     */
    - (void)redrawWithDrawers:(NSArray<id<YJDrawer>> *)drawer, ...NS_REQUIRES_NIL_TERMINATION;
    

    核心代码:

    - (void)redrawWithDrawers:(NSArray<id<YJDrawer>> *)drawer, ...
    {
        NSMutableArray *drawers = [NSMutableArray array];
        if (drawer) {
            [drawers addObjectsFromArray:drawer];
            va_list args;
    
            NSArray *arg;
            va_start(args, drawer);
    
            while ((arg = va_arg(args, NSArray<id<YJDrawer>> *))) {
                [drawers addObjectsFromArray:arg];
            }
            va_end(args);
        }
        self.drawers = drawers;
    
        [self setNeedsDisplay];
    }
    
    - (void)drawRect:(CGRect)rect
    {
        if (!self.drawers || self.drawers.count == 0) return;
    
        if (_delegates.knowBeginDraw) {
            if (![self.delegate drawerViewShouldBeginDraw:self]) return;
        }
    
        CGContextRef ctx = UIGraphicsGetCurrentContext();
        for (id<YJDrawer> drawer in self.drawers) {
            [drawer drawInContext:ctx];
        }
    
        if (_delegates.knowEndDraw)
            [self.delegate drawerViewDidEndDraw:self];
    }
    

    首先是整合所传参数,并调用[self setNeedsDisplay];,然后在- (void)drawRect:(CGRect)rect中遍历画笔进行绘制。

    现在只要将画布添加到视图,将创建好的画笔交给画布即可完成绘制。创建画布不需多说,和普通视图是一样的,下面介绍如何创建画笔,这里是主要计算的地方,但是无法脱离业务逻辑,不过查看了大部分相关APP,大概意思都差不多。
    k线图:
    image.png

    如图所示,需要绘制的可以划分为

    • 背景横线/纵线
    • 左侧/中间文字
    • 上半部分包含蜡烛、均线,下半部分为柱状图

    这里只介绍蜡烛和均线,其他比较简单,具体实现可查看demo。

    思路:

    首先明确一点,这不是scrollView,只是在随着手势变化的时候不断变化数据,重新绘制界面,来达到类似scrollView的效果。横向移动是数据个数不变,截取范围变动;放大缩小是数据个数需要发生变化,并且以某一个数据为基准来向周围截取数据。

    不使用scrollView的原因包含以下几点:

    1.每根蜡烛的高度并不是固定的,随着手势的变化可能发生变化。
    2.界面内所展示的为整数个蜡烛,不存在半根的情况。
    3.滑动时,当滑动距离达到整数倍蜡烛宽度时才会滑动,并不像scrollView实时滑动。
    4.放大缩小时,当缩放比例达到一定值时才会重新绘制。并且在数据充足时,以两点横向中间点为准心,即中心点的那根蜡烛要保持位置不动,只变宽度,两边的点宽度位置都会变好。当一端数据不够时,改变为这一端不变,只对另一端做变化。当缩放到一定值时,需要将蜡烛图变为曲线图,曲线在一定范围内仍然有放大缩小功能,规则同上。

    首先不考虑变化,看下绘制静态界面
    API:

    @interface YJKLineDrawerConnector : YJDrawerConnector
    #pragma mark - readonly
    
    /**
     蜡烛间距
     */
    @property (nonatomic, assign, readonly) CGFloat candleSpace;
    
    /**
     每根蜡烛的宽度
     */
    @property (nonatomic, assign, readonly) CGFloat candleWidth;
    
    /**
     是否处于缩小蜡烛到点的状态
     */
    @property (nonatomic, assign, readonly) BOOL pointCandle;
    
    /**
     代替蜡烛的线画笔,当pointCandle为yes时可用
     */
    @property (nonatomic, copy, readonly) NSArray<id<YJDrawer>> *upTrends;
    
    /**
     蜡烛画笔集合
     */
    @property (nonatomic, copy, readonly) NSArray<YJCandleDrawer *> *candles;
    
    /**
     下视图画笔集合,成交量等
     */
    @property (nonatomic, copy, readonly) NSArray<YJShapeDrawer *> *shapes;
    
    /**
     均线画笔字典,键与MATypes对应
     */
    @property (nonatomic, copy, readonly) NSDictionary<NSNumber *, YJShapeDrawer *> *MAShapes;
    #pragma mark - readwrite
    
    /**
     一屏展示多少根蜡烛
     */
    @property (nonatomic, assign) NSInteger candleCount;
    
    /**
     中线宽度,默认1
     */
    @property (nonatomic, assign) CGFloat candleMiddleLineW;
    
    /**
     均线类型,例如@[@5, @10],默认@[@5, @10, @20, @30]
     */
    @property (nonatomic, copy) NSArray<NSNumber *> *MATypes;
    
    /**
     蜡烛可放大的最大宽度
     */
    @property (nonatomic, assign) CGFloat maxCandleWidth;
    
    /**
     蜡烛可缩小的最小宽度
     */
    @property (nonatomic, assign) CGFloat minCandleWidth;
    
    /**
     蜡烛默认初始宽度
     */
    @property (nonatomic, assign) CGFloat defaultCandleWidth;
    
    /**
     蜡烛间距可放大的最大值
     */
    @property (nonatomic, assign) CGFloat maxCandleSpace;
    
    /**
     蜡烛间距可缩小的最小值
     */
    @property (nonatomic, assign) CGFloat minCandleSpace;
    
    /**
     蜡烛间距默认值
     */
    @property (nonatomic, assign) CGFloat defaultCandleSpace;
    
    /**
     根据蜡烛个数和所在区域计算蜡烛宽度及蜡烛间距
    
     @param count 蜡烛个数
     @param rect 所在区域
     @param space 蜡烛间距(指针)
     @return 蜡烛宽度
     */
    - (CGFloat)calculateCandleWidthByCount:(NSInteger)count inRect:(CGRect)rect withSpace:(CGFloat *)space;
    
    /**
     根据所在区域和缩放比例计算合适的蜡烛个数
    
     @param rect 所在区域
     @param scale 缩放比例
     @return 蜡烛个数
     */
    - (NSInteger)suggestCandleCountInRect:(CGRect)rect withScale:(CGFloat)scale;
    
    /**
     查找包含某point的蜡烛画笔
    
     @param point 参考点
     @param find 是否找到(指针)
     @return 蜡烛画笔
     */
    - (NSInteger)indexOfCandleAtPoint:(CGPoint)point ifFind:(BOOL *)find;
    
    /**
     准备蜡烛画笔(包括切换为点的画笔),下视图画笔(暂时只有成交量,待扩展),上下纵线
    
     @param candleRect 蜡烛所在区域
     @param paddingV 蜡烛图区域上下内容边距
     @param volumeRect 下视图区域
     @param paddingV2 下视图上下内容边距
     */
    - (void)prepareCandlesInRect:(CGRect)candleRect paddingV:(CGFloat)paddingV volumeInRect:(CGRect)volumeRect paddingV2:(CGFloat)paddingV2;
    
    /**
     准备均线画笔
    
     @param rect 均线所在区域
     @param paddingV 均线所在区域的上下内容边距
     */
    - (void)prepareMAInRect:(CGRect)rect paddingV:(CGFloat)paddingV;
    
    /**
     单独准备蜡烛画笔(包含切换为点)
    
     @param rect 所在区域
     @param paddingV 内容边距
     @return 画笔集合
     */
    - (NSArray<id<YJDrawer>> *)prepareCandlesInRect:(CGRect)rect paddingV:(CGFloat)paddingV;
    
    /**
     单独准备蜡烛点画笔
    
     @param rect 所在区域
     @param paddingV 内容边距
     @return 画笔集合
     */
    - (NSArray<id<YJDrawer>> *)prepareUpTrendsInRect:(CGRect)rect paddingV:(CGFloat)paddingV;
    
    /**
     单独准备下视图画笔
    
     @param rect 所在区域
     @param paddingV 内容边距
     @return 画笔集合
     */
    - (NSArray<YJShapeDrawer *> *)prepareDownShapesInRect:(CGRect)rect paddingV:(CGFloat)paddingV;
    
    @end
    

    API中提供了单独计算某一部分的功能,但是考虑的性能的问题,将其中几部分合并到一起计算更佳,性能问题可以看我另一篇中的处理:http://www.jianshu.com/p/a480fea92094

    来看这个方法即可:

    - (void)prepareCandlesInRect:(CGRect)candleRect paddingV:(CGFloat)paddingV volumeInRect:(CGRect)volumeRect paddingV2:(CGFloat)paddingV2
    {
        CGFloat uValue = 0;
        if (![self prepareCandleRect:candleRect paddingV:paddingV uValue:&uValue]) {
            return;
        }
        
        CGFloat uVolumeValue = 0;
        [self prepareVolumeRect:volumeRect paddingV:paddingV2 uValue:&uVolumeValue];
        
        if (!self.pointCandle) {
            [self.MATypes enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                [self MADrawerForCount:obj.integerValue reset:YES];
            }];
        }
        
        NSMutableArray *vUpLines = [NSMutableArray array];
        NSMutableArray *vDownLines = [NSMutableArray array];
        __block YJStockModel *preModel = nil;
        
        [self.datas enumerateObjectsUsingBlock:^(YJStockModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            //ytext
            
            //candle
            [self prepareCandleWithStock:obj atIdx:idx uValue:uValue InRect:candleRect paddingV:paddingV];
            if (self.pointCandle) {
                //pointCandle
                
            } else {
                //MA
                [self.MATypes enumerateObjectsUsingBlock:^(NSNumber * _Nonnull type, NSUInteger index, BOOL * _Nonnull stop) {
                    [self prepareMALine:type.integerValue withStock:obj atIdx:idx uValue:uValue paddingV:paddingV midX:CGRectGetMidX(self.candleDrawers[idx].shapeDrawer.frame)];
                }];
            }
            
            //volume
            [self prepareVolumeWithStock:obj atIdx:idx uValue:uVolumeValue inRect:volumeRect paddingV:paddingV2];
            
            //vline
            id<YJDrawer> vlineDrawer = [self prepareVLineWithStock:obj atIdx:idx inRect:candleRect uSpace:self.candleSpace + self.candleWidth preModel:preModel];
            [vUpLines addObject:vlineDrawer];
            
            id<YJDrawer> vlineDrawer2 = [self prepareVLineWithLine:(YJLineDrawer *)vlineDrawer inRect:volumeRect];
            [vDownLines addObject:vlineDrawer2];
            
            //hline
        }];
        
        self.candles = self.candleDrawers;
        self.shapes = self.shapeDrawers;
        
        if (!self.pointCandle) {
            self.MAShapes = self.MAShapeDrawers;
        }
        
        self.upVLines = vUpLines;
        self.downVLines = vDownLines;
    }
    

    这是一个集中处理的方法,首先第一个准备蜡烛画笔

    - (BOOL)prepareCandleRect:(CGRect)rect paddingV:(CGFloat)paddingV uValue:(CGFloat *)uValue
    {
        BOOL goOn = [self prepareCandleWidthAndSpaceInRect:rect];
        if (!goOn) return NO;
        if (self.pointCandle) {
            self.MAShapes = nil;
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                [self prepareUpTrendsInRect:rect paddingV:paddingV];
            });
        } else {
            _upTrends = nil;
        }
        CGFloat totalH = CGRectGetHeight(rect) - paddingV * 2;
        //点->值
        *uValue = totalH / (self.upYMaxValue - self.upYMinValue);
        //从右往左
        if (self.candleDrawers.count > self.datas.count) {
            [self.candleDrawers removeObjectsInRange:(NSRange){0, self.candleDrawers.count-self.datas.count}];
        }
        return YES;
    }
    

    在这个方法中,首先计算蜡烛宽度与蜡烛间距,如果蜡烛宽度达到了要变蜡烛为线的阀值,则清空均线画笔,并且设置_pointCandle为YES,在后续生产中用来决定生产哪种画笔;反之,如果是蜡烛,则清空线的画笔,设置_pointCandle为NO。如果蜡烛宽度小到极限,这里设置极限值为1,则不再往下进行。另外通过取值范围以及所要绘制到的区域,做映射,获取1个单位值对应界面上的几个p,CGFloat totalH = CGRectGetHeight(rect) - paddingV * 2; //点->值 *uValue = totalH / (self.upYMaxValue - self.upYMinValue);

    - (BOOL)prepareCandleWidthAndSpaceInRect:(CGRect)rect
    {
        CGFloat candleSpace = 0;
        self.candleWidth = [self calculateCandleWidthByCount:self.candleCount inRect:rect withSpace:&candleSpace];
        self.candleSpace = candleSpace;
        
        if (self.candleWidth >= self.candleLimitWidth) {
            if (self.candleWidth <= self.minCandleWidth) {
                _pointCandle = YES;
                
                self.MAShapes = nil;
            } else {
                _pointCandle = NO;
                _upTrends = nil;
            }
        } else {
            return NO;
        }
        return YES;
    }
    

    计算蜡烛宽度以及间距,放到后面说。
    然后是准备下面的成交量画笔,略。
    如果要展示均线,则准备均线画笔,具体可查看代码,比较简单。
    然后遍历数据,开始生产画笔。

    - (id<YJDrawer>)prepareCandleWithStock:(YJStockModel *)stock atIdx:(NSUInteger)idx uValue:(CGFloat)uValue InRect:(CGRect)rect paddingV:(CGFloat)paddingV
    {
        CGFloat candleH = ABS(stock.nOpen.doubleValue - stock.nClose.doubleValue) * uValue ?: self.defaultLineWidth;
        CGFloat candleX = CGRectGetMaxX(rect) - (self.candleSpace + self.candleWidth) * (idx + 1);
        CGFloat candleY = (self.upYMaxValue - MAX(stock.nOpen.doubleValue, stock.nClose.doubleValue)) * uValue + CGRectGetMinY(rect) + paddingV;
        
        CGRect candleRect = CGRectMake(candleX, candleY, self.candleWidth, candleH);
        
        YJCandleDrawer *candleDrawer = [self candleDrawerAtIdx:idx];
        
        candleDrawer.shapeDrawer.frame = candleRect;
        if (stock.nOpen < stock.nClose) {
            candleDrawer.shapeDrawer.drawType = YJShapDrawTypeStroke;
        } else {
            candleDrawer.shapeDrawer.drawType = YJShapDrawTypeFill;
        }
        
        CGFloat lineW = self.candleMiddleLineW;
        CGFloat lineY = (self.upYMaxValue - stock.nHigh.doubleValue) * uValue + CGRectGetMinY(rect) + paddingV;
        CGFloat lineH = (stock.nHigh.doubleValue - stock.nLow.doubleValue) * uValue;
        CGPoint highestPoint = CGPointMake(candleX + self.candleWidth/2, lineY);
        CGPoint lowestPoint = CGPointMake(candleX + self.candleWidth/2, lineY + lineH);
        
        candleDrawer.lineDrawer.startPoint = highestPoint;
        candleDrawer.lineDrawer.endPoint = lowestPoint;
        candleDrawer.lineDrawer.width = lineW;
        
        candleDrawer.color = [YJHelper klineColor:stock];
        
        return candleDrawer;
    }
    
    蜡烛画笔思路:
    • 高度,先得到蜡烛业务值的高度差,然后再转换为像素值
      CGFloat candleH = ABS(stock.nOpen.doubleValue - stock.nClose.doubleValue) * uValue ?: self.defaultLineWidth;
      如果开盘值和收盘值相等,则只画一根横线。
    • x坐标,因为蜡烛是从右到左与数据来一一对应,所以要从右边开始放
      CGFloat candleX = CGRectGetMaxX(rect) - (self.candleSpace + self.candleWidth) * (idx + 1);
    • y坐标,同样先得到业务值的差值,再转化为像素
      CGFloat candleY = (self.upYMaxValue - MAX(stock.nOpen.doubleValue, stock.nClose.doubleValue)) * uValue + CGRectGetMinY(rect) + paddingV;
      到此,便可得出蜡烛的frame,然后是设置颜色,填充方式等。最后,计算阴阳线,同样先获取业务值,再转化为像素值,横向中点和蜡烛重合即可。
      计算均线,可查看代码,思路比较简单。
    补充一下计算蜡烛宽度以及蜡烛间距的方法
    - (CGFloat)calculateCandleWidthByCount:(NSInteger)count inRect:(CGRect)rect withSpace:(CGFloat *)space
    {
        CGFloat totalW = CGRectGetWidth(rect);
        //设定每屏最后留蜡烛间距的空隙,开始不留空隙
        CGFloat candleWAndSpaceW = totalW / count;
        
        CGFloat scale = (self.maxCandleWidth - self.minCandleWidth) / (self.maxCandleSpace - self.minCandleSpace);
        
        CGFloat spaceW = (candleWAndSpaceW - self.minCandleWidth + self.minCandleSpace * scale) / (scale + 1);
        CGFloat candleW = candleWAndSpaceW - spaceW;
        if (space) {
            *space = spaceW;
        }
        return candleW;
    }
    

    这里有几个阀值,蜡烛最大宽度,最小宽度,间距最大宽度,最小宽度。比如蜡烛最大为7,最小为3,间距最大为3,最小为0,他们之间的对应关系是什么,好像是一道初中题,搞了半天才想明白。。。这里需要传入的参数是蜡烛个数以及界面展示区域,首先通过区域和个数可以得到蜡烛宽+间距宽的值,然后再用初中生的公式算一下,即可得到各自对应的值。

    下一步,如何结合手势来变化数据:
    • YJGestureView
      该视图中包含了各种需要的手势(tap、pan、longPress、pinch),以及滑动结束的减速效果,并且还集成了左右加载,长按显示十字光标,加载loading。
    pan手势:

    其中
    - (void)dealWithPan:(UIPanGestureRecognizer *)ges translation:(CGFloat)translation
    方法中在没有开启左右加载时,只是将事件传递出去

    if (_delegates.knowPan) {
            if ([self.delegate gestureView:self shouldResetDisWithMoving:translation]) {
                [ges setTranslation:CGPointZero inView:ges.view];
            }
        }
    

    外界需要返回YES/NO来决定是否重置位移。
    在YJKLineView中接收该事件。

    - (BOOL)gestureView:(YJGestureView *)gestureView shouldResetDisWithMoving:(CGFloat)distance
    {
        if (self.currentIdx == self.datas.count - self.connector.candleCount && distance > 0) {
            [gestureView startDragOnLeftEdge];
            return YES;
        } else {
            [gestureView endDragOnLeftEdge];
        }
        
        if (self.currentIdx == 0 && distance < 0) {
            [gestureView startDragOnRightEdge];
            return YES;
        } else {
            [gestureView endDragOnRightEdge];
        }
        
        self.tmpDistance += distance;
        NSInteger i = floor(self.tmpDistance / (self.connector.candleWidth + self.connector.candleSpace));
        NSInteger idx = i + self.preIdx;
        if (ABS(idx - self.currentIdx) < 1) return YES;
        self.currentIdx = idx;
        if (self.currentIdx < 0) self.currentIdx = 0;
        if (self.currentIdx > self.datas.count - self.connector.candleCount) self.currentIdx = self.datas.count - self.connector.candleCount;
        
        [self draw];
        return YES;
    }
    

    如果数据达到了边缘值,则开启左右加载模式,其中self.currentIdx表示左边第一个值在数据数组中的下标。如果没有达到边缘值,通过一个变量来保存位移累计的距离,用这个距离除以蜡烛宽+蜡烛间距来得到需要移动的个数,这里需要做一个判断,如果要移动的个数+之前手势结束时所得到的self.currentIdx,这里用self.preIdx接收上次手势结束时self.currentIdx的值,该和减去当前self.currentIdx,如果相差不足1,则不发生变化,返回YES,继续累加。反之,修改当前self.currentIdx为该和,首先确保self.currentIdx最小为0,如果self.currentIdx > self.datas.count - self.connector.candleCount,即以self.currentIdx计算时,所剩数据已经不够展示所要展示的蜡烛个数是,修改self.currentIdx = self.datas.count - self.connector.candleCount;通过self.currentIdx和self.connector.candleCount即可从datas截取所需要的数据,交给connector去处理了。
    至于减速效果,这个比较简单

    if (ges.state == UIGestureRecognizerStateBegan) {
            self.currentXVelocity = 0;
        }
        if (ges.state == UIGestureRecognizerStateEnded && !self.dragOnRightEdge && !self.dragOnLeftEdge) {
            self.currentXVelocity = [ges velocityInView:ges.view].x;
            if (ABS(self.currentXVelocity) > 5) {
                CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(followVelocity:)];
                [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
            }
        }
    

    在pan手势结束时,获取速度,如果需要减速效果,则开启一个定时器CADisplayLink,用这个是为了保持与界面刷新频率一致,在这个定时器中按照一定比例缩小这个速度,并将self.currentXVelocity/60.做为位移值传递出去,剩下的处理和上面的一致,当速度小到一定值时,停止定时器即可。

    - (void)followVelocity:(CADisplayLink *)link
    {
        self.currentXVelocity *= 0.97;
        if (ABS(self.currentXVelocity/60.) < 1) {
            [link invalidate];
            self.currentXVelocity = 0;
        }
        [self dealWithPan:self.panGes translation:self.currentXVelocity/60.];
    }
    

    然后就是需要做一些判断处理,一些细微的东西,具体看下代码吧。
    左右加载实现思路:
    在YJGestureView上加一层view来做左右移动效果,加载指示图至于该图下面,何时开启左右加载以及结束都需要数据来决定,所以在YJGestureView中暴露了一下接口来开启和关闭

    - (void)startDragOnLeftEdge;
    - (void)endDragOnLeftEdge;
    
    - (void)startDragOnRightEdge;
    - (void)endDragOnRightEdge;
    

    当达到阀值的时候,会触发对应的加载动作,这是保持位移变化即可。对应加载动作暴露一下方法。

    - (void)beginLeftRefreshing;
    - (void)endLeftRefreshing;
    
    - (void)beginRightRefreshing;
    - (void)endRightRefreshing;
    

    以左加载为例

    if (self.dragOnLeftEdge) {
            [self leftRefreshView];
            CGRect frame = CGRectOffset(self.scrollView.frame, translation, 0);
            
            if ((ges.state == UIGestureRecognizerStatePossible || ges.state == UIGestureRecognizerStateEnded) && self.currentXVelocity < 100) {
                if (self.hasLeftRefreshView && frame.origin.x > CGRectGetWidth(self.leftRefreshView.frame)) {
                    [self beginLeftRefreshing];
                } else {
                    [self endDragOnLeftEdge];
                }
                return;
            }
            
            if (frame.origin.x <= 0) {
                [self endDragOnLeftEdge];
            } else {
                self.scrollView.frame = frame;
                [ges setTranslation:CGPointZero inView:ges.view];
                if (self.currentXVelocity > 100 && (frame.origin.x > CGRectGetWidth(self.leftRefreshView.frame) * 2)) {
                    self.currentXVelocity = 0;
                }
                return;
            }
        }
    

    如果开启了左加载,首先懒加载左加载指示图(可通过代理方法自定义),然后判断当手势取消并且速度小于某值时,判断如果有左加载视图(self.hasLeftRefreshView),如果有则判断如果左视图已经全部显示出来,则开始左加载,否则自动关闭左加载模式。如果手势没有结束,则对视图左移动操作。

    pinch手势:

    为了实现之前说的那种放大缩小的效果,在pinch开始时,需要记录中心点的位置以及两点的距离,当两点移动时,用新的距离/开始的距离,得到当前相对于开始时的倍数,然后将这个比例和中心点传递出去,并且可以由外面决定是否重新设置中心点和开始的距离。
    在YJKLineView中接收这个事件。

    - (BOOL)gestureView:(YJGestureView *)gestureView shouldResetScale:(CGFloat)scale centerPoint:(CGPoint)centerPoint
    {
        NSInteger count = [self.connector suggestCandleCountInRect:self.klineView.bounds withScale:scale];
        if (count == self.connector.candleCount) return YES;
        
        CGFloat space = 0;
        CGFloat expectW = [self.connector calculateCandleWidthByCount:count inRect:self.klineView.bounds withSpace:&space];
        if (expectW > self.connector.maxCandleWidth ||
            expectW < 2)
            return YES;
        
        if (!self.preFindCandleIndex) {
            BOOL find = NO;
            NSInteger index = [self.connector indexOfCandleAtPoint:centerPoint ifFind:&find];
            if (find) {
                self.preFindCandleIndex = index + self.currentIdx;
            }
        }
        if (self.preFindCandleIndex) {
            //获取candle中心点在屏幕的位置
            CGFloat candleCenterX = centerPoint.x;
            //计算重新绘制后这个candle右边可以有几个candle
            CGFloat w = space + expectW;
            NSInteger rightCount = floor((CGRectGetWidth(self.klineView.bounds) - candleCenterX - w/2)/w);
            NSInteger rightStartIndex = self.preFindCandleIndex - rightCount;
            self.currentIdx = rightStartIndex;
        }
        if (self.currentIdx < 0) {
            self.currentIdx = 0;
        }
        if (self.currentIdx + count > self.datas.count) {
            self.currentIdx = self.datas.count - count;
        }
        if (self.currentIdx < 0) {
            self.currentIdx = 0;
        }
        if (count > self.datas.count) {
            count = self.datas.count - self.currentIdx;
        }
        
        self.connector.candleCount = count;
        [self draw];
        return YES;
    }
    

    首先通过scale计算scale后大概的蜡烛个数,如果和现在相等,则返回并重置。否则计算期望的蜡烛宽度及间距,判断是否触发临界值,如果触发,同样不做处理。否则先通过中间点找到当前界面包含该点的蜡烛,加上self.currentIdx即为在数据数组中下标,以该蜡烛为基准,计算左右各可以放几根蜡烛,修改self.currentIdx,同样确保self.currentIdx最小为0,同样保证显示完全,再次保证最小为0,如果展示不全,则能展示多少展示多少,修改蜡烛个数。同样通过self.currentIdx和self.connector.candleCount即可从datas截取所需要的数据,交给connector去处理了。

    tap和longPress手势没有过多处理逻辑,略。

    以上为核心思路及部分代码,具体还是要查看demo中的完整代码。如果哪位同仁有过类似经验或者更好的思路,欢迎评论交流!

    相关文章

      网友评论

          本文标题:一次k线图的实践

          本文链接:https://www.haomeiwen.com/subject/sojvbxtx.html