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.

300 lines
9.4 KiB

5 years ago
  1. //
  2. // M13ProgressViewLetterpress.m
  3. // M13ProgressSuite
  4. //
  5. // Created by Brandon McQuilkin on 4/28/14.
  6. // Copyright (c) 2014 Brandon McQuilkin. All rights reserved.
  7. //
  8. #import "M13ProgressViewLetterpress.h"
  9. @interface LetterpressView : UIView
  10. @property (nonatomic, strong) M13ProgressViewLetterpress *progressView;
  11. @property (nonatomic, assign) CGRect drawRect;
  12. @end
  13. @interface M13ProgressViewLetterpress ()
  14. /**The start progress for the progress animation.*/
  15. @property (nonatomic, assign) CGFloat animationFromValue;
  16. /**The end progress for the progress animation.*/
  17. @property (nonatomic, assign) CGFloat animationToValue;
  18. /**The start time interval for the animaiton.*/
  19. @property (nonatomic, assign) CFTimeInterval animationStartTime;
  20. /**Link to the display to keep animations in sync.*/
  21. @property (nonatomic, strong) CADisplayLink *displayLink;
  22. /**
  23. The display link that controls the spring animation.
  24. */
  25. @property (nonatomic, strong) CADisplayLink *springDisplayLink;
  26. @end
  27. @implementation M13ProgressViewLetterpress
  28. {
  29. CGFloat rotation;
  30. CGFloat restRotation;
  31. CGFloat velocity;
  32. LetterpressView *letterpressView;
  33. }
  34. - (id)init
  35. {
  36. self = [super init];
  37. if (self) {
  38. [self setup];
  39. }
  40. return self;
  41. }
  42. - (id)initWithFrame:(CGRect)frame
  43. {
  44. self = [super initWithFrame:frame];
  45. if (self) {
  46. [self setup];
  47. }
  48. return self;
  49. }
  50. - (id)initWithCoder:(NSCoder *)aDecoder
  51. {
  52. self = [super initWithCoder:aDecoder];
  53. if (self) {
  54. [self setup];
  55. }
  56. return self;
  57. }
  58. - (void)dealloc
  59. {
  60. [_springDisplayLink invalidate];
  61. }
  62. - (void)setup
  63. {
  64. //Set defauts
  65. self.animationDuration = 1.0;
  66. _numberOfGridPoints = CGPointMake(3, 3);
  67. _notchSize = CGSizeMake(1, 1);
  68. _pointShape = M13ProgressViewLetterpressPointShapeCircle;
  69. _pointSpacing = 0.0;
  70. rotation = 0;
  71. restRotation = 0;
  72. _springConstant = 200;
  73. _dampingCoefficient = 15;
  74. _mass = 1;
  75. velocity = 0;
  76. //Set default colors
  77. self.primaryColor = [UIColor colorWithRed:0 green:122/255.0 blue:1.0 alpha:1.0];
  78. self.secondaryColor = [UIColor colorWithRed:181/255.0 green:182/255.0 blue:183/255.0 alpha:1.0];
  79. //Draw and animate a sublayer, since autolayout does not like CATransforms.
  80. letterpressView = [[LetterpressView alloc] init];
  81. letterpressView.backgroundColor = [UIColor clearColor];
  82. letterpressView.progressView = self;
  83. [self setFrame:self.frame];
  84. [self addSubview:letterpressView];
  85. //Set own background color
  86. self.backgroundColor = [UIColor clearColor];
  87. self.clipsToBounds = NO;
  88. //Setup the display link for rotation
  89. _springDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(rotateWithDisplayLink:)];
  90. [_springDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:(id)kCFRunLoopCommonModes];
  91. }
  92. - (void)setFrame:(CGRect)frame
  93. {
  94. [super setFrame:frame];
  95. //Need to inset the layer since we don't want the corner's cliped
  96. CGFloat radius = MIN(self.frame.size.width, self.frame.size.height);
  97. CGFloat size = radius / sqrtf(2.0);
  98. letterpressView.drawRect = CGRectIntegral(CGRectMake((self.frame.size.width - size) / 2.0, (self.frame.size.height - size) / 2.0, size, size));
  99. letterpressView.frame = CGRectIntegral(CGRectMake((self.frame.size.width - size) / 2.0, (self.frame.size.height - size) / 2.0, size, size));
  100. }
  101. - (void)setNeedsDisplay
  102. {
  103. [super setNeedsDisplay];
  104. [letterpressView setNeedsDisplay];
  105. }
  106. - (CGSize)intrinsicContentSize
  107. {
  108. //Everything is based on scale. No minimum size.
  109. return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);
  110. }
  111. #pragma mark - Properties
  112. - (void)setNumberOfGridPoints:(CGPoint)numberOfGridPoints
  113. {
  114. _numberOfGridPoints = numberOfGridPoints;
  115. [self setNeedsDisplay];
  116. }
  117. - (void)setPointShape:(M13ProgressViewLetterpressPointShape)pointShape
  118. {
  119. _pointShape = pointShape;
  120. [self setNeedsDisplay];
  121. }
  122. - (void)setPointSpacing:(CGFloat)pointSpacing
  123. {
  124. if (pointSpacing > 1) {
  125. pointSpacing = 1;
  126. } else if (pointSpacing < 0) {
  127. pointSpacing = 0;
  128. }
  129. _pointSpacing = pointSpacing;
  130. [self setNeedsDisplay];
  131. }
  132. - (void)setNotchSize:(CGSize)notchSize
  133. {
  134. _notchSize = notchSize;
  135. [self setNeedsDisplay];
  136. }
  137. #pragma mark - Animation
  138. - (void)setProgress:(CGFloat)progress animated:(BOOL)animated
  139. {
  140. if (animated == NO) {
  141. if (_displayLink) {
  142. //Kill running animations
  143. [_displayLink invalidate];
  144. _displayLink = nil;
  145. }
  146. [super setProgress:progress animated:NO];
  147. [self setNeedsDisplay];
  148. } else {
  149. _animationStartTime = CACurrentMediaTime();
  150. _animationFromValue = self.progress;
  151. _animationToValue = progress;
  152. if (!_displayLink) {
  153. //Create and setup the display link
  154. [self.displayLink invalidate];
  155. self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(animateProgress:)];
  156. [self.displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
  157. } /*else {
  158. //Reuse the current display link
  159. }*/
  160. }
  161. }
  162. - (void)animateProgress:(CADisplayLink *)displayLink
  163. {
  164. dispatch_async(dispatch_get_main_queue(), ^{
  165. CGFloat dt = (displayLink.timestamp - self.animationStartTime) / self.animationDuration;
  166. if (dt >= 1.0) {
  167. //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.
  168. [self.displayLink invalidate];
  169. self.displayLink = nil;
  170. [super setProgress:self.animationToValue animated:NO];
  171. [self setNeedsDisplay];
  172. return;
  173. }
  174. //Set progress
  175. [super setProgress:self.animationFromValue + dt * (self.animationToValue - self.animationFromValue) animated:YES];
  176. [self setNeedsDisplay];
  177. });
  178. }
  179. - (void)rotateWithDisplayLink:(CADisplayLink *)displayLink
  180. {
  181. //Take account for lag
  182. for (int i = 0; i < displayLink.frameInterval; i++){
  183. //Calculate the new angle
  184. CGFloat displacement = rotation - restRotation;
  185. CGFloat kx = _springConstant * displacement;
  186. CGFloat bv = _dampingCoefficient * velocity;
  187. CGFloat acceleration = (kx + bv) / _mass;
  188. velocity -= (acceleration * displayLink.duration);
  189. rotation += (velocity * displayLink.duration);
  190. //Set the angle
  191. [letterpressView setTransform:CGAffineTransformMakeRotation(rotation * M_PI / 180)];
  192. UIView *view = [[self subviews] lastObject];
  193. [view setTransform:CGAffineTransformMakeRotation(rotation * M_PI / 180)];
  194. //If we are slowing down, animate to a new angle.
  195. if (fabs(velocity) < 1) {
  196. restRotation += (arc4random() & 2 ? 90 : -90);
  197. }
  198. }
  199. }
  200. @end
  201. @implementation LetterpressView
  202. #pragma mark - Drawing
  203. - (void)drawRect:(CGRect)rect
  204. {
  205. CGContextRef ctx = UIGraphicsGetCurrentContext();
  206. //Calculate the corners of the square of the points that we will not draw
  207. CGPoint ignoreTopLeft = CGPointMake((_progressView.numberOfGridPoints.x - _progressView.notchSize.width) / 2, 0);
  208. CGPoint ignoreBottomRight = CGPointMake(_progressView.numberOfGridPoints.x - ((_progressView.numberOfGridPoints.x - _progressView.notchSize.width) / 2), (_progressView.numberOfGridPoints.y - _progressView.notchSize.height) / 2);
  209. //Calculate the point size
  210. CGSize pointSize = CGSizeMake(_drawRect.size.width / _progressView.numberOfGridPoints.x, _drawRect.size.height / _progressView.numberOfGridPoints.y);
  211. //Setup
  212. CGRect pointRect = CGRectZero;
  213. int index = -1;
  214. int indexToFillTo = (int)(_progressView.numberOfGridPoints.x * _progressView.numberOfGridPoints.y * _progressView.progress);
  215. //Draw
  216. for (int y = (int)_progressView.numberOfGridPoints.y - 1; y >= 0; y--) {
  217. for (int x = 0; x < _progressView.numberOfGridPoints.x; x++) {
  218. index += 1;
  219. //Are we in a forbidden zone
  220. if (x >= ignoreTopLeft.x && x < ignoreBottomRight.x && y >= ignoreTopLeft.y && y < ignoreBottomRight.y) {
  221. //Move to the next point
  222. continue;
  223. }
  224. //Calculat the rect of the point
  225. pointRect.size = pointSize;
  226. pointRect.origin = CGPointMake(pointSize.width * x, pointSize.height * y);
  227. pointRect = CGRectInset(pointRect, pointSize.width * _progressView.pointSpacing, pointSize.height * _progressView.pointSpacing);
  228. //Set the fill color
  229. if (index < indexToFillTo) {
  230. CGContextSetFillColorWithColor(ctx, _progressView.primaryColor.CGColor);
  231. } else {
  232. CGContextSetFillColorWithColor(ctx, _progressView.secondaryColor.CGColor);
  233. }
  234. //Draw the shape
  235. if (_progressView.pointShape == M13ProgressViewLetterpressPointShapeSquare) {
  236. CGContextFillRect(ctx, pointRect);
  237. } else if (_progressView.pointShape == M13ProgressViewLetterpressPointShapeCircle) {
  238. UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:pointRect cornerRadius:(pointRect.size.width / 2.0)];
  239. CGContextBeginPath(ctx);
  240. CGContextAddPath(ctx, path.CGPath);
  241. CGContextFillPath(ctx);
  242. }
  243. }
  244. }
  245. }
  246. @end