Basic implementation of transitioning API for layout specs

This commit is contained in:
Levi McCallum
2016-02-04 17:02:55 -08:00
parent b033b544d5
commit 9dc358196a
9 changed files with 289 additions and 63 deletions

View File

@@ -460,6 +460,10 @@
CC7FD9E21BB603FF005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; };
D785F6621A74327E00291744 /* ASScrollNode.h in Headers */ = {isa = PBXBuildFile; fileRef = D785F6601A74327E00291744 /* ASScrollNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
D785F6631A74327E00291744 /* ASScrollNode.m in Sources */ = {isa = PBXBuildFile; fileRef = D785F6611A74327E00291744 /* ASScrollNode.m */; };
DB55C2611C6408D6004EDCF5 /* _ASTransitionContext.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C25F1C6408D6004EDCF5 /* _ASTransitionContext.h */; };
DB55C2631C6408D6004EDCF5 /* _ASTransitionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = DB55C2601C6408D6004EDCF5 /* _ASTransitionContext.m */; };
DB55C2661C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */; settings = {ATTRIBUTES = (Public, ); }; };
DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */; settings = {ATTRIBUTES = (Public, ); }; };
DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; };
DBC452DB1C5BF64600B16017 /* NSArray+Diffing.h in Headers */ = {isa = PBXBuildFile; fileRef = DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */; };
DBC452DC1C5BF64600B16017 /* NSArray+Diffing.m in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */; };
@@ -781,6 +785,9 @@
D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = "<group>"; };
D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = "<group>"; };
D785F6611A74327E00291744 /* ASScrollNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASScrollNode.m; sourceTree = "<group>"; };
DB55C25F1C6408D6004EDCF5 /* _ASTransitionContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = _ASTransitionContext.h; path = ../_ASTransitionContext.h; sourceTree = "<group>"; };
DB55C2601C6408D6004EDCF5 /* _ASTransitionContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = _ASTransitionContext.m; path = ../_ASTransitionContext.m; sourceTree = "<group>"; };
DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASContextTransitioning.h; sourceTree = "<group>"; };
DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Diffing.h"; sourceTree = "<group>"; };
DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Diffing.m"; sourceTree = "<group>"; };
DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ArrayDiffingTests.m; sourceTree = "<group>"; };
@@ -959,6 +966,7 @@
ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */,
ACC945AA1BA9E7C1005E1FB8 /* ASViewController.m */,
6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */,
DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */,
058D09E1195D050800B7D73C /* Details */,
058D0A01195D050800B7D73C /* Private */,
AC6456051B0A333200CF11B8 /* Layout */,
@@ -1102,6 +1110,8 @@
058D0A01195D050800B7D73C /* Private */ = {
isa = PBXGroup;
children = (
DB55C25F1C6408D6004EDCF5 /* _ASTransitionContext.h */,
DB55C2601C6408D6004EDCF5 /* _ASTransitionContext.m */,
AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */,
AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.m */,
9C65A7291BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h */,
@@ -1343,6 +1353,7 @@
DE6EA3221C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */,
1950C4491A3BB5C1005C8279 /* ASEqualityHelpers.h in Headers */,
257754A81BEE44CD00737CA5 /* ASTextKitContext.h in Headers */,
DB55C2611C6408D6004EDCF5 /* _ASTransitionContext.h in Headers */,
464052221A3F83C40061C0BA /* ASFlowLayoutController.h in Headers */,
257754AF1BEE44CD00737CA5 /* ASTextKitRenderer+TextChecking.h in Headers */,
058D0A57195D05DC00B7D73C /* ASHighlightOverlayLayer.h in Headers */,
@@ -1352,6 +1363,7 @@
430E7C8F1B4C23F100697A4C /* ASIndexPath.h in Headers */,
ACF6ED221B17843500DA7C62 /* ASInsetLayoutSpec.h in Headers */,
ACF6ED4B1B17847A00DA7C62 /* ASInternalHelpers.h in Headers */,
DB55C2661C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */,
ACF6ED241B17843500DA7C62 /* ASLayout.h in Headers */,
251B8EFB1BBB3D690087C538 /* ASDataController+Subclasses.h in Headers */,
ACF6ED2A1B17843500DA7C62 /* ASLayoutable.h in Headers */,
@@ -1501,6 +1513,7 @@
34EFC76E1B701CF400AD841F /* ASRatioLayoutSpec.h in Headers */,
34EFC7651B701CCC00AD841F /* ASRelativeSize.h in Headers */,
254C6B741BF94DF4003EC431 /* ASTextNodeWordKerner.h in Headers */,
DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */,
68B0277B1C1A79D60041016B /* ASDisplayNode+Beta.h in Headers */,
B350622D1B010EFD0018CF92 /* ASScrollDirection.h in Headers */,
254C6B751BF94DF4003EC431 /* ASTextKitHelpers.h in Headers */,
@@ -1775,6 +1788,7 @@
ACF6ED231B17843500DA7C62 /* ASInsetLayoutSpec.mm in Sources */,
ACF6ED4C1B17847A00DA7C62 /* ASInternalHelpers.mm in Sources */,
ACF6ED251B17843500DA7C62 /* ASLayout.mm in Sources */,
DB55C2631C6408D6004EDCF5 /* _ASTransitionContext.m in Sources */,
9C5FA3531B8F6ADF00A62714 /* ASLayoutOptions.mm in Sources */,
9C5FA35F1B90C9A500A62714 /* ASLayoutOptionsPrivate.mm in Sources */,
251B8EFA1BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m in Sources */,

View File

@@ -0,0 +1,33 @@
//
// ASContextTransitioning.h
// AsyncDisplayKit
//
// Created by Levi McCallum on 2/4/16.
// Copyright © 2016 Facebook. All rights reserved.
//
#import <AsyncDisplayKit/AsyncDisplayKit.h>
@protocol ASContextTransitioning <NSObject>
/**
@abstract The frame for the given node before the transition began.
@discussion Returns CGRectNull if the node was not in the hierarchy before the transition.
*/
- (CGRect)initialFrameForNode:(ASDisplayNode *)node;
/**
@abstract The frame for the given node when the transition completes.
@discussion Returns CGRectNull if the node is no longer in the hierarchy after the transition.
*/
- (CGRect)finalFrameForNode:(ASDisplayNode *)node;
- (NSArray<ASLayout *> *)sublayouts;
/**
@abstract Invoke this method when the transition is completed in `transitionLayout:`
@discussion Passing NO to `didComplete` will set the original layout as the new layout.
*/
- (void)completeTransition:(BOOL)didComplete;
@end

View File

@@ -14,6 +14,7 @@
#import <AsyncDisplayKit/ASThread.h>
@class ASLayoutSpec;
@protocol ASContextTransitioning;
NS_ASSUME_NONNULL_BEGIN
@@ -155,6 +156,24 @@ NS_ASSUME_NONNULL_BEGIN
- (void)invalidateCalculatedLayout;
/** @name Layout Transitioning */
/**
@discussion Called right before new nodes are inserted. A great place to setup layer attributes before animation.
*/
- (void)willTransitionLayout:(id<ASContextTransitioning>)context;
/**
@discussion A place to perform your animation. New nodes have been inserted here. You can also use this time to re-order the hierarchy.
*/
- (void)transitionLayout:(id<ASContextTransitioning>)context;
/**
@discussion A place to clean up your nodes after the transition
*/
- (void)didCompleteTransitionLayout:(id<ASContextTransitioning>)context;
/** @name Drawing */

View File

@@ -571,6 +571,17 @@ NS_ASSUME_NONNULL_BEGIN
@end
@interface ASDisplayNode (Transitioning)
/**
@abstract Invalidates the current layout and begins a relayout of the node to the new layout returned in `calculateLayoutThatFits:`.
@discussion Animation is optional, but will still proceed through the `transitionLayout` methods with `isAnimated == NO`.
*/
- (void)transitionLayoutWithAnimation:(BOOL)animated;
@end
/**
* Convenience methods for debugging.

View File

@@ -84,7 +84,7 @@ NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp = @"AS
#define TIME_SCOPED(outVar)
#endif
@interface ASDisplayNode () <_ASDisplayLayerDelegate>
@interface ASDisplayNode () <_ASDisplayLayerDelegate, _ASTransitionContextDelegate>
@property (assign, nonatomic) BOOL implicitNodeHierarchyManagement;
@@ -996,6 +996,119 @@ static inline CATransform3D _calculateTransformFromReferenceToTarget(ASDisplayNo
return CGRectApplyAffineTransform(rect, flattenedTransform);
}
#pragma mark - Layout Transition
- (void)transitionLayoutWithAnimation:(BOOL)animated
{
[self transitionLayoutToFit:_constrainedSize animated:animated];
}
- (void)transitionLayoutToFit:(ASSizeRange)constrainedSize animated:(BOOL)animated
{
[self invalidateCalculatedLayout];
[self measureWithSizeRange:constrainedSize]; // Generate a new layout
[self __transitionLayoutWithAnimation:animated];
}
- (void)__transitionLayoutWithAnimation:(BOOL)animated
{
_transitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated delegate:self];
[self willTransitionLayout:_transitionContext];
if ([_insertedSubnodes count]) {
for (_ASDisplayNodePosition *position in _insertedSubnodes) {
[self _implicitlyInsertSubnode:position.node atIndex:position.index];
}
_insertedSubnodes = @[];
}
[self transitionLayout:_transitionContext];
}
- (void)willTransitionLayout:(id<ASContextTransitioning>)context
{
}
- (void)transitionLayout:(id<ASContextTransitioning>)context
{
for (ASLayout *subnodeLayout in [context sublayouts]) {
((ASDisplayNode *)subnodeLayout.layoutableObject).frame = [self _adjustedFrameForLayout:subnodeLayout];
}
[context completeTransition:YES];
}
- (void)didCompleteTransitionLayout:(id<ASContextTransitioning>)context
{
if ([_deletedSubnodes count]) {
for (_ASDisplayNodePosition *position in _deletedSubnodes) {
[self _implicitlyRemoveSubnode:position.node atIndex:position.index];
}
_deletedSubnodes = @[];
}
}
- (CGRect)_adjustedFrameForLayout:(ASLayout *)layout
{
CGRect subnodeFrame = CGRectZero;
CGPoint adjustedOrigin = layout.position;
if (isfinite(adjustedOrigin.x) == NO) {
ASDisplayNodeAssert(0, @"subnodeLayout has an invalid position");
adjustedOrigin.x = 0;
}
if (isfinite(adjustedOrigin.y) == NO) {
ASDisplayNodeAssert(0, @"subnodeLayout has an invalid position");
adjustedOrigin.y = 0;
}
subnodeFrame.origin = adjustedOrigin;
CGSize adjustedSize = layout.size;
if (isfinite(adjustedSize.width) == NO) {
ASDisplayNodeAssert(0, @"subnodeLayout has an invalid size");
adjustedSize.width = 0;
}
if (isfinite(adjustedSize.height) == NO) {
ASDisplayNodeAssert(0, @"subnodeLayout has an invalid position");
adjustedSize.height = 0;
}
subnodeFrame.size = adjustedSize;
return subnodeFrame;
}
#pragma mark - _ASTransitionContextDelegate
- (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete
{
[self didCompleteTransitionLayout:context];
_transitionContext = nil;
}
- (CGRect)transitionContext:(_ASTransitionContext *)context initialFrameForNode:(ASDisplayNode *)node
{
for (ASDisplayNode *subnode in _subnodes) {
if (ASObjectIsEqual(node, subnode)) {
return node.frame;
}
}
return CGRectNull;
}
- (CGRect)transitionContext:(_ASTransitionContext *)context finalFrameForNode:(ASDisplayNode *)node
{
for (ASLayout *layout in _layout.sublayouts) {
if (ASObjectIsEqual(node, layout.layoutableObject)) {
return [self _adjustedFrameForLayout:layout];
}
}
return CGRectNull;
}
- (NSArray<ASLayout *> *)sublayoutsForTransitioningContext:(_ASTransitionContext *)context
{
return _layout.sublayouts;
}
#pragma mark - _ASDisplayLayerDelegate
- (void)willDisplayAsyncLayer:(_ASDisplayLayer *)layer
@@ -2053,77 +2166,27 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
if (!_flags.isMeasured) {
return;
}
// Assume that _layout was flattened and is 1-level deep.
ASDisplayNode *subnode = nil;
CGRect subnodeFrame = CGRectZero;
for (ASLayout *subnodeLayout in _layout.sublayouts) {
if (![[self class] usesImplicitHierarchyManagement]) {
ASDisplayNodeAssert([_subnodes containsObject:subnodeLayout.layoutableObject], @"Sublayouts must only contain subnodes' layout. self = %@, subnodes = %@", self, _subnodes);
}
CGPoint adjustedOrigin = subnodeLayout.position;
if (isfinite(adjustedOrigin.x) == NO) {
ASDisplayNodeAssert(0, @"subnodeLayout has an invalid position");
adjustedOrigin.x = 0;
}
if (isfinite(adjustedOrigin.y) == NO) {
ASDisplayNodeAssert(0, @"subnodeLayout has an invalid position");
adjustedOrigin.y = 0;
}
subnodeFrame.origin = adjustedOrigin;
CGSize adjustedSize = subnodeLayout.size;
if (isfinite(adjustedSize.width) == NO) {
ASDisplayNodeAssert(0, @"subnodeLayout has an invalid size");
adjustedSize.width = 0;
}
if (isfinite(adjustedSize.height) == NO) {
ASDisplayNodeAssert(0, @"subnodeLayout has an invalid position");
adjustedSize.height = 0;
}
subnodeFrame.size = adjustedSize;
subnode = ((ASDisplayNode *)subnodeLayout.layoutableObject);
[subnode setFrame:subnodeFrame];
}
if ([[self class] usesImplicitHierarchyManagement]) {
for (_ASDisplayNodePosition *position in _deletedSubnodes) {
[self _implicitlyRemoveSubnode:position.node atIndex:position.index];
}
for (_ASDisplayNodePosition *position in _insertedSubnodes) {
[self _implicitlyInsertSubnode:position.node atIndex:position.index];
[self __transitionLayoutWithAnimation:NO];
} else {
// Assume that _layout was flattened and is 1-level deep.
CGRect subnodeFrame = CGRectZero;
for (ASLayout *subnodeLayout in _layout.sublayouts) {
ASDisplayNodeAssert([_subnodes containsObject:subnodeLayout.layoutableObject], @"Sublayouts must only contain subnodes' layout. self = %@, subnodes = %@", self, _subnodes);
subnodeFrame = [self _adjustedFrameForLayout:subnodeLayout];
((ASDisplayNode *)subnodeLayout.layoutableObject).frame = subnodeFrame;
}
}
}
- (void)_implicitlyInsertSubnode:(ASDisplayNode *)node atIndex:(NSUInteger)idx
{
ASDisplayNodeAssertThreadAffinity(self);
if (!_managedSubnodes) {
_managedSubnodes = [NSMutableArray array];
}
ASDisplayNodeAssert(idx <= [_managedSubnodes count], @"index needs to be in range of the current managed subnodes");
if (idx == [_managedSubnodes count]) {
[_managedSubnodes addObject:node];
} else {
[_managedSubnodes insertObject:node atIndex:idx];
}
[self addSubnode:node];
[self insertSubnode:node atIndex:idx];
}
- (void)_implicitlyRemoveSubnode:(ASDisplayNode *)node atIndex:(NSUInteger)idx
{
ASDisplayNodeAssertThreadAffinity(self);
if (!_managedSubnodes) {
_managedSubnodes = [NSMutableArray array];
}
[_managedSubnodes removeObjectAtIndex:idx];
[node removeFromSupernode];
}

View File

@@ -68,3 +68,4 @@
#import <AsyncDisplayKit/NSMutableAttributedString+TextKitAdditions.h>
#import <AsyncDisplayKit/UICollectionViewLayout+ASConvenience.h>
#import <AsyncDisplayKit/UIView+ASConvenience.h>
#import <AsyncDisplayKit/ASContextTransitioning.h>

View File

@@ -17,6 +17,7 @@
#import "ASSentinel.h"
#import "ASThread.h"
#import "ASLayoutOptions.h"
#import "_ASTransitionContext.h"
@protocol _ASDisplayLayerDelegate;
@class _ASDisplayLayer;
@@ -63,10 +64,8 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
ASSizeRange _constrainedSize;
UIEdgeInsets _hitTestSlop;
NSMutableArray *_subnodes;
// Subnodes implicitly managed by layout changes
NSMutableArray<ASDisplayNode *> *_managedSubnodes;
_ASTransitionContext *_transitionContext;
NSArray<_ASDisplayNodePosition *> *_insertedSubnodes;
NSArray<_ASDisplayNodePosition *> *_deletedSubnodes;
@@ -149,6 +148,11 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
- (void)__layout;
- (void)__setSupernode:(ASDisplayNode *)supernode;
/**
Clamps the layout's origin or position to 0 if any of the calculated values are infinite.
*/
- (CGRect)_adjustedFrameForLayout:(ASLayout *)layout;
// Private API for helper functions / unit tests. Use ASDisplayNodeDisableHierarchyNotifications() to control this.
- (BOOL)__visibilityNotificationsDisabled;
- (BOOL)__selfOrParentHasVisibilityNotificationsDisabled;

View File

@@ -0,0 +1,32 @@
//
// _ASTransitionContext.h
// AsyncDisplayKit
//
// Created by Levi McCallum on 2/4/16.
// Copyright © 2016 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "ASContextTransitioning.h"
@class ASLayout;
@class _ASTransitionContext;
@protocol _ASTransitionContextDelegate <NSObject>
- (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete;
- (CGRect)transitionContext:(_ASTransitionContext *)context initialFrameForNode:(ASDisplayNode *)node;
- (CGRect)transitionContext:(_ASTransitionContext *)context finalFrameForNode:(ASDisplayNode *)node;
- (NSArray<ASLayout *> *)sublayoutsForTransitioningContext:(_ASTransitionContext *)context;
@end
@interface _ASTransitionContext : NSObject <ASContextTransitioning>
@property (assign, readonly, nonatomic, getter=isAnimated) BOOL animated;
- (instancetype)initWithAnimation:(BOOL)animated delegate:(id<_ASTransitionContextDelegate>)delegate;
@end

View File

@@ -0,0 +1,49 @@
//
// _ASTransitionContext.m
// AsyncDisplayKit
//
// Created by Levi McCallum on 2/4/16.
// Copyright © 2016 Facebook. All rights reserved.
//
#import "_ASTransitionContext.h"
@interface _ASTransitionContext ()
@property (weak, nonatomic) id<_ASTransitionContextDelegate> delegate;
@end
@implementation _ASTransitionContext
- (instancetype)initWithAnimation:(BOOL)animated delegate:(id<_ASTransitionContextDelegate>)delegate
{
self = [super init];
if (self) {
_animated = animated;
_delegate = delegate;
}
return self;
}
- (CGRect)initialFrameForNode:(ASDisplayNode *)node
{
return [_delegate transitionContext:self initialFrameForNode:node];
}
- (CGRect)finalFrameForNode:(ASDisplayNode *)node
{
return [_delegate transitionContext:self finalFrameForNode:node];
}
- (NSArray<ASLayout *> *)sublayouts
{
return [_delegate sublayoutsForTransitioningContext:self];
}
- (void)completeTransition:(BOOL)didComplete
{
[_delegate transitionContext:self didComplete:didComplete];
}
@end