最近花了大概有一个月的时间做了一个k线图以及相关的功能,因为是第一次接触这类项目,鉴于里面碎碎的东西比较多,而且大部分这类项目都是这个样子,所以这里做个小结,防止过几天忘了。
demo地址
https://github.com/Zyj163/kLine_demo
代码可以直接运行,里面包含测试数据,运行后查看效果。
这里主要的难点在与计算方面,绘制部分简单介绍,具体可查看demo。
分时图比较简单,也不做介绍,具体可查看demo。
结构梳理
项目整体结构:
resource/testJson -> 测试数据
draw -> 核心实现
connector -> 数据转画笔(大部分计算在这里)
drawer -> 画笔(绘图实现的地方)
model -> 数据模型
view -> 视图展示
stock -> 组装层
thief -> 工具类
整体实现思路
在外部获取数据后,交给stockView,stockView会缓存并管理所有数据,并组装具体视图(如YJKLineView)与对应的connector,具体视图是画布与手势等视图的集合,该类会接收手势视图的事件回调,来决定需要处理哪些数据,将需要处理的数据交给自己的connector生产出画笔,然后将画笔交给对应的画布绘制。
首先从基础开始
drawer
- YJDrawer
所有画笔需要遵守的协议,用以暴露公共方法
@protocol YJDrawer
/**
绘制到指定上下文
@param ctx 指定上下文
*/
- (void)drawInContext:(CGContextRef)ctx;
- (void)resetLayers;
@property (nonatomic, copy, readonly) NSArray *layers;
@end
具体每个画笔的实现查看源码,都是CG框架的一些东西,代码中还实现了CALayer及其子类代替CG框架的方案,目前性能尚不稳定,暂不考虑。
- YJDrawerView
接口主要主要使用的方法:
/**
根据传入的画笔重新绘制
@param drawer 画笔集合
*/
- (void)redrawWithDrawers:(NSArray> *)drawer, ...NS_REQUIRES_NIL_TERMINATION;
核心代码:
- (void)redrawWithDrawers:(NSArray> *)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> *))) {
[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 drawer in self.drawers) {
[drawer drawInContext:ctx];
}
if (_delegates.knowEndDraw)
[self.delegate drawerViewDidEndDraw:self];
}
首先是整合所传参数,并调用[self setNeedsDisplay];
,然后在- (void)drawRect:(CGRect)rect
中遍历画笔进行绘制。
现在只要将画布添加到视图,将创建好的画笔交给画布即可完成绘制。创建画布不需多说,和普通视图是一样的,下面介绍如何创建画笔,这里是主要计算的地方,但是无法脱离业务逻辑,不过查看了大部分相关APP,大概意思都差不多。
k线图:
如图所示,需要绘制的可以划分为
- 背景横线/纵线
- 左侧/中间文字
- 上半部分包含蜡烛、均线,下半部分为柱状图
这里只介绍蜡烛和均线,其他比较简单,具体实现可查看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> *upTrends;
/**
蜡烛画笔集合
*/
@property (nonatomic, copy, readonly) NSArray *candles;
/**
下视图画笔集合,成交量等
*/
@property (nonatomic, copy, readonly) NSArray *shapes;
/**
均线画笔字典,键与MATypes对应
*/
@property (nonatomic, copy, readonly) NSDictionary *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 *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> *)prepareCandlesInRect:(CGRect)rect paddingV:(CGFloat)paddingV;
/**
单独准备蜡烛点画笔
@param rect 所在区域
@param paddingV 内容边距
@return 画笔集合
*/
- (NSArray> *)prepareUpTrendsInRect:(CGRect)rect paddingV:(CGFloat)paddingV;
/**
单独准备下视图画笔
@param rect 所在区域
@param paddingV 内容边距
@return 画笔集合
*/
- (NSArray *)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 vlineDrawer = [self prepareVLineWithStock:obj atIdx:idx inRect:candleRect uSpace:self.candleSpace + self.candleWidth preModel:preModel];
[vUpLines addObject:vlineDrawer];
id 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)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中的完整代码。如果哪位同仁有过类似经验或者更好的思路,欢迎评论交流!