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
300 lines
9.4 KiB
//
|
|
// 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
|