Files
twui/lib/UIKit/TUIProgressBar.m

383 lines
15 KiB
Objective-C

/*
Copyright 2012 Twitter, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this work except in compliance with the License.
You may obtain a copy of the License in the LICENSE file, or at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "TUIProgressBar.h"
#import "CAAnimation+TUIExtensions.h"
#import "TUICGAdditions.h"
NSString *GHUIProgressBarSetNeedsDisplayObservationContext = @"GHUIProgressBarSetNeedsDisplayObservationContext";
CGFloat const GHUIProgressBarBarberPoleAnimationDuration = 5.0;
CGFloat const GHUIProgressBarBarberPolePatternWidth = 16.0;
CGFloat const GHUIProgressBarIdealTrackHeight = 12.0;
// Simple struct used to pass information from the overlay drawing into the
// pattern that it uses.
//
// bounds - the bounds of the pattern segment
// contentsScale - the backing contents scale, necessary to correctly scale the
// drawing of the pattern to match the backing scale factor.
//
struct TUIProgressBarPatternInfoStruct {
CGRect bounds;
CGFloat contentsScale;
};
void GHUIProgressPatternDrawCallback(void *info, CGContextRef context);
@interface TUIProgressBar ()
@property (nonatomic, strong) TUIView *animationView; //Merely used for easy cleanup as needed
@property (nonatomic, readonly) CGPoint farLeftAnimatingPosition;
@property (nonatomic, readonly) CGPoint farRightAnimatingPosition;
@property (nonatomic, readonly) TUIProgressBarStyle style;
- (CGRect)fillRect;
@end
@implementation TUIProgressBar
@synthesize drawTrack = _drawTrack;
@synthesize drawFill = _drawFill;
@synthesize progress = _progress;
@synthesize indeterminate = _indeterminate;
@synthesize animationView = _animationContainer;
@synthesize style = _style;
#pragma mark TUIView
- (id)initWithFrame:(CGRect)frame
{
return [self initWithFrame:frame style:TUIProgressBarStyleBlue];
}
- (id)initWithFrame:(CGRect)frame style:(TUIProgressBarStyle)style {
self = [super initWithFrame:frame];
if(self == nil) return nil;
self.backgroundColor = [NSColor clearColor];
self.clipsToBounds = YES;
_style = style;
CGRect (^trackRectForFrame)(CGRect) = ^ (CGRect givenFrame) {
return CGRectMake(NSMinX(givenFrame) + 0.5, NSMinY(givenFrame) + 1.5, NSWidth(givenFrame) - 1.0, NSHeight(givenFrame) - 2.0);
};
CGFloat (^trackRadiusForTrackRect)(CGRect) = ^ (CGRect trackRect) {
return ceil(NSHeight(trackRect) / 2.0);
};
self.drawTrack = ^ (TUIView *view, CGRect dirtyRect) {
CGRect trackRect = trackRectForFrame(view.bounds);
CGFloat radius = trackRadiusForTrackRect(trackRect);
//Backing grad
[NSGraphicsContext saveGraphicsState];
CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
CGContextAddRoundRect(context, trackRect, radius);
CGContextClip(context);
NSGradient *backingGrad = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithCalibratedWhite:(51.0/255.0) alpha:0.6] endingColor:[NSColor colorWithCalibratedWhite:(85.0/255.0) alpha:0.6]];
[backingGrad drawInRect:trackRect angle:270.0];
[NSGraphicsContext restoreGraphicsState];
//Stroke
[NSGraphicsContext saveGraphicsState];
context = [[NSGraphicsContext currentContext] graphicsPort];
CGContextSetBlendMode([[NSGraphicsContext currentContext] graphicsPort], kCGBlendModeMultiply);
[[NSColor colorWithCalibratedWhite:(66.0/255.0) alpha:1.0] set];
CGContextAddRoundRect(context, trackRect, radius);
CGContextStrokePath(context);
[NSGraphicsContext restoreGraphicsState];
//Drop shadow
[NSGraphicsContext saveGraphicsState];
CGContextSetBlendMode([[NSGraphicsContext currentContext] graphicsPort], kCGBlendModeScreen);
[[NSColor colorWithCalibratedWhite:1.0 alpha:0.54] set];
CGFloat y = NSMinY(view.bounds);
[NSBezierPath strokeLineFromPoint:NSMakePoint(NSMinX(view.bounds) + radius, y) toPoint:NSMakePoint((NSMaxX(view.bounds) - radius), y)];
[NSGraphicsContext restoreGraphicsState];
[NSGraphicsContext saveGraphicsState];
//Inner shadow
[[NSColor colorWithCalibratedWhite:0.0 alpha:0.16] set];
y = NSMaxY(trackRect) - 1.0;
[NSBezierPath strokeLineFromPoint:NSMakePoint((NSMinX(view.bounds) + (radius - 3.0)), y) toPoint:NSMakePoint((NSMaxX(view.bounds) - (radius - 3.0)), y)];
[NSGraphicsContext restoreGraphicsState];
};
self.drawFill = ^ (TUIView *view, CGRect dirtyRect) {
TUIProgressBar *progressBar = (TUIProgressBar *)view;
if (!progressBar.indeterminate && progressBar.progress == 0.0) //Eesh… I feel dirty comparing to 0 here
return;
CGContextRef currentContext = [[NSGraphicsContext currentContext] graphicsPort];
CGRect fillRect = [progressBar fillRect];
CGFloat radius = (NSHeight(fillRect) / 2.0);
//Backing grad
[NSGraphicsContext saveGraphicsState];
CGContextAddRoundRect(currentContext, fillRect, radius);
CGContextClip(currentContext);
NSGradient *backingGrad = nil;
if (style == TUIProgressBarStyleGray) {
backingGrad = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithCalibratedWhite:(140.0/255.0) alpha:1.0] endingColor:[NSColor colorWithCalibratedWhite:(186.0/255.0) alpha:1.0]];
} else {
NSColor *startColor = [NSColor colorWithCalibratedRed:(6.0/255.0) green:(109.0/255.0) blue:(176.0/255.0) alpha:1.0];
NSColor *mid1Color = [NSColor colorWithCalibratedRed:0.0 green:(136.0/255.0) blue:(213.0/255.0) alpha:1.0];
NSColor *mid2Color = [NSColor colorWithCalibratedRed:(36.0/255.0) green:(144.0/255.0) blue:(228.0/255.0) alpha:1.0];
NSColor *endColor = [NSColor colorWithCalibratedRed:(27.0/255.0) green:(160.0/255.0) blue:(245.0/255.0) alpha:1.0];
backingGrad = [[NSGradient alloc] initWithColorsAndLocations:startColor, 0.0, mid1Color, 0.49, mid2Color, 0.51, endColor, 1.0, nil];
}
[backingGrad drawInRect:fillRect angle:90.0];
if (style != TUIProgressBarStyleGray) {
//Inner Shadow
[[NSColor colorWithCalibratedWhite:1.0 alpha:0.29] set];
CGFloat y = NSMaxY(fillRect) - 1.0;
[NSBezierPath strokeLineFromPoint:NSMakePoint(NSMinX(fillRect), y) toPoint:NSMakePoint(NSMaxX(fillRect), y)];
}
[NSGraphicsContext restoreGraphicsState];
//Stroke
[NSGraphicsContext saveGraphicsState];
CGContextSetBlendMode(currentContext, kCGBlendModeOverlay);
if (style == TUIProgressBarStyleGray) {
//Inner Shadow
[[NSColor colorWithCalibratedWhite:1.0 alpha:0.19] set];
CGFloat y = NSMaxY(fillRect) - 1.0;
[NSBezierPath strokeLineFromPoint:NSMakePoint(NSMinX(fillRect), y) toPoint:NSMakePoint(NSMaxX(fillRect), y)];
//(stroke)
[[NSColor colorWithCalibratedWhite:(71.0/255.0) alpha:1.0] set];
} else {
[[NSColor colorWithCalibratedRed:(27.0/255.0) green:(54.0/255.0) blue:(149.0/255.0) alpha:1.0] set];
}
CGContextAddRoundRect(currentContext, fillRect, radius);
CGContextStrokePath(currentContext);
[NSGraphicsContext restoreGraphicsState];
if (style != TUIProgressBarStyleGray) {
//Colour overlay
[NSGraphicsContext saveGraphicsState];
CGContextSetBlendMode(currentContext, kCGBlendModeColor);
[[NSColor colorWithCalibratedRed:(9.0/255.0) green:(115.0/255.0) blue:(198.0/255.0) alpha:1.0] set];
CGContextAddRoundRect(currentContext, fillRect, radius);
CGContextFillPath(currentContext);
[NSGraphicsContext restoreGraphicsState];
}
};
[self addObserver:self forKeyPath:@"drawTrack" options:0 context:&GHUIProgressBarSetNeedsDisplayObservationContext];
[self addObserver:self forKeyPath:@"drawFill" options:0 context:&GHUIProgressBarSetNeedsDisplayObservationContext];
[self addObserver:self forKeyPath:@"progress" options:0 context:&GHUIProgressBarSetNeedsDisplayObservationContext];
[self addObserver:self forKeyPath:@"indeterminate" options:0 context:&GHUIProgressBarSetNeedsDisplayObservationContext];
return self;
}
- (void)dealloc
{
[self removeObserver:self forKeyPath:@"drawTrack"];
[self removeObserver:self forKeyPath:@"drawFill"];
[self removeObserver:self forKeyPath:@"progress"];
[self removeObserver:self forKeyPath:@"indeterminate"];
}
- (void)drawRect:(CGRect)dirtyRect {
[self.backgroundColor set];
CGContextFillRect(TUIGraphicsGetCurrentContext(), self.bounds);
if (self.drawTrack != nil)
self.drawTrack(self, dirtyRect);
if (self.drawFill != nil)
self.drawFill(self, dirtyRect);
}
- (CGSize)sizeThatFits:(CGSize)size {
return CGSizeMake(size.width, GHUIProgressBarIdealTrackHeight);
}
- (CGRect)fillRect
{
CGFloat drawingProgress = (self.indeterminate ? 1.0 : self.progress);
if (drawingProgress > 1.0)
drawingProgress = 1.0;
static const CGFloat minimumWidth = 10.0f;
CGRect fillRect = self.bounds;
fillRect.size.width = MAX(ceil(drawingProgress * fillRect.size.width), minimumWidth);
CGFloat delta = (self.style == TUIProgressBarStyleGray ? 0.5 : 1.5);
fillRect = CGRectInset(fillRect, delta, delta);
fillRect.size.height --;
fillRect.origin.y ++;
return fillRect;
}
#pragma mark API
- (CABasicAnimation *)basicAnimationForView:(TUIView *)view animationKey:(NSString *)animationKey
{
CABasicAnimation *barberPoleAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
barberPoleAnimation.duration = GHUIProgressBarBarberPoleAnimationDuration;
barberPoleAnimation.toValue = [NSValue valueWithPoint:self.farLeftAnimatingPosition];
barberPoleAnimation.tui_completionBlock = ^ {
view.layer.position = self.farRightAnimatingPosition;
CABasicAnimation *newAnimation = [self basicAnimationForView:view animationKey:animationKey];
[view.layer addAnimation:newAnimation forKey:animationKey];
};
return barberPoleAnimation;
}
- (CGPoint)farLeftAnimatingPosition
{
return CGPointMake(NSMinX(self.animationView.superview.bounds) + 9.0, NSMidY(self.animationView.superview.bounds));
}
- (CGPoint)farRightAnimatingPosition
{
return CGPointMake(NSMaxX(self.animationView.superview.bounds), NSMidY(self.animationView.superview.bounds));
}
- (void)setIndeterminate:(BOOL)indeterminate
{
static NSString *animationKey = @"GHUIBarberPoleAnimation";
_indeterminate = indeterminate;
if (!indeterminate) {
[self.animationView.superview removeFromSuperview];
self.animationView = nil;
return;
}
if (self.animationView != nil) { //We already have an animation view added but the progress bar could have been taken off screen, so stopped animating
NSArray *animationKeys = self.animationView.layer.animationKeys;
if (animationKeys == nil || ![animationKeys containsObject:animationKey]) {
[self.animationView.layer addAnimation:[self basicAnimationForView:self.animationView animationKey:animationKey] forKey:animationKey];
}
return;
}
// Animation container
TUIView *animationClippingView = [[TUIView alloc] initWithFrame:[self fillRect]];
animationClippingView.clipsToBounds = YES;
animationClippingView.opaque = NO;
animationClippingView.backgroundColor = [NSColor clearColor];
CGPathRef clipPath = TUICGPathCreateRoundedRect(animationClippingView.bounds, ceil(NSHeight(animationClippingView.bounds) / 2.0));
CAShapeLayer *clipLayer = [[CAShapeLayer alloc] init];
clipLayer.path = clipPath;
animationClippingView.layer.mask = clipLayer;
CGPathRelease(clipPath);
CGRect animationViewFrame = CGRectMake(NSMinX(animationClippingView.bounds),NSMinY(animationClippingView.bounds), (NSWidth(animationClippingView.bounds) * 2.0), NSHeight(animationClippingView.bounds));
self.animationView = [[TUIView alloc] initWithFrame:animationViewFrame];
self.animationView.opaque = NO;
self.animationView.backgroundColor = [NSColor clearColor];
__unsafe_unretained TUIProgressBar *weakSelf = self;
self.animationView.drawRect = ^ (TUIView *view, CGRect dirtyRect) {
TUIProgressBar *strongSelf = weakSelf;
CGRect patternBounds = CGRectMake(0.0, 0.0, GHUIProgressBarBarberPolePatternWidth, NSHeight(view.bounds));
[NSGraphicsContext saveGraphicsState];
CGContextRef currentContext = [[NSGraphicsContext currentContext] graphicsPort];
CGColorSpaceRef patternColorSpace = CGColorSpaceCreatePattern(NULL);
CGContextSetFillColorSpace(currentContext, patternColorSpace);
CGColorSpaceRelease(patternColorSpace);
const struct TUIProgressBarPatternInfoStruct info = {.bounds = patternBounds, .contentsScale = view.layer.contentsScale};
const struct CGPatternCallbacks callbacks = {0, &GHUIProgressPatternDrawCallback, NULL};
CGPatternRef pattern = CGPatternCreate((void *)&info, patternBounds, CGAffineTransformIdentity, (GHUIProgressBarBarberPolePatternWidth * strongSelf.layer.contentsScale), (NSHeight(view.bounds) * strongSelf.layer.contentsScale), kCGPatternTilingConstantSpacing, true, &callbacks);
CGFloat components = 1.0; //It's a coloured pattern so just alpha is fine
CGContextSetFillPattern(currentContext, pattern, &components);
CGContextFillRect(currentContext, view.bounds);
CGPatternRelease(pattern);
[NSGraphicsContext restoreGraphicsState];
};
[self addSubview:animationClippingView];
[animationClippingView addSubview:self.animationView];
[self.animationView.layer addAnimation:[self basicAnimationForView:self.animationView animationKey:animationKey] forKey:animationKey];
[self setNeedsDisplay];
}
- (void)setProgress:(CGFloat)progress
{
_progress = progress;
if (progress > 0.0 && self.indeterminate)
self.indeterminate = NO;
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == &GHUIProgressBarSetNeedsDisplayObservationContext) {
[self setNeedsDisplay];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - Pattern Drawing Callbacks
void GHUIProgressPatternDrawCallback(void *info, CGContextRef context)
{
const struct TUIProgressBarPatternInfoStruct *passedInfo = info;
CGFloat contentsScale = passedInfo->contentsScale;
CGContextScaleCTM(context, contentsScale, contentsScale);
CGRect bounds = passedInfo->bounds;
CGContextSetBlendMode([[NSGraphicsContext currentContext] graphicsPort], kCGBlendModeOverlay);
CGMutablePathRef fillPath = CGPathCreateMutable();
CGPathMoveToPoint(fillPath, NULL, NSMinX(bounds), NSMinY(bounds));
CGPathAddLineToPoint(fillPath, NULL, NSMidX(bounds), NSMaxY(bounds));
CGPathAddLineToPoint(fillPath, NULL, NSMaxX(bounds), NSMaxY(bounds));
CGPathAddLineToPoint(fillPath, NULL, NSMidX(bounds), NSMinY(bounds));
CGPathCloseSubpath(fillPath);
CGContextAddPath(context, fillPath);
CGColorRef fillColor = CGColorCreateGenericGray(1.0, 0.24);
CGContextSetFillColorWithColor(context, fillColor);
CGColorRelease(fillColor);
CGContextFillPath(context);
CGPathRelease(fillPath);
}
@end