ios - animate - Retroceso de la animación del núcleo de la llamada
ios animation swift (4)
Portado a 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
}
}
Uso:
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)
}
}
¿Hay una manera fácil de volver a llamar cuando una Animación Core alcanza ciertos puntos mientras se ejecuta (por ejemplo, al 50% y al 66% de finalización?
Actualmente estoy pensando en configurar un NSTimer, pero eso no es tan preciso como me gustaría.
Finalmente he desarrollado una solución para este problema.
Esencialmente deseo que me devuelvan la llamada para cada fotograma y hacer lo que necesito hacer.
No hay una manera obvia de observar el progreso de una animación, sin embargo, es posible:
Primero, debemos crear una nueva subclase de CALayer que tenga una propiedad animable llamada ''progreso''.
Añadimos la capa a nuestro árbol, y luego creamos una animación que impulsará el valor de progreso de 0 a 1 durante la duración de la animación.
Dado que nuestra propiedad de progreso puede ser animada, se llama a drawInContext en nuestra subclase para cada fotograma de una animación. Esta función no necesita volver a dibujar nada, sin embargo, se puede usar para llamar a una función delegada :)
Aquí está la interfaz de clase:
@protocol TAProgressLayerProtocol <NSObject>
- (void)progressUpdatedTo:(CGFloat)progress;
@end
@interface TAProgressLayer : CALayer
@property CGFloat progress;
@property (weak) id<TAProgressLayerProtocol> delegate;
@end
Y la implementación:
@implementation TAProgressLayer
// We must copy across our custom properties since Core Animation makes a copy
// of the layer that it''s 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
Luego podemos agregar la capa a nuestra capa principal:
TAProgressLayer *progressLayer = [TAProgressLayer layer];
progressLayer.frame = CGRectMake(0, -1, 1, 1);
progressLayer.delegate = self;
[_sceneView.layer addSublayer:progressLayer];
Y animarlo junto con las otras animaciones:
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"];
Finalmente, se devolverá la llamada al delegado a medida que avance la animación:
- (void)progressUpdatedTo:(CGFloat)progress
{
// Do whatever you need to do...
}
Hice una implementación Swift (2.0) de la subclase CALayer sugerida por tarmes en la respuesta aceptada:
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)
}
}
}
Si no quiere piratear a un CALayer para informarle sobre el progreso, hay otro enfoque. Conceptualmente, puede utilizar un CADisplayLink para garantizar una devolución de llamada en cada fotograma, y luego simplemente medir el tiempo transcurrido desde el inicio de la animación dividido por la duración para calcular el porcentaje completado.
La biblioteca de código abierto INTUAnimationEngine empaqueta esta funcionalidad de manera muy clara en una API que se parece casi exactamente a la animación basada en bloques de UIView:
// INTUAnimationEngine.h
// ...
+ (NSInteger)animateWithDuration:(NSTimeInterval)duration
delay:(NSTimeInterval)delay
animations:(void (^)(CGFloat percentage))animations
completion:(void (^)(BOOL finished))completion;
// ...
Todo lo que necesita hacer es llamar a este método al mismo tiempo que inicia otras animaciones, pasando los mismos valores por duration
y delay
, y luego, para cada fotograma de la animación, el bloque de animations
se ejecutará con el porcentaje actual completado. Y si desea tener la tranquilidad de que sus tiempos están perfectamente sincronizados, puede manejar sus animaciones exclusivamente desde INTUAnimationEngine.