How to manage CALayer animations throughout a hier

2019-06-06 02:50发布

问题:

This is a follow-up question to How to synchronize CALayer and UIView animations up and down a complex hierarchy

Lets say I have a composite layer (Top) that is a subclass of CALayer and has any number of children. Top has 2 child layers within it. The first sublayer (A) should always be a fixed width - lets say 100 pixels wide. The second sublayer (B) should be the remainder of the size of Top. Both A and B should occupy the entire height of Top. This is pretty straightforward to code up in layoutSubviews.

Let's presume that Top has no knowledge of A or B. Also presume that Top has a delegate that controls when it should be animated (the delegate provides actionForLayer:forKey: and no other CALayer delegate functions).

I'd like to devise a strategy where for every possible size of Top, the user will always see A and B rendered according to the constraints listed above - even when the size of Top is being animated, even when it is being animated with any variety of animation parameters (durations, functions, offsets, etc).

Just as Top's animations are driven from some containing view or layer through its delegate - it seems that A and B should have their animations setup their containing layer - Top. I want to keep things well-composed, so I don't want the layout of A & B within Top to need to be understood by anything other than Top.

So - the question is what's the best strategy to chain the animations down the layer tree to keep all of the animation parameters in sync?

回答1:

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];
        //fixedLayer.delegate = self; // no reason to ever animate this layer since it is static
        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"];
        // transform properties, like from, to & by value from boundsAnim (outer) to the inner layer's animation
        sublayerBoundsAnim = subAnim;
    } else {
        CAKeyframeAnimation *subAnim = [CAKeyframeAnimation animationWithKeyPath:@"bounds"];
        // copy/interpolate keyframes
        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.timeOffset = 0.5;
            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.