You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

978 lines
43 KiB

1 month ago
1 month ago
  1. //
  2. // ChartViewController.m
  3. // HC
  4. //
  5. // Created by huilinLi on 2025/11/27.
  6. //
  7. #import "ChartViewController.h"
  8. #import "StockKLineModel.h"
  9. #import "StockInfoCardView.h"
  10. #import "ChartAPI.h"
  11. static CGFloat kLineUnitWidth = 5.0; // 单位宽度
  12. static CGFloat kPriceLabelAreaWidth = 35.0;
  13. static CGFloat kPriceLabelPadding = 10.0;
  14. static CGFloat kKLineHeight = 300.0;
  15. static CGFloat kContainerHeight = 120.0;
  16. @interface ChartViewController () <UIGestureRecognizerDelegate, UIScrollViewDelegate>
  17. @property (nonatomic, strong) StockInfoCardView *cardContainer;
  18. @property (nonatomic, strong) UIView *kSelectContainer;
  19. @property (nonatomic, strong) UIView *kLineContainer;
  20. @property (nonatomic, strong) UIView *macdContainer;
  21. @property (nonatomic, strong) UIView *kdjContainer;
  22. @property (nonatomic, strong) UIView *otherContainer;
  23. @property (nonatomic, strong) UIScrollView *kLineScrollView;
  24. @property (nonatomic, strong) NSArray *kLineData;
  25. @property (nonatomic, assign) NSInteger visibleKLineCount;
  26. @property (nonatomic, strong) NSArray<UILabel *> *priceLabels;
  27. @property (nonatomic, strong) UILabel *startDateLabel;
  28. @property (nonatomic, strong) UILabel *endDateLabel;
  29. @property (nonatomic, strong) UILabel *maLegendLabel; // MA5
  30. @property (nonatomic, strong) UILabel *macdLegendLabel; // DIF DEA
  31. @property (nonatomic, strong) UILabel *kdjLegendLabel; // K D
  32. @property (nonatomic, strong) UILabel *highPriceMarkLabel;
  33. @property (nonatomic, strong) UILabel *lowPriceMarkLabel;
  34. @property (nonatomic, assign) CGFloat currentMaxPrice;
  35. @property (nonatomic, assign) CGFloat currentMinPrice;
  36. @property (nonatomic, assign) CGFloat macdMaxValue;
  37. @property (nonatomic, assign) CGFloat macdMinValue;
  38. @property (nonatomic, assign) CGFloat kdjMaxValue;
  39. @property (nonatomic, assign) CGFloat kdjMinValue;
  40. // K线图层
  41. @property (nonatomic, strong) CAShapeLayer *redCandleLayer;
  42. @property (nonatomic, strong) CAShapeLayer *greenCandleLayer;
  43. // 均线图层
  44. @property (nonatomic, strong) CAShapeLayer *ma5Layer;
  45. @property (nonatomic, strong) CAShapeLayer *ma10Layer;
  46. @property (nonatomic, strong) CAShapeLayer *ma30Layer;
  47. // MACD图层
  48. @property (nonatomic, strong) CAShapeLayer *macdRedBarLayer;
  49. @property (nonatomic, strong) CAShapeLayer *macdGreenBarLayer;
  50. @property (nonatomic, strong) CAShapeLayer *difLayer;
  51. @property (nonatomic, strong) CAShapeLayer *deaLayer;
  52. @property (nonatomic, strong) CAShapeLayer *zeroLineLayer;
  53. // KDJ图层
  54. @property (nonatomic, strong) CAShapeLayer *kLayer;
  55. @property (nonatomic, strong) CAShapeLayer *dLayer;
  56. @property (nonatomic, strong) CAShapeLayer *jLayer;
  57. @property (nonatomic, strong) UIView *crossVerticalLine;
  58. @property (nonatomic, strong) UIView *crossHorizontalLine;
  59. @property (nonatomic, strong) UILabel *crossPriceLabel;
  60. @property (nonatomic, strong) UILabel *crossDateLabel;
  61. @property (nonatomic, assign) BOOL isLongPressing;
  62. @end
  63. @implementation ChartViewController
  64. #pragma mark - viewDidLoad
  65. - (void)viewDidLoad {
  66. [super viewDidLoad];
  67. self.view.backgroundColor = [UIColor blackColor];
  68. self.visibleKLineCount = 40;
  69. [self generateMockData];
  70. [self calculateMA];
  71. [self calculateMACD];
  72. [self calculateKDJ];
  73. [self setupSubviews];
  74. [self setupConstraints];
  75. [self setupDojiViews];
  76. CGFloat chartVisibleWidth = self.view.bounds.size.width - kPriceLabelAreaWidth;
  77. if (chartVisibleWidth > 0) {
  78. kLineUnitWidth = chartVisibleWidth / self.visibleKLineCount;
  79. }
  80. dispatch_async(dispatch_get_main_queue(), ^{
  81. CGFloat totalChartWidth = kLineUnitWidth * self.kLineData.count;
  82. self.kLineScrollView.contentSize = CGSizeMake(totalChartWidth, self.kLineContainer.bounds.size.height);
  83. // 滚到最右侧
  84. if (self.kLineScrollView.contentSize.width > self.kLineScrollView.bounds.size.width) {
  85. CGPoint offset = CGPointMake(self.kLineScrollView.contentSize.width - self.kLineScrollView.bounds.size.width, 0);//(横向偏移,纵向)
  86. [self.kLineScrollView setContentOffset:offset animated:NO];// 禁用动画效果
  87. }
  88. [self drawAllCharts];
  89. });
  90. }
  91. #pragma mark - 绘制
  92. - (void)drawAllCharts {
  93. if (self.kLineData.count == 0) return;
  94. // 获取当前ScrollView滚到了哪里
  95. CGFloat contentOffsetX = self.kLineScrollView.contentOffset.x;
  96. // 可视区域宽度
  97. CGFloat visibleWidth = self.kLineScrollView.bounds.size.width;
  98. NSInteger startIndex = floor(contentOffsetX / kLineUnitWidth);// 向下取整
  99. NSInteger endIndex = ceil((contentOffsetX + visibleWidth) / kLineUnitWidth);// 向上取整
  100. // 防止越界
  101. if (startIndex < 0) startIndex = 0;
  102. if (endIndex >= self.kLineData.count) endIndex = self.kLineData.count - 1;
  103. if (startIndex > endIndex) endIndex = startIndex;
  104. //start,end和偏移量传给所有子视图
  105. [self drawKLineChartFromIndex:startIndex toIndex:endIndex contentOffset:contentOffsetX];
  106. [self drawMACDChartFromIndex:startIndex toIndex:endIndex contentOffset:contentOffsetX];
  107. [self drawKDJChartFromIndex:startIndex toIndex:endIndex contentOffset:contentOffsetX];
  108. // 更新指标参数
  109. if (!self.isLongPressing) {
  110. NSInteger visibleEndIndex = endIndex;
  111. CGFloat visibleRightX = contentOffsetX + self.kLineScrollView.bounds.size.width;
  112. NSInteger calculatedIndex = floor(visibleRightX / kLineUnitWidth) - 1;
  113. if (calculatedIndex < 0) calculatedIndex = 0;
  114. if (calculatedIndex >= self.kLineData.count) calculatedIndex = self.kLineData.count - 1;
  115. visibleEndIndex = calculatedIndex;
  116. [self updateLegendsWithIndex:visibleEndIndex];
  117. }
  118. }
  119. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  120. if (self.isLongPressing) {
  121. self.isLongPressing = NO;
  122. self.crossVerticalLine.hidden = YES;
  123. self.crossHorizontalLine.hidden = YES;
  124. self.crossPriceLabel.hidden = YES;
  125. self.crossDateLabel.hidden = YES;
  126. }
  127. [self drawAllCharts];
  128. }
  129. #pragma mark - 绘制K线+均线
  130. - (void)drawKLineChartFromIndex:(NSInteger)startIndex toIndex:(NSInteger)endIndex contentOffset:(CGFloat)contentOffsetX {
  131. // 图层大小=容器大小
  132. CGRect bounds = self.kLineContainer.bounds;
  133. self.redCandleLayer.frame = bounds;
  134. self.greenCandleLayer.frame = bounds;
  135. self.ma5Layer.frame = bounds;
  136. self.ma10Layer.frame = bounds;
  137. self.ma30Layer.frame = bounds;
  138. CGFloat chartHeight = bounds.size.height;
  139. // 最高最低价
  140. CGFloat maxPrice = 0, minPrice = 0;
  141. NSInteger maxIndex = -1, minIndex = -1;
  142. [self calculateMinMaxPriceForStartIndex:startIndex endIndex:endIndex maxPrice:&maxPrice minPrice:&minPrice maxIndex:&maxIndex minIndex:&minIndex];//将计算出的最高价赋值给max/minPrice
  143. self.currentMaxPrice = maxPrice;
  144. self.currentMinPrice = minPrice;
  145. CGFloat priceRange = maxPrice - minPrice;
  146. if (priceRange <= 0) {
  147. self.redCandleLayer.path = nil;
  148. return;
  149. }
  150. // 更新价格和日期
  151. [self updatePriceLabelsWithMaxPrice:maxPrice minPrice:minPrice];
  152. [self updateDateLabelsStartIndex:startIndex endIndex:endIndex];
  153. // 准备路径
  154. UIBezierPath *redPath = [UIBezierPath bezierPath];// 涨的路径
  155. UIBezierPath *greenPath = [UIBezierPath bezierPath];// 跌的路径
  156. UIBezierPath *ma5Path = [UIBezierPath bezierPath];// 均线路径
  157. UIBezierPath *ma10Path = [UIBezierPath bezierPath];
  158. UIBezierPath *ma30Path = [UIBezierPath bezierPath];
  159. CGFloat kLineWidth = kLineUnitWidth * 0.8;// k线实体宽度0.2是间隔
  160. BOOL f5 = YES, f10 = YES, f30 = YES;
  161. // 路径是否是第一个点(第一个点用moveToPoint,后面用addLineToPoint
  162. for (NSInteger i = startIndex; i <= endIndex; i++) {
  163. StockKLineModel *model = self.kLineData[i];
  164. // 坐标转换
  165. CGFloat xCenter = [self getScreenCenterXAtIndex:i contentOffset:contentOffsetX];
  166. CGFloat xLeft = xCenter - kLineWidth / 2.0;
  167. // 高度 * (最高价 - 当前价)/ 价格区间
  168. CGFloat yOpen = chartHeight * (maxPrice - model.open) / priceRange;
  169. CGFloat yClose = chartHeight * (maxPrice - model.close) / priceRange;
  170. CGFloat yHigh = chartHeight * (maxPrice - model.high) / priceRange;
  171. CGFloat yLow = chartHeight * (maxPrice - model.low) / priceRange;
  172. UIBezierPath *targetPath = (model.close >= model.open) ? redPath : greenPath;
  173. [targetPath moveToPoint:CGPointMake(xCenter, yHigh)];
  174. [targetPath addLineToPoint:CGPointMake(xCenter, yLow)];// 影线
  175. // 实体
  176. [targetPath appendPath:[UIBezierPath bezierPathWithRect:
  177. CGRectMake(xLeft,// 左边界
  178. MIN(yOpen, yClose),// 上边界
  179. kLineWidth,//
  180. MAX(1.0, fabs(yClose - yOpen)))]];//
  181. // 绘制均线
  182. void (^drawMA)(UIBezierPath*, CGFloat, BOOL*) = ^(UIBezierPath *p, CGFloat v, BOOL *f) {
  183. if (v > 0) {
  184. CGFloat y = chartHeight * (maxPrice - v) / priceRange;// y轴
  185. if (*f) {// 是第一个点
  186. [p moveToPoint:CGPointMake(xCenter, y)];// 移动到起点
  187. *f = NO; }// 起点已设置
  188. else { [p addLineToPoint:CGPointMake(xCenter, y)]; }// 连线,从上一个点链接这个点
  189. }
  190. };
  191. drawMA(ma5Path, model.MA5, &f5);
  192. drawMA(ma10Path, model.MA10, &f10);
  193. drawMA(ma30Path, model.MA30, &f30);
  194. }
  195. [CATransaction begin];// 开启Core Animation事务
  196. [CATransaction setDisableActions:YES];// 禁止动画
  197. self.redCandleLayer.path = redPath.CGPath;// 图层绑定
  198. self.greenCandleLayer.path = greenPath.CGPath;
  199. self.ma5Layer.path = ma5Path.CGPath;
  200. self.ma10Layer.path = ma10Path.CGPath;
  201. self.ma30Layer.path = ma30Path.CGPath;
  202. [CATransaction commit];// 事务提交
  203. // 更新最高最低价的箭头位置
  204. [self updateMaxMinArrowsWithMaxIndex:maxIndex minIndex:minIndex maxPrice:maxPrice minPrice:minPrice chartHeight:chartHeight range:priceRange contentOffsetX:contentOffsetX];
  205. }
  206. #pragma mark - 绘制MACD
  207. - (void)drawMACDChartFromIndex:(NSInteger)startIndex toIndex:(NSInteger)endIndex contentOffset:(CGFloat)contentOffsetX {
  208. CGRect bounds = self.macdContainer.bounds;
  209. self.macdRedBarLayer.frame = bounds;
  210. self.macdGreenBarLayer.frame = bounds;
  211. self.difLayer.frame = bounds;
  212. self.deaLayer.frame = bounds;
  213. self.zeroLineLayer.frame = bounds;
  214. CGFloat h = bounds.size.height;
  215. // 计算极值
  216. CGFloat maxV = -MAXFLOAT, minV = MAXFLOAT;// 先给正负无穷
  217. for (NSInteger i = startIndex; i <= endIndex; i++) {
  218. StockKLineModel *m = self.kLineData[i];
  219. maxV = MAX(maxV, MAX(m.macdBar, MAX(m.dif, m.dea)));
  220. minV = MIN(minV, MIN(m.macdBar, MIN(m.dif, m.dea)));
  221. }
  222. if (maxV == minV) { maxV += 1; minV -= 1; }
  223. self.macdMaxValue = maxV;
  224. self.macdMinValue = minV;
  225. CGFloat range = maxV - minV;
  226. CGFloat zeroY = h * (maxV - 0) / range;
  227. UIBezierPath *rPath = [UIBezierPath bezierPath];
  228. UIBezierPath *gPath = [UIBezierPath bezierPath];
  229. UIBezierPath *dPath = [UIBezierPath bezierPath];
  230. UIBezierPath *ePath = [UIBezierPath bezierPath];
  231. UIBezierPath *zPath = [UIBezierPath bezierPath];
  232. [zPath moveToPoint:CGPointMake(0, zeroY)];// 0轴起点
  233. [zPath addLineToPoint:CGPointMake(bounds.size.width, zeroY)];// 0轴终点
  234. CGFloat barWidth = kLineUnitWidth * 0.2;
  235. BOOL first = YES;// 快线慢线起点标记
  236. for (NSInteger i = startIndex; i <= endIndex; i++) {
  237. StockKLineModel *m = self.kLineData[i];
  238. CGFloat xCenter = [self getScreenCenterXAtIndex:i contentOffset:contentOffsetX];
  239. CGFloat xLeft = xCenter - barWidth/2.0;
  240. CGFloat yBar = h * (maxV - m.macdBar) / range;
  241. CGFloat barY = (m.macdBar > 0) ? yBar : zeroY;// 上边界
  242. CGFloat barH = MAX(0.5, fabs(zeroY - yBar));// 高度,大于0.5,省得看不见
  243. UIBezierPath *tp = (m.macdBar > 0) ? rPath : gPath;
  244. [tp appendPath:[UIBezierPath bezierPathWithRect:
  245. CGRectMake(xLeft,
  246. barY,
  247. barWidth,
  248. barH)]];
  249. CGFloat yD = h * (maxV - m.dif) / range;
  250. CGFloat yE = h * (maxV - m.dea) / range;
  251. if (first) {
  252. [dPath moveToPoint:CGPointMake(xCenter, yD)];
  253. [ePath moveToPoint:CGPointMake(xCenter, yE)];
  254. first = NO;
  255. } else {
  256. [dPath addLineToPoint:CGPointMake(xCenter, yD)];
  257. [ePath addLineToPoint:CGPointMake(xCenter, yE)];
  258. }
  259. }
  260. [CATransaction begin];
  261. [CATransaction setDisableActions:YES];
  262. self.macdRedBarLayer.path = rPath.CGPath;
  263. self.macdGreenBarLayer.path = gPath.CGPath;
  264. self.difLayer.path = dPath.CGPath;
  265. self.deaLayer.path = ePath.CGPath;
  266. self.zeroLineLayer.path = zPath.CGPath;
  267. [CATransaction commit];
  268. }
  269. #pragma mark - 绘制KDJ
  270. - (void)drawKDJChartFromIndex:(NSInteger)startIndex toIndex:(NSInteger)endIndex contentOffset:(CGFloat)contentOffsetX {
  271. self.kLayer.frame = self.kdjContainer.bounds;
  272. self.dLayer.frame = self.kdjContainer.bounds;
  273. self.jLayer.frame = self.kdjContainer.bounds;
  274. CGFloat h = self.kdjContainer.bounds.size.height;
  275. CGFloat maxV = 0, minV = 100;
  276. for (NSInteger i = startIndex; i <= endIndex; i++) {
  277. StockKLineModel *m = self.kLineData[i];
  278. maxV = MAX(maxV, MAX(m.K, MAX(m.D, m.J)));
  279. minV = MIN(minV, MIN(m.K, MIN(m.D, m.J)));
  280. }
  281. self.kdjMaxValue = maxV;
  282. self.kdjMinValue = minV;
  283. CGFloat range = maxV - minV;
  284. if (range <= 0) range = 1;
  285. UIBezierPath *kp = [UIBezierPath bezierPath];
  286. UIBezierPath *dp = [UIBezierPath bezierPath];
  287. UIBezierPath *jp = [UIBezierPath bezierPath];
  288. BOOL first = YES;
  289. for (NSInteger i = startIndex; i <= endIndex; i++) {
  290. StockKLineModel *m = self.kLineData[i];
  291. CGFloat xCenter = [self getScreenCenterXAtIndex:i contentOffset:contentOffsetX];
  292. CGFloat yK = h * (maxV - m.K) / range;
  293. CGFloat yD = h * (maxV - m.D) / range;
  294. CGFloat yJ = h * (maxV - m.J) / range;
  295. if (first) {
  296. [kp moveToPoint:CGPointMake(xCenter, yK)];
  297. [dp moveToPoint:CGPointMake(xCenter, yD)];
  298. [jp moveToPoint:CGPointMake(xCenter, yJ)];
  299. first = NO;
  300. } else {
  301. [kp addLineToPoint:CGPointMake(xCenter, yK)];
  302. [dp addLineToPoint:CGPointMake(xCenter, yD)];
  303. [jp addLineToPoint:CGPointMake(xCenter, yJ)];
  304. }
  305. }
  306. [CATransaction begin];
  307. [CATransaction setDisableActions:YES];
  308. self.kLayer.path = kp.CGPath;
  309. self.dLayer.path = dp.CGPath;
  310. self.jLayer.path = jp.CGPath;
  311. [CATransaction commit];
  312. }
  313. #pragma mark - 坐标计算
  314. // 获取某根K线在屏幕上的x轴中心坐标
  315. - (CGFloat)getScreenCenterXAtIndex:(NSInteger)index contentOffset:(CGFloat)offsetX {
  316. // 半个单位宽+索引*
  317. CGFloat x = kLineUnitWidth * (index + 0.5);
  318. // 屏幕坐标 = 绝对坐标 - 滚动偏移量 + 左侧空白区
  319. return x - offsetX + kPriceLabelAreaWidth;
  320. }
  321. #pragma mark - 极值箭头更新
  322. - (void)updateMaxMinArrowsWithMaxIndex:(NSInteger)maxIdx minIndex:(NSInteger)minIdx maxPrice:(CGFloat)maxPrice minPrice:(CGFloat)minPrice chartHeight:(CGFloat)height range:(CGFloat)range contentOffsetX:(CGFloat)offsetX {
  323. void (^updateLabel)(UILabel *, NSInteger, CGFloat) = ^(UILabel *label, NSInteger index, CGFloat price) {
  324. if (index >= 0 && index < self.kLineData.count) {
  325. label.hidden = NO;
  326. CGFloat xCenter = [self getScreenCenterXAtIndex:index contentOffset:offsetX];
  327. CGFloat y = height * (maxPrice - price) / range;// y轴位置
  328. // 默认放在k线右边
  329. label.text = [NSString stringWithFormat:@"← %.2f", price];
  330. [label sizeToFit];// 根据文字尺寸自适应标签长度
  331. CGFloat targetX = xCenter + kLineUnitWidth/2.0 + 2 + label.bounds.size.width/2.0;
  332. label.center = CGPointMake(targetX, y);
  333. // 如果label不在屏幕内,就放到k线左边
  334. if (CGRectGetMaxX(label.frame) > self.kLineContainer.bounds.size.width) {
  335. label.text = [NSString stringWithFormat:@"%.2f →", price];
  336. [label sizeToFit];
  337. targetX = xCenter - kLineUnitWidth/2.0 - 2 - label.bounds.size.width/2.0;
  338. label.center = CGPointMake(targetX, y);
  339. }
  340. } else {
  341. label.hidden = YES;
  342. }
  343. };
  344. StockKLineModel *maxModel = self.kLineData[maxIdx];
  345. updateLabel(self.highPriceMarkLabel, maxIdx, maxModel.high);
  346. StockKLineModel *minModel = self.kLineData[minIdx];
  347. updateLabel(self.lowPriceMarkLabel, minIdx, minModel.low);
  348. }
  349. #pragma mark - 更新指标数值
  350. - (void)updateLegendsWithIndex:(NSInteger)index {
  351. if (index < 0 || index >= self.kLineData.count) return;
  352. StockKLineModel *m = self.kLineData[index];
  353. NSMutableAttributedString * (^createStr)(NSString *, UIColor *) = ^(NSString *txt, UIColor *col) {
  354. return [[NSMutableAttributedString alloc] initWithString:txt attributes:@{NSForegroundColorAttributeName: col}];
  355. };
  356. // MA
  357. NSMutableAttributedString *maStr = [[NSMutableAttributedString alloc] init];
  358. [maStr appendAttributedString:createStr([NSString stringWithFormat:@"MA5:%.2f ", m.MA5], [UIColor yellowColor])];
  359. [maStr appendAttributedString:createStr([NSString stringWithFormat:@"MA10:%.2f ", m.MA10], [UIColor magentaColor])];
  360. [maStr appendAttributedString:createStr([NSString stringWithFormat:@"MA30:%.2f", m.MA30], [UIColor cyanColor])];
  361. self.maLegendLabel.attributedText = maStr;
  362. // MACD
  363. NSMutableAttributedString *macdStr = [[NSMutableAttributedString alloc] init];
  364. [macdStr appendAttributedString:createStr([NSString stringWithFormat:@"DIF:%.2f ", m.dif], [UIColor whiteColor])];
  365. [macdStr appendAttributedString:createStr([NSString stringWithFormat:@"DEA:%.2f ", m.dea], [UIColor yellowColor])];
  366. UIColor *barColor = (m.macdBar > 0) ? [UIColor redColor] : [UIColor greenColor];
  367. [macdStr appendAttributedString:createStr([NSString stringWithFormat:@"MACD:%.2f", m.macdBar], barColor)];
  368. self.macdLegendLabel.attributedText = macdStr;
  369. // KDJ
  370. NSMutableAttributedString *kdjStr = [[NSMutableAttributedString alloc] init];
  371. [kdjStr appendAttributedString:createStr([NSString stringWithFormat:@"K:%.2f ", m.K], [UIColor whiteColor])];
  372. [kdjStr appendAttributedString:createStr([NSString stringWithFormat:@"D:%.2f ", m.D], [UIColor yellowColor])];
  373. [kdjStr appendAttributedString:createStr([NSString stringWithFormat:@"J:%.2f", m.J], [UIColor magentaColor])];
  374. self.kdjLegendLabel.attributedText = kdjStr;
  375. }
  376. #pragma mark - 手势与缩放
  377. - (void)handlePinchGesture:(UIPinchGestureRecognizer *)gesture {
  378. if (self.isLongPressing) {// 缩放不显示十字星
  379. self.isLongPressing = NO;
  380. self.crossVerticalLine.hidden = YES;
  381. self.crossHorizontalLine.hidden = YES;
  382. self.crossPriceLabel.hidden = YES;
  383. self.crossDateLabel.hidden = YES;
  384. }
  385. if (gesture.state == UIGestureRecognizerStateChanged) {
  386. CGFloat scale = gesture.scale;
  387. CGFloat minUnitWidth = (self.view.bounds.size.width - kPriceLabelAreaWidth) / 500.0;
  388. CGFloat maxUnitWidth = 40.0;
  389. CGFloat newUnitWidth = MAX(minUnitWidth, MIN(kLineUnitWidth * scale, maxUnitWidth));
  390. // 屏幕中心点缩放
  391. CGFloat ratio = (self.kLineScrollView.contentOffset.x + self.kLineScrollView.bounds.size.width/2.0) / self.kLineScrollView.contentSize.width;
  392. kLineUnitWidth = newUnitWidth;// 全局更新
  393. // 新的滚动视图的内容宽度
  394. CGFloat newContentWidth = kLineUnitWidth * self.kLineData.count;
  395. self.kLineScrollView.contentSize = CGSizeMake(newContentWidth, self.kLineContainer.bounds.size.height);
  396. // 新的偏移量
  397. CGFloat newOffset = ratio * newContentWidth - self.kLineScrollView.bounds.size.width/2.0;
  398. self.kLineScrollView.contentOffset = CGPointMake(MAX(0, newOffset), 0);
  399. [self drawAllCharts];
  400. gesture.scale = 1.0;
  401. }
  402. }
  403. #pragma mark - 数据生成
  404. - (void)generateMockData {
  405. NSMutableArray *arr = [NSMutableArray array];
  406. CGFloat lastClose = 100.0;
  407. for (int i = 0; i < 2000; i++) {
  408. StockKLineModel *model = [[StockKLineModel alloc] init];
  409. NSDate *date = [NSDate dateWithTimeIntervalSinceNow:-(2000 - i) * 24 * 3600];
  410. NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
  411. [fmt setDateFormat:@"yyyy-MM-dd"];
  412. model.date = [fmt stringFromDate:date];
  413. CGFloat volatility = lastClose * 0.02;
  414. CGFloat randomChange = ((arc4random() % 100) / 100.0 - 0.5) * 2 * volatility;
  415. model.open = lastClose + ((arc4random() % 100) / 100.0 - 0.5) * volatility * 0.5;
  416. model.close = model.open + randomChange;
  417. CGFloat maxOC = MAX(model.open, model.close);
  418. CGFloat minOC = MIN(model.open, model.close);
  419. model.high = maxOC + (arc4random() % 100) / 100.0 * 1.0;
  420. model.low = minOC - (arc4random() % 100) / 100.0 * 1.0;
  421. if (model.low < 0) model.low = 0.01;
  422. [arr addObject:model];
  423. lastClose = model.close;
  424. }
  425. self.kLineData = arr;
  426. }
  427. - (void)calculateMA {
  428. for (int i = 0; i < self.kLineData.count; i++) {
  429. StockKLineModel *model = self.kLineData[i];
  430. model.MA5 = [self getMAWithIndex:i count:5];
  431. model.MA10 = [self getMAWithIndex:i count:10];
  432. model.MA30 = [self getMAWithIndex:i count:30];
  433. }
  434. }
  435. - (CGFloat)getMAWithIndex:(NSInteger)index count:(NSInteger)count {
  436. if (index < count - 1) return 0;
  437. CGFloat sum = 0;
  438. for (NSInteger i = index; i > index - count; i--) {
  439. StockKLineModel *m = self.kLineData[i];
  440. sum += m.close;
  441. }
  442. return sum / count;
  443. }
  444. - (void)calculateMACD {
  445. if (self.kLineData.count == 0) return;
  446. const CGFloat kShortEMA = 2.0 / (12 + 1);
  447. const CGFloat kLongEMA = 2.0 / (26 + 1);
  448. const CGFloat kSignalEMA = 2.0 / (9 + 1);
  449. CGFloat lastShortEMA = 0, lastLongEMA = 0, lastDEA = 0;
  450. for (int i = 0; i < self.kLineData.count; i++) {
  451. StockKLineModel *model = self.kLineData[i];
  452. CGFloat close = model.close;
  453. if (i == 0) {
  454. lastShortEMA = close; lastLongEMA = close;
  455. model.dif = 0; model.dea = 0; model.macdBar = 0;
  456. } else {
  457. lastShortEMA = kShortEMA * close + (1 - kShortEMA) * lastShortEMA;
  458. lastLongEMA = kLongEMA * close + (1 - kLongEMA) * lastLongEMA;
  459. model.dif = lastShortEMA - lastLongEMA;
  460. model.dea = kSignalEMA * model.dif + (1 - kSignalEMA) * lastDEA;
  461. model.macdBar = 2.0 * (model.dif - model.dea);
  462. }
  463. lastDEA = model.dea;
  464. }
  465. }
  466. - (void)calculateKDJ {
  467. CGFloat k = 50.0, d = 50.0;
  468. for (int i = 0; i < self.kLineData.count; i++) {
  469. StockKLineModel *model = self.kLineData[i];
  470. NSInteger startIndex = MAX(0, i - 8);
  471. CGFloat maxHigh = -MAXFLOAT;
  472. CGFloat minLow = MAXFLOAT;
  473. for (NSInteger j = startIndex; j <= i; j++) {
  474. StockKLineModel *m = self.kLineData[j];
  475. maxHigh = MAX(maxHigh, m.high);
  476. minLow = MIN(minLow, m.low);
  477. }
  478. CGFloat rsv = 0;
  479. if (maxHigh != minLow) rsv = (model.close - minLow) / (maxHigh - minLow) * 100.0;
  480. k = (2.0 * k + rsv) / 3.0;
  481. d = (2.0 * d + k) / 3.0;
  482. model.K = k; model.D = d; model.J = 3.0 * k - 2.0 * d;
  483. }
  484. }
  485. #pragma mark 计算极值
  486. - (void)calculateMinMaxPriceForStartIndex:(NSInteger)startIndex endIndex:(NSInteger)endIndex maxPrice:(CGFloat *)maxPrice minPrice:(CGFloat *)minPrice maxIndex:(NSInteger *)maxIdx minIndex:(NSInteger *)minIdx {
  487. *maxPrice = -MAXFLOAT; *minPrice = MAXFLOAT;
  488. *maxIdx = -1; *minIdx = -1;
  489. if (self.kLineData.count == 0) return;
  490. for (NSInteger i = startIndex; i <= endIndex; i++) {
  491. StockKLineModel *m = self.kLineData[i];
  492. if (m.high > *maxPrice) { *maxPrice = m.high; *maxIdx = i; }
  493. if (m.low < *minPrice) { *minPrice = m.low; *minIdx = i; }
  494. if (m.MA5 > 0) { *maxPrice = MAX(*maxPrice, m.MA5); *minPrice = MIN(*minPrice, m.MA5); }
  495. if (m.MA10 > 0) { *maxPrice = MAX(*maxPrice, m.MA10); *minPrice = MIN(*minPrice, m.MA10); }
  496. if (m.MA30 > 0) { *maxPrice = MAX(*maxPrice, m.MA30); *minPrice = MIN(*minPrice, m.MA30); }
  497. }
  498. if (*maxPrice > *minPrice) {
  499. CGFloat d = *maxPrice - *minPrice;
  500. *maxPrice += d * 0.05; *minPrice -= d * 0.05;
  501. }
  502. }
  503. #pragma mark - setupSubviews
  504. -(void) setupSubviews {
  505. UIColor *bgColor = [UIColor colorWithRed:26.0/255.0 green:26.0/255.0 blue:26.0/255.0 alpha:1.0];
  506. _cardContainer = [[StockInfoCardView alloc] init];
  507. _cardContainer.translatesAutoresizingMaskIntoConstraints = NO;
  508. [_cardContainer setupView];
  509. [self.view addSubview:_cardContainer];
  510. _kSelectContainer = [[UIView alloc] init];
  511. _kSelectContainer.backgroundColor = bgColor;
  512. _kSelectContainer.translatesAutoresizingMaskIntoConstraints = NO;
  513. [self.view addSubview:_kSelectContainer];
  514. [self addKSelectOptions];
  515. self.kLineContainer = [self createContainerViewWithColor:bgColor];
  516. self.macdContainer = [self createContainerViewWithColor:bgColor];
  517. self.kdjContainer = [self createContainerViewWithColor:bgColor];
  518. self.otherContainer = [self createContainerViewWithColor:bgColor];
  519. self.kLineScrollView = [[UIScrollView alloc] init];
  520. self.kLineScrollView.backgroundColor = [UIColor clearColor];
  521. self.kLineScrollView.showsHorizontalScrollIndicator = NO;
  522. self.kLineScrollView.translatesAutoresizingMaskIntoConstraints = NO;
  523. self.kLineScrollView.delegate = self;
  524. [self.kLineContainer addSubview:self.kLineScrollView];
  525. [NSLayoutConstraint activateConstraints:@[
  526. [self.kLineScrollView.topAnchor constraintEqualToAnchor:self.kLineContainer.topAnchor],
  527. [self.kLineScrollView.bottomAnchor constraintEqualToAnchor:self.kLineContainer.bottomAnchor],
  528. [self.kLineScrollView.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:kPriceLabelAreaWidth],
  529. [self.kLineScrollView.trailingAnchor constraintEqualToAnchor:self.kLineContainer.trailingAnchor],
  530. ]];
  531. UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
  532. [self.kLineScrollView addGestureRecognizer:pinchGesture];
  533. [self setupAllLayers];
  534. [self setupLabels];
  535. self.maLegendLabel = [self createLegendLabel];
  536. [self.view addSubview:self.maLegendLabel];
  537. self.macdLegendLabel = [self createLegendLabel];
  538. [self.view addSubview:self.macdLegendLabel];
  539. self.kdjLegendLabel = [self createLegendLabel];
  540. [self.view addSubview:self.kdjLegendLabel];
  541. [NSLayoutConstraint activateConstraints:@[
  542. [self.maLegendLabel.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:kPriceLabelAreaWidth],
  543. [self.maLegendLabel.bottomAnchor constraintEqualToAnchor:self.kLineContainer.topAnchor],
  544. [self.macdLegendLabel.leadingAnchor constraintEqualToAnchor:self.macdContainer.leadingAnchor constant:kPriceLabelAreaWidth],
  545. [self.macdLegendLabel.bottomAnchor constraintEqualToAnchor:self.macdContainer.topAnchor],
  546. [self.kdjLegendLabel.leadingAnchor constraintEqualToAnchor:self.kdjContainer.leadingAnchor constant:kPriceLabelAreaWidth],
  547. [self.kdjLegendLabel.bottomAnchor constraintEqualToAnchor:self.kdjContainer.topAnchor]
  548. ]];
  549. self.highPriceMarkLabel = [self createMarkLabel];
  550. [self.kLineContainer addSubview:self.highPriceMarkLabel];
  551. self.lowPriceMarkLabel = [self createMarkLabel];
  552. [self.kLineContainer addSubview:self.lowPriceMarkLabel];
  553. }
  554. - (UIView *)createContainerViewWithColor:(UIColor *)color {
  555. UIView *uiView = [[UIView alloc] init];
  556. uiView.backgroundColor = color;
  557. uiView.translatesAutoresizingMaskIntoConstraints = NO;
  558. uiView.clipsToBounds = YES;
  559. [self.view addSubview:uiView];
  560. return uiView;
  561. }
  562. #pragma mark - setupConstraints
  563. - (void)setupConstraints {
  564. [NSLayoutConstraint activateConstraints:@[
  565. [_cardContainer.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
  566. [_cardContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
  567. [_cardContainer.widthAnchor constraintEqualToAnchor:self.view.widthAnchor],
  568. [_cardContainer.heightAnchor constraintEqualToConstant:100],
  569. [_kSelectContainer.topAnchor constraintEqualToAnchor:_cardContainer.bottomAnchor constant:5],
  570. [_kSelectContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
  571. [_kSelectContainer.widthAnchor constraintEqualToAnchor:self.view.widthAnchor],
  572. [_kSelectContainer.heightAnchor constraintEqualToConstant:40],
  573. ]];
  574. // 循环设置图表容器约束
  575. NSArray *containers = @[self.kLineContainer, self.macdContainer, self.kdjContainer, self.otherContainer];
  576. NSArray *heights = @[@(kKLineHeight), @(kContainerHeight), @(kContainerHeight), @(kContainerHeight)];
  577. UIView *previousView = _kSelectContainer;
  578. for (int i = 0; i < containers.count; i++) {
  579. UIView *container = containers[i];
  580. // 间距 第一个是15MACD是30,其他是15
  581. CGFloat spacing = (i == 1) ? 30.0 : 15.0;
  582. [NSLayoutConstraint activateConstraints:@[
  583. [container.topAnchor constraintEqualToAnchor:previousView.bottomAnchor constant:spacing],
  584. [container.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
  585. [container.widthAnchor constraintEqualToAnchor:self.view.widthAnchor],
  586. [container.heightAnchor constraintEqualToConstant:[heights[i] floatValue]]
  587. ]];
  588. previousView = container;
  589. }
  590. }
  591. - (void)setupAllLayers {
  592. // K线
  593. _redCandleLayer = [self createLayerColor:[UIColor redColor] width:1.0 filled:YES];
  594. _greenCandleLayer = [self createLayerColor:[UIColor greenColor] width:1.0 filled:YES];
  595. [self.kLineContainer.layer insertSublayer:_redCandleLayer atIndex:0];
  596. [self.kLineContainer.layer insertSublayer:_greenCandleLayer atIndex:0];
  597. // 均线
  598. _ma5Layer = [self createLayerColor:[UIColor yellowColor] width:1.0 filled:NO];
  599. _ma10Layer = [self createLayerColor:[UIColor magentaColor] width:1.0 filled:NO];
  600. _ma30Layer = [self createLayerColor:[UIColor cyanColor] width:1.0 filled:NO];
  601. [self.kLineContainer.layer insertSublayer:_ma5Layer below:_redCandleLayer];
  602. [self.kLineContainer.layer insertSublayer:_ma10Layer below:_ma5Layer];
  603. [self.kLineContainer.layer insertSublayer:_ma30Layer below:_ma10Layer];
  604. // MACD
  605. _macdRedBarLayer = [self createLayerColor:[UIColor redColor] width:0 filled:YES];
  606. _macdGreenBarLayer = [self createLayerColor:[UIColor greenColor] width:0 filled:YES];
  607. _difLayer = [self createLayerColor:[UIColor whiteColor] width:1.0 filled:NO];
  608. _deaLayer = [self createLayerColor:[UIColor yellowColor] width:1.0 filled:NO];
  609. _zeroLineLayer = [self createLayerColor:[UIColor grayColor] width:0.5 filled:NO];
  610. [self.macdContainer.layer insertSublayer:_zeroLineLayer atIndex:0];
  611. [self.macdContainer.layer insertSublayer:_macdRedBarLayer above:_zeroLineLayer];
  612. [self.macdContainer.layer insertSublayer:_macdGreenBarLayer above:_macdRedBarLayer];
  613. [self.macdContainer.layer insertSublayer:_difLayer above:_macdGreenBarLayer];
  614. [self.macdContainer.layer insertSublayer:_deaLayer above:_difLayer];
  615. // KDJ
  616. _kLayer = [self createLayerColor:[UIColor whiteColor] width:1.0 filled:NO];
  617. _dLayer = [self createLayerColor:[UIColor yellowColor] width:1.0 filled:NO];
  618. _jLayer = [self createLayerColor:[UIColor magentaColor] width:1.0 filled:NO];
  619. [self.kdjContainer.layer insertSublayer:_kLayer atIndex:0];
  620. [self.kdjContainer.layer insertSublayer:_dLayer above:_kLayer];
  621. [self.kdjContainer.layer insertSublayer:_jLayer above:_dLayer];
  622. }
  623. - (CAShapeLayer *)createLayerColor:(UIColor *)color width:(CGFloat)width filled:(BOOL)fill {
  624. CAShapeLayer *layer = [CAShapeLayer layer];
  625. layer.lineWidth = width;
  626. layer.strokeColor = color.CGColor;
  627. layer.fillColor = fill ? color.CGColor : [UIColor clearColor].CGColor;
  628. layer.lineCap = kCALineCapSquare;
  629. layer.lineJoin = kCALineJoinRound;// 圆角连接,避免折线拐角出现尖锐锯齿
  630. return layer;
  631. }
  632. - (UILabel *)createLegendLabel {
  633. UILabel *label = [[UILabel alloc] init];
  634. label.font = [UIFont systemFontOfSize:10];
  635. label.textColor = [UIColor whiteColor];
  636. label.backgroundColor = [UIColor clearColor];
  637. label.translatesAutoresizingMaskIntoConstraints = NO;
  638. return label;
  639. }
  640. - (UILabel *)createMarkLabel {
  641. UILabel *label = [[UILabel alloc] init];
  642. label.font = [UIFont systemFontOfSize:10];
  643. label.textColor = [UIColor whiteColor];
  644. label.backgroundColor = [UIColor clearColor];
  645. label.hidden = YES;
  646. return label;
  647. }
  648. - (void)setupLabels {
  649. NSMutableArray *priceLabelArr = [NSMutableArray array];
  650. CGFloat chartUsableHeight = kKLineHeight - kPriceLabelPadding;
  651. for (NSInteger i = 0; i < 5; i++) {
  652. UILabel *label = [[UILabel alloc] init];
  653. label.text = @"--";
  654. label.textColor = [UIColor lightGrayColor];
  655. label.font = [UIFont systemFontOfSize:10];
  656. label.translatesAutoresizingMaskIntoConstraints = NO;
  657. [self.kLineContainer addSubview:label];
  658. [NSLayoutConstraint activateConstraints:@[
  659. [label.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:2],
  660. [label.topAnchor constraintEqualToAnchor:self.kLineContainer.topAnchor constant: (chartUsableHeight / 4 * i)]
  661. ]];
  662. [priceLabelArr addObject:label];
  663. }
  664. self.priceLabels = priceLabelArr;
  665. self.startDateLabel = [[UILabel alloc] init];
  666. self.startDateLabel.textColor = [UIColor lightGrayColor];
  667. self.startDateLabel.font = [UIFont systemFontOfSize:10];
  668. self.startDateLabel.translatesAutoresizingMaskIntoConstraints = NO;
  669. [self.view addSubview:self.startDateLabel];
  670. self.endDateLabel = [[UILabel alloc] init];
  671. self.endDateLabel.textColor = [UIColor lightGrayColor];
  672. self.endDateLabel.font = [UIFont systemFontOfSize:10];
  673. self.endDateLabel.textAlignment = NSTextAlignmentRight;
  674. self.endDateLabel.translatesAutoresizingMaskIntoConstraints = NO;
  675. [self.view addSubview:self.endDateLabel];
  676. [NSLayoutConstraint activateConstraints:@[
  677. [self.startDateLabel.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:kPriceLabelAreaWidth],
  678. [self.startDateLabel.topAnchor constraintEqualToAnchor:self.kLineContainer.bottomAnchor constant:2],
  679. [self.endDateLabel.trailingAnchor constraintEqualToAnchor:self.kLineContainer.trailingAnchor],
  680. [self.endDateLabel.topAnchor constraintEqualToAnchor:self.kLineContainer.bottomAnchor constant:2],
  681. ]];
  682. }
  683. #pragma mark - 左侧价格刻度计算
  684. - (void)updatePriceLabelsWithMaxPrice:(CGFloat)maxPrice minPrice:(CGFloat)minPrice {
  685. CGFloat range = maxPrice - minPrice;
  686. for (NSInteger i = 0; i < 5; i++) {
  687. self.priceLabels[i].text = [NSString stringWithFormat:@"%.2f", maxPrice - range / 4.0 * i];
  688. }
  689. }
  690. # pragma mark - 日期计算
  691. - (void)updateDateLabelsStartIndex:(NSInteger)startIndex endIndex:(NSInteger)endIndex {
  692. if (self.kLineData.count > 0) {
  693. if (startIndex < self.kLineData.count) self.startDateLabel.text = ((StockKLineModel *)self.kLineData[startIndex]).date;
  694. if (endIndex < self.kLineData.count) self.endDateLabel.text = ((StockKLineModel *)self.kLineData[endIndex]).date;
  695. }
  696. }
  697. #pragma mark - k线选择
  698. - (void)addKSelectOptions {
  699. NSArray *titles = @[@"分时",@"日k", @"周k", @"月k", @"更多", @"设置"];
  700. UIStackView *stackView = [[UIStackView alloc] init];
  701. stackView.axis = UILayoutConstraintAxisHorizontal;
  702. stackView.distribution = UIStackViewDistributionFillEqually;
  703. stackView.alignment = UIStackViewAlignmentCenter;
  704. stackView.spacing = 5.0;
  705. stackView.translatesAutoresizingMaskIntoConstraints = NO;
  706. for (NSString *title in titles) {
  707. UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
  708. [button setTitle:title forState:UIControlStateNormal];
  709. button.titleLabel.font = [UIFont systemFontOfSize:14];
  710. [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
  711. [stackView addArrangedSubview:button];
  712. }
  713. [_kSelectContainer addSubview:stackView];
  714. [NSLayoutConstraint activateConstraints:@[
  715. [stackView.leadingAnchor constraintEqualToAnchor:_kSelectContainer.leadingAnchor constant:10],
  716. [stackView.trailingAnchor constraintEqualToAnchor:_kSelectContainer.trailingAnchor constant:-10],
  717. [stackView.topAnchor constraintEqualToAnchor:_kSelectContainer.topAnchor constant:15],
  718. [stackView.bottomAnchor constraintEqualToAnchor:_kSelectContainer.bottomAnchor constant:-15]
  719. ]];
  720. }
  721. #pragma mark - 十字星
  722. - (void)setupDojiViews {
  723. // 竖线
  724. self.crossVerticalLine = [[UIView alloc] init];
  725. self.crossVerticalLine.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
  726. self.crossVerticalLine.hidden = YES;
  727. self.crossVerticalLine.userInteractionEnabled = NO; // 不挡手势
  728. [self.view addSubview:self.crossVerticalLine];
  729. // 横线
  730. self.crossHorizontalLine = [[UIView alloc] init];
  731. self.crossHorizontalLine.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
  732. self.crossHorizontalLine.hidden = YES;
  733. self.crossHorizontalLine.userInteractionEnabled = NO;
  734. [self.view addSubview:self.crossHorizontalLine];
  735. // 价格
  736. self.crossPriceLabel = [[UILabel alloc] init];
  737. self.crossPriceLabel.backgroundColor = [UIColor systemBlueColor];
  738. self.crossPriceLabel.textColor = [UIColor whiteColor];
  739. self.crossPriceLabel.font = [UIFont systemFontOfSize:9];
  740. self.crossPriceLabel.textAlignment = NSTextAlignmentCenter;
  741. self.crossPriceLabel.clipsToBounds = YES;
  742. self.crossPriceLabel.layer.cornerRadius = 2.0;
  743. self.crossPriceLabel.hidden = YES;
  744. [self.view addSubview:self.crossPriceLabel];
  745. // 日期
  746. self.crossDateLabel = [[UILabel alloc] init];
  747. self.crossDateLabel.backgroundColor = [UIColor systemBlueColor];
  748. self.crossDateLabel.textColor = [UIColor whiteColor];
  749. self.crossDateLabel.font = [UIFont systemFontOfSize:9];
  750. self.crossDateLabel.textAlignment = NSTextAlignmentCenter;// 居中对齐
  751. //NSTextAlignmentJustified 两端对齐
  752. self.crossDateLabel.clipsToBounds = YES;
  753. self.crossDateLabel.layer.cornerRadius = 2.0;
  754. self.crossDateLabel.hidden = YES;
  755. [self.view addSubview:self.crossDateLabel];
  756. NSArray *targetViews = @[self.kLineScrollView, self.macdContainer, self.kdjContainer];
  757. for (UIView *view in targetViews) {
  758. // 必须在循环里创建新的手势对象,因为一个手势只能绑定一个View
  759. UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
  760. longPress.minimumPressDuration = 0.3; // 设置触发时间
  761. [view addGestureRecognizer:longPress];
  762. }
  763. }
  764. - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture {
  765. CGPoint touchPoint = [gesture locationInView:self.kLineScrollView];
  766. CGPoint touchPointInView = [gesture locationInView:self.view];
  767. CGFloat offsetX = self.kLineScrollView.contentOffset.x;
  768. NSInteger index = floor(touchPoint.x / kLineUnitWidth);// 向下取整
  769. if (index < 0) index = 0;
  770. if (index >= self.kLineData.count) index = self.kLineData.count - 1;
  771. if (gesture.state == UIGestureRecognizerStateBegan || gesture.state == UIGestureRecognizerStateChanged) {// 手势开始/移动
  772. self.isLongPressing = YES;
  773. self.crossVerticalLine.hidden = NO;
  774. self.crossHorizontalLine.hidden = NO;
  775. self.crossPriceLabel.hidden = NO;
  776. self.crossDateLabel.hidden = NO;// 显示十字星
  777. // 更新竖线位置
  778. CGFloat xCenterInScroll = kLineUnitWidth * 0.5 + kLineUnitWidth * index;
  779. //滚动视图x坐标 - 滚动偏移量 + 价格标签宽度
  780. CGFloat xCenterInView = xCenterInScroll - offsetX + kPriceLabelAreaWidth;
  781. // 不要越界
  782. if (xCenterInView < kPriceLabelAreaWidth || xCenterInView > self.view.bounds.size.width) return;
  783. CGFloat topY = self.kLineContainer.frame.origin.y;// k线容器在主视图的顶部y
  784. CGFloat bottomY = CGRectGetMaxY(self.otherContainer.frame);// 空容器的底部
  785. self.crossVerticalLine.frame = CGRectMake(xCenterInView, topY, 0.5, bottomY - topY);
  786. CGFloat displayValue = 0.0;
  787. BOOL isTouchValid = NO; // 用于标记是否摸到了有效的图表区域
  788. // 判断手指在哪个容器里
  789. if (CGRectContainsPoint(self.kLineContainer.frame, touchPointInView)) {// CGRectContainsPoint(矩形区域,) 布尔:
  790. isTouchValid = YES;
  791. CGFloat relativeY = touchPointInView.y - self.kLineContainer.frame.origin.y;
  792. CGFloat height = self.kLineContainer.frame.size.height;
  793. displayValue = self.currentMaxPrice - (relativeY / height) * (self.currentMaxPrice - self.currentMinPrice);
  794. } else if (CGRectContainsPoint(self.macdContainer.frame, touchPointInView)) {
  795. isTouchValid = YES;
  796. CGFloat relativeY = touchPointInView.y - self.macdContainer.frame.origin.y;
  797. CGFloat height = self.macdContainer.frame.size.height;
  798. displayValue = self.macdMaxValue - (relativeY / height) * (self.macdMaxValue - self.macdMinValue);
  799. } else if (CGRectContainsPoint(self.kdjContainer.frame, touchPointInView)) {
  800. isTouchValid = YES;
  801. CGFloat relativeY = touchPointInView.y - self.kdjContainer.frame.origin.y;
  802. CGFloat height = self.kdjContainer.frame.size.height;
  803. displayValue = self.kdjMaxValue - (relativeY / height) * (self.kdjMaxValue - self.kdjMinValue);
  804. }
  805. // 只有触摸在图表内才更新横线
  806. if (isTouchValid) {
  807. self.crossHorizontalLine.frame = CGRectMake(kPriceLabelAreaWidth, touchPointInView.y, self.view.bounds.size.width - kPriceLabelAreaWidth, 0.5);
  808. self.crossPriceLabel.text = [NSString stringWithFormat:@" %.2f ", displayValue];
  809. [self.crossPriceLabel sizeToFit];
  810. self.crossPriceLabel.center = CGPointMake(kPriceLabelAreaWidth / 2.0, touchPointInView.y);
  811. }
  812. StockKLineModel *model = self.kLineData[index];
  813. self.crossDateLabel.text = [NSString stringWithFormat:@" %@ ", model.date];
  814. [self.crossDateLabel sizeToFit];
  815. CGFloat dateLabelY = CGRectGetMaxY(self.kLineContainer.frame);
  816. self.crossDateLabel.center = CGPointMake(xCenterInView, dateLabelY);
  817. [self updateLegendsWithIndex:index];
  818. } else if (gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled) {// 结束/取消
  819. self.isLongPressing = NO;
  820. self.crossVerticalLine.hidden = YES;
  821. self.crossHorizontalLine.hidden = YES;
  822. self.crossPriceLabel.hidden = YES;
  823. self.crossDateLabel.hidden = YES;
  824. [self drawAllCharts];
  825. }
  826. }
  827. @end