mirror of
https://github.com/Instagram/IGListKit
synced 2026-05-23 01:08:27 +00:00
update IGListCollectionViewLayout to allow for partial layout invalidation
Summary: * Currently, we invalidate the entire layout whenever we make any updates, like inserting new rows at the bottom. * This is one of the most common causes of frame drop on feed, so let's allow partial invalidation based on the minimum modified section. * For example, if we delete section 10, move section 4, and insert section 12, we would re-calculate the layout starting at section 4. * This gets us the majority of the performance gains and it's relatively simple. In the future, we can make further optimizations, like 1) index path level invalidation and 2) finding the smallest modified index path whose properties (ex: size) have actually changed. Reviewed By: rnystrom Differential Revision: D6510140 fbshipit-source-id: 6ff1766b400c5aa82abc29ae76ab96660c3bb106
This commit is contained in:
parent
296baf5f85
commit
7a23ed521d
2 changed files with 78 additions and 17 deletions
|
|
@ -87,6 +87,15 @@ struct IGListSectionEntry {
|
|||
// An array of frames for each cell in the section.
|
||||
std::vector<CGRect> itemBounds;
|
||||
|
||||
// last item distance in scroll direction, used for partial invalidation
|
||||
CGFloat lastItemCoordInScrollDirection;
|
||||
|
||||
// last item distance in fixed direction, used for partial invalidation
|
||||
CGFloat lastItemCoordInFixedDirection;
|
||||
|
||||
// last next row distance in scroll direction, used for partial invalidation
|
||||
CGFloat lastNextRowCoordInScrollDirection;
|
||||
|
||||
// Returns YES when the section has visible content (header and/or items).
|
||||
BOOL isValid() {
|
||||
return !CGSizeEqualToSize(bounds.size, CGSizeZero);
|
||||
|
|
@ -133,7 +142,9 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
@implementation IGListCollectionViewLayout {
|
||||
std::vector<IGListSectionEntry> _sectionData;
|
||||
NSMutableDictionary<NSIndexPath *, UICollectionViewLayoutAttributes *> *_attributesCache;
|
||||
BOOL _cachedLayoutInvalid;
|
||||
|
||||
// invalidate starting at this section
|
||||
NSInteger _minimumInvalidatedSection;
|
||||
|
||||
/**
|
||||
The workflow for getting sticky headers working:
|
||||
|
|
@ -171,7 +182,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
UICollectionElementKindSectionHeader: [NSMutableDictionary new],
|
||||
UICollectionElementKindSectionFooter: [NSMutableDictionary new],
|
||||
}];
|
||||
_cachedLayoutInvalid = YES;
|
||||
_minimumInvalidatedSection = NSNotFound;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
|
@ -337,7 +348,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
|| [context invalidateEverything]
|
||||
|| [context invalidateDataSourceCounts]
|
||||
|| context.ig_invalidateAllAttributes) {
|
||||
_cachedLayoutInvalid = YES;
|
||||
_minimumInvalidatedSection = 0; // invalidates all
|
||||
}
|
||||
|
||||
if (context.ig_invalidateSupplementaryAttributes) {
|
||||
|
|
@ -376,9 +387,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
}
|
||||
|
||||
- (void)prepareLayout {
|
||||
if (_cachedLayoutInvalid) {
|
||||
[self cacheLayout];
|
||||
}
|
||||
[self calculateLayoutIfNeeded];
|
||||
}
|
||||
|
||||
#pragma mark - Public API
|
||||
|
|
@ -397,8 +406,10 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
|
||||
#pragma mark - Private API
|
||||
|
||||
- (void)cacheLayout {
|
||||
_cachedLayoutInvalid = NO;
|
||||
- (void)calculateLayoutIfNeeded {
|
||||
if (_minimumInvalidatedSection == NSNotFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
// purge attribute caches so they are rebuilt
|
||||
[_attributesCache removeAllObjects];
|
||||
|
|
@ -412,7 +423,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
const UIEdgeInsets contentInset = collectionView.contentInset;
|
||||
const CGRect contentInsetAdjustedCollectionViewBounds = UIEdgeInsetsInsetRect(collectionView.bounds, contentInset);
|
||||
|
||||
auto sectionData = std::vector<IGListSectionEntry>(sectionCount);
|
||||
_sectionData.resize(sectionCount);
|
||||
|
||||
CGFloat itemCoordInScrollDirection = 0.0;
|
||||
CGFloat itemCoordInFixedDirection = 0.0;
|
||||
|
|
@ -421,9 +432,18 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
// union item frames and optionally the header to find a bounding box of the entire section
|
||||
CGRect rollingSectionBounds;
|
||||
|
||||
for (NSInteger section = 0; section < sectionCount; section++) {
|
||||
// populate last valid section information
|
||||
const NSInteger lastValidSection = _minimumInvalidatedSection - 1;
|
||||
if (lastValidSection >= 0 && lastValidSection < sectionCount) {
|
||||
itemCoordInScrollDirection = _sectionData[lastValidSection].lastItemCoordInScrollDirection;
|
||||
itemCoordInFixedDirection = _sectionData[lastValidSection].lastItemCoordInFixedDirection;
|
||||
nextRowCoordInScrollDirection = _sectionData[lastValidSection].lastNextRowCoordInScrollDirection;
|
||||
rollingSectionBounds = _sectionData[lastValidSection].bounds;
|
||||
}
|
||||
|
||||
for (NSInteger section = _minimumInvalidatedSection; section < sectionCount; section++) {
|
||||
const NSInteger itemCount = [dataSource collectionView:collectionView numberOfItemsInSection:section];
|
||||
sectionData[section].itemBounds = std::vector<CGRect>(itemCount);
|
||||
_sectionData[section].itemBounds = std::vector<CGRect>(itemCount);
|
||||
|
||||
const CGSize headerSize = [delegate collectionView:collectionView layout:self referenceSizeForHeaderInSection:section];
|
||||
const CGSize footerSize = [delegate collectionView:collectionView layout:self referenceSizeForFooterInSection:section];
|
||||
|
|
@ -498,7 +518,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
itemLengthInFixedDirection);
|
||||
const CGRect frame = IGListRectIntegralScaled(rawFrame);
|
||||
|
||||
sectionData[section].itemBounds[item] = frame;
|
||||
_sectionData[section].itemBounds[item] = frame;
|
||||
|
||||
// track the max size of the row to find the coord of the next row, adjust for leading inset while iterating items
|
||||
nextRowCoordInScrollDirection = MAX(CGRectGetMaxInDirection(frame, self.scrollDirection) - UIEdgeInsetsLeadingInsetInDirection(insets, self.scrollDirection), nextRowCoordInScrollDirection);
|
||||
|
|
@ -524,7 +544,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
headerSize.width,
|
||||
paddedLengthInFixedDirection);
|
||||
|
||||
sectionData[section].headerBounds = headerBounds;
|
||||
_sectionData[section].headerBounds = headerBounds;
|
||||
|
||||
const CGRect footerBounds = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ?
|
||||
CGRectMake(insets.left,
|
||||
|
|
@ -536,7 +556,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
footerSize.width,
|
||||
paddedLengthInFixedDirection);
|
||||
|
||||
sectionData[section].footerBounds = footerBounds;
|
||||
_sectionData[section].footerBounds = footerBounds;
|
||||
|
||||
// union the header before setting the bounds of the section
|
||||
// only do this when the header has a size, otherwise the union stretches to box empty space
|
||||
|
|
@ -547,17 +567,22 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
rollingSectionBounds = CGRectUnion(rollingSectionBounds, footerBounds);
|
||||
}
|
||||
|
||||
sectionData[section].bounds = rollingSectionBounds;
|
||||
sectionData[section].insets = insets;
|
||||
_sectionData[section].bounds = rollingSectionBounds;
|
||||
_sectionData[section].insets = insets;
|
||||
|
||||
// bump the coord for the next section with the right insets
|
||||
itemCoordInFixedDirection += UIEdgeInsetsTrailingInsetInDirection(insets, fixedDirection);
|
||||
|
||||
// find the farthest point in the section and add the trailing inset to find the next row's coord
|
||||
nextRowCoordInScrollDirection = MAX(nextRowCoordInScrollDirection, CGRectGetMaxInDirection(rollingSectionBounds, self.scrollDirection) + UIEdgeInsetsTrailingInsetInDirection(insets, self.scrollDirection));
|
||||
|
||||
// keep track of coordinates for partial invalidation
|
||||
_sectionData[section].lastItemCoordInScrollDirection = itemCoordInScrollDirection;
|
||||
_sectionData[section].lastItemCoordInFixedDirection = itemCoordInFixedDirection;
|
||||
_sectionData[section].lastNextRowCoordInScrollDirection = nextRowCoordInScrollDirection;
|
||||
}
|
||||
|
||||
_sectionData = sectionData;
|
||||
_minimumInvalidatedSection = NSNotFound;
|
||||
}
|
||||
|
||||
- (NSRange)rangeOfSectionsInRect:(CGRect)rect {
|
||||
|
|
|
|||
|
|
@ -1099,4 +1099,40 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
|
|||
XCTAssertNil([self.layout layoutAttributesForItemAtIndexPath:genIndexPath(0, 4)]);
|
||||
}
|
||||
|
||||
- (void)test_whenUpdatingSizes_thatLayoutUpdates {
|
||||
[self setUpWithStickyHeaders:NO topInset:0];
|
||||
|
||||
[self prepareWithData:@[
|
||||
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
|
||||
lineSpacing:0
|
||||
interitemSpacing:0
|
||||
headerHeight:0
|
||||
footerHeight:0
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:CGSizeMake(10, 10)],
|
||||
[[IGLayoutTestItem alloc] initWithSize:CGSizeMake(10, 10)],
|
||||
]],
|
||||
]];
|
||||
|
||||
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 10, 10);
|
||||
IGAssertEqualFrame([self cellForSection:0 item:1].frame, 10, 0, 10, 10);
|
||||
|
||||
[self prepareWithData:@[
|
||||
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
|
||||
lineSpacing:0
|
||||
interitemSpacing:0
|
||||
headerHeight:0
|
||||
footerHeight:0
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:CGSizeMake(20, 20)],
|
||||
[[IGLayoutTestItem alloc] initWithSize:CGSizeMake(20, 20)],
|
||||
[[IGLayoutTestItem alloc] initWithSize:CGSizeMake(20, 20)],
|
||||
]],
|
||||
]];
|
||||
|
||||
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 20, 20);
|
||||
IGAssertEqualFrame([self cellForSection:0 item:1].frame, 20, 0, 20, 20);
|
||||
IGAssertEqualFrame([self cellForSection:0 item:2].frame, 40, 0, 20, 20);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
|||
Loading…
Reference in a new issue