handle float number fraction part for layout.

Summary:
[IGListCollectionViewLayout]: handle float number fraction part for layout.
When width of item is a float number with non-zero fraction part.
After integer scaled, it might add up smaller or bigger than maxWidth.
Ex with 6 items with 13 padding on each side
1. iPhone 7, maxWidth = 349, scaledItemWidth = 58 -> 58 * 6 = 348, which is smaller than 349
-> this can cause issue where is extra one line at end
2. iPone 7 plus, maxWidth = 388, scaledItemWidth = 64.6667 -> 64.6667 * 6 = 388.0002, which is bigger than 388
-> this can cause issue where last item is mistakenly shifted to next row

To fix it, add epsilon = 1 to allow some error range
Also add stretchToEdge BOOL flag to handle case 1 to decide whether to strech the width of last item.

Reviewed By: ryanolsonk, jessesquires

Differential Revision: D4720156

fbshipit-source-id: 765f6b13b7d601394d65788c30ae69ac1b37c3f2
This commit is contained in:
Shiyi Zhao
2017-03-17 12:26:10 -07:00
committed by Facebook Github Bot
parent a80245a696
commit 24308a9881
3 changed files with 98 additions and 7 deletions

View File

@@ -83,10 +83,12 @@ NS_ASSUME_NONNULL_BEGIN
Create and return a new collection view layout.
@param stickyHeaders Set to `YES` to stick section headers to the top of the bounds while scrolling.
@param topContentInset The top content inset used to offset the sticky headers. Ignored if stickyHeaders is `NO`.
@param stretchToEdge Specifies whether to stretch width of last item to right edge when distance from last item to right edge < epsilon(1)
@return A new collection view layout.
*/
- (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders
topContentInset:(CGFloat)topContentInset NS_DESIGNATED_INITIALIZER;
topContentInset:(CGFloat)topContentInset
stretchToEdge:(BOOL)stretchToEdge NS_DESIGNATED_INITIALIZER;
/**
:nodoc:

View File

@@ -76,6 +76,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
@property (nonatomic, assign, readonly) BOOL stickyHeaders;
@property (nonatomic, assign, readonly) CGFloat topContentInset;
@property (nonatomic, assign, readonly) BOOL stretchToEdge;
@end
@@ -98,10 +99,12 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
}
- (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders
topContentInset:(CGFloat)topContentInset {
topContentInset:(CGFloat)topContentInset
stretchToEdge:(BOOL)stretchToEdge {
if (self = [super init]) {
_stickyHeaders = stickyHeaders;
_topContentInset = topContentInset;
_stretchToEdge = stretchToEdge;
_attributesCache = [NSMutableDictionary new];
_headerAttributesCache = [NSMutableDictionary new];
_cachedLayoutInvalid = YES;
@@ -110,7 +113,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
return [self initWithStickyHeaders:NO topContentInset:0];
return [self initWithStickyHeaders:NO topContentInset:0 stretchToEdge:NO];
}
#pragma mark - UICollectionViewLayout
@@ -349,19 +352,21 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
// the farthest right the frame of an item in this section can go
const CGFloat maxX = width - insets.right;
for (NSInteger item = 0; item < itemCount; item++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];
const CGSize size = [delegate collectionView:collectionView layout:self sizeForItemAtIndexPath:indexPath];
IGAssert(size.width <= paddedWidth, @"Width of item %zi in section %zi must be less than container %.0f accounting for section insets %@",
item, section, width, NSStringFromUIEdgeInsets(insets));
const CGFloat itemWidth = MIN(size.width, paddedWidth);
CGFloat itemWidth = MIN(size.width, paddedWidth);
// if the x + width of the item busts the width of the container
// or if this is the first item and the header has a non-zero size
// newline to the next row and reset
if (itemX + itemWidth > maxX
// define epsilon to avoid float overflow issue
const CGFloat epsilon = 1.0;
if (itemX + itemWidth > maxX + epsilon
|| (item == 0 && headerExists)) {
itemY = nextRowY;
itemX = insets.left;
@@ -371,6 +376,11 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
itemY += lineSpacing;
}
}
const CGFloat distanceToRighEdge = paddedWidth - (itemX + itemWidth);
if (self.stretchToEdge && distanceToRighEdge > 0 && distanceToRighEdge <= epsilon) {
itemWidth = paddedWidth - itemX;
}
const CGRect frame = IGListRectIntegralScaled(CGRectMake(itemX,
itemY + insets.top,

View File

@@ -50,7 +50,11 @@ XCTAssertEqual(CGRectGetHeight(expected), CGRectGetHeight(frame)); \
}
- (void)setUpWithStickyHeaders:(BOOL)sticky topInset:(CGFloat)inset {
self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:sticky topContentInset:inset];
[self setUpWithStickyHeaders:sticky topInset:inset stretchToEdge:NO];
}
- (void)setUpWithStickyHeaders:(BOOL)sticky topInset:(CGFloat)inset stretchToEdge:(BOOL)stretchToEdge {
self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:sticky topContentInset:inset stretchToEdge:stretchToEdge];
self.dataSource = [IGLayoutTestDataSource new];
self.collectionView = [[UICollectionView alloc] initWithFrame:kTestFrame collectionViewLayout:self.layout];
self.collectionView.dataSource = self.dataSource;
@@ -590,6 +594,81 @@ XCTAssertEqual(CGRectGetHeight(expected), CGRectGetHeight(frame)); \
IGAssertEqualFrame([self cellForSection:0 item:1].frame, 0, 20, 40, 20);
}
- (void)test_whenItemsAddedWidthSmallerThanWidth_DifferenceSmallerThanEpsilon {
[self setUpWithStickyHeaders:NO topInset:0 stretchToEdge:YES];
const CGSize size = CGSizeMake(33, 33);
[self prepareWithData:@[
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
lineSpacing:0
interitemSpacing:0
headerHeight:0
items:@[
[[IGLayoutTestItem alloc] initWithSize:size],
[[IGLayoutTestItem alloc] initWithSize:size],
[[IGLayoutTestItem alloc] initWithSize:size],
]],
]];
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33);
IGAssertEqualFrame([self cellForSection:0 item:1].frame, 33, 0, 33, 33);
IGAssertEqualFrame([self cellForSection:0 item:2].frame, 66, 0, 34, 33);
}
- (void)test_whenItemsAddedWidthSmallerThanWidth_DifferenceBiggerThanEpsilon {
[self setUpWithStickyHeaders:NO topInset:0 stretchToEdge:YES];
[self prepareWithData:@[
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
lineSpacing:0
interitemSpacing:0
headerHeight:0
items:@[
[[IGLayoutTestItem alloc] initWithSize:CGSizeMake(33, 33)],
[[IGLayoutTestItem alloc] initWithSize:CGSizeMake(65, 33)],
]],
]];
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33);
IGAssertEqualFrame([self cellForSection:0 item:1].frame, 33, 0, 65, 33);
}
- (void)test_whenItemsAddedWithBiggerThanWidth_DifferenceSmallerThanEpsilon {
[self setUpWithStickyHeaders:NO topInset:0];
[self prepareWithData:@[
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
lineSpacing:0
interitemSpacing:0
headerHeight:0
items:@[
[[IGLayoutTestItem alloc] initWithSize:CGSizeMake(50, 50)],
[[IGLayoutTestItem alloc] initWithSize:CGSizeMake(51, 50)],
]],
]];
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 50, 50);
IGAssertEqualFrame([self cellForSection:0 item:1].frame, 50, 0, 51, 50);
}
- (void)test_whenItemsAddedWithBiggerThanWidth_DifferenceBiggerThanEpsilon {
[self setUpWithStickyHeaders:NO topInset:0];
[self prepareWithData:@[
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
lineSpacing:0
interitemSpacing:0
headerHeight:0
items:@[
[[IGLayoutTestItem alloc] initWithSize:CGSizeMake(50, 50)],
[[IGLayoutTestItem alloc] initWithSize:CGSizeMake(52, 50)],
]],
]];
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 50, 50);
IGAssertEqualFrame([self cellForSection:0 item:1].frame, 0, 50, 52, 50);
}
- (void)test_ {
[self setUpWithStickyHeaders:NO topInset:0];
self.collectionView.frame = CGRectMake(0, 0, 414, 736);