一次k线图的实践

最近花了大概有一个月的时间做了一个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中的完整代码。如果哪位同仁有过类似经验或者更好的思路,欢迎评论交流!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容