The layout transition API makes it easy to animate between a node's generated layouts in response to some internal state change in a node.
Imagine you wanted to implement this sign up form and animate in the new field when tapping the next button:
A standard way to implement this would be to create a container node called SignupNode
that includes two FieldNode
s and a button node as subnodes. We'll include a property on the SignupNode
called fieldState
that will be used to select which FieldNode
to show when the node calculates its layout. The internal layout spec of the SignupNode
container would look something like this:
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
FieldNode *field;
if (self.fieldState == SignupNodeName) {
field = self.nameField;
} else {
field = self.ageField;
}
ASStackLayoutSpec *stack = [[ASStackLayoutSpec alloc] init];
[stack setChildren:@[field, self.buttonNode]];
UIEdgeInsets insets = UIEdgeInsetsMake(15.0, 15.0, 15.0, 15.0);
return [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:stack];
}
To trigger a transition from the nameField
to the ageField
in this example, we'll update the SignupNode's fieldState property and begin the transition with transitionLayoutWithAnimation:
. This method will invalidate the current calculated layout and recompute a new layout with the ageField
now in the stack.
self.signupNode.fieldState = SignupNodeAge;
[self.signupNode transitionLayoutWithAnimation:YES];
In the default implementation of this API, the layout will recalculate the new layout and use its sublayouts to size and position the SignupNode
's subnodes without animation. Future versions of this API will likely include a default animation between layouts and we're open to feedback on what you'd like to see here. However, we'll need to implement a custom animation block to handle the signup form case.
The example below represents an override of animateLayoutTransition:
in SignupNode
. This method is called after the new layout has been calculated via transitionLayoutWithAnimation:
and in the implementation we'll perform a specific animation based upon the fieldState
property that was set before the animation was triggered.
- (void)animateLayoutTransition:(id<ASContextTransitioning>)context
{
if (self.fieldState == SignupNodeName) {
CGRect initialNameFrame = [context initialFrameForNode:self.ageField];
initialNameFrame.origin.x += initialNameFrame.size.width;
self.nameField.frame = initialNameFrame;
self.nameField.alpha = 0.0;
CGRect finalEmailFrame = [context finalFrameForNode:self.nameField];
finalEmailFrame.origin.x -= finalEmailFrame.size.width;
[UIView animateWithDuration:0.4 animations:^{
self.nameField.frame = [context finalFrameForNode:self.nameField];
self.nameField.alpha = 1.0;
self.ageField.frame = finalEmailFrame;
self.ageField.alpha = 0.0;
} completion:^(BOOL finished) {
[context completeTransition:finished];
}];
} else {
CGRect initialAgeFrame = [context initialFrameForNode:self.nameField];
initialAgeFrame.origin.x += initialAgeFrame.size.width;
self.ageField.frame = initialAgeFrame;
self.ageField.alpha = 0.0;
CGRect finalNameFrame = [context finalFrameForNode:self.ageField];
finalNameFrame.origin.x -= finalNameFrame.size.width;
[UIView animateWithDuration:0.4 animations:^{
self.ageField.frame = [context finalFrameForNode:self.ageField];
self.ageField.alpha = 1.0;
self.nameField.frame = finalNameFrame;
self.nameField.alpha = 0.0;
} completion:^(BOOL finished) {
[context completeTransition:finished];
}];
}
}
The passed ASContextTransitioning context object in this method contains relevant information to help you determine the state of the nodes before and after the transition. It includes getters into old and new constrained sizes, inserted and removed nodes, and even the raw old and new ASLayout
objects. In the SignupNode
example, we're using it to determine the frame for each of the fields and animate them in an out of place.
It is imperative to call completeTransition:
on the context object once your animation has finished, as it will perform the necessary internal steps for the newly calculated layout to become the current calculatedLayout
.
Note that there hasn't been a use of addSubnode:
or removeFromSupernode
during the transition. AsyncDisplayKit's layout transition API analyzes the differences in the node hierarchy between the old and new layout, implicitly performing node insertions and removals for automatically. Nodes are inserted before your implementation of animateLayoutTransition:
is called and this is a good place to manually manage the hierarchy before you begin the animation. Removals are preformed in didCompleteLayoutTransition:
after you call completeTransition:
on the context object. If you need to manually perform deletions, override didCompleteLayoutTransition:
and perform your custom operations. Note that this will override the default behavior and it is recommended to either call super
or walk through the removedSubnodes
getter in the context object to perform the cleanup.
Passing NO
to transitionLayoutWithAnimation:
will still run through your animateLayoutTransition:
and didCompleteLayoutTransition:
implementations with the [context isAnimated]
property set to NO
. It is your choice on how to handle this case — if at all. An easy way to provide a default implementation this is to call super
:
- (void)animateLayoutTransition:(id<ASContextTransitioning>)context
{
if ([context isAnimated]) {
// perform animation
} else {
[super animateLayoutTransition:context];
}
}
There will be times you'll simply want to respond to bounds changes to your node and animate the recalculation of its layout. To handle this case, call transitionLayoutWithSizeRange:animated:
on your node. This method is similar to transitionLayoutWithAnimation:
, but will not trigger an animation if the passed ASSizeRange
is equal to the current constrainedSizeForCalculatedLayout
value. This is great for responding to rotation events and view controller size changes:
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[self.node transitionLayoutWithSizeRange:ASSizeRangeMake(size, size) animated:YES];
} completion:nil];
}
@levi this is looking amazing! Makes the recent work you did much more accessible. Excited to try this out. Having read this, I would say it may be good to define what kind of type
self.fieldState
is (or specify that the internal state can be handled by anything the developer wishes, since this API will just re-render- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
), so that developers have a complete understanding on how they can re-create this example. I'm sure I'll have more feedback as I use it. In terms of having a default transition animation, I am strongly in favour for this, I imagine a simple fade would suffice.