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.
977 lines
43 KiB
977 lines
43 KiB
//
|
|
// ChartViewController.m
|
|
// HC
|
|
//
|
|
// Created by huilinLi on 2025/11/27.
|
|
//
|
|
|
|
#import "ChartViewController.h"
|
|
#import "StockKLineModel.h"
|
|
#import "StockInfoCardView.h"
|
|
|
|
static CGFloat kLineUnitWidth = 5.0; // 单位宽度
|
|
static CGFloat kPriceLabelAreaWidth = 35.0;
|
|
static CGFloat kPriceLabelPadding = 10.0;
|
|
static CGFloat kKLineHeight = 300.0;
|
|
static CGFloat kContainerHeight = 120.0;
|
|
|
|
@interface ChartViewController () <UIGestureRecognizerDelegate, UIScrollViewDelegate>
|
|
|
|
@property (nonatomic, strong) StockInfoCardView *cardContainer;
|
|
@property (nonatomic, strong) UIView *kSelectContainer;
|
|
@property (nonatomic, strong) UIView *kLineContainer;
|
|
@property (nonatomic, strong) UIView *macdContainer;
|
|
@property (nonatomic, strong) UIView *kdjContainer;
|
|
@property (nonatomic, strong) UIView *otherContainer;
|
|
@property (nonatomic, strong) UIScrollView *kLineScrollView;
|
|
@property (nonatomic, strong) NSArray *kLineData;
|
|
@property (nonatomic, assign) NSInteger visibleKLineCount;
|
|
@property (nonatomic, strong) NSArray<UILabel *> *priceLabels;
|
|
@property (nonatomic, strong) UILabel *startDateLabel;
|
|
@property (nonatomic, strong) UILabel *endDateLabel;
|
|
@property (nonatomic, strong) UILabel *maLegendLabel; // MA5
|
|
@property (nonatomic, strong) UILabel *macdLegendLabel; // DIF DEA
|
|
@property (nonatomic, strong) UILabel *kdjLegendLabel; // K D
|
|
@property (nonatomic, strong) UILabel *highPriceMarkLabel;
|
|
@property (nonatomic, strong) UILabel *lowPriceMarkLabel;
|
|
@property (nonatomic, assign) CGFloat currentMaxPrice;
|
|
@property (nonatomic, assign) CGFloat currentMinPrice;
|
|
@property (nonatomic, assign) CGFloat macdMaxValue;
|
|
@property (nonatomic, assign) CGFloat macdMinValue;
|
|
@property (nonatomic, assign) CGFloat kdjMaxValue;
|
|
@property (nonatomic, assign) CGFloat kdjMinValue;
|
|
|
|
// K线图层
|
|
@property (nonatomic, strong) CAShapeLayer *redCandleLayer;
|
|
@property (nonatomic, strong) CAShapeLayer *greenCandleLayer;
|
|
|
|
// 均线图层
|
|
@property (nonatomic, strong) CAShapeLayer *ma5Layer;
|
|
@property (nonatomic, strong) CAShapeLayer *ma10Layer;
|
|
@property (nonatomic, strong) CAShapeLayer *ma30Layer;
|
|
|
|
// MACD图层
|
|
@property (nonatomic, strong) CAShapeLayer *macdRedBarLayer;
|
|
@property (nonatomic, strong) CAShapeLayer *macdGreenBarLayer;
|
|
@property (nonatomic, strong) CAShapeLayer *difLayer;
|
|
@property (nonatomic, strong) CAShapeLayer *deaLayer;
|
|
@property (nonatomic, strong) CAShapeLayer *zeroLineLayer;
|
|
|
|
// KDJ图层
|
|
@property (nonatomic, strong) CAShapeLayer *kLayer;
|
|
@property (nonatomic, strong) CAShapeLayer *dLayer;
|
|
@property (nonatomic, strong) CAShapeLayer *jLayer;
|
|
|
|
@property (nonatomic, strong) UIView *crossVerticalLine;
|
|
@property (nonatomic, strong) UIView *crossHorizontalLine;
|
|
@property (nonatomic, strong) UILabel *crossPriceLabel;
|
|
@property (nonatomic, strong) UILabel *crossDateLabel;
|
|
@property (nonatomic, assign) BOOL isLongPressing;
|
|
|
|
@end
|
|
|
|
@implementation ChartViewController
|
|
|
|
#pragma mark - viewDidLoad
|
|
- (void)viewDidLoad {
|
|
[super viewDidLoad];
|
|
self.view.backgroundColor = [UIColor blackColor];
|
|
|
|
self.visibleKLineCount = 40;
|
|
|
|
[self generateMockData];
|
|
|
|
[self calculateMA];
|
|
[self calculateMACD];
|
|
[self calculateKDJ];
|
|
|
|
[self setupSubviews];
|
|
[self setupConstraints];
|
|
[self setupDojiViews];
|
|
|
|
CGFloat chartVisibleWidth = self.view.bounds.size.width - kPriceLabelAreaWidth;
|
|
if (chartVisibleWidth > 0) {
|
|
kLineUnitWidth = chartVisibleWidth / self.visibleKLineCount;
|
|
}
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
CGFloat totalChartWidth = kLineUnitWidth * self.kLineData.count;
|
|
self.kLineScrollView.contentSize = CGSizeMake(totalChartWidth, self.kLineContainer.bounds.size.height);
|
|
|
|
// 滚到最右侧
|
|
if (self.kLineScrollView.contentSize.width > self.kLineScrollView.bounds.size.width) {
|
|
CGPoint offset = CGPointMake(self.kLineScrollView.contentSize.width - self.kLineScrollView.bounds.size.width, 0);//(横向偏移,纵向)
|
|
[self.kLineScrollView setContentOffset:offset animated:NO];// 禁用动画效果
|
|
}
|
|
|
|
[self drawAllCharts];
|
|
});
|
|
}
|
|
|
|
#pragma mark - 绘制
|
|
|
|
- (void)drawAllCharts {
|
|
if (self.kLineData.count == 0) return;
|
|
|
|
// 获取当前ScrollView滚到了哪里
|
|
CGFloat contentOffsetX = self.kLineScrollView.contentOffset.x;
|
|
// 可视区域宽度
|
|
CGFloat visibleWidth = self.kLineScrollView.bounds.size.width;
|
|
|
|
NSInteger startIndex = floor(contentOffsetX / kLineUnitWidth);// 向下取整
|
|
NSInteger endIndex = ceil((contentOffsetX + visibleWidth) / kLineUnitWidth);// 向上取整
|
|
|
|
// 防止越界
|
|
if (startIndex < 0) startIndex = 0;
|
|
if (endIndex >= self.kLineData.count) endIndex = self.kLineData.count - 1;
|
|
if (startIndex > endIndex) endIndex = startIndex;
|
|
|
|
// 把start,end和偏移量传给所有子视图
|
|
[self drawKLineChartFromIndex:startIndex toIndex:endIndex contentOffset:contentOffsetX];
|
|
[self drawMACDChartFromIndex:startIndex toIndex:endIndex contentOffset:contentOffsetX];
|
|
[self drawKDJChartFromIndex:startIndex toIndex:endIndex contentOffset:contentOffsetX];
|
|
|
|
// 更新指标参数
|
|
if (!self.isLongPressing) {
|
|
NSInteger visibleEndIndex = endIndex;
|
|
|
|
CGFloat visibleRightX = contentOffsetX + self.kLineScrollView.bounds.size.width;
|
|
|
|
NSInteger calculatedIndex = floor(visibleRightX / kLineUnitWidth) - 1;
|
|
|
|
if (calculatedIndex < 0) calculatedIndex = 0;
|
|
if (calculatedIndex >= self.kLineData.count) calculatedIndex = self.kLineData.count - 1;
|
|
|
|
visibleEndIndex = calculatedIndex;
|
|
|
|
[self updateLegendsWithIndex:visibleEndIndex];
|
|
}
|
|
}
|
|
|
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
|
if (self.isLongPressing) {
|
|
self.isLongPressing = NO;
|
|
self.crossVerticalLine.hidden = YES;
|
|
self.crossHorizontalLine.hidden = YES;
|
|
self.crossPriceLabel.hidden = YES;
|
|
self.crossDateLabel.hidden = YES;
|
|
}
|
|
[self drawAllCharts];
|
|
}
|
|
|
|
#pragma mark - 绘制K线+均线
|
|
|
|
- (void)drawKLineChartFromIndex:(NSInteger)startIndex toIndex:(NSInteger)endIndex contentOffset:(CGFloat)contentOffsetX {
|
|
// 图层大小=容器大小
|
|
CGRect bounds = self.kLineContainer.bounds;
|
|
self.redCandleLayer.frame = bounds;
|
|
self.greenCandleLayer.frame = bounds;
|
|
self.ma5Layer.frame = bounds;
|
|
self.ma10Layer.frame = bounds;
|
|
self.ma30Layer.frame = bounds;
|
|
|
|
CGFloat chartHeight = bounds.size.height;
|
|
|
|
// 最高最低价
|
|
CGFloat maxPrice = 0, minPrice = 0;
|
|
NSInteger maxIndex = -1, minIndex = -1;
|
|
[self calculateMinMaxPriceForStartIndex:startIndex endIndex:endIndex maxPrice:&maxPrice minPrice:&minPrice maxIndex:&maxIndex minIndex:&minIndex];//将计算出的最高价赋值给max/minPrice
|
|
|
|
self.currentMaxPrice = maxPrice;
|
|
self.currentMinPrice = minPrice;
|
|
|
|
CGFloat priceRange = maxPrice - minPrice;
|
|
if (priceRange <= 0) {
|
|
self.redCandleLayer.path = nil;
|
|
return;
|
|
}
|
|
|
|
// 更新价格和日期
|
|
[self updatePriceLabelsWithMaxPrice:maxPrice minPrice:minPrice];
|
|
[self updateDateLabelsStartIndex:startIndex endIndex:endIndex];
|
|
|
|
// 准备路径
|
|
UIBezierPath *redPath = [UIBezierPath bezierPath];// 涨的路径
|
|
UIBezierPath *greenPath = [UIBezierPath bezierPath];// 跌的路径
|
|
UIBezierPath *ma5Path = [UIBezierPath bezierPath];// 均线路径
|
|
UIBezierPath *ma10Path = [UIBezierPath bezierPath];
|
|
UIBezierPath *ma30Path = [UIBezierPath bezierPath];
|
|
|
|
CGFloat kLineWidth = kLineUnitWidth * 0.8;// k线实体宽度,0.2是间隔
|
|
BOOL f5 = YES, f10 = YES, f30 = YES;
|
|
// 路径是否是第一个点(第一个点用moveToPoint,后面用addLineToPoint)
|
|
|
|
for (NSInteger i = startIndex; i <= endIndex; i++) {
|
|
StockKLineModel *model = self.kLineData[i];
|
|
|
|
// 坐标转换
|
|
CGFloat xCenter = [self getScreenCenterXAtIndex:i contentOffset:contentOffsetX];
|
|
CGFloat xLeft = xCenter - kLineWidth / 2.0;
|
|
|
|
// 高度 * (最高价 - 当前价)/ 价格区间
|
|
CGFloat yOpen = chartHeight * (maxPrice - model.open) / priceRange;
|
|
CGFloat yClose = chartHeight * (maxPrice - model.close) / priceRange;
|
|
CGFloat yHigh = chartHeight * (maxPrice - model.high) / priceRange;
|
|
CGFloat yLow = chartHeight * (maxPrice - model.low) / priceRange;
|
|
|
|
UIBezierPath *targetPath = (model.close >= model.open) ? redPath : greenPath;
|
|
|
|
[targetPath moveToPoint:CGPointMake(xCenter, yHigh)];
|
|
[targetPath addLineToPoint:CGPointMake(xCenter, yLow)];// 影线
|
|
// 实体
|
|
[targetPath appendPath:[UIBezierPath bezierPathWithRect:
|
|
CGRectMake(xLeft,// 左边界
|
|
MIN(yOpen, yClose),// 上边界
|
|
kLineWidth,// 宽
|
|
MAX(1.0, fabs(yClose - yOpen)))]];// 高
|
|
|
|
// 绘制均线
|
|
void (^drawMA)(UIBezierPath*, CGFloat, BOOL*) = ^(UIBezierPath *p, CGFloat v, BOOL *f) {
|
|
if (v > 0) {
|
|
CGFloat y = chartHeight * (maxPrice - v) / priceRange;// y轴
|
|
if (*f) {// 是第一个点
|
|
[p moveToPoint:CGPointMake(xCenter, y)];// 移动到起点
|
|
*f = NO; }// 起点已设置
|
|
else { [p addLineToPoint:CGPointMake(xCenter, y)]; }// 连线,从上一个点链接这个点
|
|
}
|
|
};
|
|
drawMA(ma5Path, model.MA5, &f5);
|
|
drawMA(ma10Path, model.MA10, &f10);
|
|
drawMA(ma30Path, model.MA30, &f30);
|
|
}
|
|
|
|
[CATransaction begin];// 开启Core Animation事务
|
|
[CATransaction setDisableActions:YES];// 禁止动画
|
|
self.redCandleLayer.path = redPath.CGPath;// 图层绑定
|
|
self.greenCandleLayer.path = greenPath.CGPath;
|
|
self.ma5Layer.path = ma5Path.CGPath;
|
|
self.ma10Layer.path = ma10Path.CGPath;
|
|
self.ma30Layer.path = ma30Path.CGPath;
|
|
[CATransaction commit];// 事务提交
|
|
|
|
// 更新最高最低价的箭头位置
|
|
[self updateMaxMinArrowsWithMaxIndex:maxIndex minIndex:minIndex maxPrice:maxPrice minPrice:minPrice chartHeight:chartHeight range:priceRange contentOffsetX:contentOffsetX];
|
|
}
|
|
|
|
#pragma mark - 绘制MACD
|
|
- (void)drawMACDChartFromIndex:(NSInteger)startIndex toIndex:(NSInteger)endIndex contentOffset:(CGFloat)contentOffsetX {
|
|
CGRect bounds = self.macdContainer.bounds;
|
|
self.macdRedBarLayer.frame = bounds;
|
|
self.macdGreenBarLayer.frame = bounds;
|
|
self.difLayer.frame = bounds;
|
|
self.deaLayer.frame = bounds;
|
|
self.zeroLineLayer.frame = bounds;
|
|
|
|
CGFloat h = bounds.size.height;
|
|
|
|
// 计算极值
|
|
CGFloat maxV = -MAXFLOAT, minV = MAXFLOAT;// 先给正负无穷
|
|
for (NSInteger i = startIndex; i <= endIndex; i++) {
|
|
StockKLineModel *m = self.kLineData[i];
|
|
maxV = MAX(maxV, MAX(m.macdBar, MAX(m.dif, m.dea)));
|
|
minV = MIN(minV, MIN(m.macdBar, MIN(m.dif, m.dea)));
|
|
}
|
|
if (maxV == minV) { maxV += 1; minV -= 1; }
|
|
self.macdMaxValue = maxV;
|
|
self.macdMinValue = minV;
|
|
CGFloat range = maxV - minV;
|
|
CGFloat zeroY = h * (maxV - 0) / range;
|
|
|
|
UIBezierPath *rPath = [UIBezierPath bezierPath];
|
|
UIBezierPath *gPath = [UIBezierPath bezierPath];
|
|
UIBezierPath *dPath = [UIBezierPath bezierPath];
|
|
UIBezierPath *ePath = [UIBezierPath bezierPath];
|
|
UIBezierPath *zPath = [UIBezierPath bezierPath];
|
|
[zPath moveToPoint:CGPointMake(0, zeroY)];// 0轴起点
|
|
[zPath addLineToPoint:CGPointMake(bounds.size.width, zeroY)];// 0轴终点
|
|
|
|
CGFloat barWidth = kLineUnitWidth * 0.2;
|
|
BOOL first = YES;// 快线慢线起点标记
|
|
|
|
for (NSInteger i = startIndex; i <= endIndex; i++) {
|
|
StockKLineModel *m = self.kLineData[i];
|
|
|
|
CGFloat xCenter = [self getScreenCenterXAtIndex:i contentOffset:contentOffsetX];
|
|
CGFloat xLeft = xCenter - barWidth/2.0;
|
|
|
|
CGFloat yBar = h * (maxV - m.macdBar) / range;
|
|
CGFloat barY = (m.macdBar > 0) ? yBar : zeroY;// 上边界
|
|
|
|
CGFloat barH = MAX(0.5, fabs(zeroY - yBar));// 高度,大于0.5,省得看不见
|
|
|
|
UIBezierPath *tp = (m.macdBar > 0) ? rPath : gPath;
|
|
[tp appendPath:[UIBezierPath bezierPathWithRect:
|
|
CGRectMake(xLeft,
|
|
barY,
|
|
barWidth,
|
|
barH)]];
|
|
|
|
CGFloat yD = h * (maxV - m.dif) / range;
|
|
CGFloat yE = h * (maxV - m.dea) / range;
|
|
|
|
if (first) {
|
|
[dPath moveToPoint:CGPointMake(xCenter, yD)];
|
|
[ePath moveToPoint:CGPointMake(xCenter, yE)];
|
|
first = NO;
|
|
} else {
|
|
[dPath addLineToPoint:CGPointMake(xCenter, yD)];
|
|
[ePath addLineToPoint:CGPointMake(xCenter, yE)];
|
|
}
|
|
}
|
|
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:YES];
|
|
self.macdRedBarLayer.path = rPath.CGPath;
|
|
self.macdGreenBarLayer.path = gPath.CGPath;
|
|
self.difLayer.path = dPath.CGPath;
|
|
self.deaLayer.path = ePath.CGPath;
|
|
self.zeroLineLayer.path = zPath.CGPath;
|
|
[CATransaction commit];
|
|
}
|
|
|
|
#pragma mark - 绘制KDJ
|
|
- (void)drawKDJChartFromIndex:(NSInteger)startIndex toIndex:(NSInteger)endIndex contentOffset:(CGFloat)contentOffsetX {
|
|
self.kLayer.frame = self.kdjContainer.bounds;
|
|
self.dLayer.frame = self.kdjContainer.bounds;
|
|
self.jLayer.frame = self.kdjContainer.bounds;
|
|
|
|
CGFloat h = self.kdjContainer.bounds.size.height;
|
|
|
|
CGFloat maxV = 0, minV = 100;
|
|
for (NSInteger i = startIndex; i <= endIndex; i++) {
|
|
StockKLineModel *m = self.kLineData[i];
|
|
maxV = MAX(maxV, MAX(m.K, MAX(m.D, m.J)));
|
|
minV = MIN(minV, MIN(m.K, MIN(m.D, m.J)));
|
|
}
|
|
self.kdjMaxValue = maxV;
|
|
self.kdjMinValue = minV;
|
|
CGFloat range = maxV - minV;
|
|
if (range <= 0) range = 1;
|
|
|
|
UIBezierPath *kp = [UIBezierPath bezierPath];
|
|
UIBezierPath *dp = [UIBezierPath bezierPath];
|
|
UIBezierPath *jp = [UIBezierPath bezierPath];
|
|
BOOL first = YES;
|
|
|
|
for (NSInteger i = startIndex; i <= endIndex; i++) {
|
|
StockKLineModel *m = self.kLineData[i];
|
|
CGFloat xCenter = [self getScreenCenterXAtIndex:i contentOffset:contentOffsetX];
|
|
|
|
CGFloat yK = h * (maxV - m.K) / range;
|
|
CGFloat yD = h * (maxV - m.D) / range;
|
|
CGFloat yJ = h * (maxV - m.J) / range;
|
|
|
|
if (first) {
|
|
[kp moveToPoint:CGPointMake(xCenter, yK)];
|
|
[dp moveToPoint:CGPointMake(xCenter, yD)];
|
|
[jp moveToPoint:CGPointMake(xCenter, yJ)];
|
|
first = NO;
|
|
} else {
|
|
[kp addLineToPoint:CGPointMake(xCenter, yK)];
|
|
[dp addLineToPoint:CGPointMake(xCenter, yD)];
|
|
[jp addLineToPoint:CGPointMake(xCenter, yJ)];
|
|
}
|
|
}
|
|
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:YES];
|
|
self.kLayer.path = kp.CGPath;
|
|
self.dLayer.path = dp.CGPath;
|
|
self.jLayer.path = jp.CGPath;
|
|
[CATransaction commit];
|
|
}
|
|
|
|
#pragma mark - 坐标计算
|
|
// 获取某根K线在屏幕上的x轴中心坐标
|
|
- (CGFloat)getScreenCenterXAtIndex:(NSInteger)index contentOffset:(CGFloat)offsetX {
|
|
// 半个单位宽+索引*宽
|
|
CGFloat x = kLineUnitWidth * (index + 0.5);
|
|
// 屏幕坐标 = 绝对坐标 - 滚动偏移量 + 左侧空白区
|
|
return x - offsetX + kPriceLabelAreaWidth;
|
|
}
|
|
|
|
#pragma mark - 极值箭头更新
|
|
- (void)updateMaxMinArrowsWithMaxIndex:(NSInteger)maxIdx minIndex:(NSInteger)minIdx maxPrice:(CGFloat)maxPrice minPrice:(CGFloat)minPrice chartHeight:(CGFloat)height range:(CGFloat)range contentOffsetX:(CGFloat)offsetX {
|
|
|
|
void (^updateLabel)(UILabel *, NSInteger, CGFloat) = ^(UILabel *label, NSInteger index, CGFloat price) {
|
|
if (index >= 0 && index < self.kLineData.count) {
|
|
label.hidden = NO;
|
|
CGFloat xCenter = [self getScreenCenterXAtIndex:index contentOffset:offsetX];
|
|
CGFloat y = height * (maxPrice - price) / range;// y轴位置
|
|
|
|
// 默认放在k线右边
|
|
label.text = [NSString stringWithFormat:@"← %.2f", price];
|
|
[label sizeToFit];// 根据文字尺寸自适应标签长度
|
|
CGFloat targetX = xCenter + kLineUnitWidth/2.0 + 2 + label.bounds.size.width/2.0;
|
|
label.center = CGPointMake(targetX, y);
|
|
|
|
// 如果label不在屏幕内,就放到k线左边
|
|
if (CGRectGetMaxX(label.frame) > self.kLineContainer.bounds.size.width) {
|
|
label.text = [NSString stringWithFormat:@"%.2f →", price];
|
|
[label sizeToFit];
|
|
targetX = xCenter - kLineUnitWidth/2.0 - 2 - label.bounds.size.width/2.0;
|
|
label.center = CGPointMake(targetX, y);
|
|
}
|
|
} else {
|
|
label.hidden = YES;
|
|
}
|
|
};
|
|
|
|
StockKLineModel *maxModel = self.kLineData[maxIdx];
|
|
updateLabel(self.highPriceMarkLabel, maxIdx, maxModel.high);
|
|
|
|
StockKLineModel *minModel = self.kLineData[minIdx];
|
|
updateLabel(self.lowPriceMarkLabel, minIdx, minModel.low);
|
|
}
|
|
|
|
#pragma mark - 更新指标数值
|
|
- (void)updateLegendsWithIndex:(NSInteger)index {
|
|
if (index < 0 || index >= self.kLineData.count) return;
|
|
|
|
StockKLineModel *m = self.kLineData[index];
|
|
|
|
NSMutableAttributedString * (^createStr)(NSString *, UIColor *) = ^(NSString *txt, UIColor *col) {
|
|
return [[NSMutableAttributedString alloc] initWithString:txt attributes:@{NSForegroundColorAttributeName: col}];
|
|
};
|
|
|
|
// MA
|
|
NSMutableAttributedString *maStr = [[NSMutableAttributedString alloc] init];
|
|
[maStr appendAttributedString:createStr([NSString stringWithFormat:@"MA5:%.2f ", m.MA5], [UIColor yellowColor])];
|
|
[maStr appendAttributedString:createStr([NSString stringWithFormat:@"MA10:%.2f ", m.MA10], [UIColor magentaColor])];
|
|
[maStr appendAttributedString:createStr([NSString stringWithFormat:@"MA30:%.2f", m.MA30], [UIColor cyanColor])];
|
|
self.maLegendLabel.attributedText = maStr;
|
|
|
|
// MACD
|
|
NSMutableAttributedString *macdStr = [[NSMutableAttributedString alloc] init];
|
|
[macdStr appendAttributedString:createStr([NSString stringWithFormat:@"DIF:%.2f ", m.dif], [UIColor whiteColor])];
|
|
[macdStr appendAttributedString:createStr([NSString stringWithFormat:@"DEA:%.2f ", m.dea], [UIColor yellowColor])];
|
|
UIColor *barColor = (m.macdBar > 0) ? [UIColor redColor] : [UIColor greenColor];
|
|
[macdStr appendAttributedString:createStr([NSString stringWithFormat:@"MACD:%.2f", m.macdBar], barColor)];
|
|
self.macdLegendLabel.attributedText = macdStr;
|
|
|
|
// KDJ
|
|
NSMutableAttributedString *kdjStr = [[NSMutableAttributedString alloc] init];
|
|
[kdjStr appendAttributedString:createStr([NSString stringWithFormat:@"K:%.2f ", m.K], [UIColor whiteColor])];
|
|
[kdjStr appendAttributedString:createStr([NSString stringWithFormat:@"D:%.2f ", m.D], [UIColor yellowColor])];
|
|
[kdjStr appendAttributedString:createStr([NSString stringWithFormat:@"J:%.2f", m.J], [UIColor magentaColor])];
|
|
self.kdjLegendLabel.attributedText = kdjStr;
|
|
}
|
|
|
|
#pragma mark - 手势与缩放
|
|
- (void)handlePinchGesture:(UIPinchGestureRecognizer *)gesture {
|
|
if (self.isLongPressing) {// 缩放不显示十字星
|
|
self.isLongPressing = NO;
|
|
self.crossVerticalLine.hidden = YES;
|
|
self.crossHorizontalLine.hidden = YES;
|
|
self.crossPriceLabel.hidden = YES;
|
|
self.crossDateLabel.hidden = YES;
|
|
}
|
|
|
|
if (gesture.state == UIGestureRecognizerStateChanged) {
|
|
CGFloat scale = gesture.scale;
|
|
CGFloat minUnitWidth = (self.view.bounds.size.width - kPriceLabelAreaWidth) / 500.0;
|
|
CGFloat maxUnitWidth = 40.0;
|
|
|
|
CGFloat newUnitWidth = MAX(minUnitWidth, MIN(kLineUnitWidth * scale, maxUnitWidth));
|
|
|
|
// 屏幕中心点缩放
|
|
CGFloat ratio = (self.kLineScrollView.contentOffset.x + self.kLineScrollView.bounds.size.width/2.0) / self.kLineScrollView.contentSize.width;
|
|
kLineUnitWidth = newUnitWidth;// 全局更新
|
|
|
|
// 新的滚动视图的内容宽度
|
|
CGFloat newContentWidth = kLineUnitWidth * self.kLineData.count;
|
|
self.kLineScrollView.contentSize = CGSizeMake(newContentWidth, self.kLineContainer.bounds.size.height);
|
|
|
|
// 新的偏移量
|
|
CGFloat newOffset = ratio * newContentWidth - self.kLineScrollView.bounds.size.width/2.0;
|
|
self.kLineScrollView.contentOffset = CGPointMake(MAX(0, newOffset), 0);
|
|
|
|
[self drawAllCharts];
|
|
gesture.scale = 1.0;
|
|
}
|
|
}
|
|
|
|
#pragma mark - 数据生成
|
|
- (void)generateMockData {
|
|
NSMutableArray *arr = [NSMutableArray array];
|
|
CGFloat lastClose = 100.0;
|
|
for (int i = 0; i < 2000; i++) {
|
|
StockKLineModel *model = [[StockKLineModel alloc] init];
|
|
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:-(2000 - i) * 24 * 3600];
|
|
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
|
|
[fmt setDateFormat:@"yyyy-MM-dd"];
|
|
model.date = [fmt stringFromDate:date];
|
|
CGFloat volatility = lastClose * 0.02;
|
|
CGFloat randomChange = ((arc4random() % 100) / 100.0 - 0.5) * 2 * volatility;
|
|
model.open = lastClose + ((arc4random() % 100) / 100.0 - 0.5) * volatility * 0.5;
|
|
model.close = model.open + randomChange;
|
|
CGFloat maxOC = MAX(model.open, model.close);
|
|
CGFloat minOC = MIN(model.open, model.close);
|
|
model.high = maxOC + (arc4random() % 100) / 100.0 * 1.0;
|
|
model.low = minOC - (arc4random() % 100) / 100.0 * 1.0;
|
|
if (model.low < 0) model.low = 0.01;
|
|
[arr addObject:model];
|
|
lastClose = model.close;
|
|
}
|
|
self.kLineData = arr;
|
|
}
|
|
|
|
- (void)calculateMA {
|
|
for (int i = 0; i < self.kLineData.count; i++) {
|
|
StockKLineModel *model = self.kLineData[i];
|
|
model.MA5 = [self getMAWithIndex:i count:5];
|
|
model.MA10 = [self getMAWithIndex:i count:10];
|
|
model.MA30 = [self getMAWithIndex:i count:30];
|
|
}
|
|
}
|
|
- (CGFloat)getMAWithIndex:(NSInteger)index count:(NSInteger)count {
|
|
if (index < count - 1) return 0;
|
|
CGFloat sum = 0;
|
|
for (NSInteger i = index; i > index - count; i--) {
|
|
StockKLineModel *m = self.kLineData[i];
|
|
sum += m.close;
|
|
}
|
|
return sum / count;
|
|
}
|
|
- (void)calculateMACD {
|
|
if (self.kLineData.count == 0) return;
|
|
const CGFloat kShortEMA = 2.0 / (12 + 1);
|
|
const CGFloat kLongEMA = 2.0 / (26 + 1);
|
|
const CGFloat kSignalEMA = 2.0 / (9 + 1);
|
|
CGFloat lastShortEMA = 0, lastLongEMA = 0, lastDEA = 0;
|
|
for (int i = 0; i < self.kLineData.count; i++) {
|
|
StockKLineModel *model = self.kLineData[i];
|
|
CGFloat close = model.close;
|
|
if (i == 0) {
|
|
lastShortEMA = close; lastLongEMA = close;
|
|
model.dif = 0; model.dea = 0; model.macdBar = 0;
|
|
} else {
|
|
lastShortEMA = kShortEMA * close + (1 - kShortEMA) * lastShortEMA;
|
|
lastLongEMA = kLongEMA * close + (1 - kLongEMA) * lastLongEMA;
|
|
model.dif = lastShortEMA - lastLongEMA;
|
|
model.dea = kSignalEMA * model.dif + (1 - kSignalEMA) * lastDEA;
|
|
model.macdBar = 2.0 * (model.dif - model.dea);
|
|
}
|
|
lastDEA = model.dea;
|
|
}
|
|
}
|
|
- (void)calculateKDJ {
|
|
CGFloat k = 50.0, d = 50.0;
|
|
for (int i = 0; i < self.kLineData.count; i++) {
|
|
StockKLineModel *model = self.kLineData[i];
|
|
NSInteger startIndex = MAX(0, i - 8);
|
|
CGFloat maxHigh = -MAXFLOAT;
|
|
CGFloat minLow = MAXFLOAT;
|
|
for (NSInteger j = startIndex; j <= i; j++) {
|
|
StockKLineModel *m = self.kLineData[j];
|
|
maxHigh = MAX(maxHigh, m.high);
|
|
minLow = MIN(minLow, m.low);
|
|
}
|
|
CGFloat rsv = 0;
|
|
if (maxHigh != minLow) rsv = (model.close - minLow) / (maxHigh - minLow) * 100.0;
|
|
k = (2.0 * k + rsv) / 3.0;
|
|
d = (2.0 * d + k) / 3.0;
|
|
model.K = k; model.D = d; model.J = 3.0 * k - 2.0 * d;
|
|
}
|
|
}
|
|
|
|
#pragma mark 计算极值
|
|
- (void)calculateMinMaxPriceForStartIndex:(NSInteger)startIndex endIndex:(NSInteger)endIndex maxPrice:(CGFloat *)maxPrice minPrice:(CGFloat *)minPrice maxIndex:(NSInteger *)maxIdx minIndex:(NSInteger *)minIdx {
|
|
*maxPrice = -MAXFLOAT; *minPrice = MAXFLOAT;
|
|
*maxIdx = -1; *minIdx = -1;
|
|
if (self.kLineData.count == 0) return;
|
|
for (NSInteger i = startIndex; i <= endIndex; i++) {
|
|
StockKLineModel *m = self.kLineData[i];
|
|
if (m.high > *maxPrice) { *maxPrice = m.high; *maxIdx = i; }
|
|
if (m.low < *minPrice) { *minPrice = m.low; *minIdx = i; }
|
|
if (m.MA5 > 0) { *maxPrice = MAX(*maxPrice, m.MA5); *minPrice = MIN(*minPrice, m.MA5); }
|
|
if (m.MA10 > 0) { *maxPrice = MAX(*maxPrice, m.MA10); *minPrice = MIN(*minPrice, m.MA10); }
|
|
if (m.MA30 > 0) { *maxPrice = MAX(*maxPrice, m.MA30); *minPrice = MIN(*minPrice, m.MA30); }
|
|
}
|
|
if (*maxPrice > *minPrice) {
|
|
CGFloat d = *maxPrice - *minPrice;
|
|
*maxPrice += d * 0.05; *minPrice -= d * 0.05;
|
|
}
|
|
}
|
|
|
|
#pragma mark - setupSubviews
|
|
-(void) setupSubviews {
|
|
UIColor *bgColor = [UIColor colorWithRed:26.0/255.0 green:26.0/255.0 blue:26.0/255.0 alpha:1.0];
|
|
|
|
_cardContainer = [[StockInfoCardView alloc] init];
|
|
_cardContainer.translatesAutoresizingMaskIntoConstraints = NO;
|
|
[_cardContainer setupView];
|
|
[self.view addSubview:_cardContainer];
|
|
|
|
_kSelectContainer = [[UIView alloc] init];
|
|
_kSelectContainer.backgroundColor = bgColor;
|
|
_kSelectContainer.translatesAutoresizingMaskIntoConstraints = NO;
|
|
[self.view addSubview:_kSelectContainer];
|
|
[self addKSelectOptions];
|
|
|
|
self.kLineContainer = [self createContainerViewWithColor:bgColor];
|
|
self.macdContainer = [self createContainerViewWithColor:bgColor];
|
|
self.kdjContainer = [self createContainerViewWithColor:bgColor];
|
|
self.otherContainer = [self createContainerViewWithColor:bgColor];
|
|
|
|
self.kLineScrollView = [[UIScrollView alloc] init];
|
|
self.kLineScrollView.backgroundColor = [UIColor clearColor];
|
|
self.kLineScrollView.showsHorizontalScrollIndicator = NO;
|
|
self.kLineScrollView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
self.kLineScrollView.delegate = self;
|
|
[self.kLineContainer addSubview:self.kLineScrollView];
|
|
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
[self.kLineScrollView.topAnchor constraintEqualToAnchor:self.kLineContainer.topAnchor],
|
|
[self.kLineScrollView.bottomAnchor constraintEqualToAnchor:self.kLineContainer.bottomAnchor],
|
|
[self.kLineScrollView.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:kPriceLabelAreaWidth],
|
|
[self.kLineScrollView.trailingAnchor constraintEqualToAnchor:self.kLineContainer.trailingAnchor],
|
|
]];
|
|
|
|
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
|
|
[self.kLineScrollView addGestureRecognizer:pinchGesture];
|
|
|
|
[self setupAllLayers];
|
|
[self setupLabels];
|
|
|
|
self.maLegendLabel = [self createLegendLabel];
|
|
[self.view addSubview:self.maLegendLabel];
|
|
self.macdLegendLabel = [self createLegendLabel];
|
|
[self.view addSubview:self.macdLegendLabel];
|
|
self.kdjLegendLabel = [self createLegendLabel];
|
|
[self.view addSubview:self.kdjLegendLabel];
|
|
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
[self.maLegendLabel.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:kPriceLabelAreaWidth],
|
|
[self.maLegendLabel.bottomAnchor constraintEqualToAnchor:self.kLineContainer.topAnchor],
|
|
[self.macdLegendLabel.leadingAnchor constraintEqualToAnchor:self.macdContainer.leadingAnchor constant:kPriceLabelAreaWidth],
|
|
[self.macdLegendLabel.bottomAnchor constraintEqualToAnchor:self.macdContainer.topAnchor],
|
|
[self.kdjLegendLabel.leadingAnchor constraintEqualToAnchor:self.kdjContainer.leadingAnchor constant:kPriceLabelAreaWidth],
|
|
[self.kdjLegendLabel.bottomAnchor constraintEqualToAnchor:self.kdjContainer.topAnchor]
|
|
]];
|
|
|
|
self.highPriceMarkLabel = [self createMarkLabel];
|
|
[self.kLineContainer addSubview:self.highPriceMarkLabel];
|
|
self.lowPriceMarkLabel = [self createMarkLabel];
|
|
[self.kLineContainer addSubview:self.lowPriceMarkLabel];
|
|
}
|
|
|
|
- (UIView *)createContainerViewWithColor:(UIColor *)color {
|
|
UIView *uiView = [[UIView alloc] init];
|
|
uiView.backgroundColor = color;
|
|
uiView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
uiView.clipsToBounds = YES;
|
|
[self.view addSubview:uiView];
|
|
return uiView;
|
|
}
|
|
|
|
#pragma mark - setupConstraints
|
|
- (void)setupConstraints {
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
[_cardContainer.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
|
|
[_cardContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
|
[_cardContainer.widthAnchor constraintEqualToAnchor:self.view.widthAnchor],
|
|
[_cardContainer.heightAnchor constraintEqualToConstant:100],
|
|
|
|
[_kSelectContainer.topAnchor constraintEqualToAnchor:_cardContainer.bottomAnchor constant:5],
|
|
[_kSelectContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
|
[_kSelectContainer.widthAnchor constraintEqualToAnchor:self.view.widthAnchor],
|
|
[_kSelectContainer.heightAnchor constraintEqualToConstant:40],
|
|
]];
|
|
|
|
// 循环设置图表容器约束
|
|
NSArray *containers = @[self.kLineContainer, self.macdContainer, self.kdjContainer, self.otherContainer];
|
|
NSArray *heights = @[@(kKLineHeight), @(kContainerHeight), @(kContainerHeight), @(kContainerHeight)];
|
|
|
|
UIView *previousView = _kSelectContainer;
|
|
|
|
for (int i = 0; i < containers.count; i++) {
|
|
UIView *container = containers[i];
|
|
// 间距 第一个是15,MACD是30,其他是15
|
|
CGFloat spacing = (i == 1) ? 30.0 : 15.0;
|
|
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
[container.topAnchor constraintEqualToAnchor:previousView.bottomAnchor constant:spacing],
|
|
[container.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
|
[container.widthAnchor constraintEqualToAnchor:self.view.widthAnchor],
|
|
[container.heightAnchor constraintEqualToConstant:[heights[i] floatValue]]
|
|
]];
|
|
|
|
previousView = container;
|
|
}
|
|
}
|
|
|
|
- (void)setupAllLayers {
|
|
// K线
|
|
_redCandleLayer = [self createLayerColor:[UIColor redColor] width:1.0 filled:YES];
|
|
_greenCandleLayer = [self createLayerColor:[UIColor greenColor] width:1.0 filled:YES];
|
|
[self.kLineContainer.layer insertSublayer:_redCandleLayer atIndex:0];
|
|
[self.kLineContainer.layer insertSublayer:_greenCandleLayer atIndex:0];
|
|
|
|
// 均线
|
|
_ma5Layer = [self createLayerColor:[UIColor yellowColor] width:1.0 filled:NO];
|
|
_ma10Layer = [self createLayerColor:[UIColor magentaColor] width:1.0 filled:NO];
|
|
_ma30Layer = [self createLayerColor:[UIColor cyanColor] width:1.0 filled:NO];
|
|
[self.kLineContainer.layer insertSublayer:_ma5Layer below:_redCandleLayer];
|
|
[self.kLineContainer.layer insertSublayer:_ma10Layer below:_ma5Layer];
|
|
[self.kLineContainer.layer insertSublayer:_ma30Layer below:_ma10Layer];
|
|
|
|
// MACD
|
|
_macdRedBarLayer = [self createLayerColor:[UIColor redColor] width:0 filled:YES];
|
|
_macdGreenBarLayer = [self createLayerColor:[UIColor greenColor] width:0 filled:YES];
|
|
_difLayer = [self createLayerColor:[UIColor whiteColor] width:1.0 filled:NO];
|
|
_deaLayer = [self createLayerColor:[UIColor yellowColor] width:1.0 filled:NO];
|
|
_zeroLineLayer = [self createLayerColor:[UIColor grayColor] width:0.5 filled:NO];
|
|
[self.macdContainer.layer insertSublayer:_zeroLineLayer atIndex:0];
|
|
[self.macdContainer.layer insertSublayer:_macdRedBarLayer above:_zeroLineLayer];
|
|
[self.macdContainer.layer insertSublayer:_macdGreenBarLayer above:_macdRedBarLayer];
|
|
[self.macdContainer.layer insertSublayer:_difLayer above:_macdGreenBarLayer];
|
|
[self.macdContainer.layer insertSublayer:_deaLayer above:_difLayer];
|
|
|
|
// KDJ
|
|
_kLayer = [self createLayerColor:[UIColor whiteColor] width:1.0 filled:NO];
|
|
_dLayer = [self createLayerColor:[UIColor yellowColor] width:1.0 filled:NO];
|
|
_jLayer = [self createLayerColor:[UIColor magentaColor] width:1.0 filled:NO];
|
|
[self.kdjContainer.layer insertSublayer:_kLayer atIndex:0];
|
|
[self.kdjContainer.layer insertSublayer:_dLayer above:_kLayer];
|
|
[self.kdjContainer.layer insertSublayer:_jLayer above:_dLayer];
|
|
}
|
|
|
|
- (CAShapeLayer *)createLayerColor:(UIColor *)color width:(CGFloat)width filled:(BOOL)fill {
|
|
CAShapeLayer *layer = [CAShapeLayer layer];
|
|
layer.lineWidth = width;
|
|
layer.strokeColor = color.CGColor;
|
|
layer.fillColor = fill ? color.CGColor : [UIColor clearColor].CGColor;
|
|
layer.lineCap = kCALineCapSquare;
|
|
layer.lineJoin = kCALineJoinRound;// 圆角连接,避免折线拐角出现尖锐锯齿
|
|
return layer;
|
|
}
|
|
|
|
- (UILabel *)createLegendLabel {
|
|
UILabel *label = [[UILabel alloc] init];
|
|
label.font = [UIFont systemFontOfSize:10];
|
|
label.textColor = [UIColor whiteColor];
|
|
label.backgroundColor = [UIColor clearColor];
|
|
label.translatesAutoresizingMaskIntoConstraints = NO;
|
|
return label;
|
|
}
|
|
|
|
- (UILabel *)createMarkLabel {
|
|
UILabel *label = [[UILabel alloc] init];
|
|
label.font = [UIFont systemFontOfSize:10];
|
|
label.textColor = [UIColor whiteColor];
|
|
label.backgroundColor = [UIColor clearColor];
|
|
label.hidden = YES;
|
|
return label;
|
|
}
|
|
|
|
- (void)setupLabels {
|
|
NSMutableArray *priceLabelArr = [NSMutableArray array];
|
|
CGFloat chartUsableHeight = kKLineHeight - kPriceLabelPadding;
|
|
for (NSInteger i = 0; i < 5; i++) {
|
|
UILabel *label = [[UILabel alloc] init];
|
|
label.text = @"--";
|
|
label.textColor = [UIColor lightGrayColor];
|
|
label.font = [UIFont systemFontOfSize:10];
|
|
label.translatesAutoresizingMaskIntoConstraints = NO;
|
|
[self.kLineContainer addSubview:label];
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
[label.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:2],
|
|
[label.topAnchor constraintEqualToAnchor:self.kLineContainer.topAnchor constant: (chartUsableHeight / 4 * i)]
|
|
]];
|
|
[priceLabelArr addObject:label];
|
|
}
|
|
self.priceLabels = priceLabelArr;
|
|
|
|
self.startDateLabel = [[UILabel alloc] init];
|
|
self.startDateLabel.textColor = [UIColor lightGrayColor];
|
|
self.startDateLabel.font = [UIFont systemFontOfSize:10];
|
|
self.startDateLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
|
[self.view addSubview:self.startDateLabel];
|
|
|
|
self.endDateLabel = [[UILabel alloc] init];
|
|
self.endDateLabel.textColor = [UIColor lightGrayColor];
|
|
self.endDateLabel.font = [UIFont systemFontOfSize:10];
|
|
self.endDateLabel.textAlignment = NSTextAlignmentRight;
|
|
self.endDateLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
|
[self.view addSubview:self.endDateLabel];
|
|
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
[self.startDateLabel.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:kPriceLabelAreaWidth],
|
|
[self.startDateLabel.topAnchor constraintEqualToAnchor:self.kLineContainer.bottomAnchor constant:2],
|
|
[self.endDateLabel.trailingAnchor constraintEqualToAnchor:self.kLineContainer.trailingAnchor],
|
|
[self.endDateLabel.topAnchor constraintEqualToAnchor:self.kLineContainer.bottomAnchor constant:2],
|
|
]];
|
|
}
|
|
|
|
#pragma mark - 左侧价格刻度计算
|
|
- (void)updatePriceLabelsWithMaxPrice:(CGFloat)maxPrice minPrice:(CGFloat)minPrice {
|
|
CGFloat range = maxPrice - minPrice;
|
|
for (NSInteger i = 0; i < 5; i++) {
|
|
self.priceLabels[i].text = [NSString stringWithFormat:@"%.2f", maxPrice - range / 4.0 * i];
|
|
}
|
|
}
|
|
|
|
# pragma mark - 日期计算
|
|
- (void)updateDateLabelsStartIndex:(NSInteger)startIndex endIndex:(NSInteger)endIndex {
|
|
if (self.kLineData.count > 0) {
|
|
if (startIndex < self.kLineData.count) self.startDateLabel.text = ((StockKLineModel *)self.kLineData[startIndex]).date;
|
|
if (endIndex < self.kLineData.count) self.endDateLabel.text = ((StockKLineModel *)self.kLineData[endIndex]).date;
|
|
}
|
|
}
|
|
|
|
#pragma mark - k线选择
|
|
- (void)addKSelectOptions {
|
|
NSArray *titles = @[@"分时",@"日k", @"周k", @"月k", @"更多", @"设置"];
|
|
UIStackView *stackView = [[UIStackView alloc] init];
|
|
stackView.axis = UILayoutConstraintAxisHorizontal;
|
|
stackView.distribution = UIStackViewDistributionFillEqually;
|
|
stackView.alignment = UIStackViewAlignmentCenter;
|
|
stackView.spacing = 5.0;
|
|
stackView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
for (NSString *title in titles) {
|
|
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
[button setTitle:title forState:UIControlStateNormal];
|
|
button.titleLabel.font = [UIFont systemFontOfSize:14];
|
|
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
|
[stackView addArrangedSubview:button];
|
|
}
|
|
[_kSelectContainer addSubview:stackView];
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
[stackView.leadingAnchor constraintEqualToAnchor:_kSelectContainer.leadingAnchor constant:10],
|
|
[stackView.trailingAnchor constraintEqualToAnchor:_kSelectContainer.trailingAnchor constant:-10],
|
|
[stackView.topAnchor constraintEqualToAnchor:_kSelectContainer.topAnchor constant:15],
|
|
[stackView.bottomAnchor constraintEqualToAnchor:_kSelectContainer.bottomAnchor constant:-15]
|
|
]];
|
|
}
|
|
|
|
#pragma mark - 十字星
|
|
- (void)setupDojiViews {
|
|
// 竖线
|
|
self.crossVerticalLine = [[UIView alloc] init];
|
|
self.crossVerticalLine.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
|
|
self.crossVerticalLine.hidden = YES;
|
|
self.crossVerticalLine.userInteractionEnabled = NO; // 不挡手势
|
|
[self.view addSubview:self.crossVerticalLine];
|
|
|
|
// 横线
|
|
self.crossHorizontalLine = [[UIView alloc] init];
|
|
self.crossHorizontalLine.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
|
|
self.crossHorizontalLine.hidden = YES;
|
|
self.crossHorizontalLine.userInteractionEnabled = NO;
|
|
[self.view addSubview:self.crossHorizontalLine];
|
|
|
|
// 价格
|
|
self.crossPriceLabel = [[UILabel alloc] init];
|
|
self.crossPriceLabel.backgroundColor = [UIColor systemBlueColor];
|
|
self.crossPriceLabel.textColor = [UIColor whiteColor];
|
|
self.crossPriceLabel.font = [UIFont systemFontOfSize:9];
|
|
self.crossPriceLabel.textAlignment = NSTextAlignmentCenter;
|
|
self.crossPriceLabel.clipsToBounds = YES;
|
|
self.crossPriceLabel.layer.cornerRadius = 2.0;
|
|
self.crossPriceLabel.hidden = YES;
|
|
[self.view addSubview:self.crossPriceLabel];
|
|
|
|
// 日期
|
|
self.crossDateLabel = [[UILabel alloc] init];
|
|
self.crossDateLabel.backgroundColor = [UIColor systemBlueColor];
|
|
self.crossDateLabel.textColor = [UIColor whiteColor];
|
|
self.crossDateLabel.font = [UIFont systemFontOfSize:9];
|
|
self.crossDateLabel.textAlignment = NSTextAlignmentCenter;// 居中对齐
|
|
//NSTextAlignmentJustified 两端对齐
|
|
self.crossDateLabel.clipsToBounds = YES;
|
|
self.crossDateLabel.layer.cornerRadius = 2.0;
|
|
self.crossDateLabel.hidden = YES;
|
|
[self.view addSubview:self.crossDateLabel];
|
|
|
|
NSArray *targetViews = @[self.kLineScrollView, self.macdContainer, self.kdjContainer];
|
|
for (UIView *view in targetViews) {
|
|
// 必须在循环里创建新的手势对象,因为一个手势只能绑定一个View
|
|
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
|
longPress.minimumPressDuration = 0.3; // 设置触发时间
|
|
[view addGestureRecognizer:longPress];
|
|
}
|
|
}
|
|
|
|
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture {
|
|
CGPoint touchPoint = [gesture locationInView:self.kLineScrollView];
|
|
CGPoint touchPointInView = [gesture locationInView:self.view];
|
|
|
|
CGFloat offsetX = self.kLineScrollView.contentOffset.x;
|
|
|
|
NSInteger index = floor(touchPoint.x / kLineUnitWidth);// 向下取整
|
|
if (index < 0) index = 0;
|
|
if (index >= self.kLineData.count) index = self.kLineData.count - 1;
|
|
|
|
if (gesture.state == UIGestureRecognizerStateBegan || gesture.state == UIGestureRecognizerStateChanged) {// 手势开始/移动
|
|
self.isLongPressing = YES;
|
|
self.crossVerticalLine.hidden = NO;
|
|
self.crossHorizontalLine.hidden = NO;
|
|
self.crossPriceLabel.hidden = NO;
|
|
self.crossDateLabel.hidden = NO;// 显示十字星
|
|
|
|
// 更新竖线位置
|
|
CGFloat xCenterInScroll = kLineUnitWidth * 0.5 + kLineUnitWidth * index;
|
|
//滚动视图x坐标 - 滚动偏移量 + 价格标签宽度
|
|
CGFloat xCenterInView = xCenterInScroll - offsetX + kPriceLabelAreaWidth;
|
|
|
|
// 不要越界
|
|
if (xCenterInView < kPriceLabelAreaWidth || xCenterInView > self.view.bounds.size.width) return;
|
|
|
|
CGFloat topY = self.kLineContainer.frame.origin.y;// k线容器在主视图的顶部y
|
|
CGFloat bottomY = CGRectGetMaxY(self.otherContainer.frame);// 空容器的底部
|
|
self.crossVerticalLine.frame = CGRectMake(xCenterInView, topY, 0.5, bottomY - topY);
|
|
|
|
CGFloat displayValue = 0.0;
|
|
BOOL isTouchValid = NO; // 用于标记是否摸到了有效的图表区域
|
|
|
|
// 判断手指在哪个容器里
|
|
if (CGRectContainsPoint(self.kLineContainer.frame, touchPointInView)) {// CGRectContainsPoint(矩形区域, 点) 布尔:
|
|
isTouchValid = YES;
|
|
CGFloat relativeY = touchPointInView.y - self.kLineContainer.frame.origin.y;
|
|
CGFloat height = self.kLineContainer.frame.size.height;
|
|
displayValue = self.currentMaxPrice - (relativeY / height) * (self.currentMaxPrice - self.currentMinPrice);
|
|
|
|
} else if (CGRectContainsPoint(self.macdContainer.frame, touchPointInView)) {
|
|
isTouchValid = YES;
|
|
CGFloat relativeY = touchPointInView.y - self.macdContainer.frame.origin.y;
|
|
CGFloat height = self.macdContainer.frame.size.height;
|
|
displayValue = self.macdMaxValue - (relativeY / height) * (self.macdMaxValue - self.macdMinValue);
|
|
|
|
} else if (CGRectContainsPoint(self.kdjContainer.frame, touchPointInView)) {
|
|
isTouchValid = YES;
|
|
CGFloat relativeY = touchPointInView.y - self.kdjContainer.frame.origin.y;
|
|
CGFloat height = self.kdjContainer.frame.size.height;
|
|
displayValue = self.kdjMaxValue - (relativeY / height) * (self.kdjMaxValue - self.kdjMinValue);
|
|
}
|
|
|
|
// 只有触摸在图表内才更新横线
|
|
if (isTouchValid) {
|
|
self.crossHorizontalLine.frame = CGRectMake(kPriceLabelAreaWidth, touchPointInView.y, self.view.bounds.size.width - kPriceLabelAreaWidth, 0.5);
|
|
|
|
self.crossPriceLabel.text = [NSString stringWithFormat:@" %.2f ", displayValue];
|
|
[self.crossPriceLabel sizeToFit];
|
|
self.crossPriceLabel.center = CGPointMake(kPriceLabelAreaWidth / 2.0, touchPointInView.y);
|
|
}
|
|
|
|
StockKLineModel *model = self.kLineData[index];
|
|
self.crossDateLabel.text = [NSString stringWithFormat:@" %@ ", model.date];
|
|
[self.crossDateLabel sizeToFit];
|
|
CGFloat dateLabelY = CGRectGetMaxY(self.kLineContainer.frame);
|
|
self.crossDateLabel.center = CGPointMake(xCenterInView, dateLabelY);
|
|
|
|
[self updateLegendsWithIndex:index];
|
|
|
|
} else if (gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled) {// 结束/取消
|
|
self.isLongPressing = NO;
|
|
self.crossVerticalLine.hidden = YES;
|
|
self.crossHorizontalLine.hidden = YES;
|
|
self.crossPriceLabel.hidden = YES;
|
|
self.crossDateLabel.hidden = YES;
|
|
|
|
[self drawAllCharts];
|
|
}
|
|
}
|
|
|
|
|
|
@end
|