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