Есть ли простой способ возврата, когда Core Animation достигает определенных точек при его запуске (например, при 50% и 66% завершения?
В настоящее время я думаю об установке NSTimer, но это не так точно, как хотелось бы.
Есть ли простой способ возврата, когда Core Animation достигает определенных точек при его запуске (например, при 50% и 66% завершения?
В настоящее время я думаю об установке NSTimer, но это не так точно, как хотелось бы.
Наконец-то я разработал решение этой проблемы.
По существу, я хочу, чтобы меня вызывали назад для каждого фрейма и делали то, что мне нужно было сделать.
Нет очевидного способа наблюдать за ходом анимации, но на самом деле это возможно:
Во-первых, нам нужно создать новый подкласс 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...
}
Если вы не хотите взломать 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.
Я сделал 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)
}
}
}
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)
}
}