From 9c82ae92842ca1559706da071bcce186d03cca96 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 24 Feb 2017 16:38:17 -0800 Subject: [PATCH] Add ASRectTable (#3077) --- AsyncDisplayKit.xcodeproj/project.pbxproj | 12 ++++ AsyncDisplayKit/ASDisplayNode.mm | 12 +++- AsyncDisplayKit/Layout/ASLayout.h | 6 ++ AsyncDisplayKit/Layout/ASLayout.mm | 14 ++++ AsyncDisplayKit/Private/ASRectTable.h | 64 +++++++++++++++++ AsyncDisplayKit/Private/ASRectTable.m | 71 +++++++++++++++++++ AsyncDisplayKit/_ASTransitionContext.m | 14 +--- AsyncDisplayKitTests/ASRectTableTests.m | 51 +++++++++++++ .../Sample/ViewController.m | 1 + 9 files changed, 231 insertions(+), 14 deletions(-) create mode 100644 AsyncDisplayKit/Private/ASRectTable.h create mode 100644 AsyncDisplayKit/Private/ASRectTable.m create mode 100644 AsyncDisplayKitTests/ASRectTableTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 4f6b22b10f..fcf1c500b0 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -303,6 +303,9 @@ C78F7E2B1BF7809800CDEAFC /* ASTableNode.h in Headers */ = {isa = PBXBuildFile; fileRef = B0F880581BEAEC7500D17647 /* ASTableNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC034A011E5FAF9700626263 /* ASElementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = CC0349FF1E5FAF9700626263 /* ASElementMap.h */; }; CC034A021E5FAF9700626263 /* ASElementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = CC034A001E5FAF9700626263 /* ASElementMap.m */; }; + CC034A0D1E60C3D500626263 /* ASRectTable.h in Headers */ = {isa = PBXBuildFile; fileRef = CC034A0B1E60C3D500626263 /* ASRectTable.h */; }; + CC034A0E1E60C3D500626263 /* ASRectTable.m in Sources */ = {isa = PBXBuildFile; fileRef = CC034A0C1E60C3D500626263 /* ASRectTable.m */; }; + CC034A101E60C9BF00626263 /* ASRectTableTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */; }; CC051F1F1D7A286A006434CB /* ASCALayerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC051F1E1D7A286A006434CB /* ASCALayerTests.m */; }; CC0AEEA41D66316E005D1C78 /* ASUICollectionViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC0AEEA31D66316E005D1C78 /* ASUICollectionViewTests.m */; }; CC0F885B1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.m in Sources */ = {isa = PBXBuildFile; fileRef = CC0F88591E42807F00576FED /* ASCollectionViewFlowLayoutInspector.m */; }; @@ -690,6 +693,9 @@ BDC2D162BD55A807C1475DA5 /* Pods-AsyncDisplayKitTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.profile.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.profile.xcconfig"; sourceTree = ""; }; CC0349FF1E5FAF9700626263 /* ASElementMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASElementMap.h; sourceTree = ""; }; CC034A001E5FAF9700626263 /* ASElementMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASElementMap.m; sourceTree = ""; }; + CC034A0B1E60C3D500626263 /* ASRectTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRectTable.h; sourceTree = ""; }; + CC034A0C1E60C3D500626263 /* ASRectTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASRectTable.m; sourceTree = ""; }; + CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASRectTableTests.m; sourceTree = ""; }; CC051F1E1D7A286A006434CB /* ASCALayerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCALayerTests.m; sourceTree = ""; }; CC0AEEA31D66316E005D1C78 /* ASUICollectionViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASUICollectionViewTests.m; sourceTree = ""; }; CC0F88591E42807F00576FED /* ASCollectionViewFlowLayoutInspector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewFlowLayoutInspector.m; sourceTree = ""; }; @@ -963,6 +969,7 @@ 058D09C5195D04C000B7D73C /* Tests */ = { isa = PBXGroup; children = ( + CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */, CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */, CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */, @@ -1136,6 +1143,8 @@ CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */, CC0349FF1E5FAF9700626263 /* ASElementMap.h */, CC034A001E5FAF9700626263 /* ASElementMap.m */, + CC034A0B1E60C3D500626263 /* ASRectTable.h */, + CC034A0C1E60C3D500626263 /* ASRectTable.m */, CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */, CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */, 6947B0BB1E36B4E30007C478 /* Layout */, @@ -1411,6 +1420,7 @@ B35062591B010F070018CF92 /* ASBaseDefines.h in Headers */, B35062131B010EFD0018CF92 /* ASBasicImageDownloader.h in Headers */, B35062151B010EFD0018CF92 /* ASBatchContext.h in Headers */, + CC034A0D1E60C3D500626263 /* ASRectTable.h in Headers */, B35061F31B010EFD0018CF92 /* ASCellNode.h in Headers */, 34EFC7631B701CBF00AD841F /* ASCenterLayoutSpec.h in Headers */, CC55A7111E52A0F200594372 /* ASResponderChainEnumerator.h in Headers */, @@ -1754,6 +1764,7 @@ 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.mm in Sources */, 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */, CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.m in Sources */, + CC034A101E60C9BF00626263 /* ASRectTableTests.m in Sources */, F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */, ACF6ED5D1B178DC700DA7C62 /* ASDimensionTests.mm in Sources */, 695BE2551DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm in Sources */, @@ -1860,6 +1871,7 @@ B35062501B010EFD0018CF92 /* ASDisplayNode+DebugTiming.mm in Sources */, DEC146B91C37A16A004A0EE7 /* ASCollectionInternal.m in Sources */, 254C6B891BF94F8A003EC431 /* ASTextKitRenderer+Positioning.mm in Sources */, + CC034A0E1E60C3D500626263 /* ASRectTable.m in Sources */, 68355B341CB579B9001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */, E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */, B35062511B010EFD0018CF92 /* ASDisplayNode+UIViewBridge.mm in Sources */, diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index d2864ff5a5..2f0a677119 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1336,8 +1336,16 @@ ASLayoutElementFinalLayoutElementDefault - (void)_locked_layoutSublayouts { - for (ASLayout *subnodeLayout in _calculatedDisplayNodeLayout->layout.sublayouts) { - ((ASDisplayNode *)subnodeLayout.layoutElement).frame = subnodeLayout.frame; + ASLayout *layout = _calculatedDisplayNodeLayout->layout; + for (ASDisplayNode *node in _subnodes) { + CGRect frame = [layout frameForElement:node]; + if (CGRectIsNull(frame)) { + // There is no frame for this node in our layout. + // This currently can happen if we get a CA layout pass + // while waiting for the client to run animateLayoutTransition: + } else { + node.frame = frame; + } } } diff --git a/AsyncDisplayKit/Layout/ASLayout.h b/AsyncDisplayKit/Layout/ASLayout.h index b41f1259db..b592ddb13b 100644 --- a/AsyncDisplayKit/Layout/ASLayout.h +++ b/AsyncDisplayKit/Layout/ASLayout.h @@ -70,6 +70,12 @@ ASDISPLAYNODE_EXTERN_C_END */ @property (nonatomic, copy, readonly) NSArray *sublayouts; +/** + * The frame for the given element, or CGRectNull if + * the element is not a direct descendent of this layout. + */ +- (CGRect)frameForElement:(id)layoutElement; + /** * @abstract Returns a valid frame for the current layout computed with the size and position. * @discussion Clamps the layout's origin or position to 0 if any of the calculated values are infinite. diff --git a/AsyncDisplayKit/Layout/ASLayout.mm b/AsyncDisplayKit/Layout/ASLayout.mm index f01f03592f..aaa569ee4a 100644 --- a/AsyncDisplayKit/Layout/ASLayout.mm +++ b/AsyncDisplayKit/Layout/ASLayout.mm @@ -18,6 +18,7 @@ #import #import +#import CGPoint const CGPointNull = {NAN, NAN}; @@ -60,6 +61,8 @@ static inline NSString * descriptionIndents(NSUInteger indents) */ @property (nonatomic, strong) NSMutableArray> *sublayoutLayoutElements; +@property (nonatomic, strong, readonly) ASRectTable, id> *elementToRectTable; + @end @implementation ASLayout @@ -101,6 +104,12 @@ static inline NSString * descriptionIndents(NSUInteger indents) } _sublayouts = sublayouts != nil ? [sublayouts copy] : @[]; + + _elementToRectTable = [ASRectTable rectTableForWeakObjectPointers]; + for (ASLayout *layout in sublayouts) { + [_elementToRectTable setRect:layout.frame forKey:layout.layoutElement]; + } + _flattened = NO; _retainSublayoutLayoutElements = NO; } @@ -221,6 +230,11 @@ static inline NSString * descriptionIndents(NSUInteger indents) return _layoutElementType; } +- (CGRect)frameForElement:(id)layoutElement +{ + return [_elementToRectTable rectForKey:layoutElement]; +} + - (CGRect)frame { CGRect subnodeFrame = CGRectZero; diff --git a/AsyncDisplayKit/Private/ASRectTable.h b/AsyncDisplayKit/Private/ASRectTable.h new file mode 100644 index 0000000000..3617f44b17 --- /dev/null +++ b/AsyncDisplayKit/Private/ASRectTable.h @@ -0,0 +1,64 @@ +// +// ASRectTable.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 2/24/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * An alias for an NSMapTable created to store rects. + * + * You should not call -objectForKey:, -setObject:forKey:, or -allObjects + * on these objects. + */ +typedef NSMapTable ASRectTable; + +/** + * A category for creating & using map tables meant for storing CGRects. + * + * This category is private, so name collisions are not worth worrying about. + */ +@interface NSMapTable (ASRectTableMethods) + +/** + * Creates a new rect table with (NSMapTableStrongMemory | NSMapTableObjectPointerPersonality) for keys. + */ ++ (ASRectTable *)rectTableForStrongObjectPointers; + +/** + * Creates a new rect table with (NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) for keys. + */ ++ (ASRectTable *)rectTableForWeakObjectPointers; + +/** + * Retrieves the rect for a given key, or CGRectNull if the key is not found. + * + * @param key An object to lookup the rect for. + */ +- (CGRect)rectForKey:(KeyType)key; + +/** + * Sets the given rect for the associated key. + * + * @param rect The rect to store as value. + * @param key The key to use for the rect. + */ +- (void)setRect:(CGRect)rect forKey:(KeyType)key; + +/** + * Removes the rect for the given key, if one exists. + * + * @param key The key to remove. + */ +- (void)removeRectForKey:(KeyType)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/Private/ASRectTable.m b/AsyncDisplayKit/Private/ASRectTable.m new file mode 100644 index 0000000000..1bd32fb53f --- /dev/null +++ b/AsyncDisplayKit/Private/ASRectTable.m @@ -0,0 +1,71 @@ +// +// ASRectTable.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 2/24/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import "ASRectTable.h" + +__attribute__((const)) +static NSUInteger ASRectSize(const void *ptr) +{ + return sizeof(CGRect); +} + +@implementation NSMapTable (ASRectTableMethods) + ++ (instancetype)rectTableWithKeyPointerFunctions:(NSPointerFunctions *)keyFuncs +{ + static NSPointerFunctions *cgRectFuncs; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cgRectFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStructPersonality | NSPointerFunctionsCopyIn | NSPointerFunctionsMallocMemory]; + cgRectFuncs.sizeFunction = &ASRectSize; + }); + + return [[NSMapTable alloc] initWithKeyPointerFunctions:keyFuncs valuePointerFunctions:cgRectFuncs capacity:0]; +} + ++ (instancetype)rectTableForStrongObjectPointers +{ + static NSPointerFunctions *strongObjectPointerFuncs; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + strongObjectPointerFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality]; + }); + return [self rectTableWithKeyPointerFunctions:strongObjectPointerFuncs]; +} + ++ (instancetype)rectTableForWeakObjectPointers +{ + static NSPointerFunctions *weakObjectPointerFuncs; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + weakObjectPointerFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality]; + }); + return [self rectTableWithKeyPointerFunctions:weakObjectPointerFuncs]; +} + +- (CGRect)rectForKey:(id)key +{ + CGRect *ptr = (__bridge CGRect *)[self objectForKey:key]; + if (ptr == NULL) { + return CGRectNull; + } + return *ptr; +} + +- (void)setRect:(CGRect)rect forKey:(id)key +{ + __unsafe_unretained id obj = (__bridge id)▭ + [self setObject:obj forKey:key]; +} + +- (void)removeRectForKey:(id)key +{ + [self removeObjectForKey:key]; +} + +@end diff --git a/AsyncDisplayKit/_ASTransitionContext.m b/AsyncDisplayKit/_ASTransitionContext.m index 695fba8c1d..fac52d28e0 100644 --- a/AsyncDisplayKit/_ASTransitionContext.m +++ b/AsyncDisplayKit/_ASTransitionContext.m @@ -54,22 +54,12 @@ NSString * const ASTransitionContextToLayoutKey = @"org.asyncdisplaykit.ASTransi - (CGRect)initialFrameForNode:(ASDisplayNode *)node { - for (ASDisplayNode *subnode in [_layoutDelegate currentSubnodesWithTransitionContext:self]) { - if (node == subnode) { - return node.frame; - } - } - return CGRectZero; + return [[self layoutForKey:ASTransitionContextFromLayoutKey] frameForElement:node]; } - (CGRect)finalFrameForNode:(ASDisplayNode *)node { - for (ASLayout *layout in [self layoutForKey:ASTransitionContextToLayoutKey].sublayouts) { - if (layout.layoutElement == node) { - return [layout frame]; - } - } - return CGRectZero; + return [[self layoutForKey:ASTransitionContextToLayoutKey] frameForElement:node]; } - (NSArray *)subnodesForKey:(NSString *)key diff --git a/AsyncDisplayKitTests/ASRectTableTests.m b/AsyncDisplayKitTests/ASRectTableTests.m new file mode 100644 index 0000000000..55c1dbab15 --- /dev/null +++ b/AsyncDisplayKitTests/ASRectTableTests.m @@ -0,0 +1,51 @@ +// +// ASRectTableTests.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 2/24/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import + +#import "ASRectTable.h" +#import "ASXCTExtensions.h" + +@interface ASRectTableTests : XCTestCase +@end + +@implementation ASRectTableTests + +- (void)testThatItStoresRects +{ + ASRectTable *table = [ASRectTable rectTableForWeakObjectPointers]; + NSObject *key0 = [[NSObject alloc] init]; + NSObject *key1 = [[NSObject alloc] init]; + ASXCTAssertEqualRects([table rectForKey:key0], CGRectNull); + ASXCTAssertEqualRects([table rectForKey:key1], CGRectNull); + CGRect rect0 = CGRectMake(0, 0, 100, 100); + CGRect rect1 = CGRectMake(0, 0, 50, 50); + [table setRect:rect0 forKey:key0]; + [table setRect:rect1 forKey:key1]; + + ASXCTAssertEqualRects([table rectForKey:key0], rect0); + ASXCTAssertEqualRects([table rectForKey:key1], rect1); +} + + +- (void)testCopying +{ + ASRectTable *table = [ASRectTable rectTableForWeakObjectPointers]; + NSObject *key = [[NSObject alloc] init]; + ASXCTAssertEqualRects([table rectForKey:key], CGRectNull); + CGRect rect0 = CGRectMake(0, 0, 100, 100); + CGRect rect1 = CGRectMake(0, 0, 50, 50); + [table setRect:rect0 forKey:key]; + ASRectTable *copy = [table copy]; + [copy setRect:rect1 forKey:key]; + + ASXCTAssertEqualRects([table rectForKey:key], rect0); + ASXCTAssertEqualRects([copy rectForKey:key], rect1); +} + +@end diff --git a/examples/ASDKLayoutTransition/Sample/ViewController.m b/examples/ASDKLayoutTransition/Sample/ViewController.m index 71f025cd69..dd8d375d13 100644 --- a/examples/ASDKLayoutTransition/Sample/ViewController.m +++ b/examples/ASDKLayoutTransition/Sample/ViewController.m @@ -53,6 +53,7 @@ _textNodeTwo = [[ASTextNode alloc] init]; _textNodeTwo.attributedText = [[NSAttributedString alloc] initWithString:@"It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English."]; + ASSetDebugNames(_textNodeOne, _textNodeTwo); // Setup button NSString *buttonTitle = @"Start Layout Transition";