KSGaugeView.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. //
  2. // KSGaugeView.m
  3. // KulexiuForStudent
  4. //
  5. // Created by 王智 on 2022/10/25.
  6. //
  7. #import "KSGaugeView.h"
  8. #import "KSGaugeColorView.h"
  9. #import "KSGaugeSectorView.h"
  10. /* Degrees to radians conversion macro */
  11. #define DEGREES_TO_RADIANS(degrees) (degrees) / 180.0 * M_PI
  12. @interface KSGaugeView ()
  13. {
  14. /* View center */
  15. CGPoint center;
  16. /* Needle layer */
  17. CALayer *rootNeedleLayer;
  18. /* Annimation completion */
  19. void (^animationCompletion)(BOOL);
  20. /* Scale variables */
  21. CGFloat scaleRotation;
  22. CGFloat divisionValue;
  23. CGFloat subdivisionValue;
  24. CGFloat subdivisionAngle;
  25. }
  26. @end
  27. @implementation KSGaugeView
  28. - (instancetype)initWithFrame:(CGRect)frame {
  29. self = [super initWithFrame:frame];
  30. if (self)
  31. {
  32. self.backgroundColor = [UIColor clearColor];
  33. [self initialize];
  34. }
  35. return self;
  36. }
  37. - (void)initialize {
  38. _scaleStartAngle = 90;
  39. _scaleEndAngle = 270;
  40. _maxValue = 50.0;
  41. _minValue = -50.0;
  42. _scaleDivisions = 10;
  43. _scaleSubdivisions = 10;
  44. _scaleDivisionsWidth = 2;
  45. _scaleDivisionsLength = 15;
  46. _scaleSubdivisionsWidth = 1;
  47. _scaleSubdivisionsLength = 8;
  48. [self initScale];
  49. [self addShaperView];
  50. [self addColorView];
  51. }
  52. // 添加扇形区域
  53. - (void)addShaperView {
  54. KSGaugeSectorView *sectorView = [[KSGaugeSectorView alloc] init];
  55. sectorView.backgroundColor = [UIColor clearColor];
  56. [self addSubview:sectorView];
  57. [sectorView mas_makeConstraints:^(MASConstraintMaker *make) {
  58. make.top.bottom.left.right.mas_equalTo(self);
  59. }];
  60. }
  61. - (void)addColorView {
  62. KSGaugeColorView *colorView = [[KSGaugeColorView alloc] init];
  63. colorView.backgroundColor = [UIColor clearColor];
  64. [self addSubview:colorView];
  65. [colorView mas_makeConstraints:^(MASConstraintMaker *make) {
  66. make.top.bottom.left.right.mas_equalTo(self);
  67. }];
  68. }
  69. - (void)initScale {
  70. scaleRotation = (int)(_scaleStartAngle + 180) % 360;
  71. divisionValue = (_maxValue - _minValue) / _scaleDivisions;
  72. subdivisionValue = divisionValue / _scaleSubdivisions;
  73. subdivisionAngle = (_scaleEndAngle - _scaleStartAngle) / (_scaleDivisions * _scaleSubdivisions);
  74. }
  75. - (void)drawRect:(CGRect)rect {
  76. CGFloat originX = rect.size.width / 2.0f;
  77. CGFloat originY = rect.size.height - 25;
  78. center = CGPointMake(originX, originY);
  79. CGContextRef context = UIGraphicsGetCurrentContext();
  80. // 绘制lable
  81. [self drawTextLabel:rect contenx:context];
  82. // 绘制表盘
  83. [self drawDevision:rect context:context];
  84. // 绘制指针
  85. if (![self.layer.sublayers containsObject:rootNeedleLayer]) {
  86. rootNeedleLayer = [CALayer new];
  87. // For performance puporse, the needle layer is not scaled to [0-1] range
  88. rootNeedleLayer.frame = CGRectMake(0, 40, rect.size.width, (originY - 40) * 2);
  89. [self.layer addSublayer:rootNeedleLayer];
  90. }
  91. [self drawNeedle:rect context:context];
  92. }
  93. - (void)drawNeedle:(CGRect)rect context:(CGContextRef)context {
  94. CGFloat originX = rect.size.width / 2.0f;
  95. CGFloat originY = rect.size.height - 25;
  96. center = CGPointMake(originX, originY);
  97. // Left Needle
  98. CAShapeLayer *needleLayer = [CAShapeLayer layer];
  99. UIBezierPath *needlePath = [UIBezierPath bezierPath];
  100. [needlePath moveToPoint:CGPointMake(originX, originY - 40)];
  101. [needlePath addLineToPoint:CGPointMake(originX-1, originY-40)];
  102. [needlePath addLineToPoint:CGPointMake(originX-1, 0)];
  103. [needlePath addLineToPoint:CGPointMake(originX+1, 0)];
  104. [needlePath addLineToPoint:CGPointMake(originX+1, originY-40)];
  105. [needlePath addLineToPoint:CGPointMake(originX, originY-40)];
  106. [needlePath closePath];
  107. needleLayer.path = needlePath.CGPath;
  108. needleLayer.backgroundColor = [[UIColor clearColor] CGColor];
  109. needleLayer.fillColor = [UIColor whiteColor].CGColor;
  110. [rootNeedleLayer addSublayer:needleLayer];
  111. }
  112. - (void)drawTextLabel:(CGRect)rect contenx:(CGContextRef)context {
  113. int totalTicks = _scaleDivisions * _scaleSubdivisions + 1;
  114. for (int i = 0; i < totalTicks; i++) {
  115. float value = [self valueForTick:i];
  116. float div = (_maxValue - _minValue) / _scaleDivisions;
  117. float mod = (int)value % (int)div;
  118. //
  119. if ((fabsf(mod - 0) < 0.000001) || (fabsf(mod - div) < 0.000001)) {
  120. NSString *valueString = [NSString stringWithFormat:@"%0.0f",value];
  121. UIFont* font = [UIFont fontWithName:@"DIN Alternate Bold" size:14.0f];
  122. NSDictionary* stringAttrs = @{ NSFontAttributeName : font, NSForegroundColorAttributeName : HexRGB(0x999999) };
  123. NSAttributedString* attrStr = [[NSAttributedString alloc] initWithString:valueString attributes:stringAttrs];
  124. CGSize fontSize;
  125. fontSize = [valueString sizeWithAttributes:stringAttrs];
  126. CGFloat radius = rect.size.height - 30;
  127. CGFloat angle = 180 - subdivisionAngle*i;
  128. CGPoint drawPoint = [self calcCicleCoordinateWithCenter:center angle:angle radius:radius];
  129. CGPoint lablePoint = CGPointZero;
  130. if (angle > -90) {
  131. lablePoint = CGPointMake(drawPoint.x - fontSize.width/2, drawPoint.y - fontSize.height/2);
  132. }
  133. else {
  134. lablePoint = CGPointMake(drawPoint.x + fontSize.width/2, drawPoint.y + fontSize.height/2);
  135. }
  136. [attrStr drawAtPoint:lablePoint];
  137. }
  138. }
  139. }
  140. - (CGPoint)calcCicleCoordinateWithCenter:(CGPoint)centerPoint angle:(CGFloat)angle radius:(CGFloat)radius {
  141. CGFloat positionX = radius * cosf(angle*M_PI/180);
  142. CGFloat positionY = radius * sinf(angle*M_PI/180);
  143. return CGPointMake(centerPoint.x + positionX, centerPoint.y-positionY);
  144. }
  145. - (void)drawDevision:(CGRect)rect context:(CGContextRef)context {
  146. [self rotateContext:context fromCenter:center withAngle:DEGREES_TO_RADIANS(180 + _scaleStartAngle)];
  147. int totalTicks = _scaleDivisions * _scaleSubdivisions + 1;
  148. for (int i = 0; i < totalTicks; i++) {
  149. CGFloat y1 = 18;
  150. CGFloat y2 = y1 + _scaleSubdivisionsLength;
  151. CGFloat y3 = y1 + _scaleDivisionsLength;
  152. float value = [self valueForTick:i];
  153. float div = (_maxValue - _minValue) / _scaleDivisions;
  154. float mod = (int)value % (int)div;
  155. // Division
  156. if ((fabsf(mod - 0) < 0.000001) || (fabsf(mod - div) < 0.000001))
  157. {
  158. // Initialize Core Graphics settings
  159. UIColor *color = HexRGB(0x2dc7aa);
  160. CGContextSetStrokeColorWithColor(context, color.CGColor);
  161. CGContextSetLineWidth(context, _scaleDivisionsWidth);
  162. // Draw tick
  163. CGContextMoveToPoint(context, center.x, y1);
  164. CGContextAddLineToPoint(context, center.x, y3);
  165. CGContextStrokePath(context);
  166. // 绘制文字
  167. // NSString *valueString = [NSString stringWithFormat:@"%0.0f",value];
  168. // UIFont* font = [UIFont fontWithName:@"DIN Alternate Bold" size:14.0f];
  169. // NSDictionary* stringAttrs = @{ NSFontAttributeName : font, NSForegroundColorAttributeName : HexRGB(0x999999) };
  170. // NSAttributedString* attrStr = [[NSAttributedString alloc] initWithString:valueString attributes:stringAttrs];
  171. // CGSize fontWidth;
  172. // fontWidth = [valueString sizeWithAttributes:stringAttrs];
  173. // [attrStr drawAtPoint:CGPointMake(center.x - fontWidth.width / 2.0, y1 - _scaleDivisionsLength - 5)];
  174. }
  175. else {
  176. // Initialize Core Graphics settings
  177. UIColor *color = HexRGBAlpha(0x2dc7aa, 0.5);
  178. CGContextSetStrokeColorWithColor(context, color.CGColor);
  179. CGContextSetLineWidth(context, _scaleSubdivisionsWidth);
  180. CGContextMoveToPoint(context, center.x, y1);
  181. // Draw tick
  182. CGContextMoveToPoint(context, center.x, y1);
  183. CGContextAddLineToPoint(context, center.x, y2);
  184. CGContextStrokePath(context);
  185. }
  186. // Rotate to next tick
  187. [self rotateContext:context fromCenter:center withAngle:DEGREES_TO_RADIANS(subdivisionAngle)];
  188. }
  189. }
  190. /**
  191. * Scale tick value computation
  192. */
  193. - (float)valueForTick:(int)tick
  194. {
  195. return tick * (divisionValue / _scaleSubdivisions) + _minValue;
  196. }
  197. #pragma mark - Tools
  198. /**
  199. * Core Graphics rotation in context
  200. */
  201. - (void)rotateContext:(CGContextRef)context fromCenter:(CGPoint)center_ withAngle:(CGFloat)angle
  202. {
  203. CGContextTranslateCTM(context, center_.x, center_.y);
  204. CGContextRotateCTM(context, angle);
  205. CGContextTranslateCTM(context, -center_.x, -center_.y);
  206. }
  207. /**
  208. * Needle angle computation
  209. */
  210. - (CGFloat)needleAngleForValue:(double)value
  211. {
  212. return DEGREES_TO_RADIANS(_scaleStartAngle + (value - _minValue) / (_maxValue - _minValue) * (_scaleEndAngle - _scaleStartAngle)) + M_PI;
  213. }
  214. #pragma mark - Value update
  215. /**
  216. * Update gauge value
  217. */
  218. - (void)updateValue:(float)value
  219. {
  220. // Clamp value if out of range
  221. if (value > _maxValue)
  222. value = _maxValue;
  223. else if (value < _minValue)
  224. value = _minValue;
  225. else
  226. value = value;
  227. // Set value
  228. _value = value;
  229. }
  230. /**
  231. * Update gauge value with animation
  232. */
  233. - (void)setValue:(float)value animated:(BOOL)animated
  234. {
  235. [self setValue:value animated:animated duration:0.2];
  236. }
  237. /**
  238. * Update gauge value with animation and fire a completion block
  239. */
  240. - (void)setValue:(float)value animated:(BOOL)animated completion:(void (^)(BOOL finished))completion
  241. {
  242. [self setValue:value animated:animated duration:0.2 completion:completion];
  243. }
  244. /**
  245. * Update gauge value with animation and duration
  246. */
  247. - (void)setValue:(float)value animated:(BOOL)animated duration:(NSTimeInterval)duration
  248. {
  249. [self setValue:value animated:animated duration:duration completion:nil];
  250. }
  251. /**
  252. * Update gauge value with animation, duration and fire a completion block
  253. */
  254. - (void)setValue:(float)value animated:(BOOL)animated duration:(NSTimeInterval)duration completion:(void (^)(BOOL finished))completion
  255. {
  256. animationCompletion = completion;
  257. double lastValue = _value;
  258. [self updateValue:value];
  259. double middleValue = lastValue + (((lastValue + (_value - lastValue) / 2.0) >= 0) ? (_value - lastValue) / 2.0 : (lastValue - _value) / 2.0);
  260. if (middleValue > 50) {
  261. middleValue = 50;
  262. }
  263. else if (middleValue < -50) {
  264. middleValue = -50;
  265. }
  266. // Needle animation to target value
  267. // An intermediate "middle" value is used to make sure the needle will follow the right rotation direction
  268. CAKeyframeAnimation * animation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
  269. animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
  270. animation.removedOnCompletion = YES;
  271. animation.duration = animated ? duration : 0.0;
  272. animation.delegate = (id<CAAnimationDelegate>)self;
  273. animation.values = @[[NSValue valueWithCATransform3D:CATransform3DMakeRotation([self needleAngleForValue:lastValue] , 0, 0, 1.0)],
  274. [NSValue valueWithCATransform3D:CATransform3DMakeRotation([self needleAngleForValue:middleValue], 0, 0, 1.0)],
  275. [NSValue valueWithCATransform3D:CATransform3DMakeRotation([self needleAngleForValue:_value] , 0, 0, 1.0)]];
  276. rootNeedleLayer.transform = [[animation.values lastObject] CATransform3DValue];
  277. [rootNeedleLayer addAnimation:animation forKey:kCATransition];
  278. }
  279. #pragma mark - CAAnimation delegate
  280. - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
  281. {
  282. if (animationCompletion)
  283. animationCompletion(flag);
  284. animationCompletion = nil;
  285. }
  286. #pragma mark - Properties
  287. - (void)setValue:(float)value
  288. {
  289. [self setValue:value animated:YES];
  290. }
  291. /*
  292. // Only override drawRect: if you perform custom drawing.
  293. // An empty implementation adversely affects performance during animation.
  294. - (void)drawRect:(CGRect)rect {
  295. // Drawing code
  296. }
  297. */
  298. @end