首页 文章

动画边界上的动画CAShapeLayer路径更改

提问于
浏览
38

我有一个CAShapeLayer(它是UIView子类的一层),只要视图的 bounds 大小发生变化,它就会更新 path . 为此,我重写了图层的 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"];
        }
    }
}

我从阅读DavidRö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];

问题

这主要是有效的,但我有两个问题:

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

  • 使用标准的UIView spring 动画时,视图边界的动画和图层的路径稍微不同步 . 也就是说,路径动画也以弹性方式执行,但它并不完全遵循视图的界限 .

  • More generally, is this the best approach? 似乎有一个形状图层,其路径依赖于其边界大小,并且应该与任何边界变化同步动画,这应该只是工作,但我很难过 . 我觉得必须有更好的方法 .

我尝试过的其他事情

  • 覆盖图层的 actionForKey: 或视图的 actionForLayer:forKey: ,以便返回 path 属性的自定义动画对象 . 我认为这将是首选方式,但我没有找到一种方法来获取应由封闭动画块设置的事务属性 . 即使从动画块内部调用, [CATransaction animationDuration ]等也始终返回默认值 .

Is there a way to (a) determine that you are currently inside an animation block, and (b) to get the animation properties (duration, animation curve etc.) that have been set in that block?

项目

这是动画:橙色三角形是形状图层的路径 . 黑色轮廓是托管形状图层的视图的框架 .

Screenshot

Have a look at the sample project on GitHub . (此项目适用于iOS 8,需要运行Xcode 6,抱歉 . )

更新

Jose Luis Piedrahita pointed methis article by Nick Lockwood,其中建议采用以下方法:

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

  • 如果是这样,在图层的"normal"动画属性(例如 @"bounds" )之一上调用 super 将隐式告诉您是否在动画块内 . 如果视图(图层的委托)返回一个有效的对象(而不是 nilNSNull ),我们就是 .

  • [super actionForKey:] 返回的动画中设置路径动画的参数(持续时间,计时功能等)并返回路径动画 .

这确实在iOS 7.x下运行良好 . 但是,在iOS 8下,从 actionForLayer:forKey: 返回的对象不是标准(并记录在案) CA...Animation ,而是私有 _UIViewAdditiveAnimationAction 类( NSObject 子类)的实例 . 由于没有记录此类的属性,因此我无法轻松地使用它们来创建路径动画 .

_Edit:或者它可能只是起作用 . 正如尼克在他的文章中提到的,像 backgroundColor 这样的一些属性仍然在iOS 8上返回标准 CA...Animation . 我将不得不尝试一下 .

4 回答

  • 4

    我认为它在边界和路径动画之间是不同步的,因为UIVIew spring和CABasicAnimation之间的定时功能不同 .

    也许您可以尝试使用动画转换,它也应该转换路径(未经测试),在完成动画后,您可以设置边界 .

    另一种可能的方法是对路径进行快照,将其设置为图层的内容,然后为边界设置动画,然后内容应该遵循动画 .

  • 1

    我认为你有问题,因为你同时使用图层的框架和它的路径 .

    我会选择具有自定义drawRect的CustomView:它可以绘制您需要的东西,然后就可以了

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

    Sor根本不需要使用pathes

    以下是我使用这种方法的方法https://dl.dropboxusercontent.com/u/73912254/triangle.mov

  • 3

    找到动画 path 的动画解决方案 CAShapeLayer ,同时 bounds 动画:

    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
    
  • 0

    我知道这是一个老问题,但我可以为您提供一个类似于洛克伍德先生方法的解决方案 . 可悲的是,这里的源代码很快,所以你会需要将其转换为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)
        }
    
    }
    

相关问题