I'm trying to combine CADisplayLink and CAAnimation so that I can animate some layers with CAAnimation, and adjust others in realtime based on the changing presentation layer of the animated layers. I realize that CAAnimations are being driven outside of my process while the CADisplayLink callback is in my process, so synchronization will never be completely tight. However, I was hoping that it would be better than what I'm seeing (I'm seeing probably up to a 4 frame lag).
Boiled down to a simple bit of sample code, I create a bunch of pairs of layers. Half of which are outlines, the other half solid squares - all items have the same size. The goal is to animate these pairs of rectangles so that their outlines and squares are always together (ie. the same frame).
Every cycle, I set a new position for the solid layer and let the CADisplayLink callback set the outline layer based on the presentation layer position of its complimentary solid layer. On the simulator, this seems to work most of the time, at least for a small number of layers. Once I get up to 24 pairs of layers, some outlines start to lag. On the device (iPad Air and iPhone 6), the outline lags even with just one square. I'm doing all tests w/ iOs 8.
Below is my ViewController implementation.
@implementation ViewController
{
CADisplayLink *_displayLink;
NSUInteger _startingLayerIndex;
}
#define NUMLAYERS 1
- (void)viewDidLoad {
[super viewDidLoad];
_startingLayerIndex = self.view.layer.sublayers.count;
for (int i=0; i<NUMLAYERS; ++i) {
CALayer *outlineLayer = [[CALayer alloc] init];
outlineLayer.borderColor = [[UIColor blackColor] CGColor];
outlineLayer.borderWidth = 1.0;
CALayer *fillLayer = [[CALayer alloc] init];
fillLayer.backgroundColor = [[UIColor redColor] CGColor];
{
CGRect r = CGRectMake(self.view.center.x, self.view.center.y, 0, 0);
r = CGRectInset(r, -22, -22);
outlineLayer.frame = r;
fillLayer.frame = r;
}
[self.view.layer addSublayer:fillLayer];
[self.view.layer addSublayer:outlineLayer];
}
UITapGestureRecognizer *gr = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
[self.view addGestureRecognizer:gr];
// WARNING - will create an ARC cycle. I don't care though, throwaway code
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(refreshDisplay:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
_displayLink.paused = true;
}
-(void) refreshDisplay:(CADisplayLink *) displayLink {
[CATransaction begin];
[CATransaction setDisableActions:true];
bool keepGoing = false;
for (int i=0; i<NUMLAYERS; ++i) {
CALayer *animatedLayer = [self.view.layer.sublayers objectAtIndex:(2*i)+_startingLayerIndex];
if (animatedLayer.animationKeys.count) {
keepGoing = true;
}
CALayer *trackingLayer = [self.view.layer.sublayers objectAtIndex:(2*i)+_startingLayerIndex+1];
trackingLayer.position = [animatedLayer.presentationLayer position];
}
_displayLink.paused = !keepGoing;
[CATransaction commit];
}
CGFloat randInRange(CGFloat r) {
return (rand()%((int)r*2))/2.0;
}
-(void) onTap:(UITapGestureRecognizer *) gr {
_displayLink.paused = false;
[CATransaction begin];
[CATransaction setAnimationDuration:2.0];
//[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
for (int i=0; i<NUMLAYERS; ++i) {
CGSize sz = self.view.frame.size;
CGPoint p = CGPointMake(randInRange(sz.width), randInRange(sz.height));
[[self.view.layer.sublayers objectAtIndex:(2*i)+_startingLayerIndex] setPosition:p];
}
[CATransaction commit];
}
@end
I've tried 3 different timing functions and I see the same results. I've also checked the framerate w/ instruments and I'm getting 58-60 fps and still seeing visible separation.
I'm wondering if anybody knows a better way to do this. I'm aware that I could convert all animations to be driven by either CAAnimation or CADisplayLink. However, in my case, neither is appealing. I have hundreds of layers on the screen, all of them interconnected (making up an always responsive and completely animated view) - sometimes hierarchically. Core Animation makes life much more manageable (& I believe more efficient, though it might be that "efficiency" that's the root cause of my lag) - so I don't want to throw that out. I'm not excited by having to parameterize all of my animations. There are a number of layers that are dependent on the frames of numerous other layers. Being able to run an animation for the bulk of the work, then just doing some corner cases "by hand" in real time is very appealing from a design perspective (especially concerning maintenance).
Also, I realize that UIKit Dynamics is built to provide good basis for an animated and responsive view. However, I'm adjusting sizes of layers, and the positions of the layers on the screen would require a constraint and attachment. Scaling that to hundreds, even thousands of layers seems problematic, especially when the CPU is already constrained.