动画更改边界时,如何对CAShapeLayer路径进行动画处理

43
我有一个CAShapeLayer(它是UIView子类的图层),其路径应在视图的bounds大小更改时更新。为此,我重写了图层的setBounds:方法以重置路径。
@interface CustomShapeLayer : CAShapeLayer
@end

@implementation CustomShapeLayer

- (void)setBounds:(CGRect)bounds
{
    [super setBounds:bounds];
    self.path = [[self shapeForBounds:bounds] CGPath];
}

这很好用,直到我动画改变边界。 我希望路径能够在任何动画边界变化(在UIView动画块中)旁边动画,以便形状图层始终适应其大小。
由于path属性默认情况下不会动画,因此我想出了这个方法:
覆盖图层的addAnimation:forKey:方法。 在其中,找出是否正在向图层添加边界动画。 如果是,通过复制传递给该方法的所有属性从而创建第二个显式动画,以便沿着边界动画路径。 代码如下:
- (void)addAnimation:(CAAnimation *)animation forKey:(NSString *)key
{
    [super addAnimation:animation forKey:key];

    if ([animation isKindOfClass:[CABasicAnimation class]]) {
        CABasicAnimation *basicAnimation = (CABasicAnimation *)animation;

        if ([basicAnimation.keyPath isEqualToString:@"bounds.size"]) {
            CABasicAnimation *pathAnimation = [basicAnimation copy];
            pathAnimation.keyPath = @"path";
            // The path property has not been updated to the new value yet
            pathAnimation.fromValue = (id)self.path;
            // Compute the new value for path
            pathAnimation.toValue = (id)[[self shapeForBounds:self.bounds] CGPath];

            [self addAnimation:pathAnimation forKey:@"path"];
        }
    }
}

我从阅读David Rönnqvist的View-Layer Synergy article中得到这个想法。(这段代码适用于iOS 8。在iOS 7上,似乎你需要检查动画的keyPath@"bounds"而不是@"bounds.size"。)
触发视图动画边界变化的调用代码将如下所示:
[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{    self.customShapeView.bounds = newBounds;} completion:nil];

问题

这个大部分都能正常工作,但我遇到了两个问题:

  1. 偶尔,当触发此动画时前一个动画仍在进行中时,我会在CGPathApply()中崩溃(EXC_BAD_ACCESS)。我不确定这是否与我的特定实现有关。编辑:算了。我忘记将UIBezierPath转换为CGPathRef感谢@antonioviero

  2. 使用标准的UIView弹簧动画时,视图边界和图层路径的动画略有不同步。也就是说,路径动画也以弹簧方式执行,但它并没有完全跟随视图的边界。

  3. 更一般地说,这是最好的方法吗?似乎拥有一个形状图层,其路径依赖于其边界大小,并且应该与任何边界更改同步动画,这似乎是应该可以正常工作的™,但我很难过。我觉得一定有更好的方法。

我尝试过的其他方法

  • 覆盖图层的actionForKey:或视图的actionForLayer:forKey:,以返回自定义动画对象来控制path属性。我认为这是首选的方法,但我没有找到方法来获取由包含动画块设置的事务属性。即使在动画块中调用,[CATransaction animationDuration]等始终返回默认值。

有没有一种方法可以(a)确定您当前是否在动画块内,并且(b)获取已在该块中设置的动画属性(持续时间,动画曲线等)?

项目

以下是动画内容:橙色三角形是形状图层的路径。黑色轮廓是承载形状图层的视图的框架。

Screenshot

请在GitHub上查看示例项目。(此项目适用于iOS 8,需要Xcode 6才能运行,抱歉。)

更新

Jose Luis Piedrahita 指向了 这篇来自Nick Lockwood的文章,其中建议采取以下方法:

  1. 覆盖视图的 actionForLayer:forKey: 或层的 actionForKey: 方法,并检查传递给此方法的键是否是您要动画化的键 (@"path")。

  2. 如果是这样,调用层的一个“普通”可动画属性(例如 @"bounds")上的 super 将隐式地告诉您是否在动画块内。如果视图 (层的委托) 返回一个有效对象 (而不是 nilNSNull),则我们处于动画块内。

  3. [super actionForKey:] 返回的动画中设置路径动画的参数 (持续时间、时间函数等),然后返回路径动画。

在iOS 7.x下,这确实非常有效。然而,在iOS 8下,从actionForLayer:forKey:返回的对象不是标准(并且已记录)的CA...Animation,而是私有_UIViewAdditiveAnimationAction类的实例(一个NSObject子类)。由于此类的属性未记录,因此我无法轻松地使用它们来创建路径动画。

_编辑:或者它可能仍然有效。正如Nick在他的文章中提到的那样,在iOS 8上,一些属性(如backgroundColor)仍然返回标准的CA...Animation。我必须试一试。


path 是一个可动画化的属性。我并不认为这是最好的方法。正如你自己所看到的,它看起来不太牢固。我希望在几分钟内想出一个解决方案。 - duci9y
1
我会手动使用CADisplayLink完成所有操作 - 在我看来,这将消除大部分问题。 - cocoapriest
@omz 这也是我的观察结果。如果记录下正在发生的事情,路径动画会被正确配置为CASpringAnimation,但它的行为却像一个基本动画。如果你对另一个图层属性(如不透明度或变换)应用相同的技术,它会像预期的那样振荡。 - David Rönnqvist
对于第二点,我会说路径并不是弹性的。只是边界跳来跳去的错觉使得路径看起来也在跳来跳去。 - David Rönnqvist
@omz:谢谢,我明白了。 - Ole Begemann
显示剩余4条评论
4个回答

7
我知道这是一个老问题,但是我可以为您提供一种解决方案,它与Lockwood先生的方法类似。不幸的是,这里的源代码是swift语言的,因此您需要将其转换为ObjC。
如前所述,如果该层是视图的背景层,您可以在视图本身中拦截CAAction。然而,这对于背景层在多个视图中使用的情况并不方便。
好消息是actionForLayer:forKey:实际上调用了背景层中的actionForKey:。
正是在背景层的actionForKey:中,我们可以拦截这些调用,并为路径改变时提供动画。
以下是一个用Swift编写的示例图层:
class AnimatedBackingLayer: CAShapeLayer
{

    override var bounds: CGRect
    {
        didSet
        {
            if !CGRectIsEmpty(bounds)
            {
                path = UIBezierPath(roundedRect: CGRectInset(bounds, 10, 10), cornerRadius: 5).CGPath
            }
        }
    }

    override func actionForKey(event: String) -> CAAction?
    {
        if event == "path"
        {
            if let action = super.actionForKey("backgroundColor") as? CABasicAnimation
            {
                let animation = CABasicAnimation(keyPath: event)
                animation.fromValue = path
                // Copy values from existing action
                animation.autoreverses = action.autoreverses
                animation.beginTime = action.beginTime
                animation.delegate = action.delegate
                animation.duration = action.duration
                animation.fillMode = action.fillMode
                animation.repeatCount = action.repeatCount
                animation.repeatDuration = action.repeatDuration
                animation.speed = action.speed
                animation.timingFunction = action.timingFunction
                animation.timeOffset = action.timeOffset
                return animation
            }
        }
        return super.actionForKey(event)
    }

}

我将 actionForKey 改为 animationForKey,现在它可以正常工作了。 - Dong Ma
非常感谢您!即使通过将“action”转换为“CASpringAnimation”,也可以完美地运行,包括弹簧动画。 - Vin Gazoil
我无法让它工作。有人有示例项目吗? - James

3

我认为您之所以遇到问题,是因为您同时操作了图层的框架和路径。

我建议使用自定义视图(CustomView),并通过自定义的drawRect:方法来绘制您需要的图形,然后只需执行以下操作:

[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{    self.customView.bounds = newBounds;} completion:nil];

所以根本不需要使用路径

使用这种方法,我得到了以下结果 https://dl.dropboxusercontent.com/u/73912254/triangle.mov


2

当动画bounds时,找到了一个解决方案,可以对CAShapeLayerpath进行动画处理:

typedef UIBezierPath *(^PathGeneratorBlock)();

@interface AnimatedPathShapeLayer : CAShapeLayer

@property (copy, nonatomic) PathGeneratorBlock pathGenerator;

@end

@implementation AnimatedPathShapeLayer

- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
    if ([key rangeOfString:@"bounds.size"].location == 0) {
        CAShapeLayer *presentationLayer = self.presentationLayer;
        CABasicAnimation *pathAnim = [anim copy];
        pathAnim.keyPath = @"path";
        pathAnim.fromValue = (id)[presentationLayer path];
        pathAnim.toValue = (id)self.pathGenerator().CGPath;
        self.path = [presentationLayer path];
        [super addAnimation:pathAnim forKey:@"path"];
    }
    [super addAnimation:anim forKey:key];
}

- (void)removeAnimationForKey:(NSString *)key {
    if ([key rangeOfString:@"bounds.size"].location == 0) {
        [super removeAnimationForKey:@"path"];
    }
    [super removeAnimationForKey:key];
}

@end

//

@interface ShapeLayeredView : UIView

@property (strong, nonatomic) AnimatedPathShapeLayer *layer;

@end

@implementation ShapeLayeredView

@dynamic layer;

+ (Class)layerClass {
    return [AnimatedPathShapeLayer class];
}

- (instancetype)initWithGenerator:(PathGeneratorBlock)pathGenerator {
    self = [super init];
    if (self) {
        self.layer.pathGenerator = pathGenerator;
    }
    return self;
}

@end

你能将这个也翻译成 Swift 语言吗?我在用 Swift 语言实现时遇到了些问题。 - Henny Lee
@HennyLee使用简单的字符串常量替换了@keypath,这应该有助于转换为Swift。 - k06a

1
我认为边界和路径动画不同步的原因是UIVIew弹簧和CABasicAnimation之间的时间函数不同。也许你可以尝试使用animate transform,它应该也会转换路径(未经测试),在动画完成后,您可以设置边界。另一种可能的方法是对路径进行快照,将其设置为层的内容,然后动画边界,内容应该随动画而移动。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接