Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Commit 678df37

Browse files
maickiAdlai Holler
authored andcommitted
[Layout Transition API] Simplify applying layout transition (#1886)
* Simplify applying layout transition in preparation for bigger layout transition API work * Change from apply to complete in if layout transitions are involved and _applyLayout: to _setCalculatedLayout: for layout * Change to applySubnodeInsertions and applySubnodeRemovals * Change from completeTransition to commitTransition and flip logic around when to trampoline to the main thread for implicit hierarchy management * More internal API improvements * Fix merge conflicts * Rename _layout to _calculatedLayout
1 parent ff2c47c commit 678df37

4 files changed

Lines changed: 163 additions & 96 deletions

File tree

AsyncDisplayKit/ASDisplayNode.mm

Lines changed: 157 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -642,30 +642,23 @@ - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize
642642
{
643643
ASDN::MutexLocker l(__instanceLock__);
644644
if (! [self shouldMeasureWithSizeRange:constrainedSize]) {
645-
return _layout;
645+
return _calculatedLayout;
646646
}
647647

648648
[self cancelLayoutTransitionsInProgress];
649649

650-
ASLayout *previousLayout = _layout;
650+
ASLayout *previousLayout = _calculatedLayout;
651651
ASLayout *newLayout = [self calculateLayoutThatFits:constrainedSize];
652652

653-
if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) {
654-
_pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self
655-
pendingLayout:newLayout
656-
previousLayout:previousLayout];
657-
} else {
658-
ASLayoutTransition *layoutTransition = nil;
659-
if (self.usesImplicitHierarchyManagement) {
660-
layoutTransition = [[ASLayoutTransition alloc] initWithNode:self
661-
pendingLayout:newLayout
662-
previousLayout:previousLayout];
663-
}
664-
665-
[self _applyLayout:newLayout layoutTransition:layoutTransition];
666-
[self _completeLayoutCalculation];
653+
_pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self
654+
pendingLayout:newLayout
655+
previousLayout:previousLayout];
656+
657+
if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO) {
658+
// Complete the pending layout transition immediately
659+
[self _completePendingLayoutTransition];
667660
}
668-
661+
669662
return newLayout;
670663
}
671664

@@ -686,12 +679,12 @@ - (BOOL)shouldMeasureWithSizeRange:(ASSizeRange)constrainedSize
686679
// Only generate a new layout if:
687680
// - The current layout is dirty
688681
// - The passed constrained size is different than the layout's constrained size
689-
return ([self _hasDirtyLayout] || !ASSizeRangeEqualToSizeRange(constrainedSize, _layout.constrainedSizeRange));
682+
return ([self _hasDirtyLayout] || !ASSizeRangeEqualToSizeRange(constrainedSize, _calculatedLayout.constrainedSizeRange));
690683
}
691684

692685
- (BOOL)_hasDirtyLayout
693686
{
694-
return _layout == nil || _layout.isDirty;
687+
return _calculatedLayout == nil || _calculatedLayout.isDirty;
695688
}
696689

697690
- (ASLayoutableType)layoutableType
@@ -710,13 +703,14 @@ - (void)transitionLayoutWithAnimation:(BOOL)animated
710703
shouldMeasureAsync:(BOOL)shouldMeasureAsync
711704
measurementCompletion:(void(^)())completion
712705
{
713-
if (_layout == nil) {
706+
if (_calculatedLayout == nil) {
714707
// constrainedSizeRange returns a struct and is invalid to call on nil.
715708
// Defaulting to CGSizeZero can cause negative values in client layout code.
716709
return;
717710
}
711+
718712
[self invalidateCalculatedLayout];
719-
[self transitionLayoutWithSizeRange:_layout.constrainedSizeRange
713+
[self transitionLayoutWithSizeRange:_calculatedLayout.constrainedSizeRange
720714
animated:animated
721715
shouldMeasureAsync:shouldMeasureAsync
722716
measurementCompletion:completion];
@@ -778,12 +772,11 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize
778772
return;
779773
}
780774

781-
ASLayout *previousLayout = _layout;
782-
[self _applyLayout:newLayout layoutTransition:nil];
775+
ASLayout *previousLayout = _calculatedLayout;
776+
[self setCalculatedLayout:newLayout];
783777

784778
ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) {
785-
[node _applyPendingLayoutContext];
786-
[node _completeLayoutCalculation];
779+
[node _completePendingLayoutTransition];
787780
node.hierarchyState &= (~ASHierarchyStateLayoutPending);
788781
});
789782

@@ -793,48 +786,26 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize
793786
completion();
794787
}
795788

789+
// Setup pending layout transition for animation
796790
_pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self
797791
pendingLayout:newLayout
798-
previousLayout:previousLayout];
792+
previousLayout:previousLayout];
793+
// Setup context for pending layout transition. we need to hold a strong reference to the context
794+
_pendingLayoutTransitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated
795+
layoutDelegate:_pendingLayoutTransition
796+
completionDelegate:self];
797+
798+
// Apply the subnode insertion immediately to be able to animate the nodes
799799
[_pendingLayoutTransition applySubnodeInsertions];
800-
801-
_transitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated
802-
layoutDelegate:_pendingLayoutTransition
803-
completionDelegate:self];
804-
[self animateLayoutTransition:_transitionContext];
800+
801+
// Kick off animating the layout transition
802+
[self animateLayoutTransition:_pendingLayoutTransitionContext];
805803
});
806804
};
807805

808806
ASPerformBlockOnBackgroundThread(transitionBlock);
809807
}
810808

811-
- (void)_completeLayoutCalculation
812-
{
813-
ASDN::MutexLocker l(__instanceLock__);
814-
[self calculatedLayoutDidChange];
815-
816-
// We generate placeholders at measureWithSizeRange: time so that a node is guaranteed to have a placeholder ready to go.
817-
// This is also because measurement is usually asynchronous, but placeholders need to be set up synchronously.
818-
// First measurement is guaranteed to be before the node is onscreen, so we can create the image async. but still have it appear sync.
819-
if (_placeholderEnabled && [self _displaysAsynchronously] && self.contents == nil) {
820-
821-
// Zero-sized nodes do not require a placeholder.
822-
CGSize layoutSize = (_layout ? _layout.size : CGSizeZero);
823-
if (CGSizeEqualToSize(layoutSize, CGSizeZero)) {
824-
return;
825-
}
826-
827-
if (!_placeholderImage) {
828-
_placeholderImage = [self placeholderImage];
829-
}
830-
}
831-
}
832-
833-
- (void)calculatedLayoutDidChange
834-
{
835-
// subclass override
836-
}
837-
838809
- (void)cancelLayoutTransitionsInProgress
839810
{
840811
ASDN::MutexLocker l(__instanceLock__);
@@ -888,25 +859,108 @@ - (BOOL)_shouldAbortTransitionWithID:(int32_t)transitionID
888859
return (!_transitionInProgress || _transitionID != transitionID);
889860
}
890861

862+
#pragma mark - Layout Transition API / ASDisplayNode (Beta)
863+
864+
/*
865+
* Hook for subclasse to perform an animation based on the given ASContextTransitioning. By default this just layouts
866+
* applies all subnodes without animation and calls completes the transition on the context.
867+
*/
891868
- (void)animateLayoutTransition:(id<ASContextTransitioning>)context
892869
{
893-
[self __layoutSubnodes];
870+
[self __layoutSublayouts];
894871
[context completeTransition:YES];
895872
}
896873

874+
/*
875+
* Hook for subclasses to clean up nodes after the transition happened. Furthermore this can be used from subclasses
876+
* to manually perform deletions.
877+
*/
897878
- (void)didCompleteLayoutTransition:(id<ASContextTransitioning>)context
898879
{
899880
[_pendingLayoutTransition applySubnodeRemovals];
900-
[self _completeLayoutCalculation];
901-
_pendingLayoutTransition = nil;
902881
}
903882

904883
#pragma mark - _ASTransitionContextCompletionDelegate
905884

885+
/*
886+
* After completeTransition: is called on the ASContextTransitioning object in animateLayoutTransition: this
887+
* delegate method will be called that start the completion process of the
888+
*/
906889
- (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete
907890
{
908891
[self didCompleteLayoutTransition:context];
909-
_transitionContext = nil;
892+
_pendingLayoutTransitionContext = nil;
893+
894+
[self _pendingLayoutTransitionDidComplete];
895+
}
896+
897+
#pragma mark - Layout
898+
899+
/*
900+
* Completes the pending layout transition immediately without going through the the Layout Transition Animation API
901+
*/
902+
- (void)_completePendingLayoutTransition
903+
{
904+
ASDN::MutexLocker l(__instanceLock__);
905+
if (_pendingLayoutTransition) {
906+
[self setCalculatedLayout:_pendingLayoutTransition.pendingLayout];
907+
[self _completeLayoutTransition:_pendingLayoutTransition];
908+
}
909+
[self _pendingLayoutTransitionDidComplete];
910+
}
911+
912+
/*
913+
* Can be directly called to commit the given layout transition immediately to complete without calling through to the
914+
* Layout Transition Animation API
915+
*/
916+
- (void)_completeLayoutTransition:(ASLayoutTransition *)layoutTransition
917+
{
918+
// Layout transition is not supported for non implicit hierarchy managed nodes yet
919+
if (layoutTransition == nil || self.usesImplicitHierarchyManagement == NO) {
920+
return;
921+
}
922+
923+
// Trampoline to the main thread if necessary
924+
if (ASDisplayNodeThreadIsMain() || layoutTransition.isSynchronous == NO) {
925+
[layoutTransition commitTransition];
926+
} else {
927+
// Subnode insertions and removals need to happen always on the main thread if at least one subnode is already loaded
928+
ASPerformBlockOnMainThread(^{
929+
[layoutTransition commitTransition];
930+
});
931+
}
932+
}
933+
934+
- (void)_pendingLayoutTransitionDidComplete
935+
{
936+
ASDN::MutexLocker l(__instanceLock__);
937+
938+
// Subclass hook
939+
[self calculatedLayoutDidChange];
940+
941+
// We generate placeholders at measureWithSizeRange: time so that a node is guaranteed to have a placeholder ready to go.
942+
// This is also because measurement is usually asynchronous, but placeholders need to be set up synchronously.
943+
// First measurement is guaranteed to be before the node is onscreen, so we can create the image async. but still have it appear sync.
944+
if (_placeholderEnabled && [self _displaysAsynchronously] && self.contents == nil) {
945+
946+
// Zero-sized nodes do not require a placeholder.
947+
CGSize layoutSize = (_calculatedLayout ? _calculatedLayout.size : CGSizeZero);
948+
if (CGSizeEqualToSize(layoutSize, CGSizeZero)) {
949+
return;
950+
}
951+
952+
if (!_placeholderImage) {
953+
_placeholderImage = [self placeholderImage];
954+
}
955+
}
956+
957+
// Cleanup pending layout transition
958+
_pendingLayoutTransition = nil;
959+
}
960+
961+
- (void)calculatedLayoutDidChange
962+
{
963+
// subclass override
910964
}
911965

912966
#pragma mark - Asynchronous display
@@ -1068,7 +1122,7 @@ - (void)__setNeedsLayout
10681122

10691123
__instanceLock__.lock();
10701124

1071-
if (_layout == nil) {
1125+
if (_calculatedLayout == nil) {
10721126
// Can't proceed without a layout as no constrained size would be available
10731127
__instanceLock__.unlock();
10741128
return;
@@ -1086,11 +1140,11 @@ - (void)__setNeedsLayout
10861140
}
10871141

10881142
// This is the root node. Trigger a full measurement pass on *current* thread. Old constrained size is re-used.
1089-
[self measureWithSizeRange:_layout.constrainedSizeRange];
1143+
[self measureWithSizeRange:_calculatedLayout.constrainedSizeRange];
10901144

10911145
CGRect oldBounds = self.bounds;
10921146
CGSize oldSize = oldBounds.size;
1093-
CGSize newSize = _layout.size;
1147+
CGSize newSize = _calculatedLayout.size;
10941148

10951149
if (! CGSizeEqualToSize(oldSize, newSize)) {
10961150
self.bounds = (CGRect){ oldBounds.origin, newSize };
@@ -1168,24 +1222,6 @@ - (void)measureNodeWithBoundsIfNecessary:(CGRect)bounds
11681222
}
11691223
}
11701224

1171-
- (void)layout
1172-
{
1173-
ASDisplayNodeAssertMainThread();
1174-
1175-
if ([self _hasDirtyLayout]) {
1176-
return;
1177-
}
1178-
1179-
[self __layoutSubnodes];
1180-
}
1181-
1182-
- (void)__layoutSubnodes
1183-
{
1184-
for (ASLayout *subnodeLayout in _layout.sublayouts) {
1185-
((ASDisplayNode *)subnodeLayout.layoutableObject).frame = [subnodeLayout frame];
1186-
}
1187-
}
1188-
11891225
- (void)layoutDidFinish
11901226
{
11911227
// Hook for subclasses
@@ -2080,19 +2116,30 @@ - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
20802116
- (ASLayout *)calculatedLayout
20812117
{
20822118
ASDN::MutexLocker l(__instanceLock__);
2083-
return _layout;
2119+
return _calculatedLayout;
2120+
}
2121+
2122+
- (void)setCalculatedLayout:(ASLayout *)calculatedLayout
2123+
{
2124+
ASDN::MutexLocker l(__instanceLock__);
2125+
2126+
ASDisplayNodeAssertTrue(calculatedLayout.layoutableObject == self);
2127+
ASDisplayNodeAssertTrue(calculatedLayout.size.width >= 0.0);
2128+
ASDisplayNodeAssertTrue(calculatedLayout.size.height >= 0.0);
2129+
2130+
_calculatedLayout = calculatedLayout;
20842131
}
20852132

20862133
- (CGSize)calculatedSize
20872134
{
20882135
ASDN::MutexLocker l(__instanceLock__);
2089-
return _layout.size;
2136+
return _calculatedLayout.size;
20902137
}
20912138

20922139
- (ASSizeRange)constrainedSizeForCalculatedLayout
20932140
{
20942141
ASDN::MutexLocker l(__instanceLock__);
2095-
return _layout.constrainedSizeRange;
2142+
return _calculatedLayout.constrainedSizeRange;
20962143
}
20972144

20982145
- (void)setLayoutSpecBlock:(ASLayoutSpecBlock)layoutSpecBlock
@@ -2149,7 +2196,7 @@ - (void)invalidateCalculatedLayout
21492196

21502197
// This will cause the next call to -measureWithSizeRange: to actually compute a new layout
21512198
// instead of returning the current layout
2152-
_layout.dirty = YES;
2199+
_calculatedLayout.dirty = YES;
21532200
}
21542201

21552202
- (void)__didLoad
@@ -2516,7 +2563,7 @@ - (void)_applyPendingLayoutContext
25162563
- (void)_applyLayout:(ASLayout *)layout layoutTransition:(ASLayoutTransition *)layoutTransition
25172564
{
25182565
ASDN::MutexLocker l(__instanceLock__);
2519-
_layout = layout;
2566+
_calculatedLayout = layout;
25202567

25212568
ASDisplayNodeAssertTrue(layout.layoutableObject == self);
25222569
ASDisplayNodeAssertTrue(layout.size.width >= 0.0);
@@ -2531,15 +2578,35 @@ - (void)_applyLayout:(ASLayout *)layout layoutTransition:(ASLayoutTransition *)l
25312578

25322579
// Subnode insertions and removals need to happen always on the main thread if at least one subnode is already loaded
25332580
ASPerformBlockOnMainThread(^{
2534-
[layoutTransition startTransition];
2581+
[layoutTransition commitTransition];
25352582
});
25362583

25372584
return;
25382585
}
25392586

2540-
[layoutTransition startTransition];
2587+
[layoutTransition commitTransition];
25412588
}
25422589

2590+
- (void)layout
2591+
{
2592+
ASDisplayNodeAssertMainThread();
2593+
2594+
if ([self _hasDirtyLayout]) {
2595+
return;
2596+
}
2597+
2598+
[self __layoutSublayouts];
2599+
}
2600+
2601+
- (void)__layoutSublayouts
2602+
{
2603+
for (ASLayout *subnodeLayout in _calculatedLayout.sublayouts) {
2604+
((ASDisplayNode *)subnodeLayout.layoutableObject).frame = [subnodeLayout frame];
2605+
}
2606+
}
2607+
2608+
#pragma mark - Display
2609+
25432610
- (void)displayWillStart
25442611
{
25452612
ASDisplayNodeAssertMainThread();

0 commit comments

Comments
 (0)