ComponentKit/Core/CKComponentAnimation.mm (92 lines of code) (raw):
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "CKComponentAnimation.h"
#import "CKComponentSubclass.h"
@interface CKAppliedAnimationContext : NSObject
- (instancetype)initWithTargetLayer:(CALayer *)layer key:(NSString *)key;
@property (nonatomic, strong, readonly) CALayer *targetLayer;
@property (nonatomic, copy, readonly) NSString *key;
@end
static CKComponentAnimationHooks hooksForCAAnimation(CKComponent *component, CAAnimation *originalAnimation, NSString *layerPath) noexcept
{
RCCAssertNotNil(component, @"Component being animated must be non-nil");
RCCAssertNotNil(originalAnimation, @"Animation being added must be non-nil");
// Don't mutate the animation the component returned, in case it is a static or otherwise reused. (Also copy
// immediately to protect against the *caller* mutating the animation after this point but before it's used.)
CAAnimation *copiedAnimation = [originalAnimation copy];
return {
.didRemount = [^(id context){
CALayer *layer = layerPath ? [component.viewForAnimation valueForKeyPath:layerPath] : component.viewForAnimation.layer;
if (auto const lp = layerPath) {
RCCAssertWithCategory(layer != nil, [component className],
@"%@ has no mounted layer at key path %@, so it cannot be animated", [component className], lp);
} else {
RCCAssertWithCategory(layer != nil, [component className],
@"%@ has no mounted layer, so it cannot be animated", [component className]);
}
NSString *key = [[NSUUID UUID] UUIDString];
auto const animationAddTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
copiedAnimation.beginTime += animationAddTime;
[layer addAnimation:copiedAnimation forKey:key];
return [[CKAppliedAnimationContext alloc] initWithTargetLayer:layer key:key];
} copy],
.cleanup = ^(CKAppliedAnimationContext *context){
[context.targetLayer removeAnimationForKey:context.key];
}
};
}
static CKComponentAnimationHooks hooksForFinalUnmountAnimation(const CKComponentFinalUnmountAnimation &a,
UIView *const hostView) noexcept
{
const auto component = a.component;
CAAnimation *const animation = [a.animation copy];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
return CKComponentAnimationHooks {
.willRemount = ^() {
const auto viewForAnimation = [component viewForAnimation];
RCCAssertWithCategory(viewForAnimation != nil, [component className],
@"Can't animate component without a view. Check if %@ has a view.", [component className]);
const auto snapshotView = [viewForAnimation snapshotViewAfterScreenUpdates:NO];
snapshotView.layer.anchorPoint = viewForAnimation.layer.anchorPoint;
snapshotView.frame = [viewForAnimation convertRect:viewForAnimation.bounds toView:hostView];
snapshotView.userInteractionEnabled = NO;
return snapshotView;
},
.didRemount = ^(UIView *const snapshotView){
[hostView addSubview:snapshotView];
auto const animationAddTime = [snapshotView.layer convertTime:CACurrentMediaTime() fromLayer:nil];
animation.beginTime += animationAddTime;
[snapshotView.layer addAnimation:animation forKey:nil];
return snapshotView;
},
.cleanup = ^(UIView *const snapshotView){
[snapshotView removeFromSuperview];
}
}.byAddingCompletion(a.completion);
}
CKComponentAnimation::CKComponentAnimation(CKComponent *component, CAAnimation *animation, NSString *layerPath, CKComponentAnimationCompletion completion) noexcept
: hooks(hooksForCAAnimation(component, animation, layerPath).byAddingCompletion(completion)) {}
CKComponentAnimation::CKComponentAnimation(const CKComponentFinalUnmountAnimation &animation, UIView *const hostView) noexcept
: hooks(hooksForFinalUnmountAnimation(animation, hostView)) {}
CKComponentAnimation::CKComponentAnimation(const CKComponentAnimationHooks &h, CKComponentAnimationCompletion completion) noexcept : hooks(h.byAddingCompletion(completion)) {}
id CKComponentAnimation::willRemount() const
{
return hooks.willRemount ? hooks.willRemount() : nil;
}
id CKComponentAnimation::didRemount(id context) const
{
return hooks.didRemount ? hooks.didRemount(context) : nil;
}
void CKComponentAnimation::cleanup(id context) const
{
if (hooks.cleanup) {
hooks.cleanup(context);
}
}
@implementation CKAppliedAnimationContext
- (instancetype)initWithTargetLayer:(CALayer *)targetLayer key:(NSString *)key
{
if (self = [super init]) {
_targetLayer = targetLayer;
_key = [key copy];
}
return self;
}
@end