This question continues from a previous answer.
I have the following CAShapeLayer
:
- (CAShapeLayer *)gaugeCircleLayer {
if (_gaugeCircleLayer == nil) {
_gaugeCircleLayer = [CAShapeLayer layer];
_gaugeCircleLayer.lineWidth = self.gaugeWidth;
_gaugeCircleLayer.fillColor = [UIColor clearColor].CGColor;
_gaugeCircleLayer.strokeColor = self.gaugeTintColor.CGColor;
_gaugeCircleLayer.strokeStart = 0.0f;
_gaugeCircleLayer.strokeEnd = self.value;
_gaugeCircleLayer.lineCap = kCALineCapRound;
_gaugeCircleLayer.path = [self insideCirclePath].CGPath;
CAShapeLayer *cap = [CAShapeLayer layer];
cap.shadowColor = [UIColor blackColor].CGColor;
cap.shadowRadius = 8.0;
cap.shadowOpacity = 0.9;
cap.shadowOffset = CGSizeMake(0, 0);
cap.fillColor = [UIColor grayColor].CGColor;
[_gaugeCircleLayer addSublayer:cap];
}
return _gaugeCircleLayer;
}
Then I have the following UIBezierPath
:
- (UIBezierPath *)insideCirclePath {
CGPoint arcCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:arcCenter
radius:CGRectGetWidth(self.bounds) / 2.0f
startAngle:(3.0f * M_PI_2)
endAngle:(3.0f * M_PI_2) + (2.0f * M_PI)
clockwise:YES];
return path;
}
This produces something like the following:
What I am trying to produce with the cap
sublayer is the drop shadow at the end and I'd also be interested to know how to get a GradientLayer working similar to the image below:
The problem is that the sublayer is not appearing anywhere and I'm not quite sure why. I'm also not 100% sure on whether or not this is the best way to produce the desired effect.
UPDATE:
The following bit of code creates a cap, though I'm not quite sure how to add it to my UIBezierPath
properly:
let cap = CAShapeLayer()
cap.shadowColor = UIColor.blackColor().CGColor
cap.shadowRadius = 8.0
cap.shadowOpacity = 0.9
cap.shadowOffset = CGSize(width: 0, height: 0)
cap.path = UIBezierPath(ovalInRect: CGRectMake(0, 40, 20, 20)).CGPath
cap.fillColor = UIColor.grayColor().CGColor
layer.addSublayer(cap)
I don't know if this will be useful to you, since it doesn't use the CHCircleGaugeView. I was working on several projects related to this question, so I mashed them together and made some changes to produce a progress view that has a color gradient background with a tip that overlaps the end at 100%. I haven't gotten to the point where I make the rounded tip disappear at 0%, but I'll get there eventually. Here are a couple of views of it at 2 different progress levels,
The view is created with a polar gradient drawn in drawRect, masked by an annulus. The rounded tip is a separate layer that's a half circle on the end of a line connected to the center of the circle that's revolved around its center with a transform based on the progress level. Here's the code,
@implementation AnnulusProgressView{ // subclass of UIView
int slices;
CGFloat circleRadius;
CAShapeLayer *maskLayer;
CGFloat segmentAngle;
CGFloat startAngle;
CAShapeLayer *tipView;
NSMutableArray *colors;
int sign;
}
-(instancetype)initWithFrame:(CGRect)frame wantsBackgroundRing:(BOOL)wantsBackground backgroundRingColor:(UIColor *)ringColor {
if (self = [super initWithFrame:frame]) {
slices = 360;
_ringThickness = 12;
circleRadius = self.bounds.size.width/2;
_startColor = [UIColor colorWithHue:0.24 saturation:1 brightness:0.8 alpha:1];
_endColor = [UIColor colorWithHue:0.03 saturation:1 brightness:1 alpha:1];
[self setupColorArray];
_backgroundRing = wantsBackground? [self createBackgroundRingWithColor:ringColor] : nil;
self.layer.mask = [self createAnnulusMask];
}
return self;
}
-(void)setStartColor:(UIColor *)startColor {
_startColor = startColor;
[self setupColorArray];
}
-(void)setEndColor:(UIColor *)endColor {
_endColor = endColor;
[self setupColorArray];
}
-(void)setupColorArray {
colors = [NSMutableArray new];
CGFloat startHue, startSaturation, startBrightness, startAlpha, endHue, endSaturation, endBrightness, endAlpha;
[self.startColor getHue:&startHue saturation:&startSaturation brightness:&startBrightness alpha:&startAlpha];
[self.endColor getHue:&endHue saturation:&endSaturation brightness:&endBrightness alpha:&endAlpha];
for(int i=0;i<slices;i++){
CGFloat hue = startHue + (endHue - startHue)*i/slices;
CGFloat brightness = startBrightness + (endBrightness - startBrightness)*i/slices;
CGFloat saturation = startSaturation + (endSaturation - startSaturation)*i/slices;
CGFloat alpha = startAlpha + (endAlpha - startAlpha)*i/slices;
UIColor *color = [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:alpha];
[colors addObject:color];
}
self.progress = _progress;
}
-(UIView *)createBackgroundRingWithColor:(UIColor *) color {
UIView *bgRing = [[UIView alloc] initWithFrame:self.frame];
bgRing.backgroundColor = color;
bgRing.layer.mask = [self createAnnulusMask];
[(CAShapeLayer *)bgRing.layer.mask setStrokeEnd:startAngle + 2*M_PI ];
return bgRing;
}
-(void)didMoveToSuperview {
if (self.backgroundRing) [self.superview insertSubview:self.backgroundRing belowSubview:self];
tipView = [self tipView];
[self.superview.layer addSublayer:tipView];
}
-(CAShapeLayer *)tipView {
CAShapeLayer *tip = [CAShapeLayer layer];
tip.frame = self.frame;
tip.fillColor = [UIColor redColor].CGColor;
UIBezierPath *shape = [UIBezierPath bezierPath];
CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
[shape moveToPoint:center];
CGPoint bottomPoint = CGPointMake(center.x, center.y + circleRadius - self.ringThickness*2);
CGFloat tipCenterY = bottomPoint.y + self.ringThickness - 1;
[shape addLineToPoint: bottomPoint];
[shape addLineToPoint:CGPointMake(bottomPoint.x+2, bottomPoint.y)];
double fractionAlongTangent = self.ringThickness;
[shape addCurveToPoint:CGPointMake(center.x, center.y + circleRadius) controlPoint1:CGPointMake(center.x - self.ringThickness*1.5, tipCenterY - fractionAlongTangent) controlPoint2:CGPointMake(center.x - self.ringThickness*1.5, tipCenterY + fractionAlongTangent)];
[shape closePath];
tip.path = shape.CGPath;
tip.shadowColor = [UIColor darkGrayColor].CGColor;
tip.shadowOpacity = 0.8;
tip.shadowOffset = CGSizeMake(-6, 0);
tip.shadowRadius = 2;
return tip;
}
- (void)setProgress:(CGFloat)progress{
sign = (progress >= _progress)? 1 : -1;
[self animateProgressTo:@(progress)];
}
-(void)animateProgressTo:(NSNumber *) newValueNumber{
float newValue = newValueNumber.floatValue;
_progress = (_progress + (sign * 0.1) > 1)? 1 : _progress + (sign * 0.1);
if ((_progress > newValue && sign == 1) || (_progress < newValue && sign == -1)) {
_progress = newValue;
}
maskLayer.strokeEnd = _progress;
tipView.transform = CATransform3DMakeRotation(-(1 - _progress + 0.002) * M_PI*2, 0, 0, 1); //the 0.002 closes a small gap between the tip and the annulus strokeEnd
int i = (int)(_progress*(slices - 1));
tipView.fillColor = ((UIColor *)colors[i]).CGColor;
if (sign == 1) {
if (_progress < newValue) {
[self performSelector:@selector(animateProgressTo:) withObject:@(newValue) afterDelay:0.05];
}
}else{
if (_progress > newValue) {
[self performSelector:@selector(animateProgressTo:) withObject:@(newValue) afterDelay:0.05];
}
}
NSLog(@"%f",_progress);
}
- (CAShapeLayer *)createAnnulusMask {
maskLayer = [CAShapeLayer layer];
maskLayer.frame = self.bounds;
segmentAngle = 2*M_PI/(slices);
startAngle = M_PI_2;
CGFloat endAngle = startAngle + 2*M_PI;
maskLayer.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)) radius:circleRadius - self.ringThickness startAngle:startAngle endAngle:endAngle clockwise:YES].CGPath;
maskLayer.fillColor = [UIColor clearColor].CGColor;
maskLayer.strokeColor = [UIColor blackColor].CGColor;
maskLayer.lineWidth = self.ringThickness * 2;
maskLayer.strokeEnd = self.progress;
return maskLayer;
}
-(void)drawRect:(CGRect)rect{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetAllowsAntialiasing(ctx, NO);
CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
for(int i=0;i<slices;i++){
CGContextSaveGState(ctx);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:center];
[path addArcWithCenter:center radius:circleRadius startAngle:startAngle endAngle:startAngle+segmentAngle clockwise:YES];
[path addClip];
[colors[i] setFill];
[path fill];
CGContextRestoreGState(ctx);
startAngle += segmentAngle;
}
}
This should resolve the problem.
Only remaining issue is the animation, whereby the cap is not animated.
The trick was to add the cap to the end of the gauge, and update it when the value of the gauge changed. To calculate the location, a little math magic was used. It needs to be under the gauge, so the cap is added in the trackCircleLayer
//
// CHCircleGaugeView.m
//
// Copyright (c) 2014 ChaiOne <http://www.chaione.com/>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#import "CHCircleGaugeView.h"
#import "CHCircleGaugeViewDebugMacros.h"
#import <CoreText/CoreText.h>
#import <QuartzCore/QuartzCore.h>
static CGFloat const CHKeyAnimationDuration = 0.5f;
static CGFloat const CHKeyDefaultValue = 0.0f;
static CGFloat const CHKeyDefaultFontSize = 32.0f;
static CGFloat const CHKeyDefaultTrackWidth = 0.5f;
static CGFloat const CHKeyDefaultGaugeWidth = 2.0f;
static NSString * const CHKeyDefaultNAText = @"n/a";
static NSString * const CHKeyDefaultNoAnswersText = @"%";
#define CHKeyDefaultTrackTintColor [UIColor blackColor]
#define CHKeyDefaultGaugeTintColor [UIColor blackColor]
#define CHKeyDefaultTextColor [UIColor blackColor]
@interface CHCircleGaugeView ()
@property (nonatomic, strong) CAShapeLayer *trackCircleLayer;
@property (nonatomic, strong) CAShapeLayer *gaugeCircleLayer;
// ADDED
@property (nonatomic, strong) CAShapeLayer *capLayer;
// END ADDED
@property (nonatomic, strong) UILabel *valueTextLabel;
@end
@implementation CHCircleGaugeView
#pragma mark - View Initialization
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initSetup];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self initSetup];
}
return self;
}
- (void)initSetup {
_state = CHCircleGaugeViewStateNA;
_value = CHKeyDefaultValue;
_trackTintColor = CHKeyDefaultTrackTintColor;
_gaugeTintColor = CHKeyDefaultGaugeTintColor;
_textColor = CHKeyDefaultTextColor;
_font = [UIFont systemFontOfSize:CHKeyDefaultFontSize];
_trackWidth = CHKeyDefaultTrackWidth;
_gaugeWidth = CHKeyDefaultGaugeWidth;
_notApplicableString = CHKeyDefaultNAText;
_noDataString = CHKeyDefaultNoAnswersText;
[self createGauge];
}
- (void)createGauge {
[self.layer addSublayer:self.trackCircleLayer];
[self.layer addSublayer:self.gaugeCircleLayer];
[self addSubview:self.valueTextLabel];
[self setupConstraints];
}
- (void)setupConstraints {
NSDictionary *viewDictionary = @{@"valueText" : self.valueTextLabel};
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[valueText]|" options:0 metrics:nil views:viewDictionary]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[valueText]|" options:0 metrics:nil views:viewDictionary]];
}
#pragma mark - Property Setters
- (void)setState:(CHCircleGaugeViewState)state {
if (_state != state) {
_state = state;
switch (state) {
case CHCircleGaugeViewStateNA: {
[self updateGaugeWithValue:0 animated:NO];
break;
}
case CHCircleGaugeViewStatePercentSign: {
[self updateGaugeWithValue:0 animated:NO];
break;
}
case CHCircleGaugeViewStateScore: {
[self updateGaugeWithValue:self.value animated:NO];
break;
}
default: {
ALog(@"Missing gauge state.");
break;
}
}
}
}
- (void)setValue:(CGFloat)value {
[self setValue:value animated:NO];
}
- (void)setValue:(CGFloat)value animated:(BOOL)animated {
self.state = CHCircleGaugeViewStateScore;
if (value != _value) {
[self willChangeValueForKey:NSStringFromSelector(@selector(value))];
value = MIN(1.0f, MAX(0.0f, value));
[self updateGaugeWithValue:value animated:animated];
_value = value;
[self didChangeValueForKey:NSStringFromSelector(@selector(value))];
}
}
- (void)setUnitsString:(NSString *)unitsString {
if ([_unitsString isEqualToString:unitsString] == NO) {
_unitsString = [unitsString copy];
self.valueTextLabel.attributedText = [self formattedStringForValue:self.value];
}
}
- (void)updateGaugeWithValue:(CGFloat)value animated:(BOOL)animated {
self.valueTextLabel.attributedText = [self formattedStringForValue:value];
BOOL previousDisableActionsValue = [CATransaction disableActions];
[CATransaction setDisableActions:YES];
self.gaugeCircleLayer.strokeEnd = value;
// ADDED
_capLayer.path = [self capPathForValue:value].CGPath;
// END ADDED
if (animated) {
self.gaugeCircleLayer.strokeEnd = value;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = CHKeyAnimationDuration;
pathAnimation.fromValue = [NSNumber numberWithFloat:self.value];
pathAnimation.toValue = [NSNumber numberWithFloat:value];
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.gaugeCircleLayer addAnimation:pathAnimation forKey:@"strokeEndAnimation"];
}
[CATransaction setDisableActions:previousDisableActionsValue];
}
- (void)setTrackTintColor:(UIColor *)trackTintColor {
if (_trackTintColor != trackTintColor) {
_trackTintColor = trackTintColor;
self.trackCircleLayer.strokeColor = trackTintColor.CGColor;
}
}
- (void)setGaugeTintColor:(UIColor *)gaugeTintColor {
if (_gaugeTintColor != gaugeTintColor) {
_gaugeTintColor = gaugeTintColor;
self.gaugeCircleLayer.strokeColor = gaugeTintColor.CGColor;
// ADDED
self.capLayer.fillColor = gaugeTintColor.CGColor;
// END ADDED
}
}
- (void)setTrackWidth:(CGFloat)trackWidth {
if (_trackWidth != trackWidth) {
_trackWidth = trackWidth;
self.trackCircleLayer.lineWidth = trackWidth;
[self.layer layoutSublayers];
}
}
- (void)setGaugeWidth:(CGFloat)gaugeWidth {
if (_gaugeWidth != gaugeWidth) {
_gaugeWidth = gaugeWidth;
self.gaugeCircleLayer.lineWidth = gaugeWidth;
[self.layer layoutSublayers];
}
}
- (void)setTextColor:(UIColor *)textColor {
if (_textColor != textColor) {
_textColor = textColor;
self.valueTextLabel.textColor = textColor;
}
}
- (void)setFont:(UIFont *)font {
if (_font != font) {
_font = font;
self.valueTextLabel.font = font;
}
}
- (void)setGaugeStyle:(CHCircleGaugeStyle)gaugeStyle {
if (_gaugeStyle != gaugeStyle) {
_gaugeStyle = gaugeStyle;
[self.layer layoutSublayers];
}
}
#pragma mark - Circle Shapes
- (CAShapeLayer *)trackCircleLayer {
if (_trackCircleLayer == nil) {
_trackCircleLayer = [CAShapeLayer layer];
_trackCircleLayer.lineWidth = self.trackWidth;
_trackCircleLayer.fillColor = [UIColor clearColor].CGColor;
_trackCircleLayer.strokeColor = self.trackTintColor.CGColor;
_trackCircleLayer.path = [self insideCirclePath].CGPath;
// ADDED
_capLayer = [CAShapeLayer layer];
_capLayer.shadowColor = [UIColor blackColor].CGColor;
_capLayer.shadowRadius = 8.0;
_capLayer.shadowOpacity = 0.9;
_capLayer.shadowOffset = CGSizeMake(0, 0);
_capLayer.fillColor = self.gaugeTintColor.CGColor;
_capLayer.path = [self capPathForValue:self.value].CGPath;
[_trackCircleLayer addSublayer:_capLayer];
// END ADDED
}
return _trackCircleLayer;
}
- (CAShapeLayer *)gaugeCircleLayer {
if (_gaugeCircleLayer == nil) {
_gaugeCircleLayer = [CAShapeLayer layer];
_gaugeCircleLayer.lineWidth = self.gaugeWidth;
_gaugeCircleLayer.fillColor = [UIColor clearColor].CGColor;
_gaugeCircleLayer.strokeColor = self.gaugeTintColor.CGColor;
_gaugeCircleLayer.strokeStart = 0.0f;
_gaugeCircleLayer.strokeEnd = self.value;
_gaugeCircleLayer.path = [self circlPathForCurrentGaugeStyle].CGPath;
}
return _gaugeCircleLayer;
}
// ADDED
- (UIBezierPath *)capPathForValue:(float)value {
CGPoint arcCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGFloat radius = CGRectGetWidth(self.bounds) / 2.0f;
float angle = value * 360.0;
float x = radius * sin(angle*M_PI/180.0);
float y = radius * cos(angle*M_PI/180.0);
CGPoint capArcCenter = CGPointMake(arcCenter.x + x, arcCenter.y - y);
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:capArcCenter
radius:self.gaugeWidth*_capLayer.shadowRadius / 2.0f
startAngle:(3.0f * M_PI_2)
endAngle:(3.0f * M_PI_2) + (2.0f * M_PI)
clockwise:YES];
return path;
}
// END ADDED
- (UIBezierPath *)circlPathForCurrentGaugeStyle {
switch (self.gaugeStyle) {
case CHCircleGaugeStyleInside: {
return [self insideCirclePath];
}
case CHCircleGaugeStyleOutside: {
return [self outsideCirclePath];
}
default: {
return nil;
}
}
}
- (UIBezierPath *)insideCirclePath {
CGPoint arcCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:arcCenter
radius:CGRectGetWidth(self.bounds) / 2.0f
startAngle:(3.0f * M_PI_2)
endAngle:(3.0f * M_PI_2) + (2.0f * M_PI)
clockwise:YES];
return path;
}
- (UIBezierPath *)outsideCirclePath {
CGPoint arcCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGFloat radius = (CGRectGetWidth(self.bounds) / 2.0f) + (self.trackWidth / 2.0f) + (self.gaugeWidth / 2.0f);
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:arcCenter
radius:radius
startAngle:(3.0f * M_PI_2)
endAngle:(3.0f * M_PI_2) + (2.0f * M_PI)
clockwise:YES];
return path;
}
#pragma mark - Text Label
- (UILabel *)valueTextLabel {
if (_valueTextLabel == nil) {
_valueTextLabel = [[UILabel alloc] init];
[_valueTextLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
_valueTextLabel.textAlignment = NSTextAlignmentCenter;
_valueTextLabel.attributedText = [self formattedStringForValue:self.value];
}
return _valueTextLabel;
}
- (NSAttributedString *)formattedStringForValue:(CGFloat)value {
NSAttributedString *valueString;
NSDictionary *stringAttributes = @{
NSForegroundColorAttributeName : self.textColor,
NSFontAttributeName : self.font
};
switch (self.state) {
case CHCircleGaugeViewStateNA: {
valueString = [[NSAttributedString alloc] initWithString:self.notApplicableString attributes:stringAttributes];
break;
}
case CHCircleGaugeViewStatePercentSign: {
valueString = [[NSAttributedString alloc] initWithString:self.noDataString attributes:stringAttributes];
break;
}
case CHCircleGaugeViewStateScore: {
NSString *suffix = self.unitsString ? self.unitsString : @"";
valueString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%.0f %@", value * 100.0f, suffix]
attributes:stringAttributes];
break;
}
default: {
ALog(@"Missing gauge state.");
break;
}
}
return valueString;
}
#pragma mark - KVO
// Handling KVO notifications for the value property, since
// we're proxying with the setValue:animated: method.
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:NSStringFromSelector(@selector(value))]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
#pragma mark - CALayerDelegate
- (void)layoutSublayersOfLayer:(CALayer *)layer {
[super layoutSublayersOfLayer:layer];
if (layer == self.layer) {
self.trackCircleLayer.path = [self insideCirclePath].CGPath;
self.gaugeCircleLayer.path = [self circlPathForCurrentGaugeStyle].CGPath;
}
}
@end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With