New file |
| | |
| | | // |
| | | // M13ProgressViewLetterpress.m |
| | | // M13ProgressSuite |
| | | // |
| | | // Created by Brandon McQuilkin on 4/28/14. |
| | | // Copyright (c) 2014 Brandon McQuilkin. All rights reserved. |
| | | // |
| | | |
| | | #import "M13ProgressViewLetterpress.h" |
| | | |
| | | @interface LetterpressView : UIView |
| | | |
| | | @property (nonatomic, strong) M13ProgressViewLetterpress *progressView; |
| | | @property (nonatomic, assign) CGRect drawRect; |
| | | |
| | | @end |
| | | |
| | | @interface M13ProgressViewLetterpress () |
| | | |
| | | /**The start progress for the progress animation.*/ |
| | | @property (nonatomic, assign) CGFloat animationFromValue; |
| | | /**The end progress for the progress animation.*/ |
| | | @property (nonatomic, assign) CGFloat animationToValue; |
| | | /**The start time interval for the animaiton.*/ |
| | | @property (nonatomic, assign) CFTimeInterval animationStartTime; |
| | | /**Link to the display to keep animations in sync.*/ |
| | | @property (nonatomic, strong) CADisplayLink *displayLink; |
| | | /** |
| | | The display link that controls the spring animation. |
| | | */ |
| | | @property (nonatomic, strong) CADisplayLink *springDisplayLink; |
| | | |
| | | @end |
| | | |
| | | @implementation M13ProgressViewLetterpress |
| | | { |
| | | CGFloat rotation; |
| | | CGFloat restRotation; |
| | | CGFloat velocity; |
| | | LetterpressView *letterpressView; |
| | | } |
| | | |
| | | - (id)init |
| | | { |
| | | self = [super init]; |
| | | if (self) { |
| | | [self setup]; |
| | | } |
| | | return self; |
| | | } |
| | | |
| | | - (id)initWithFrame:(CGRect)frame |
| | | { |
| | | self = [super initWithFrame:frame]; |
| | | if (self) { |
| | | [self setup]; |
| | | } |
| | | return self; |
| | | } |
| | | |
| | | - (id)initWithCoder:(NSCoder *)aDecoder |
| | | { |
| | | self = [super initWithCoder:aDecoder]; |
| | | if (self) { |
| | | [self setup]; |
| | | } |
| | | return self; |
| | | } |
| | | |
| | | - (void)dealloc |
| | | { |
| | | [_springDisplayLink invalidate]; |
| | | } |
| | | |
| | | - (void)setup |
| | | { |
| | | //Set defauts |
| | | self.animationDuration = 1.0; |
| | | _numberOfGridPoints = CGPointMake(3, 3); |
| | | _notchSize = CGSizeMake(1, 1); |
| | | _pointShape = M13ProgressViewLetterpressPointShapeCircle; |
| | | _pointSpacing = 0.0; |
| | | |
| | | rotation = 0; |
| | | restRotation = 0; |
| | | _springConstant = 200; |
| | | _dampingCoefficient = 15; |
| | | _mass = 1; |
| | | velocity = 0; |
| | | |
| | | //Set default colors |
| | | self.primaryColor = [UIColor colorWithRed:0 green:122/255.0 blue:1.0 alpha:1.0]; |
| | | self.secondaryColor = [UIColor colorWithRed:181/255.0 green:182/255.0 blue:183/255.0 alpha:1.0]; |
| | | |
| | | //Draw and animate a sublayer, since autolayout does not like CATransforms. |
| | | letterpressView = [[LetterpressView alloc] init]; |
| | | letterpressView.backgroundColor = [UIColor clearColor]; |
| | | letterpressView.progressView = self; |
| | | [self setFrame:self.frame]; |
| | | |
| | | [self addSubview:letterpressView]; |
| | | |
| | | //Set own background color |
| | | self.backgroundColor = [UIColor clearColor]; |
| | | self.clipsToBounds = NO; |
| | | |
| | | //Setup the display link for rotation |
| | | _springDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(rotateWithDisplayLink:)]; |
| | | [_springDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:(id)kCFRunLoopCommonModes]; |
| | | |
| | | } |
| | | |
| | | - (void)setFrame:(CGRect)frame |
| | | { |
| | | [super setFrame:frame]; |
| | | //Need to inset the layer since we don't want the corner's cliped |
| | | CGFloat radius = MIN(self.frame.size.width, self.frame.size.height); |
| | | CGFloat size = radius / sqrtf(2.0); |
| | | letterpressView.drawRect = CGRectIntegral(CGRectMake((self.frame.size.width - size) / 2.0, (self.frame.size.height - size) / 2.0, size, size)); |
| | | letterpressView.frame = CGRectIntegral(CGRectMake((self.frame.size.width - size) / 2.0, (self.frame.size.height - size) / 2.0, size, size)); |
| | | } |
| | | |
| | | - (void)setNeedsDisplay |
| | | { |
| | | [super setNeedsDisplay]; |
| | | [letterpressView setNeedsDisplay]; |
| | | } |
| | | |
| | | - (CGSize)intrinsicContentSize |
| | | { |
| | | //Everything is based on scale. No minimum size. |
| | | return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric); |
| | | } |
| | | |
| | | #pragma mark - Properties |
| | | |
| | | - (void)setNumberOfGridPoints:(CGPoint)numberOfGridPoints |
| | | { |
| | | _numberOfGridPoints = numberOfGridPoints; |
| | | [self setNeedsDisplay]; |
| | | } |
| | | |
| | | - (void)setPointShape:(M13ProgressViewLetterpressPointShape)pointShape |
| | | { |
| | | _pointShape = pointShape; |
| | | [self setNeedsDisplay]; |
| | | } |
| | | |
| | | - (void)setPointSpacing:(CGFloat)pointSpacing |
| | | { |
| | | if (pointSpacing > 1) { |
| | | pointSpacing = 1; |
| | | } else if (pointSpacing < 0) { |
| | | pointSpacing = 0; |
| | | } |
| | | _pointSpacing = pointSpacing; |
| | | [self setNeedsDisplay]; |
| | | } |
| | | |
| | | - (void)setNotchSize:(CGSize)notchSize |
| | | { |
| | | _notchSize = notchSize; |
| | | [self setNeedsDisplay]; |
| | | } |
| | | |
| | | #pragma mark - Animation |
| | | |
| | | - (void)setProgress:(CGFloat)progress animated:(BOOL)animated |
| | | { |
| | | if (animated == NO) { |
| | | if (_displayLink) { |
| | | //Kill running animations |
| | | [_displayLink invalidate]; |
| | | _displayLink = nil; |
| | | } |
| | | [super setProgress:progress animated:NO]; |
| | | [self setNeedsDisplay]; |
| | | } else { |
| | | _animationStartTime = CACurrentMediaTime(); |
| | | _animationFromValue = self.progress; |
| | | _animationToValue = progress; |
| | | if (!_displayLink) { |
| | | //Create and setup the display link |
| | | [self.displayLink invalidate]; |
| | | self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(animateProgress:)]; |
| | | [self.displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes]; |
| | | } /*else { |
| | | //Reuse the current display link |
| | | }*/ |
| | | } |
| | | } |
| | | |
| | | - (void)animateProgress:(CADisplayLink *)displayLink |
| | | { |
| | | dispatch_async(dispatch_get_main_queue(), ^{ |
| | | CGFloat dt = (displayLink.timestamp - self.animationStartTime) / self.animationDuration; |
| | | if (dt >= 1.0) { |
| | | //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. |
| | | [self.displayLink invalidate]; |
| | | self.displayLink = nil; |
| | | [super setProgress:self.animationToValue animated:NO]; |
| | | [self setNeedsDisplay]; |
| | | return; |
| | | } |
| | | |
| | | //Set progress |
| | | [super setProgress:self.animationFromValue + dt * (self.animationToValue - self.animationFromValue) animated:YES]; |
| | | [self setNeedsDisplay]; |
| | | |
| | | }); |
| | | } |
| | | |
| | | - (void)rotateWithDisplayLink:(CADisplayLink *)displayLink |
| | | { |
| | | //Take account for lag |
| | | for (int i = 0; i < displayLink.frameInterval; i++){ |
| | | |
| | | //Calculate the new angle |
| | | CGFloat displacement = rotation - restRotation; |
| | | CGFloat kx = _springConstant * displacement; |
| | | CGFloat bv = _dampingCoefficient * velocity; |
| | | CGFloat acceleration = (kx + bv) / _mass; |
| | | |
| | | velocity -= (acceleration * displayLink.duration); |
| | | rotation += (velocity * displayLink.duration); |
| | | |
| | | //Set the angle |
| | | [letterpressView setTransform:CGAffineTransformMakeRotation(rotation * M_PI / 180)]; |
| | | |
| | | UIView *view = [[self subviews] lastObject]; |
| | | [view setTransform:CGAffineTransformMakeRotation(rotation * M_PI / 180)]; |
| | | |
| | | //If we are slowing down, animate to a new angle. |
| | | if (fabs(velocity) < 1) { |
| | | restRotation += (arc4random() & 2 ? 90 : -90); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @end |
| | | |
| | | @implementation LetterpressView |
| | | |
| | | #pragma mark - Drawing |
| | | |
| | | - (void)drawRect:(CGRect)rect |
| | | { |
| | | CGContextRef ctx = UIGraphicsGetCurrentContext(); |
| | | |
| | | //Calculate the corners of the square of the points that we will not draw |
| | | CGPoint ignoreTopLeft = CGPointMake((_progressView.numberOfGridPoints.x - _progressView.notchSize.width) / 2, 0); |
| | | CGPoint ignoreBottomRight = CGPointMake(_progressView.numberOfGridPoints.x - ((_progressView.numberOfGridPoints.x - _progressView.notchSize.width) / 2), (_progressView.numberOfGridPoints.y - _progressView.notchSize.height) / 2); |
| | | //Calculate the point size |
| | | CGSize pointSize = CGSizeMake(_drawRect.size.width / _progressView.numberOfGridPoints.x, _drawRect.size.height / _progressView.numberOfGridPoints.y); |
| | | |
| | | //Setup |
| | | CGRect pointRect = CGRectZero; |
| | | int index = -1; |
| | | int indexToFillTo = (int)(_progressView.numberOfGridPoints.x * _progressView.numberOfGridPoints.y * _progressView.progress); |
| | | |
| | | //Draw |
| | | for (int y = (int)_progressView.numberOfGridPoints.y - 1; y >= 0; y--) { |
| | | for (int x = 0; x < _progressView.numberOfGridPoints.x; x++) { |
| | | |
| | | index += 1; |
| | | |
| | | //Are we in a forbidden zone |
| | | if (x >= ignoreTopLeft.x && x < ignoreBottomRight.x && y >= ignoreTopLeft.y && y < ignoreBottomRight.y) { |
| | | //Move to the next point |
| | | continue; |
| | | } |
| | | |
| | | //Calculat the rect of the point |
| | | pointRect.size = pointSize; |
| | | pointRect.origin = CGPointMake(pointSize.width * x, pointSize.height * y); |
| | | pointRect = CGRectInset(pointRect, pointSize.width * _progressView.pointSpacing, pointSize.height * _progressView.pointSpacing); |
| | | |
| | | //Set the fill color |
| | | if (index < indexToFillTo) { |
| | | CGContextSetFillColorWithColor(ctx, _progressView.primaryColor.CGColor); |
| | | } else { |
| | | CGContextSetFillColorWithColor(ctx, _progressView.secondaryColor.CGColor); |
| | | } |
| | | |
| | | //Draw the shape |
| | | if (_progressView.pointShape == M13ProgressViewLetterpressPointShapeSquare) { |
| | | CGContextFillRect(ctx, pointRect); |
| | | } else if (_progressView.pointShape == M13ProgressViewLetterpressPointShapeCircle) { |
| | | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:pointRect cornerRadius:(pointRect.size.width / 2.0)]; |
| | | CGContextBeginPath(ctx); |
| | | CGContextAddPath(ctx, path.CGPath); |
| | | CGContextFillPath(ctx); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | @end |