Search code examples
iosuiviewcore-animation

animate a UIView using CADisplayLink 'combined' with CAMediaTimingFunction. (to get a arbitrary curve)


I am using a CADisplayLink to do a view animation that just interpolates a value and redraws the view itself.

e.g. I have a view MyView and it has a property value, whenever value is set I call setNeedsDisplay and the view knows what to draw.

to animate this I use CADisplayLink and I want the view to 'morph' between values. I do this by just interpolating the value from the animation's start and stop Value:

- (CGFloat)interpolatedValue:(CGFloat)sourceValue withValue:(CGFloat)targetValue forProgress:(CGFloat)progress;

now getting a linear progress is easy and getting a 'specific curve' (ok) but I want to be able to take advantage of CAMediaTimingFunction to do this (or some other preexisting logic - I don't want to reinvent the wheel again' :)


Solution

  • There's this awesome gist RSTiming which might be handy in your case. You can define timing functions either using standard CAMediaTimingFunction or even define custom ones using 2 control points to define a bezier curve.

    If I got your setup, you may have something like this:

    ViewController

    #import "ViewController.h"
    #import "AnimatedView.h"
    #include <stdlib.h>
    
    @interface ViewController ()
    
    @property (nonatomic, strong) CADisplayLink *displayLink;
    @property (weak, nonatomic) IBOutlet AnimatedView *viewToAnimate;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateFrame)];
        [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    }
    
    - (void)updateFrame {
        [self.viewToAnimate updateAnimation];
    }
    
    - (IBAction)updateAnimationTapped:(id)sender {
        self.viewToAnimate.value = arc4random_uniform(101) / 100.0;
        NSLog(@"Next animation value: %f", self.viewToAnimate.value);
    }
    
    @end
    

    AnimatedView

    #import "AnimatedView.h"
    #import "RSTimingFunction.h"
    
    @interface AnimatedView()
    {
        CGFloat _lastValue;
        CGFloat _progressStep;
        CGFloat _currentProgress;
    }
    
    @property (nonatomic, strong) RSTimingFunction *animationProgress;
    
    @end
    
    @implementation AnimatedView
    
    - (instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        if ((self = [super initWithCoder:aDecoder]))
        {
            _progressStep = 0.01; // defines animation speed
            _currentProgress = 1.0;
            self.animationProgress = [RSTimingFunction timingFunctionWithName:kRSTimingFunctionEaseInEaseOut];
        }
    
        return self;
    }
    
    - (void)setValue:(CGFloat)value {
        if (_value != value)
        {
            _lastValue = _value;
            _value = value;
            _currentProgress = 0.0;
        }
    }
    
    - (void)updateAnimation
    {
        if (_currentProgress > 1.0)
            return;
    
        _currentProgress += _progressStep;
        CGFloat currentAnimationValue = _lastValue + (_value - _lastValue) * [self.animationProgress valueForX:_currentProgress];
    
        self.alpha = currentAnimationValue; // as an example animate alpha
    }
    
    @end
    

    As said you can even set 2 control points to create a timing function modelled on a cubic Bezier curve.

    self.animationProgress = [RSTimingFunction timingFunctionWithControlPoint1:CGPointMake(0.6, 0.6) controlPoint2:CGPointMake(0.1, 0.8)];
    

    Which will produce the following timing animation (produced using CAMediaTimingFunction playground)

    enter image description here