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.

378 lines
11 KiB

5 years ago
  1. //
  2. // M13ProgressViewMetro.m
  3. // M13ProgressSuite
  4. //
  5. // Created by Brandon McQuilkin on 3/8/14.
  6. // Copyright (c) 2014 Brandon McQuilkin. All rights reserved.
  7. //
  8. #import "M13ProgressViewMetro.h"
  9. #import "M13ProgressViewMetroDotPolygon.h"
  10. @interface M13ProgressViewMetro ()
  11. /**The start progress for the progress animation.*/
  12. @property (nonatomic, assign) CGFloat animationFromValue;
  13. /**The end progress for the progress animation.*/
  14. @property (nonatomic, assign) CGFloat animationToValue;
  15. /**The start time interval for the animaiton.*/
  16. @property (nonatomic, assign) CFTimeInterval animationStartTime;
  17. /**Link to the display to keep animations in sync.*/
  18. @property (nonatomic, strong) CADisplayLink *displayLink;
  19. @end
  20. @implementation M13ProgressViewMetroDot
  21. {
  22. M13ProgressViewAction currentAction;
  23. }
  24. #pragma mark Initalization
  25. - (id)init
  26. {
  27. self = [super init];
  28. if (self) {
  29. [self setup];
  30. }
  31. return self;
  32. }
  33. - (id)initWithCoder:(NSCoder *)aDecoder
  34. {
  35. self = [super initWithCoder:aDecoder];
  36. if (self) {
  37. [self setup];
  38. }
  39. return self;
  40. }
  41. - (id)initWithLayer:(id)layer
  42. {
  43. self = [super initWithLayer:layer];
  44. if (self) {
  45. [self setup];
  46. }
  47. return self;
  48. }
  49. + (id)layer
  50. {
  51. return [[M13ProgressViewMetroDot alloc] init];
  52. }
  53. - (void)setup
  54. {
  55. _highlighted = NO;
  56. currentAction = M13ProgressViewActionNone;
  57. _primaryColor = [UIColor colorWithRed:0 green:122/255.0 blue:1.0 alpha:1.0];
  58. _secondaryColor = [UIColor colorWithRed:181/255.0 green:182/255.0 blue:183/255.0 alpha:1.0];
  59. _successColor = [UIColor colorWithRed:63.0f/255.0f green:226.0f/255.0f blue:80.0f/255.0f alpha:1];
  60. _failureColor = [UIColor colorWithRed:249.0f/255.0f green:37.0f/255.0f blue:0 alpha:1];
  61. }
  62. #pragma mark Setters
  63. - (void)setHighlighted:(BOOL)highlighted
  64. {
  65. _highlighted = highlighted;
  66. [self setNeedsDisplay];
  67. }
  68. - (void)setSuccessColor:(UIColor *)successColor
  69. {
  70. _successColor = successColor;
  71. [self setNeedsDisplay];
  72. }
  73. - (void)setFailureColor:(UIColor *)failureColor
  74. {
  75. _failureColor = failureColor;
  76. [self setNeedsDisplay];
  77. }
  78. - (void)setPrimaryColor:(UIColor *)primaryColor
  79. {
  80. _primaryColor = primaryColor;
  81. [self setNeedsDisplay];
  82. }
  83. - (void)setSecondaryColor:(UIColor *)secondaryColor
  84. {
  85. _secondaryColor = secondaryColor;
  86. [self setNeedsDisplay];
  87. }
  88. - (void)performAction:(M13ProgressViewAction)action animated:(BOOL)animated
  89. {
  90. currentAction = action;
  91. [self setNeedsDisplay];
  92. }
  93. @end
  94. @implementation M13ProgressViewMetro
  95. {
  96. BOOL animating;
  97. NSUInteger currentNumberOfDots;
  98. NSTimer *animationTimer;
  99. UIBezierPath *animationPath;
  100. NSMutableArray *dots;
  101. CGFloat circleSize;
  102. }
  103. - (id)init
  104. {
  105. self = [super init];
  106. if (self) {
  107. [self setup];
  108. }
  109. return self;
  110. }
  111. - (id)initWithFrame:(CGRect)frame
  112. {
  113. self = [super initWithFrame:frame];
  114. if (self) {
  115. [self setup];
  116. }
  117. return self;
  118. }
  119. - (id)initWithCoder:(NSCoder *)aDecoder
  120. {
  121. self = [super initWithCoder:aDecoder];
  122. if (self) {
  123. [self setup];
  124. }
  125. return self;
  126. }
  127. - (void)setup
  128. {
  129. //Set defaults
  130. self.clipsToBounds = YES;
  131. animating = NO;
  132. _numberOfDots = 6;
  133. _dotSize = CGSizeMake(20, 20);
  134. self.animationDuration = 1.5;
  135. self.animationShape = M13ProgressViewMetroAnimationShapeEllipse;
  136. self.primaryColor = [UIColor colorWithRed:0 green:122/255.0 blue:1.0 alpha:1.0];
  137. self.secondaryColor = [UIColor colorWithRed:181/255.0 green:182/255.0 blue:183/255.0 alpha:1.0];
  138. _successColor = [UIColor colorWithRed:63.0f/255.0f green:226.0f/255.0f blue:80.0f/255.0f alpha:1];
  139. _failureColor = [UIColor colorWithRed:249.0f/255.0f green:37.0f/255.0f blue:0 alpha:1];
  140. _metroDot = [[M13ProgressViewMetroDotPolygon alloc] init];
  141. }
  142. #pragma mark Properties
  143. - (void)setFrame:(CGRect)frame
  144. {
  145. [super setFrame:frame];
  146. if (animating) {
  147. [self stopAnimating];
  148. [self beginAnimating];
  149. }
  150. }
  151. - (BOOL)isAnimating
  152. {
  153. return animating;
  154. }
  155. - (void)setAnimationShape:(M13ProgressViewMetroAnimationShape)animationShape
  156. {
  157. _animationShape = animationShape;
  158. if (_animationShape == M13ProgressViewMetroAnimationShapeEllipse) {
  159. //Inset the size of the dot
  160. CGFloat radius = MIN((self.bounds.size.width - _dotSize.width) / 2, (self.bounds.size.height - _dotSize.height) / 2);
  161. //Create the path
  162. animationPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2) radius:radius startAngle:M_PI_2 endAngle:(-M_PI_2 * 3) clockwise:YES];
  163. } else if (_animationShape == M13ProgressViewMetroAnimationShapeRectangle) {
  164. //Inset the size of the dot
  165. CGRect pathRect = CGRectInset(self.bounds, _dotSize.width, _dotSize.height);
  166. //Create the path
  167. animationPath = [UIBezierPath bezierPath];
  168. [animationPath moveToPoint:CGPointMake((pathRect.size.width / 2) + pathRect.origin.x, pathRect.size.height + pathRect.origin.y)];
  169. [animationPath addLineToPoint:CGPointMake(pathRect.origin.x, pathRect.size.height + pathRect.origin.y)];
  170. [animationPath addLineToPoint:CGPointMake(pathRect.origin.x, pathRect.origin.y)];
  171. [animationPath addLineToPoint:CGPointMake(pathRect.origin.x + pathRect.size.width, pathRect.origin.y)];
  172. [animationPath addLineToPoint:CGPointMake(pathRect.origin.x + pathRect.size.width, pathRect.origin.y + pathRect.size.height)];
  173. [animationPath moveToPoint:CGPointMake((pathRect.size.width / 2) + pathRect.origin.x, pathRect.size.height + pathRect.origin.y)];
  174. } else if (animationShape == M13ProgressViewMetroAnimationShapeLine) {
  175. //Create the path
  176. animationPath = [UIBezierPath bezierPath];
  177. [animationPath moveToPoint:CGPointMake(-_dotSize.width, self.bounds.size.height / 2)];
  178. [animationPath addLineToPoint:CGPointMake(self.bounds.size.width + _dotSize.width, self.bounds.size.height / 2)];
  179. }
  180. [self stopAnimating];
  181. [self beginAnimating];
  182. }
  183. - (void)setNumberOfDots:(NSUInteger)numberOfDots
  184. {
  185. _numberOfDots = numberOfDots;
  186. [self invalidateIntrinsicContentSize];
  187. [self stopAnimating];
  188. [self beginAnimating];
  189. }
  190. - (void)setDotSize:(CGSize)dotSize
  191. {
  192. _dotSize = dotSize;
  193. [self invalidateIntrinsicContentSize];
  194. [self stopAnimating];
  195. [self beginAnimating];
  196. }
  197. - (void)performAction:(M13ProgressViewAction)action animated:(BOOL)animated
  198. {
  199. for (M13ProgressViewMetroDot *dot in dots) {
  200. [dot performAction:action animated:animated];
  201. }
  202. }
  203. - (void)setProgress:(CGFloat)progress animated:(BOOL)animated
  204. {
  205. if (animated == NO) {
  206. if (_displayLink) {
  207. //Kill running animations
  208. [_displayLink invalidate];
  209. _displayLink = nil;
  210. }
  211. [super setProgress:progress animated:NO];
  212. [self showProgress];
  213. } else {
  214. _animationStartTime = CACurrentMediaTime();
  215. _animationFromValue = self.progress;
  216. _animationToValue = progress;
  217. if (!_displayLink) {
  218. //Create and setup the display link
  219. [self.displayLink invalidate];
  220. self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(animateProgress:)];
  221. [self.displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
  222. } /*else {
  223. //Reuse the current display link
  224. }*/
  225. }
  226. }
  227. - (void)animateProgress:(CADisplayLink *)displayLink
  228. {
  229. dispatch_async(dispatch_get_main_queue(), ^{
  230. CGFloat dt = (displayLink.timestamp - self.animationStartTime) / self.animationDuration;
  231. if (dt >= 1.0) {
  232. //Order is important! Otherwise concurrency will cause errors, because setProgress: will detect an animation in progress and try to stop it by itself. Once over one, set to actual progress amount. Animation is over.
  233. [self.displayLink invalidate];
  234. self.displayLink = nil;
  235. [super setProgress:self.animationToValue animated:NO];
  236. [self showProgress];
  237. return;
  238. }
  239. //Set progress
  240. [super setProgress:self.animationFromValue + dt * (self.animationToValue - self.animationFromValue) animated:YES];
  241. [self showProgress];
  242. });
  243. }
  244. - (void)showProgress
  245. {
  246. static int pastIndexToHighlightTo = 0;
  247. int indexToHighlightTo = (int)ceilf(_numberOfDots * (float)self.progress);
  248. //Only perform the animation if necessary.
  249. if (pastIndexToHighlightTo != indexToHighlightTo) {
  250. for (int i = 0; i < _numberOfDots; i++) {
  251. M13ProgressViewMetroDot *dot = dots[i];
  252. if (i <= indexToHighlightTo && self.progress != 0) {
  253. dot.highlighted = YES;
  254. } else {
  255. dot.highlighted = NO;
  256. }
  257. }
  258. pastIndexToHighlightTo = indexToHighlightTo;
  259. }
  260. }
  261. - (CGSize)intrinsicContentSize
  262. {
  263. //No real constraint on size.
  264. if (_animationShape == M13ProgressViewMetroAnimationShapeEllipse || _animationShape == M13ProgressViewMetroAnimationShapeRectangle) {
  265. return CGSizeMake(3 * _dotSize.width, 3 * _dotSize.height);
  266. } else {
  267. return CGSizeMake(_dotSize.width * _numberOfDots, _dotSize.height);
  268. }
  269. }
  270. #pragma mark Animation
  271. -(void)beginAnimating
  272. {
  273. if (!animating){
  274. animating = YES;
  275. currentNumberOfDots = 0;
  276. dots = [[NSMutableArray alloc] init];
  277. //add circles
  278. animationTimer = [NSTimer scheduledTimerWithTimeInterval: 0.20 target: self
  279. selector: @selector(createCircle) userInfo: nil repeats: YES];
  280. }
  281. }
  282. -(void)createCircle
  283. {
  284. if (currentNumberOfDots<_numberOfDots){
  285. currentNumberOfDots ++;
  286. CGRect f;
  287. if (_animationShape != M13ProgressViewMetroAnimationShapeLine) {
  288. f = CGRectMake((self.frame.size.width - _dotSize.width) / 2 - 1, self.frame.size.height - _dotSize.height - 1, _dotSize.width, _dotSize.height);
  289. } else {
  290. f = CGRectMake(- _dotSize.width, self.bounds.size.height / 2, _dotSize.width, _dotSize.height);
  291. }
  292. M13ProgressViewMetroDot *dotLayer = [_metroDot copy];
  293. dotLayer.frame = f;
  294. [self.layer addSublayer:dotLayer];
  295. [dots addObject:dotLayer];
  296. CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
  297. animation.duration = self.animationDuration;
  298. animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.15f :0.60f :0.85f :0.4f];
  299. [animation setCalculationMode:kCAAnimationPaced];
  300. animation.path = animationPath.CGPath;
  301. animation.repeatCount = HUGE_VALF;
  302. if (_animationShape != M13ProgressViewMetroAnimationShapeLine) {
  303. //No delay needed
  304. [dotLayer addAnimation:animation forKey:@"metroAnimation"];
  305. } else {
  306. //Delay repeat
  307. animation.repeatCount = 1;
  308. CAAnimationGroup *group = [CAAnimationGroup animation];
  309. group.duration = self.animationDuration * 2;
  310. group.animations = @[animation];
  311. group.repeatCount = HUGE_VALF;
  312. [dotLayer addAnimation:group forKey:@"metroAnimation"];
  313. }
  314. } else {
  315. [animationTimer invalidate];
  316. }
  317. }
  318. -(void)stopAnimating
  319. {
  320. animating = NO;
  321. [animationTimer invalidate];
  322. for (CALayer *layer in dots) {
  323. [layer removeAllAnimations];
  324. [layer removeFromSuperlayer];
  325. }
  326. }
  327. @end