Подтвердить что ты не робот

Обратный вызов прогресса анимации ядра

Есть ли простой способ возврата, когда Core Animation достигает определенных точек при его запуске (например, при 50% и 66% завершения?

В настоящее время я думаю об установке NSTimer, но это не так точно, как хотелось бы.

4b9b3361

Ответ 1

Наконец-то я разработал решение этой проблемы.

По существу, я хочу, чтобы меня вызывали назад для каждого фрейма и делали то, что мне нужно было сделать.

Нет очевидного способа наблюдать за ходом анимации, но на самом деле это возможно:

  • Во-первых, нам нужно создать новый подкласс CALayer с анимационным свойством под названием "прогресс".

  • Мы добавляем слой в наше дерево, а затем создаем анимацию, которая будет управлять значением прогресса от 0 до 1 в течение продолжительности анимации.

  • Поскольку наше свойство progress может быть анимировано, drawInContext вызывается в нашем подслое для каждого кадра анимации. Эта функция не нуждается в перерисовке, но ее можно использовать для вызова функции делегата:)

Здесь интерфейс класса:

@protocol TAProgressLayerProtocol <NSObject>

- (void)progressUpdatedTo:(CGFloat)progress;

@end

@interface TAProgressLayer : CALayer

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

@end

И реализация:

@implementation TAProgressLayer

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

- (id)initWithLayer:(id)layer
{
    self = [super initWithLayer:layer];
    if (self) {
        TAProgressLayer *otherLayer = (TAProgressLayer *)layer;
        self.progress = otherLayer.progress;
        self.delegate = otherLayer.delegate;
    }
    return self;
}

// Override needsDisplayForKey so that we can define progress as being animatable.

+ (BOOL)needsDisplayForKey:(NSString*)key {
    if ([key isEqualToString:@"progress"]) {
        return YES;
    } else {
        return [super needsDisplayForKey:key];
    }
}

// Call our callback

- (void)drawInContext:(CGContextRef)ctx
{
    if (self.delegate)
    {
        [self.delegate progressUpdatedTo:self.progress];
    }
}

@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...
}

Ответ 2

Если вы не хотите взломать CALayer, чтобы сообщить о прогрессе, есть другой подход. Концептуально вы можете использовать CADisplayLink, чтобы гарантировать обратный вызов для каждого кадра, а затем просто измерить время, прошедшее с момента начала анимации, деленное на длительность, чтобы выяснить процент завершения.

Библиотека с открытым исходным кодом INTUAnimationEngine полностью интегрирует эту функциональность в API, который почти точно похож на анимационную единицу на основе UIView:

// INTUAnimationEngine.h

// ...

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

// ...

Все, что вам нужно сделать, это вызвать этот метод одновременно с запуском других анимаций, передав те же значения для duration и delay, а затем для каждого кадра анимации блок animations будет выполнен с завершением текущего процента. И если вы хотите спокойствия, что ваши тайминги идеально синхронизированы, вы можете управлять своей анимацией исключительно из INTUAnimationEngine.

Ответ 3

Я сделал Swift (2.0) реализацию подкласса CALayer, предложенного tarmes в принятом ответе:

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 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)
        }
    }

}

Ответ 4

Портировано на Swift 4.2:

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)
    }
}