Search code examples
objective-ccocoacore-animation

Darker border around circular CALayer


I am creating a custom view to display a single colour, it is for a colour picker from an image so the colour may change fairly quickly. To prevent the colour from flashing as the cursor moved I've made it so the colour change is animated over 0.2 seconds.

I'm doing this using a CALayer. As it stands currently this works almost as intended, however, I want an outline around the layer which has a slightly darker colour the the contents, similar to the OS X Yosemite close & resize window buttons.

Circle with dark outline

The method I'm currently using does work, however, sometimes the border colour animation seems to not follow the main colour animation, sometimes it goes through lighter colours. I thought I might be able to do this with filters, but I am absolutely stumped as to how to do this, I was unable to find much documentation online. I have very little experience with CoreAnimation, so I am unsure if this is the best way to do so or if it's blindingly obvious.

- (id)initWithFrame:(NSRect)frame{
    self = [super initWithFrame:frame];
    if (self) {
        _pickerColour = [NSColor whiteColor];
        strokeColour = [NSColor whiteColor];

        [self setWantsLayer: true];

        innerLayer = [CALayer layer];
        [innerLayer setCornerRadius: frame.size.width / 2];
        [innerLayer setBorderWidth: 4];
        [innerLayer setFrame: CGRectMake(frame.origin.x - frame.size.width / 2, frame.origin.y - frame.size.width / 2, frame.size.width - 2, frame.size.height - 2)];

        [self.layer addSublayer:innerLayer];
    }

    return self;
}

- (void) setPickerColour:(NSColor *)pickerColour{
    CABasicAnimation *backgroundAnimation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
    backgroundAnimation.fillMode = kCAFillModeForwards;
    backgroundAnimation.removedOnCompletion = NO;
    backgroundAnimation.fromValue = (id)self.pickerColour.CGColor;
    backgroundAnimation.toValue = (id)pickerColour.CGColor;
    backgroundAnimation.duration = 0.1f;
    [innerLayer addAnimation:backgroundAnimation forKey:@"backgroundColor"];

    CGFloat h,s,b,a;
    [[self.pickerColour colorUsingColorSpaceName: NSCalibratedRGBColorSpace] getHue:&h saturation:&s brightness:&b alpha:&a];
    NSColor *newStrokeColour = [NSColor colorWithHue:h saturation:MIN(s, 1.0) brightness: MAX(b / 1.2, 0.0) alpha:a];

    CABasicAnimation *borderAnimation = [CABasicAnimation animationWithKeyPath:@"borderColor"];
    borderAnimation.fillMode = kCAFillModeForwards;
    borderAnimation.removedOnCompletion = NO;
    borderAnimation.fromValue = (id)strokeColour.CGColor;
    borderAnimation.toValue = (id)newStrokeColour.CGColor;
    borderAnimation.duration = 0.1f;
    [innerLayer addAnimation:borderAnimation forKey:@"borderColor"];

    strokeColour = newStrokeColour;
    _pickerColour = pickerColour;
}

- (NSColor *) pickerColour{
    return _pickerColour;
}

Solution

  • By using a CAKeyframeAnimation instead of CABasicAnimation, you can provide values and keyTimes arrays that contain a piecewise representation of your path through colorspace. You can use the same NSColor convenience method you're using, but you'll specify a sufficient number of colors to follow the desired arc in HSV space instead of the default linear trajectory through RGB space.