如何在整个层次结构中管理 CALayer 动画



这是如何在复杂层次结构中上下同步CALayer和UIView动画的后续问题

假设我有一个复合层(顶部),它是 CALayer 的一个子类,并且有任意数量的子类。 Top 中有 2 个子层。 第一个子图层 (A) 应始终为固定宽度 - 例如 100 像素宽。 第二个子图层 (B) 应该是 Top 大小的其余部分。 A 和 B 都应占据顶部的整个高度。 在布局子视图中编写代码非常简单。

让我们假设 Top 对 A 或 B 一无所知。 还假定 Top 有一个委托来控制何时应对其进行动画处理(委托提供 actionForLayer:forKey: 并且没有其他 CALayer 委托函数)。

我想设计一种策略,对于 Top 的每个可能大小,用户将始终看到根据上面列出的约束渲染的 A 和 B - 即使 Top 的大小正在被动画化,即使它正在使用任何种类的动画参数(持续时间、函数、偏移量等)进行动画处理。

正如 Top 的动画

是通过其委托从某些包含视图或图层驱动的一样 - 似乎 A 和 B 应该让他们的动画设置他们的包含层 - Top。 我想保持良好的构图,所以我不希望 Top 中的 A 和 B 布局需要被 Top 以外的任何东西理解。

所以 - 问题是将动画链接到图层树以保持所有动画参数同步的最佳策略是什么?

下面是一些通过使用 actionForLayer:forKey: 进行链接的示例代码,但中间函数必须经过一些相当复杂的工作(不包括在内)才能将所有设置从其动画转换为子图层的动画。 此示例中不包括任何处理内插边界值的代码。 例如,假设动画设置为使用不同的 fromValue 或关键帧动画。 需要为子图层求解这些值并相应地应用。

#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

我的问题是 - 有人有更好的方法来完成这项工作吗? 似乎有点可怕,我在任何地方都没有看到任何提到这种分层链。 我知道,当顶层的动画被取消时,我可能还需要做更多的工作来取消子图层动画。 仅仅依靠当前附加的动画,尤其是对该功能所在的当前时间的不关心,似乎它可能是某个地方的错误来源。

我也不确定这在野外的表现如何,因为它们不在同一个动画组中。 任何想法将不胜感激。

最新更新