Here's some sample code that does chaining through the use of actionForLayer:forKey:, but middle function has to go through some fairly involved work (which isn't included) to translate all of the settings from its animation to the sublayer's animation. Not included in this sample is any code that deals with interpolating the values of the bounds. For example, imagine a case where an animation is setup to use a different fromValue, or a keyframe animation. Those values would need to be solved for the sublayers and applied accordingly.
#import "ViewController.h"
@interface MyTopLayer : CALayer
@end
static const CGFloat fixedWidth = 100.0;
@implementation MyTopLayer
-(instancetype)init {
self = [super init];
if (self) {
self.backgroundColor = [[UIColor redColor] CGColor];
CALayer *fixedLayer = [[CALayer alloc] init];
CALayer *slackLayer = [[CALayer alloc] init];
[self addSublayer:fixedLayer];
[self addSublayer:slackLayer];
fixedLayer.anchorPoint = CGPointMake(0,0);
fixedLayer.position = CGPointMake(0,0);
slackLayer.anchorPoint = CGPointMake(0,0);
slackLayer.position = CGPointMake(fixedWidth,0);
fixedLayer.backgroundColor = [[UIColor yellowColor] CGColor];
slackLayer.backgroundColor = [[UIColor purpleColor] CGColor];
slackLayer.delegate = self;
}
return self;
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (![event isEqualToString:@"bounds"]) {
return nil;
}
CAAnimation *boundsAnim = [self animationForKey:@"bounds"];
NSLog(@"boundsAnim=%@", boundsAnim);
if (!boundsAnim) {
return (id<CAAction>)[NSNull null];
}
CAAnimation *sublayerBoundsAnim;
if ([boundsAnim isKindOfClass:[CABasicAnimation class]]) {
CABasicAnimation *subAnim = [CABasicAnimation animationWithKeyPath:@"bounds"];
sublayerBoundsAnim = subAnim;
} else {
CAKeyframeAnimation *subAnim = [CAKeyframeAnimation animationWithKeyPath:@"bounds"];
sublayerBoundsAnim = subAnim;
}
sublayerBoundsAnim.timeOffset = boundsAnim.timeOffset;
sublayerBoundsAnim.duration = boundsAnim.duration;
sublayerBoundsAnim.timingFunction = boundsAnim.timingFunction;
return sublayerBoundsAnim;
}
-(void)layoutSublayers {
{
CALayer *fixedLayer = [self.sublayers firstObject];
CGRect b = self.bounds;
b.size.width = fixedWidth;
fixedLayer.bounds = b;
}
{
CALayer *slackLayer = [self.sublayers lastObject];
CGRect b = self.bounds;
b.size.width -= fixedWidth;
slackLayer.bounds = b;
}
}
@end
@interface MyView : UIView
@end
@implementation MyView
{
bool _shouldAnimate;
}
+(Class)layerClass {
return [MyTopLayer class];
}
-(instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layer.delegate = self;
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(doubleTapRecognizer:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
[self addGestureRecognizer:doubleTapRecognizer];
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(tapRecognizer:)];
[tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer];
[self addGestureRecognizer:tapRecognizer];
}
return self;
}
CGFloat getRandWidth() {
const static int maxWidth=1024;
const static int minWidth=fixedWidth*1.1;
return minWidth+((((CGFloat)rand())/(CGFloat)RAND_MAX)*(maxWidth-minWidth));
}
-(void)tapRecognizer:(UITapGestureRecognizer*) gr {
_shouldAnimate = true;
CGFloat w = getRandWidth();
self.layer.bounds = CGRectMake(0,0,w,self.layer.bounds.size.height);
}
-(void)doubleTapRecognizer:(UITapGestureRecognizer*) gr {
_shouldAnimate = false;
CGFloat w = getRandWidth();
self.layer.bounds = CGRectMake(0,0,w,self.layer.bounds.size.height);
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (_shouldAnimate) {
if ([event isEqualToString:@"bounds"]) {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:event];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.duration = 2.0;
anim.fromValue = [NSValue valueWithCGRect:CGRectMake(0,0,100,100)];
return anim;
} else {
return nil;
}
} else {
return (id<CAAction>)[NSNull null];
}
}
@end
My question is - does anybody have a better way to get this done? It seems a little bit scary that I've not seen any mention of this sort of hierarchical chaining anywhere. I'm aware that I would probably also need to do some more work on canceling sublayer animations when the top layer's animation is canceled. Relying simply on the currently attached animation, especially w/out concern for the current time that that function is in seems like it could be a source of errors somewhere down the line.
I'm also not sure how well this would perform in the wild since they aren't in the same animation group. Any thoughts there would be greatly appreciated.