CALayer无法像UIView动画一样进行动画。

9

我有一个类,它有16个子层(sublayers),用来形成一个标志。当我对 UIView 进行动画处理时,CALayer 不会被动画处理,只是像下面的动态图一样直接到达最终状态:

进入图片描述 here

我的代码如下:

@implementation LogoView

#define CGRECTMAKE(a, b, w, h) {.origin={.x=(a),.y=(b)},.size={.width=(w),.height=(h)}}

#pragma mark - Create Subviews
const static CGRect path[] = {
        CGRECTMAKE(62.734375,-21.675000,18.900000,18.900000),
        CGRECTMAKE(29.784375,-31.725000,27.400000,27.300000),
        CGRECTMAKE(2.534375,-81.775000,18.900000,18.900000),
        CGRECTMAKE(4.384375,-57.225000,27.400000,27.300000),
        CGRECTMAKE(2.784375,62.875000,18.900000,18.900000),
        CGRECTMAKE(4.334375,29.925000,27.400000,27.300000),
        CGRECTMAKE(62.734375,2.525000,18.900000,18.900000),
        CGRECTMAKE(29.784375,4.475000,27.400000,27.300000),
        CGRECTMAKE(-21.665625,-81.775000,18.900000,18.900000),
        CGRECTMAKE(-31.765625,-57.225000,27.400000,27.300000),
        CGRECTMAKE(-81.615625,-21.425000,18.900000,18.900000),
        CGRECTMAKE(-57.215625,-31.775000,27.400000,27.300000),
        CGRECTMAKE(-81.615625,2.775000,18.900000,18.900000),
        CGRECTMAKE(-57.215625,4.425000,27.400000,27.300000),
        CGRECTMAKE(-21.415625,62.875000,18.900000,18.900000),
        CGRECTMAKE(-31.765625,29.925000,27.400000,27.300000)
    };

- (void) createSubviews
{

    self.contentMode = UIViewContentModeRedraw;
    for (int i = 0; i < 16; i++) {
        CGRect rect = CGRectApplyAffineTransform(path[i],
           CGAffineTransformMakeScale(self.frame.size.width / 213.0, 
                       self.frame.size.height / 213.0));
        UIBezierPath * b = [UIBezierPath bezierPathWithOvalInRect:
          CGRectOffset(rect, 
            self.frame.size.width/2.0, 
            self.frame.size.height/2)];
        CAShapeLayer * layer = [[CAShapeLayer alloc] init];
        layer.path = [b CGPath];
        layer.fillColor = [self.tintColor CGColor];
        [self.layer addSublayer:layer];
    }
    self.layer.needsDisplayOnBoundsChange = YES;
    self.initialLenght = self.frame.size.width;
}

- (void) layoutSublayersOfLayer:(CALayer *)layer
{
    for (int i = 0; i < 16; i++) {
        CGRect rect = CGRectApplyAffineTransform(path[i],
         CGAffineTransformMakeScale(layer.frame.size.width / 213.0,
                                   layer.frame.size.height / 213.0));
        UIBezierPath * b = [UIBezierPath 
            bezierPathWithOvalInRect:CGRectOffset(rect,
                               layer.frame.size.width/2.0,
                               layer.frame.size.height/2)];
        ((CAShapeLayer*)(layer.sublayers[i])).path = b.CGPath;
    }
}

- (void)layoutSubviews {
    [super layoutSubviews];

    // get current animation for bounds
    CAAnimation *anim = [self.layer animationForKey:@"bounds"];

    [CATransaction begin];
    if(anim) {
        // animating, apply same duration and timing function.
        [CATransaction setAnimationDuration:anim.duration];
        [CATransaction setAnimationTimingFunction:anim.timingFunction];

        CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
        [self.layer addAnimation:pathAnimation forKey:@"path"];
    }
    else {
        // not animating, we should disable implicit animations.
        [CATransaction disableActions];
    }
    self.layer.frame = self.frame;
    [CATransaction commit];
}

我正在使用以下内容进行动画制作:

    [UIView animateWithDuration:3.0 animations:^{
        [v setFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)];
    }];

如何将图层动画与视图动画同步?

你需要单独移动标志的每个元素吗?如果不需要,为什么要这么多层?为什么不直接使用drawRect:呢? - picciano
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - poliveira
5个回答

8

与UIView动画同步的图层属性动画可能会很棘手。相反,让我们使用不同的视图结构来利用内置支持,以动画化视图的transform,而不是尝试动画化图层的path。

我们将使用两个视图:一个父视图和一个子视图。父视图是LogoView,并在故事板中布局(或者你创建界面的任何方式)。LogoView添加自己的子视图LogoLayerView类。这个LogoLayerView使用CAShapeLayer作为它的层,而不是普通的CALayer。

请注意,我们只需要一个CAShapeLayer,因为路径可以包含多个不连续的区域。

我们只设置一次LogoLayerView的frame / bounds,即CGRectMake(0, 0, 213, 213),然后不再更改它。相反,当外部LogoView改变大小时,我们设置LogoLayerView的transform,使其仍填充外部LogoView。

这是结果:

logo scaling

这是代码:

LogoView.h

#import <UIKit/UIKit.h>

IB_DESIGNABLE
@interface LogoView : UIView

@end

LogoView.m

#import "LogoView.h"

#define CGRECTMAKE(a, b, w, h) {.origin={.x=(a),.y=(b)},.size={.width=(w),.height=(h)}}

const static CGRect ovalRects[] = {
    CGRECTMAKE(62.734375,-21.675000,18.900000,18.900000),
    CGRECTMAKE(29.784375,-31.725000,27.400000,27.300000),
    CGRECTMAKE(2.534375,-81.775000,18.900000,18.900000),
    CGRECTMAKE(4.384375,-57.225000,27.400000,27.300000),
    CGRECTMAKE(2.784375,62.875000,18.900000,18.900000),
    CGRECTMAKE(4.334375,29.925000,27.400000,27.300000),
    CGRECTMAKE(62.734375,2.525000,18.900000,18.900000),
    CGRECTMAKE(29.784375,4.475000,27.400000,27.300000),
    CGRECTMAKE(-21.665625,-81.775000,18.900000,18.900000),
    CGRECTMAKE(-31.765625,-57.225000,27.400000,27.300000),
    CGRECTMAKE(-81.615625,-21.425000,18.900000,18.900000),
    CGRECTMAKE(-57.215625,-31.775000,27.400000,27.300000),
    CGRECTMAKE(-81.615625,2.775000,18.900000,18.900000),
    CGRECTMAKE(-57.215625,4.425000,27.400000,27.300000),
    CGRECTMAKE(-21.415625,62.875000,18.900000,18.900000),
    CGRECTMAKE(-31.765625,29.925000,27.400000,27.300000)
};

#define LogoDimension 213.0

@interface LogoLayerView : UIView
@property (nonatomic, strong, readonly) CAShapeLayer *layer;
@end

@implementation LogoLayerView

@dynamic layer;

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

- (void)layoutSubviews {
    [super layoutSubviews];
    if (self.layer.path == nil) {
        [self initShapeLayer];
    }
}

- (void)initShapeLayer {
    self.layer.backgroundColor = [UIColor yellowColor].CGColor;
    self.layer.strokeColor = nil;
    self.layer.fillColor = [UIColor greenColor].CGColor;

    UIBezierPath *path = [UIBezierPath bezierPath];
    for (size_t i = 0; i < sizeof ovalRects / sizeof *ovalRects; ++i) {
        [path appendPath:[UIBezierPath bezierPathWithOvalInRect:ovalRects[i]]];
    }
    [path applyTransform:CGAffineTransformMakeTranslation(LogoDimension / 2, LogoDimension / 2)];
    self.layer.path = path.CGPath;
}

@end

@implementation LogoView {
    LogoLayerView *layerView;
}

- (void)layoutSubviews {
    [super layoutSubviews];

    if (layerView == nil) {
        layerView = [[LogoLayerView alloc] init];
        layerView.layer.anchorPoint = CGPointZero;
        layerView.frame = CGRectMake(0, 0, LogoDimension, LogoDimension);
        [self addSubview:layerView];
    }
    [self layoutShapeLayer];
}

- (void)layoutShapeLayer {
    CGSize mySize = self.bounds.size;
    layerView.transform = CGAffineTransformMakeScale(mySize.width / LogoDimension, mySize.height / LogoDimension);
}

@end

很棒的答案。不过需要注意的是,它只适用于以下代码:[UIView animateWithDuration:3.0 animations:^{ _logoView.transform = CGAffineTransformMakeScale(1.5, 1.5); }];而不适用于setFrame - poliveira
1
在我的测试应用程序中,我更新了标志视图宽度和高度约束的常量,然后在动画块内调用了 layoutIfNeeded。我没有转换 LogoView - rob mayoff
只有一件事需要考虑,当你说“请注意,我们只需要一个CAShapeLayer,因为一个路径可以包含多个不相连的区域。”这并不是错误的,但它需要更多的时间来处理和渲染。 - farzadshbfn
@robmayoff 当我缩放子视图时,它也会改变x和y坐标。 - iDhaval

7

您可以使CAShapeLayer.path可动画,并在-layoutSublayersOfLayer中更新自定义层。只需确保匹配UIViewCAShapeLayer子类的持续时间。

简单明了:

//: Playground - noun: a place where people can play

import UIKit
import XCPlayground

let kAnimationDuration: NSTimeInterval = 4.0

class AnimatablePathShape: CAShapeLayer {

    override func actionForKey(event: String) -> CAAction? {
        if event == "path" {
            let value = self.presentationLayer()?.valueForKey(event) ?? self.valueForKey(event)

            let anim = CABasicAnimation(keyPath: event)
            anim.duration = kAnimationDuration
            anim.fromValue = value
            anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

            return anim
        }

        return super.actionForKey(event)
    }

    override class func needsDisplayForKey(key: String) -> Bool {
        if key == "path" {
            return true
        }
        return super.needsDisplayForKey(key)
    }
}

class View: UIView {

    let shape = AnimatablePathShape()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.backgroundColor = UIColor.whiteColor().colorWithAlphaComponent(0.1)

        self.shape.fillColor = UIColor.magentaColor().CGColor

        self.layer.addSublayer(self.shape)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func yoyo(grow: Bool = true) {
        let options: UIViewAnimationOptions = [.CurveEaseInOut]

        let animations = { () -> Void in
            let scale: CGFloat = CGFloat(grow ? 4 : 1.0 / 4)

            self.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame) * scale, CGRectGetHeight(self.frame) * scale)
        }

        let completion = { (finished: Bool) -> Void in
            self.yoyo(!grow)
        }

        UIView.animateWithDuration(kAnimationDuration, delay: 0, options: options, animations: animations, completion: completion)
    }

    override func layoutSublayersOfLayer(layer: CALayer) {
        super.layoutSublayersOfLayer(layer)

        let radius = min(CGRectGetWidth(self.frame), CGRectGetHeight(self.frame)) * 0.25

        let center = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))

        let bezierPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI * 2), clockwise: true)

        self.shape.path = bezierPath.CGPath
    }
}

let view = View(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))

container.addSubview(view)

view.yoyo()

XCPlaygroundPage.currentPage.liveView = container

Playground snapshot


4
为了使层以正确的方式进行动画,您可能需要对点(dot)的CALayer(或CAShapeLayer)进行子类化,并实现以下方法:
+ (BOOL)needsDisplayForKey:(NSString *)key;
- (instancetype)initWithLayer:(id)layer;
- (void)drawInContext:(CGContextRef)context;
- (id<CAAction>)actionForKey:(NSString *)event;

你可能还需要将任何层属性标记为@dynamic
编辑:另一种(也很容易)的方法是使用CABasicAnimation而不是使用UIViewanimateWithDuration:animations:方法。

2
最初的回答:实际上原始代码没有问题,除了使用键值@"bounds"无法获得正在进行的动画,而应该使用键值@"bounds.size"
因此,我会通过在- (void) layoutSublayersOfLayer:(CALayer *)layer中完成所有工作来重构代码。
这意味着假设标志只是一个名为_logoLayer的单个CAShapeLayer内部属性,并且您还制作了一个内部方法,该方法返回在给定矩形内绘制标志的正确CGPath
- (void) layoutSublayersOfLayer:(CALayer *)layer {
    if layer == self.layer {
        CGPath *newPath = [self _logoPathInRect: self.bounds]
        CAAnimation *anim = [self.layer animationForKey:@"bounds.size"];

    [CATransaction begin];
    if(anim) {
        // animating, apply same duration and timing function.
        [CATransaction setAnimationDuration:anim.duration];
        [CATransaction setAnimationTimingFunction:anim.timingFunction];

        CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
        [self.layer addAnimation:pathAnimation forKey:@"path"];
    }
    else {
        // not animating, we should disable implicit animations.
        [CATransaction disableActions];
    }
    self._logoLayer.path = newPath
    self._logoLayer.frame = self.layer.bounds;
    [CATransaction commit];
    }
}

0

这是highmaintenance伟大的例子转换为Swift 4.2+

import UIKit
import PlaygroundSupport

let animDuration = 4.0

class AnimatablePathShape: CAShapeLayer {
    override func action(forKey event: String) -> CAAction? {
        if event == "path" {
            let anim = CABasicAnimation(keyPath: event)
            anim.duration = animDuration
            anim.fromValue = presentation()?.value(forKey: event) ?? value(forKey: event)
            anim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

            return anim
        }

        return super.action(forKey: event)
    }

    override class func needsDisplay(forKey key: String) -> Bool {
        if key == "path" {
            return true
        }

        return super.needsDisplay(forKey: key)
    }
}

class View: UIView {
    let shape = AnimatablePathShape()

    override init(frame: CGRect) {
        super.init(frame: frame)

        backgroundColor = UIColor.white.withAlphaComponent(0.1)

        shape.fillColor = UIColor.magenta.cgColor

        layer.addSublayer(shape)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func yoyo(grow: Bool = true) {
        UIView.animate(withDuration: animDuration, delay: 0, options: .curveEaseInOut, animations: {
            let scale = CGFloat(grow ? 4 : 1 / 4)

            self.frame = CGRect(x: 0, y: 0, width: self.frame.width * scale, height: self.frame.height * scale)
        }) { finished in
            if finished {
                self.yoyo(grow: !grow)
            }
        }
    }

    override func layoutSublayers(of layer: CALayer) {
        super.layoutSublayers(of: layer)

        let radius = min(frame.width, frame.height) * 0.25
        let center = CGPoint(x: frame.midX, y: frame.midY)
        let bezierPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)

        shape.path = bezierPath.cgPath
    }
}

let view = View(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))

container.addSubview(view)

view.yoyo()

PlaygroundPage.current.liveView = container

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