[Layout Transition] Add default fade in / out layout transition (#2052)

* Add default fade in / out layout transition

* Add example for layout transition

* Update for recent layout transition API changes

* To be able to do a layoutTransition the node needs to be loaded

* Rename layoutTransitionDuration to defaultLayoutTransitionDuration

* Expose default layout transition duration delay and options

* Use `UIViewAnimationOptionBeginFromCurrentState` for initial defaultLayoutTransitionOptions
This commit is contained in:
Michael Schneider
2016-08-13 17:35:20 -07:00
committed by Adlai Holler
parent 43370fe6ff
commit adcc9afb5a
17 changed files with 945 additions and 4 deletions

View File

@@ -291,6 +291,10 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
_environmentState = ASEnvironmentStateMakeDefault();
_defaultLayoutTransitionDuration = 0.2;
_defaultLayoutTransitionDelay = 0.0;
_defaultLayoutTransitionOptions = UIViewAnimationOptionBeginFromCurrentState;
_flags.canClearContentsOfLayer = YES;
_flags.canCallNeedsDisplayOfLayer = NO;
}
@@ -857,14 +861,118 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
#pragma mark Layout Transition API
- (void)setDefaultLayoutTransitionDuration:(NSTimeInterval)defaultLayoutTransitionDuration
{
ASDN::MutexLocker l(__instanceLock__);
_defaultLayoutTransitionDuration = defaultLayoutTransitionDuration;
}
- (NSTimeInterval)defaultLayoutTransitionDuration
{
ASDN::MutexLocker l(__instanceLock__);
return _defaultLayoutTransitionDuration;
}
- (void)setDefaultLayoutTransitionDelay:(NSTimeInterval)defaultLayoutTransitionDelay
{
ASDN::MutexLocker l(__instanceLock__);
_defaultLayoutTransitionDelay = defaultLayoutTransitionDelay;
}
- (NSTimeInterval)defaultLayoutTransitionDelay
{
ASDN::MutexLocker l(__instanceLock__);
return _defaultLayoutTransitionDelay;
}
- (void)setDefaultLayoutTransitionOptions:(UIViewAnimationOptions)defaultLayoutTransitionOptions
{
ASDN::MutexLocker l(__instanceLock__);
_defaultLayoutTransitionOptions = defaultLayoutTransitionOptions;
}
- (UIViewAnimationOptions)defaultLayoutTransitionOptions
{
ASDN::MutexLocker l(__instanceLock__);
return _defaultLayoutTransitionOptions;
}
/*
* Hook for subclasse to perform an animation based on the given ASContextTransitioning. By default this just layouts
* applies all subnodes without animation and calls completes the transition on the context.
* Hook for subclasse to perform an animation based on the given ASContextTransitioning. By default a fade in and out
* animation is provided.
*/
- (void)animateLayoutTransition:(id<ASContextTransitioning>)context
{
[self __layoutSublayouts];
[context completeTransition:YES];
ASDisplayNode *node = self;
NSAssert(node.isNodeLoaded == YES, @"Invalid node state");
NSAssert([context isAnimated] == YES, @"Can't animate a non-animatable context");
NSArray<ASDisplayNode *> *removedSubnodes = [context removedSubnodes];
NSMutableArray<UIView *> *removedViews = [NSMutableArray array];
NSMutableArray<ASDisplayNode *> *insertedSubnodes = [[context insertedSubnodes] mutableCopy];
NSMutableArray<ASDisplayNode *> *movedSubnodes = [NSMutableArray array];
for (ASDisplayNode *subnode in [context subnodesForKey:ASTransitionContextToLayoutKey]) {
if ([insertedSubnodes containsObject:subnode] == NO) {
// This is an existing subnode, check if it is resized, moved or both
CGRect fromFrame = [context initialFrameForNode:subnode];
CGRect toFrame = [context finalFrameForNode:subnode];
if (CGSizeEqualToSize(fromFrame.size, toFrame.size) == NO) {
// To crossfade resized subnodes, show a snapshot of it on top.
// The node itself can then be treated as a newly-inserted one.
UIView *snapshotView = [subnode.view snapshotViewAfterScreenUpdates:YES];
snapshotView.frame = [context initialFrameForNode:subnode];
snapshotView.alpha = 1;
[node.view insertSubview:snapshotView aboveSubview:subnode.view];
[removedViews addObject:snapshotView];
[insertedSubnodes addObject:subnode];
}
if (CGPointEqualToPoint(fromFrame.origin, toFrame.origin) == NO) {
[movedSubnodes addObject:subnode];
}
}
}
for (ASDisplayNode *insertedSubnode in insertedSubnodes) {
insertedSubnode.frame = [context finalFrameForNode:insertedSubnode];
insertedSubnode.alpha = 0;
}
[UIView animateWithDuration:self.defaultLayoutTransitionDuration delay:self.defaultLayoutTransitionDelay options:self.defaultLayoutTransitionOptions animations:^{
// Fade removed subnodes and views out
for (ASDisplayNode *removedSubnode in removedSubnodes) {
removedSubnode.alpha = 0;
}
for (UIView *removedView in removedViews) {
removedView.alpha = 0;
}
// Fade inserted subnodes in
for (ASDisplayNode *insertedSubnode in insertedSubnodes) {
insertedSubnode.alpha = 1;
}
// Update frame of self and moved subnodes
CGSize fromSize = [context layoutForKey:ASTransitionContextFromLayoutKey].size;
CGSize toSize = [context layoutForKey:ASTransitionContextToLayoutKey].size;
BOOL isResized = (CGSizeEqualToSize(fromSize, toSize) == NO);
if (isResized == YES) {
CGPoint position = node.frame.origin;
node.frame = CGRectMake(position.x, position.y, toSize.width, toSize.height);
}
for (ASDisplayNode *movedSubnode in movedSubnodes) {
movedSubnode.frame = [context finalFrameForNode:movedSubnode];
}
} completion:^(BOOL finished) {
for (UIView *removedView in removedViews) {
[removedView removeFromSuperview];
}
// Subnode removals are automatically performed
[context completeTransition:finished];
}];
}
/*