Ios 核心动画进度回调

Ios 核心动画进度回调,ios,core-animation,Ios,Core Animation,当核心动画在运行时达到某些点时(例如,在完成的50%和66%时),是否有一种简单的方法可以回调 我目前正在考虑设置NSTimer,但这并不像我希望的那样准确。我终于找到了解决这个问题的方法 基本上,我希望每一帧都能被召唤回来,做我需要做的事情 没有明显的方法可以观察动画的进度,但实际上是可能的: 首先,我们需要创建CALayer的一个新子类,它有一个名为“progress”的可设置动画的属性 我们将层添加到树中,然后创建一个动画,在动画持续时间内将进度值从0驱动到1 由于我们的progress

当核心动画在运行时达到某些点时(例如,在完成的50%和66%时),是否有一种简单的方法可以回调


我目前正在考虑设置NSTimer,但这并不像我希望的那样准确。

我终于找到了解决这个问题的方法

基本上,我希望每一帧都能被召唤回来,做我需要做的事情

没有明显的方法可以观察动画的进度,但实际上是可能的:

  • 首先,我们需要创建CALayer的一个新子类,它有一个名为“progress”的可设置动画的属性

  • 我们将层添加到树中,然后创建一个动画,在动画持续时间内将进度值从0驱动到1

  • 由于我们的progress属性可以设置动画,因此在子类中为动画的每一帧调用drawInContext。此函数不需要重新绘制任何内容,但可用于调用委托函数:)

下面是类接口:

@protocol TAProgressLayerProtocol <NSObject>

- (void)progressUpdatedTo:(CGFloat)progress;

@end

@interface TAProgressLayer : CALayer

@property CGFloat progress;
@property (weak) id<TAProgressLayerProtocol> delegate;

@end
然后,我们可以将该层添加到主层:

TAProgressLayer *progressLayer = [TAProgressLayer layer];
progressLayer.frame = CGRectMake(0, -1, 1, 1);
progressLayer.delegate = self;
[_sceneView.layer addSublayer:progressLayer];
并将其与其他动画一起设置动画:

CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"progress"];
anim.duration = 4.0;
anim.beginTime = 0;
anim.fromValue = @0;
anim.toValue = @1;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;

[progressLayer addAnimation:anim forKey:@"progress"];
最后,代理将在动画进行时回调:

- (void)progressUpdatedTo:(CGFloat)progress
{
    // Do whatever you need to do...
}

如果你不想让CALayer向你报告进度,还有另一种方法。从概念上讲,您可以使用CADisplayLink来保证在每一帧上都有回调,然后只需测量动画开始以来经过的时间除以持续时间即可计算完成百分比

开源库将此功能非常清晰地打包到一个API中,该API看起来几乎与基于UIView块的动画API完全相同:

// INTUAnimationEngine.h

// ...

+ (NSInteger)animateWithDuration:(NSTimeInterval)duration
                           delay:(NSTimeInterval)delay
                      animations:(void (^)(CGFloat percentage))animations
                      completion:(void (^)(BOOL finished))completion;

// ...
您需要做的就是在启动其他动画的同时调用此方法,为
持续时间
延迟
传递相同的值,然后对于动画的每一帧,
动画
块将以当前完成百分比执行。如果你想让你的计时完全同步,你可以完全从INTUAnimationEngine驱动你的动画。

我快速(2.0)实现了tarmes在接受的答案中建议的CALayer子类:

protocol TAProgressLayerProtocol {

    func progressUpdated(progress: CGFloat)

}

class TAProgressLayer : CALayer {

    // MARK: - Progress-related properties

    var progress: CGFloat = 0.0
    var progressDelegate: TAProgressLayerProtocol? = nil

    // MARK: - Initialization & Encoding

    // We must copy across our custom properties since Core Animation makes a copy
    // of the layer that it's animating.

    override init(layer: AnyObject) {
        super.init(layer: layer)
        if let other = layer as? TAProgressLayerProtocol {
            self.progress = other.progress
            self.progressDelegate = other.progressDelegate
        }
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        progressDelegate = aDecoder.decodeObjectForKey("progressDelegate") as? CALayerProgressProtocol
        progress = CGFloat(aDecoder.decodeFloatForKey("progress"))
    }

    override func encodeWithCoder(aCoder: NSCoder) {
        super.encodeWithCoder(aCoder)
        aCoder.encodeFloat(Float(progress), forKey: "progress")
        aCoder.encodeObject(progressDelegate as! AnyObject?, forKey: "progressDelegate")
    }

    init(progressDelegate: TAProgressLayerProtocol?) {
        super.init()
        self.progressDelegate = progressDelegate
    }

    // MARK: - Progress Reporting

    // Override needsDisplayForKey so that we can define progress as being animatable.
    class override func needsDisplayForKey(key: String) -> Bool {
        if (key == "progress") {
            return true
        } else {
            return super.needsDisplayForKey(key)
        }
    }

    // Call our callback

    override func drawInContext(ctx: CGContext) {
        if let del = self.progressDelegate {
            del.progressUpdated(progress)
        }
    }

}
移植到Swift 4.2: 用法:
我不知道怎么简单。。。但是您正在操作的属性上的KVO如何。。。。它敲响了一个警钟,我以前可能做过。在动画中的某些点,我想显示和隐藏其他视图。干得好。请注意,当动画完成时,您必须明确地从层中删除动画,否则drawInContext:将不会停止在每一帧上调用。好主意。我想提出一个小小的改进建议。您当前被迫将层的帧设置为1x1矩形,因为除非层的帧为非零(在此过程中创建1x1pt位图),否则不会调用
drawInContext:
。相反,您可以覆盖
[CALayer display]
,这将为零帧调用,并且默认情况下不会创建位图上下文(效率稍高)。唯一的区别是,您需要阅读
draw
实现中的
self.presentationLayer.progress
而不是
self.progress
。这应该是正确的答案。相比之下,目前公认的答案似乎是一个错误。本教程介绍如何设置基本的CADisplayLink方案:您也可以非常轻松地将自己应用于此方案。尝试本教程了解将轻松应用于FPS动画的基础知识:本教程还支持CADisplayLink方法,而不是当前公认的答案。欢迎来到堆栈溢出。虽然此代码可能会回答该问题,但提供有关此代码为什么和/或如何回答该问题的附加上下文可提高其长期价值。
protocol TAProgressLayerProtocol {

    func progressUpdated(progress: CGFloat)

}

class TAProgressLayer : CALayer {

    // MARK: - Progress-related properties

    var progress: CGFloat = 0.0
    var progressDelegate: TAProgressLayerProtocol? = nil

    // MARK: - Initialization & Encoding

    // We must copy across our custom properties since Core Animation makes a copy
    // of the layer that it's animating.

    override init(layer: AnyObject) {
        super.init(layer: layer)
        if let other = layer as? TAProgressLayerProtocol {
            self.progress = other.progress
            self.progressDelegate = other.progressDelegate
        }
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        progressDelegate = aDecoder.decodeObjectForKey("progressDelegate") as? CALayerProgressProtocol
        progress = CGFloat(aDecoder.decodeFloatForKey("progress"))
    }

    override func encodeWithCoder(aCoder: NSCoder) {
        super.encodeWithCoder(aCoder)
        aCoder.encodeFloat(Float(progress), forKey: "progress")
        aCoder.encodeObject(progressDelegate as! AnyObject?, forKey: "progressDelegate")
    }

    init(progressDelegate: TAProgressLayerProtocol?) {
        super.init()
        self.progressDelegate = progressDelegate
    }

    // MARK: - Progress Reporting

    // Override needsDisplayForKey so that we can define progress as being animatable.
    class override func needsDisplayForKey(key: String) -> Bool {
        if (key == "progress") {
            return true
        } else {
            return super.needsDisplayForKey(key)
        }
    }

    // Call our callback

    override func drawInContext(ctx: CGContext) {
        if let del = self.progressDelegate {
            del.progressUpdated(progress)
        }
    }

}
protocol CAProgressLayerDelegate: CALayerDelegate {
    func progressDidChange(to progress: CGFloat)
}

extension CAProgressLayerDelegate {
    func progressDidChange(to progress: CGFloat) {}
}

class CAProgressLayer: CALayer {
    private struct Const {
        static let animationKey: String = "progress"
    }

    @NSManaged private(set) var progress: CGFloat
    private var previousProgress: CGFloat?
    private var progressDelegate: CAProgressLayerDelegate? { return self.delegate as? CAProgressLayerDelegate }

    override init() {
        super.init()
    }

    init(frame: CGRect) {
        super.init()
        self.frame = frame
    }

    override init(layer: Any) {
        super.init(layer: layer)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.progress = CGFloat(aDecoder.decodeFloat(forKey: Const.animationKey))
    }

    override func encode(with aCoder: NSCoder) {
        super.encode(with: aCoder)
        aCoder.encode(Float(self.progress), forKey: Const.animationKey)
    }

    override class func needsDisplay(forKey key: String) -> Bool {
        if key == Const.animationKey { return true }
        return super.needsDisplay(forKey: key)
    }

    override func display() {
        super.display()
        guard let layer: CAProgressLayer = self.presentation() else { return }
        self.progress = layer.progress
        if self.progress != self.previousProgress {
            self.progressDelegate?.progressDidChange(to: self.progress)
        }
        self.previousProgress = self.progress
    }
}
class ProgressView: UIView {
    override class var layerClass: AnyClass {
        return CAProgressLayer.self
    }
}

class ExampleViewController: UIViewController, CAProgressLayerDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()

        let progressView = ProgressView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        progressView.layer.delegate = self
        view.addSubview(progressView)

        var animations = [CAAnimation]()

        let opacityAnimation = CABasicAnimation(keyPath: "opacity")
        opacityAnimation.fromValue = 0
        opacityAnimation.toValue = 1
        opacityAnimation.duration = 1
        animations.append(opacityAnimation)

        let progressAnimation = CABasicAnimation(keyPath: "progress")
        progressAnimation.fromValue = 0
        progressAnimation.toValue = 1
        progressAnimation.duration = 1
        animations.append(progressAnimation)

        let group = CAAnimationGroup()
        group.duration = 1
        group.beginTime = CACurrentMediaTime()
        group.animations = animations

        progressView.layer.add(group, forKey: nil)
    }

    func progressDidChange(to progress: CGFloat) {
        print(progress)
    }
}