skip to Main Content

I’m trying to apply an angle gradient to the dashes created with the code I’ve written inside a custom UIView class, as below. Although it needs tweaking, I’m happy with the results it produces so far.

Given the input parameters in the view initialisation (below), and a frame of 768 * 768 on an iPad Air2 in portrait mode, it produces the following gauge:

First gauge

What I’d like to do is to cause each of the dashes to step through a user-defined gradient, e.g. green to red, much like this (kludged in Photoshop):

Gauge with colours

I’ve searched high and low, and cannot find anything to achieve this. The only things that come close use different drawing methods, and I want to keep my drawing routine.

As far as I’m concerned, I should simply be able to call:

CGContextSetStrokeColorWithColor(myContext, [gradient color goes here])

inside the draw loop, and that’s it, but I don’t know how to create the relevant color array/gradient, and change the line drawing color according to an index into that array.

Any help would be much appreciated.

- (void)drawRect:(CGRect)rect {

    myContext = UIGraphicsGetCurrentContext();
    UIImage *gaugeImage = [self radials:300 andSteps:3 andLineWidth:10.0];
    UIImageView *gaugeImageView = [[UIImageView alloc] initWithImage:gaugeImage];
    [self addSubview:gaugeImageView];

}

-(UIImage *)radials:(NSInteger)degrees andSteps:(NSInteger)steps andLineWidth:(CGFloat)lineWidth{

    UIGraphicsBeginImageContext(self.bounds.size);
    myContext = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(myContext, lineWidth);
    CGContextSetStrokeColorWithColor(myContext, [[UIColor blackColor] CGColor]);

    CGPoint center = CGPointMake(self.bounds.origin.x+(self.bounds.size.width/2), self.bounds.origin.y+(self.bounds.size.height/2));
    CGFloat r1 = center.x * 0.87f;
    CGFloat r2 = center.x * 0.95f;

    CGContextTranslateCTM(myContext, center.x, center.y);
    CGContextBeginPath(myContext);

    CGFloat offset  = 0;

    if(degrees < 360){
        offset = (360-degrees) / 2;
    }

    for(int lp = offset + 0 ; lp < offset + degrees+1 ; lp+=steps){

        CGFloat theta = lp * (2 * M_PI / 360);
        CGContextMoveToPoint(myContext, 0, 0);

        r1 = center.x * 0.87f;
        if(lp % 10 == 0){
            r1 = center.x * 0.81f;
        }

        CGContextMoveToPoint(myContext, sin(theta) * r1, cos(theta) * r1);
        CGContextAddLineToPoint(myContext, sin(theta) * r2, cos(theta) * r2);
        CGContextStrokePath(myContext);
    }

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

2

Answers


  1. As far as I’m concerned, I should simply be able to call CGContextSetStrokeColorWithColor

    Reality, however, is not interested in “as far as you’re concerned”. You are describing an angle gradient. The reality is that there is no built-in Core Graphics facility for creating an angle gradient.

    However, you can do it easily with a good library such as AngleGradientLayer. It is then a simple matter to draw the angle gradient and use your gauge drawing as a mask.

    In that way, I got this — not kludged in Photoshop, but done entirely live, in iOS, using AngleGradientLayer, plus your radials:andSteps:andLineWidth: method just copied and pasted in and used to generate the mask:

    enter image description here

    Here’s the only code I had to write. First, generating the angle gradient layer:

    + (Class)layerClass {
        return [AngleGradientLayer class];
    }
    - (instancetype)initWithCoder:(NSCoder *)coder {
        self = [super initWithCoder:coder];
        if (self) {
            AngleGradientLayer *l = (AngleGradientLayer *)self.layer;
            l.colors = [NSArray arrayWithObjects:
               (id)[UIColor colorWithRed:1 green:0 blue:0 alpha:1].CGColor,
               (id)[UIColor colorWithRed:0 green:1 blue:0 alpha:1].CGColor,
               nil];
            l.startAngle = M_PI/2.0;
        }
        return self;
    }
    

    Second, the mask (this part is in Swift, but that’s irrelevant):

    let im = self.v.radials(300, andSteps: 3, andLineWidth: 10)
    let iv = UIImageView(image:im)
    self.v.mask = iv
    
    Login or Signup to reply.
  2. So, you want something like this:

    rainbow gauge view

    First, a couple of gentle suggestions:

    1. Don’t add subviews inside drawRect:. What if drawRect: gets called a second time, if for example the view’s size changes?

      Here’s what the View Programming Guide for iOS says about implementing drawRect::

      The implementation of your drawRect: method should do exactly one thing: draw your content. This method is not the place to be updating your application’s data structures or performing any tasks not related to drawing. It should configure the drawing environment, draw your content, and exit as quickly as possible. And if your drawRect: method might be called frequently, you should do everything you can to optimize your drawing code and draw as little as possible each time the method is called.

      If you need to add or remove subviews, you should do that when the view is initialized, or in layoutSubviews at the latest.

    2. There’s no need to draw into an image or use an image view at all. The whole point of drawRect: is to draw into the current graphics context, which UIKit has already set up to target the view’s backing store.

    Those suggestions aside, there is no support for angular gradients in Core Graphics. However, for your graphic, you can set the color for each tick mark separately and get a pretty good approximation, which is how I created the image above. Use +[UIColor colorWithHue:saturation:brightness:alpha:] to create your color, calculating the hue parameter based on the tick angle.

    If you factor out the drawing code into a separate class, it’s easy to use it to draw either directly to a view (in drawRect:), or to an image if you need to. Here’s the interface:

    @interface RainbowGaugeAppearance: NSObject
    
    @property (nonatomic) CGFloat startDegrees;
    @property (nonatomic) CGFloat endDegrees;
    @property (nonatomic) CGFloat degreesPerMajorTick;
    @property (nonatomic) int subdivisionsPerMajorTick;
    @property (nonatomic) CGFloat tickThickness;
    @property (nonatomic) CGFloat startHue;
    @property (nonatomic) CGFloat endHue;
    @property (nonatomic) CGFloat outerRadiusFraction;
    @property (nonatomic) CGFloat minorInnerRadiusFraction;
    @property (nonatomic) CGFloat majorInnerRadiusFraction;
    
    - (instancetype _Nonnull)init;
    - (void)drawInRect:(CGRect)rect;
    @end
    

    And the implementation:

    @implementation RainbowGaugeAppearance
    
    static CGFloat radiansForDegrees(CGFloat degrees) { return degrees * M_PI / 180; }
    
    - (instancetype _Nonnull)init {
        if (self = [super init]) {
            _startDegrees = 120;
            _endDegrees = _startDegrees + 300;
            _degreesPerMajorTick = 30;
            _subdivisionsPerMajorTick = 10;
            _tickThickness = 4;
            _outerRadiusFraction = 0.95;
            _minorInnerRadiusFraction = 0.87;
            _majorInnerRadiusFraction = 0.81;
            _startHue = 1/ 3.0;
            _endHue = 0;
        }
        return self;
    }
    
    - (void)drawInRect:(CGRect)rect {
        CGContextRef gc = UIGraphicsGetCurrentContext();
        CGContextSaveGState(gc); {
            CGContextTranslateCTM(gc, CGRectGetMidX(rect), CGRectGetMidY(rect));
            CGContextSetLineWidth(gc, self.tickThickness);
            CGContextSetLineCap(gc, kCGLineCapButt);
    
            CGFloat outerRadius = _outerRadiusFraction / 2 * rect.size.width;
            CGFloat minorInnerRadius = _minorInnerRadiusFraction / 2 * rect.size.width;
            CGFloat majorInnerRadius = _majorInnerRadiusFraction / 2 * rect.size.width;
    
            CGFloat degreesPerTick = _degreesPerMajorTick / _subdivisionsPerMajorTick;
            for (int i = 0; ; ++i) {
                CGFloat degrees = _startDegrees + i * degreesPerTick;
                if (degrees > _endDegrees) { break; }
    
                CGFloat t = (degrees - _startDegrees) / (_endDegrees - _startDegrees);
                CGFloat hue = _startHue + t * (_endHue - _startHue);
                CGContextSetStrokeColorWithColor(gc, [UIColor colorWithHue:hue saturation:0.8 brightness:1 alpha:1].CGColor);
    
                CGFloat sine = sin(radiansForDegrees(degrees));
                CGFloat cosine = cos(radiansForDegrees(degrees));
                CGFloat innerRadius = (i % _subdivisionsPerMajorTick == 0) ? majorInnerRadius : minorInnerRadius;
                CGContextMoveToPoint(gc, outerRadius * cosine, outerRadius * sine);
                CGContextAddLineToPoint(gc, innerRadius * cosine, innerRadius * sine);
                CGContextStrokePath(gc);
            }
        } CGContextRestoreGState(gc);
    }
    
    @end
    

    Using it to draw a view is then trivial:

    @implementation RainbowGaugeView {
        RainbowGaugeAppearance *_appearance;
    }
    
    - (RainbowGaugeAppearance *_Nonnull)appearance {
        if (_appearance == nil) { _appearance = [[RainbowGaugeAppearance alloc] init]; }
        return _appearance;
    }
    
    - (void)drawRect:(CGRect)rect {
        [self.appearance drawInRect:self.bounds];
    }
    
    @end
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search