I have a UIView
that draws itself using -drawRect:
and I want to animate the colors used for the drawing. Basic UIView
animation stuff doesn’t work for obvious reasons (drawRect
).
I can’t simply use CAShapeLayer
to draw and animate the view contents. I’d like to try faking the animation by hand, using a timer or CADisplayLink
in combination with setNeedsDisplay
. Is there a reasonably simple way to hide this magic behind the usual UIView
animation API?
For example, let’s say there’s a color
property I’d like to animate:
[UIView animateWithDuration:0.2 animations:^{
[customDrawingView setColor:[UIColor redColor]];
}];
Is there a way to “intercept” the animation call to read the animation parameters (time, target value) and process them by hand? I don’t want to swizzle UIView
.
I had to do something like you did quite a few times. In general you can create some classes to handle stuff like floating point interpolation or CGPoint
interpolation... and do it all properly but since the whole drawing already exists there is not much point in it.
So the UIView
animateWithDuration
will not work here but you can add your display link and do the interpolation manually. It is best done if no existing code is changed, only add a few methods:
Assuming you currently have something like this:
- (void)setColor:(UIColor *)color
{
_color = color;
[self setNeedsDisplay];
}
Now you can add setColorAnimated:
and do all the additional functionality:
You need some additional parameters (properties), use what you want:
UIColor *_sourceColor;
UIColor *_targetColor;
NSDate *_startDate;
NSDate *_endDate;
CADisplayLink *_displayLink;
And the methods:
- (void)setColorAnimated:(UIColor *)color {
_sourceColor = self.color; // set start to current color
_targetColor = color; // destination color
_startDate = [NSDate date]; // begins currently, you could add some delay if you wish
_endDate = [_startDate dateByAddingTimeInterval:.3]; // will define animation duration
[_displayLink invalidate]; // if one already exists
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onFrame)]; // create the display link
}
- (UIColor *)interpolatedColor:(UIColor *)sourceColor withColor:(UIColor *)targetColor forScale:(CGFloat)scale {
// this will interpolate between two colors
CGFloat r1, g1, b1, a1;
CGFloat r2, g2, b2, a2;
[sourceColor getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
[targetColor getRed:&r2 green:&g2 blue:&b2 alpha:&a2];
// do a linear interpolation on RGBA. You can use other
return [UIColor colorWithRed:r1+(r2-r1)*scale
green:g1+(g2-g1)*scale
blue:b1+(b2-b1)*scale
alpha:a1+(a2-a1)*scale];
}
- (void)onFrame {
// scale is valid between 0 and 1
CGFloat scale = [[NSDate date] timeIntervalSinceDate:_startDate] / [_endDate timeIntervalSinceDate:_startDate];
if(scale < .0f) {
// this can happen if delay is used
scale = .0f;
}
else if(scale > 1.0f)
{
// end animation
scale = 1.0f;
[_displayLink invalidate];
_displayLink = nil;
}
[self setColor:[self interpolatedColor:_sourceColor withColor:_targetColor forScale:scale]];
}