Core Animation が実行中に特定のポイント (たとえば、50% と 66% の完了) に達したときにコールバックする簡単な方法はありますか?
私は現在 NSTimer の設定を考えていますが、それは私が望むほど正確ではありません。
Core Animation が実行中に特定のポイント (たとえば、50% と 66% の完了) に達したときにコールバックする簡単な方法はありますか?
私は現在 NSTimer の設定を考えていますが、それは私が望むほど正確ではありません。
私はついにこの問題の解決策を開発しました。
本質的には、すべてのフレームでコールバックされ、必要なことを実行したいと考えています。
アニメーションの進行状況を観察する明白な方法はありませんが、実際には可能です:
最初に、「progress」というアニメーション可能なプロパティを持つ CALayer の新しいサブクラスを作成する必要があります。
レイヤーをツリーに追加し、アニメーションの継続時間中に進行状況の値を 0 から 1 に駆動するアニメーションを作成します。
プログレス プロパティはアニメーション化できるため、アニメーションのフレームごとにサブラスで 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'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
次に、レイヤーをメインレイヤーに追加できます。
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 にパッケージ化します。この 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 からのみアニメーションを駆動できます。
受け入れられた回答でtarmesによって提案されたCALayerサブクラスのSwift(2.0)実装を作成しました:
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)
}
}
}