mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 06:35:51 +00:00
[ASCollectionView] Relayout Nodes as Soon as Bounds Changes (#2121)
* Add failing test case for ASCollectionView rotation * [ASCollectionView] Relayout nodes immediately on bounds change
This commit is contained in:
@@ -86,7 +86,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
|
|||||||
#pragma mark -
|
#pragma mark -
|
||||||
#pragma mark ASCollectionView.
|
#pragma mark ASCollectionView.
|
||||||
|
|
||||||
@interface ASCollectionView () <ASRangeControllerDataSource, ASRangeControllerDelegate, ASCollectionDataControllerSource, ASCellNodeInteractionDelegate, ASDelegateProxyInterceptor, ASBatchFetchingScrollView, ASDataControllerEnvironmentDelegate> {
|
@interface ASCollectionView () <ASRangeControllerDataSource, ASRangeControllerDelegate, ASCollectionDataControllerSource, ASCellNodeInteractionDelegate, ASDelegateProxyInterceptor, ASBatchFetchingScrollView, ASDataControllerEnvironmentDelegate, ASCALayerExtendedDelegate> {
|
||||||
ASCollectionViewProxy *_proxyDataSource;
|
ASCollectionViewProxy *_proxyDataSource;
|
||||||
ASCollectionViewProxy *_proxyDelegate;
|
ASCollectionViewProxy *_proxyDelegate;
|
||||||
|
|
||||||
@@ -106,8 +106,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
|
|||||||
|
|
||||||
ASBatchContext *_batchContext;
|
ASBatchContext *_batchContext;
|
||||||
|
|
||||||
CGSize _maxSizeForNodesConstrainedSize;
|
CGSize _lastBoundsSizeUsedForMeasuringNodes;
|
||||||
BOOL _ignoreMaxSizeChange;
|
BOOL _ignoreNextBoundsSizeChangeForMeasuringNodes;
|
||||||
|
|
||||||
NSMutableSet *_registeredSupplementaryKinds;
|
NSMutableSet *_registeredSupplementaryKinds;
|
||||||
|
|
||||||
@@ -242,10 +242,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
|
|||||||
|
|
||||||
_superIsPendingDataLoad = YES;
|
_superIsPendingDataLoad = YES;
|
||||||
|
|
||||||
_maxSizeForNodesConstrainedSize = self.bounds.size;
|
_lastBoundsSizeUsedForMeasuringNodes = self.bounds.size;
|
||||||
// If the initial size is 0, expect a size change very soon which is part of the initial configuration
|
// If the initial size is 0, expect a size change very soon which is part of the initial configuration
|
||||||
// and should not trigger a relayout.
|
// and should not trigger a relayout.
|
||||||
_ignoreMaxSizeChange = CGSizeEqualToSize(_maxSizeForNodesConstrainedSize, CGSizeZero);
|
_ignoreNextBoundsSizeChangeForMeasuringNodes = CGSizeEqualToSize(_lastBoundsSizeUsedForMeasuringNodes, CGSizeZero);
|
||||||
|
|
||||||
_layoutFacilitator = layoutFacilitator;
|
_layoutFacilitator = layoutFacilitator;
|
||||||
|
|
||||||
@@ -843,26 +843,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
|
|||||||
self.contentInset = UIEdgeInsetsZero;
|
self.contentInset = UIEdgeInsetsZero;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! CGSizeEqualToSize(_maxSizeForNodesConstrainedSize, self.bounds.size)) {
|
|
||||||
_maxSizeForNodesConstrainedSize = self.bounds.size;
|
|
||||||
|
|
||||||
// First size change occurs during initial configuration. An expensive relayout pass is unnecessary at that time
|
|
||||||
// and should be avoided, assuming that the initial data loading automatically runs shortly afterward.
|
|
||||||
if (_ignoreMaxSizeChange) {
|
|
||||||
_ignoreMaxSizeChange = NO;
|
|
||||||
} else {
|
|
||||||
// This actually doesn't perform an animation, but prevents the transaction block from being processed in the
|
|
||||||
// data controller's prevent animation block that would interrupt an interrupted relayout happening in an animation block
|
|
||||||
// ie. ASCollectionView bounds change on rotation or multi-tasking split view resize.
|
|
||||||
[self performBatchAnimated:YES updates:^{
|
|
||||||
[_dataController relayoutAllNodes];
|
|
||||||
} completion:nil];
|
|
||||||
// We need to ensure the size requery is done before we update our layout.
|
|
||||||
[self waitUntilAllUpdatesAreCommitted];
|
|
||||||
[self.collectionViewLayout invalidateLayout];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush any pending invalidation action if needed.
|
// Flush any pending invalidation action if needed.
|
||||||
ASCollectionViewInvalidationStyle invalidationStyle = _nextLayoutInvalidationStyle;
|
ASCollectionViewInvalidationStyle invalidationStyle = _nextLayoutInvalidationStyle;
|
||||||
_nextLayoutInvalidationStyle = ASCollectionViewInvalidationStyleNone;
|
_nextLayoutInvalidationStyle = ASCollectionViewInvalidationStyleNone;
|
||||||
@@ -1309,6 +1289,40 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark ASCALayerExtendedDelegate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UICollectionView inadvertently triggers a -prepareLayout call to its layout object
|
||||||
|
* between [super setFrame:] and [self layoutSubviews] during size changes. So we need
|
||||||
|
* to get in there and re-measure our nodes before that -prepareLayout call.
|
||||||
|
* We can't wait until -layoutSubviews or the end of -setFrame:.
|
||||||
|
*
|
||||||
|
* @see @p testThatNodeCalculatedSizesAreUpdatedBeforeFirstPrepareLayoutAfterRotation
|
||||||
|
*/
|
||||||
|
- (void)layer:(CALayer *)layer didChangeBoundsWithOldValue:(CGRect)oldBounds newValue:(CGRect)newBounds
|
||||||
|
{
|
||||||
|
if (CGSizeEqualToSize(_lastBoundsSizeUsedForMeasuringNodes, newBounds.size)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastBoundsSizeUsedForMeasuringNodes = newBounds.size;
|
||||||
|
|
||||||
|
// First size change occurs during initial configuration. An expensive relayout pass is unnecessary at that time
|
||||||
|
// and should be avoided, assuming that the initial data loading automatically runs shortly afterward.
|
||||||
|
if (_ignoreNextBoundsSizeChangeForMeasuringNodes) {
|
||||||
|
_ignoreNextBoundsSizeChangeForMeasuringNodes = NO;
|
||||||
|
} else {
|
||||||
|
// This actually doesn't perform an animation, but prevents the transaction block from being processed in the
|
||||||
|
// data controller's prevent animation block that would interrupt an interrupted relayout happening in an animation block
|
||||||
|
// ie. ASCollectionView bounds change on rotation or multi-tasking split view resize.
|
||||||
|
[self performBatchAnimated:YES updates:^{
|
||||||
|
[_dataController relayoutAllNodes];
|
||||||
|
} completion:nil];
|
||||||
|
// We need to ensure the size requery is done before we update our layout.
|
||||||
|
[self waitUntilAllUpdatesAreCommitted];
|
||||||
|
[self.collectionViewLayout invalidateLayout];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - UICollectionView dead-end intercepts
|
#pragma mark - UICollectionView dead-end intercepts
|
||||||
|
|
||||||
#if ASDISPLAYNODE_ASSERTIONS_ENABLED // Remove implementations entirely for efficiency if not asserting.
|
#if ASDISPLAYNODE_ASSERTIONS_ENABLED // Remove implementations entirely for efficiency if not asserting.
|
||||||
|
|||||||
@@ -69,6 +69,20 @@ typedef BOOL(^asdisplaynode_iscancelled_block_t)(void);
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional methods that the view associated with an _ASDisplayLayer can implement.
|
||||||
|
* This is distinguished from _ASDisplayLayerDelegate in that it points to the _view_
|
||||||
|
* not the node. Unfortunately this is required by ASCollectionView, since we currently
|
||||||
|
* can't guarantee that an ASCollectionNode exists for it.
|
||||||
|
*/
|
||||||
|
@protocol ASCALayerExtendedDelegate
|
||||||
|
|
||||||
|
@optional
|
||||||
|
|
||||||
|
- (void)layer:(CALayer *)layer didChangeBoundsWithOldValue:(CGRect)oldBounds newValue:(CGRect)newBounds;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Implement one of +displayAsyncLayer:parameters:isCancelled: or +drawRect:withParameters:isCancelled: to provide drawing for your node.
|
Implement one of +displayAsyncLayer:parameters:isCancelled: or +drawRect:withParameters:isCancelled: to provide drawing for your node.
|
||||||
Use -drawParametersForAsyncLayer: to copy any properties that are involved in drawing into an immutable object for use on the display queue.
|
Use -drawParametersForAsyncLayer: to copy any properties that are involved in drawing into an immutable object for use on the display queue.
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
ASDN::RecursiveMutex _displaySuspendedLock;
|
ASDN::RecursiveMutex _displaySuspendedLock;
|
||||||
BOOL _displaySuspended;
|
BOOL _displaySuspended;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
BOOL delegateDidChangeBounds:1;
|
||||||
|
} _delegateFlags;
|
||||||
|
|
||||||
id<_ASDisplayLayerDelegate> __weak _asyncDelegate;
|
id<_ASDisplayLayerDelegate> __weak _asyncDelegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +56,12 @@
|
|||||||
return _asyncDelegate;
|
return _asyncDelegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)setDelegate:(id)delegate
|
||||||
|
{
|
||||||
|
[super setDelegate:delegate];
|
||||||
|
_delegateFlags.delegateDidChangeBounds = [delegate respondsToSelector:@selector(layer:didChangeBoundsWithOldValue:newValue:)];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)setAsyncDelegate:(id<_ASDisplayLayerDelegate>)asyncDelegate
|
- (void)setAsyncDelegate:(id<_ASDisplayLayerDelegate>)asyncDelegate
|
||||||
{
|
{
|
||||||
ASDisplayNodeAssert(!asyncDelegate || [asyncDelegate isKindOfClass:[ASDisplayNode class]], @"_ASDisplayLayer is inherently coupled to ASDisplayNode and cannot be used with another asyncDelegate. Please rethink what you are trying to do.");
|
ASDisplayNodeAssert(!asyncDelegate || [asyncDelegate isKindOfClass:[ASDisplayNode class]], @"_ASDisplayLayer is inherently coupled to ASDisplayNode and cannot be used with another asyncDelegate. Please rethink what you are trying to do.");
|
||||||
@@ -82,8 +92,15 @@
|
|||||||
|
|
||||||
- (void)setBounds:(CGRect)bounds
|
- (void)setBounds:(CGRect)bounds
|
||||||
{
|
{
|
||||||
|
if (_delegateFlags.delegateDidChangeBounds) {
|
||||||
|
CGRect oldBounds = self.bounds;
|
||||||
[super setBounds:bounds];
|
[super setBounds:bounds];
|
||||||
self.asyncdisplaykit_node.threadSafeBounds = bounds;
|
self.asyncdisplaykit_node.threadSafeBounds = bounds;
|
||||||
|
[self.delegate layer:self didChangeBoundsWithOldValue:oldBounds newValue:bounds];
|
||||||
|
} else {
|
||||||
|
[super setBounds:bounds];
|
||||||
|
self.asyncdisplaykit_node.threadSafeBounds = bounds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG // These override is strictly to help detect application-level threading errors. Avoid method overhead in release.
|
#if DEBUG // These override is strictly to help detect application-level threading errors. Avoid method overhead in release.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#import "ASCollectionNode.h"
|
#import "ASCollectionNode.h"
|
||||||
#import "ASDisplayNode+Beta.h"
|
#import "ASDisplayNode+Beta.h"
|
||||||
#import <vector>
|
#import <vector>
|
||||||
|
#import <OCMock/OCMock.h>
|
||||||
|
|
||||||
@interface ASTextCellNodeWithSetSelectedCounter : ASTextCellNode
|
@interface ASTextCellNodeWithSetSelectedCounter : ASTextCellNode
|
||||||
|
|
||||||
@@ -92,9 +93,10 @@
|
|||||||
if (self) {
|
if (self) {
|
||||||
// Populate these immediately so that they're not unexpectedly nil during tests.
|
// Populate these immediately so that they're not unexpectedly nil during tests.
|
||||||
self.asyncDelegate = [[ASCollectionViewTestDelegate alloc] initWithNumberOfSections:10 numberOfItemsInSection:10];
|
self.asyncDelegate = [[ASCollectionViewTestDelegate alloc] initWithNumberOfSections:10 numberOfItemsInSection:10];
|
||||||
|
id realLayout = [UICollectionViewFlowLayout new];
|
||||||
|
id mockLayout = [OCMockObject partialMockForObject:realLayout];
|
||||||
self.collectionView = [[ASCollectionView alloc] initWithFrame:self.view.bounds
|
self.collectionView = [[ASCollectionView alloc] initWithFrame:self.view.bounds
|
||||||
collectionViewLayout:[UICollectionViewFlowLayout new]];
|
collectionViewLayout:mockLayout];
|
||||||
self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||||
self.collectionView.asyncDataSource = self.asyncDelegate;
|
self.collectionView.asyncDataSource = self.asyncDelegate;
|
||||||
self.collectionView.asyncDelegate = self.asyncDelegate;
|
self.collectionView.asyncDelegate = self.asyncDelegate;
|
||||||
@@ -249,6 +251,7 @@
|
|||||||
__unused ASCollectionViewTestDelegate *del = testController.asyncDelegate;\
|
__unused ASCollectionViewTestDelegate *del = testController.asyncDelegate;\
|
||||||
__unused ASCollectionView *cv = testController.collectionView;\
|
__unused ASCollectionView *cv = testController.collectionView;\
|
||||||
UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];\
|
UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];\
|
||||||
|
[window makeKeyAndVisible]; \
|
||||||
window.rootViewController = testController;\
|
window.rootViewController = testController;\
|
||||||
\
|
\
|
||||||
[testController.collectionView reloadDataImmediately];\
|
[testController.collectionView reloadDataImmediately];\
|
||||||
@@ -345,4 +348,36 @@
|
|||||||
} completion:nil]);
|
} completion:nil]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (void)testThatNodeCalculatedSizesAreUpdatedBeforeFirstPrepareLayoutAfterRotation
|
||||||
|
{
|
||||||
|
updateValidationTestPrologue
|
||||||
|
id layout = cv.collectionViewLayout;
|
||||||
|
CGSize initialItemSize = [cv calculatedSizeForNodeAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
|
||||||
|
CGSize initialCVSize = cv.bounds.size;
|
||||||
|
|
||||||
|
// Capture the node size before first call to prepareLayout after frame change.
|
||||||
|
__block CGSize itemSizeAtFirstLayout = CGSizeZero;
|
||||||
|
__block CGSize boundsSizeAtFirstLayout = CGSizeZero;
|
||||||
|
[[[[layout expect] andDo:^(NSInvocation *) {
|
||||||
|
itemSizeAtFirstLayout = [cv calculatedSizeForNodeAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
|
||||||
|
boundsSizeAtFirstLayout = [cv bounds].size;
|
||||||
|
}] andForwardToRealObject] prepareLayout];
|
||||||
|
|
||||||
|
// Rotate the device
|
||||||
|
UIDeviceOrientation oldDeviceOrientation = [[UIDevice currentDevice] orientation];
|
||||||
|
[[UIDevice currentDevice] setValue:@(UIDeviceOrientationLandscapeLeft) forKey:@"orientation"];
|
||||||
|
|
||||||
|
CGSize finalItemSize = [cv calculatedSizeForNodeAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
|
||||||
|
CGSize finalCVSize = cv.bounds.size;
|
||||||
|
XCTAssertNotEqualObjects(NSStringFromCGSize(initialItemSize), NSStringFromCGSize(itemSizeAtFirstLayout));
|
||||||
|
XCTAssertNotEqualObjects(NSStringFromCGSize(initialCVSize), NSStringFromCGSize(boundsSizeAtFirstLayout));
|
||||||
|
XCTAssertEqualObjects(NSStringFromCGSize(itemSizeAtFirstLayout), NSStringFromCGSize(finalItemSize));
|
||||||
|
XCTAssertEqualObjects(NSStringFromCGSize(boundsSizeAtFirstLayout), NSStringFromCGSize(finalCVSize));
|
||||||
|
[layout verify];
|
||||||
|
|
||||||
|
// Teardown
|
||||||
|
[[UIDevice currentDevice] setValue:@(oldDeviceOrientation) forKey:@"orientation"];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
Reference in New Issue
Block a user