mirror of
https://github.com/Instagram/IGListKit
synced 2026-05-23 17:28:22 +00:00
Add horizontal scrolling support to IGListCollectionViewLayout
Summary: Issue fixed: #752 - [x] All tests pass. Demo project builds and runs. - [x] I added tests, an experiment, or detailed why my change isn't tested. - [x] I added an entry to the `CHANGELOG.md` for any breaking changes, enhancements, or bug fixes. - [x] I have reviewed the [contributing guide](https://github.com/Instagram/IGListKit/blob/master/.github/CONTRIBUTING.md) This PR generalizes the layout logic in `IGListCollectionViewLayout.mm` to handle horizontally scrolling layouts, mainly by generalizing references to `width`, `height`, `x` and `y` to take scrolling direction into account. This changes the signature of `IGListCollectionViewLayout.init` as well as the names of a few properties, so it would be a breaking change. I added a couple of unit tests specifically for horizontal layouts -- but held off from adding a horizontal version of *every* unit test for this class, as it would basically double the number of tests. But if you want that, just let me know and I'm happy to do it. Also let me know if you want me to add a demo VC to the Examples project that uses this new horizontal flow layout -- I have some demo code handy (I used it for testing), but didn't want to clutter up the PR if you didn't want/need it. Closes https://github.com/Instagram/IGListKit/pull/857 Reviewed By: ryanolsonk Differential Revision: D5547266 Pulled By: rnystrom fbshipit-source-id: 6094c45069fc265273d0f95c296fa78e47470384
This commit is contained in:
parent
5bede7b78e
commit
9e312275c3
5 changed files with 354 additions and 79 deletions
|
|
@ -15,6 +15,8 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag
|
|||
- Added `-[IGListSectionController didDeselectItemAtIndex:]` API to support default `UICollectionView` cell deselection. [Ryan Nystrom](https://github.com/rnystrom) (tbd)
|
||||
- Added `-[IGListCollectionContext selectItemAtIndex:]` Select an item through IGListCollectionContext like `-[IGListCollectionContext deselectItemAtIndex:]`. [Marvin Nazari](https://github.com/MarvinNazari) (tbd)
|
||||
|
||||
- Added horizontal scrolling support to `IGListCollectionViewLayout`. [Peter Edmonston](https://github.com/edmonston) [(#857)](https://github.com/Instagram/IGListKit/pull/857)
|
||||
|
||||
3.0.0
|
||||
-----
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
This UICollectionViewLayout subclass is for vertically-scrolling lists of data with variable widths and heights. It
|
||||
supports an infinite number of sections and items. All work is done on the main thread, and while extremely efficient,
|
||||
This UICollectionViewLayout subclass is for vertically or horizontally scrolling lists of data with variable widths and
|
||||
heights. It supports an infinite number of sections and items. All work is done on the main thread, and while extremely efficient,
|
||||
care must be taken not to stall the main thread in sizing delegate methods.
|
||||
|
||||
This layout piggybacks on the mechanics of UICollectionViewFlowLayout in that:
|
||||
|
|
@ -33,9 +33,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
|
||||
```
|
||||
|
||||
Sections and items are put into the same horizontal row until the max-x position of an item extends beyond the width
|
||||
of the collection view. When that happens, the item is "newlined" to the next row. The y position of that row is
|
||||
determined by the maximum height (including section insets) of the section/item of the previous row.
|
||||
In a vertically scrolling layout, sections and items are put into the same horizontal row until the max-x position
|
||||
of an item extends beyond the width of the collection view. When that happens, the item is "newlined" to the next row.
|
||||
The y position of that row is determined by the maximum height (including section insets) of the section/item of the previous row.
|
||||
|
||||
Ex. of a section (2,0) with a large width causing a newline.
|
||||
```
|
||||
|
|
@ -64,6 +64,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
Interitem spacing applies to items and sections within the same row. Line spacing only applies to items within the same
|
||||
section.
|
||||
|
||||
In a horizontally scrolling layout, sections and items are flowed vertically until they need to be "newlined" to the
|
||||
next column. Headers, if used, are stretched to the height of the collection view, minus the section insets.
|
||||
|
||||
Please see the unit tests for more configuration examples and expected output.
|
||||
*/
|
||||
|
|
@ -71,27 +74,49 @@ NS_SWIFT_NAME(ListCollectionViewLayout)
|
|||
@interface IGListCollectionViewLayout : UICollectionViewLayout
|
||||
|
||||
/**
|
||||
Set this to adjust the offset of the sticky headers. Can be used to change the sticky header position as UI like the
|
||||
navigation bar is scrolled offscreen. Changing this to the height of the navigation bar will give the effect of the
|
||||
headers sticking to the nav as it is collapsed.
|
||||
|
||||
@discussion Changing the value on this method will invalidate the layout every time.
|
||||
Direction in which layout will be scrollable; items will be flowed in the perpendicular direction, "newlining" when they
|
||||
run out of space along that axis or when a non-zero header is found.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat stickyHeaderOriginYAdjustment;
|
||||
@property (nonatomic, readonly) UICollectionViewScrollDirection scrollDirection;
|
||||
|
||||
/**
|
||||
Set this to adjust the offset of the sticky headers in the scrolling direction. Can be used to change the sticky
|
||||
header position as UI like the navigation bar is scrolled offscreen. In a vertically scrolling layout, changing
|
||||
this to the height of the navigation bar will give the effect of the headers sticking to the nav as it is collapsed.
|
||||
|
||||
@note Changing the value on this method will invalidate the layout every time.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat stickyHeaderYOffset;
|
||||
|
||||
|
||||
/**
|
||||
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)
|
||||
@param scrollDirection Direction along which the collection view will be scrollable (if content size exceeds the frame size)
|
||||
@param topContentInset The content inset (top or left, depending on scrolling direction) used to offset the sticky headers. Ignored if stickyHeaders is `NO`.
|
||||
@param stretchToEdge Specifies whether to stretch width (in vertically scrolling layout) or height (horizontally scrolling) of last item to right/bottom edge when distance from last item to right/bottom edge < epsilon(1)
|
||||
|
||||
@return A new collection view layout.
|
||||
*/
|
||||
- (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders
|
||||
scrollDirection:(UICollectionViewScrollDirection)scrollDirection
|
||||
topContentInset:(CGFloat)topContentInset
|
||||
stretchToEdge:(BOOL)stretchToEdge NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
Create and return a new vertically scrolling 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
|
||||
stretchToEdge:(BOOL)stretchToEdge;
|
||||
|
||||
/**
|
||||
:nodoc:
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -14,6 +14,55 @@
|
|||
|
||||
#import <IGListKit/IGListAssert.h>
|
||||
|
||||
static CGFloat UIEdgeInsetsLeadingInsetInDirection(UIEdgeInsets insets, UICollectionViewScrollDirection direction) {
|
||||
switch (direction) {
|
||||
case UICollectionViewScrollDirectionVertical: return insets.top;
|
||||
case UICollectionViewScrollDirectionHorizontal: return insets.left;
|
||||
}
|
||||
}
|
||||
|
||||
static CGFloat UIEdgeInsetsTrailingInsetInDirection(UIEdgeInsets insets, UICollectionViewScrollDirection direction) {
|
||||
switch (direction) {
|
||||
case UICollectionViewScrollDirectionVertical: return insets.bottom;
|
||||
case UICollectionViewScrollDirectionHorizontal: return insets.right;
|
||||
}
|
||||
}
|
||||
|
||||
static CGFloat CGPointGetCoordinateInDirection(CGPoint point, UICollectionViewScrollDirection direction) {
|
||||
switch (direction) {
|
||||
case UICollectionViewScrollDirectionVertical: return point.y;
|
||||
case UICollectionViewScrollDirectionHorizontal: return point.x;
|
||||
}
|
||||
}
|
||||
|
||||
static CGFloat CGRectGetLengthInDirection(CGRect rect, UICollectionViewScrollDirection direction) {
|
||||
switch (direction) {
|
||||
case UICollectionViewScrollDirectionVertical: return rect.size.height;
|
||||
case UICollectionViewScrollDirectionHorizontal: return rect.size.width;
|
||||
}
|
||||
}
|
||||
|
||||
static CGFloat CGRectGetMaxInDirection(CGRect rect, UICollectionViewScrollDirection direction) {
|
||||
switch (direction) {
|
||||
case UICollectionViewScrollDirectionVertical: return CGRectGetMaxY(rect);
|
||||
case UICollectionViewScrollDirectionHorizontal: return CGRectGetMaxX(rect);
|
||||
}
|
||||
}
|
||||
|
||||
static CGFloat CGRectGetMinInDirection(CGRect rect, UICollectionViewScrollDirection direction) {
|
||||
switch (direction) {
|
||||
case UICollectionViewScrollDirectionVertical: return CGRectGetMinY(rect);
|
||||
case UICollectionViewScrollDirectionHorizontal: return CGRectGetMinX(rect);
|
||||
}
|
||||
}
|
||||
|
||||
static CGFloat CGSizeGetLengthInDirection(CGSize size, UICollectionViewScrollDirection direction) {
|
||||
switch (direction) {
|
||||
case UICollectionViewScrollDirectionVertical: return size.height;
|
||||
case UICollectionViewScrollDirectionHorizontal: return size.width;
|
||||
}
|
||||
}
|
||||
|
||||
static NSIndexPath *headerIndexPathForSection(NSInteger section) {
|
||||
return [NSIndexPath indexPathForItem:0 inSection:section];
|
||||
}
|
||||
|
|
@ -101,7 +150,18 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
- (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders
|
||||
topContentInset:(CGFloat)topContentInset
|
||||
stretchToEdge:(BOOL)stretchToEdge {
|
||||
return [self initWithStickyHeaders:stickyHeaders
|
||||
scrollDirection:UICollectionViewScrollDirectionVertical
|
||||
topContentInset:topContentInset
|
||||
stretchToEdge:stretchToEdge];
|
||||
}
|
||||
|
||||
- (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders
|
||||
scrollDirection:(UICollectionViewScrollDirection)scrollDirection
|
||||
topContentInset:(CGFloat)topContentInset
|
||||
stretchToEdge:(BOOL)stretchToEdge {
|
||||
if (self = [super init]) {
|
||||
_scrollDirection = scrollDirection;
|
||||
_stickyHeaders = stickyHeaders;
|
||||
_topContentInset = topContentInset;
|
||||
_stretchToEdge = stretchToEdge;
|
||||
|
|
@ -139,7 +199,8 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
// do not add zero height headers or headers that are outside the rect
|
||||
const CGRect frame = attributes.frame;
|
||||
const CGRect intersection = CGRectIntersection(frame, rect);
|
||||
if (!CGRectIsEmpty(intersection) && CGRectGetHeight(frame) > 0.0) {
|
||||
if (!CGRectIsEmpty(intersection)
|
||||
&& CGRectGetLengthInDirection(frame, self.scrollDirection) > 0.0) {
|
||||
[result addObject:attributes];
|
||||
}
|
||||
}
|
||||
|
|
@ -199,18 +260,26 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
|
||||
UICollectionView *collectionView = self.collectionView;
|
||||
const IGListSectionEntry entry = _sectionData[section];
|
||||
const CGFloat minY = CGRectGetMinY(entry.bounds);
|
||||
const CGFloat minOffset = CGRectGetMinInDirection(entry.bounds, self.scrollDirection);
|
||||
|
||||
CGRect frame = entry.headerBounds;
|
||||
|
||||
if (self.stickyHeaders) {
|
||||
const CGFloat yOffset = collectionView.contentOffset.y + self.topContentInset + self.stickyHeaderOriginYAdjustment;
|
||||
CGFloat offset = CGPointGetCoordinateInDirection(collectionView.contentOffset, self.scrollDirection) + self.topContentInset + self.stickyHeaderYOffset;
|
||||
|
||||
if (section + 1 == _sectionData.size()) {
|
||||
frame.origin.y = MAX(minY, yOffset);
|
||||
offset = MAX(minOffset, offset);
|
||||
} else {
|
||||
const CGFloat maxY = CGRectGetMinY(_sectionData[section + 1].bounds) - CGRectGetHeight(frame);
|
||||
frame.origin.y = MIN(MAX(minY, yOffset), maxY);
|
||||
const CGFloat maxOffset = CGRectGetMinInDirection(_sectionData[section + 1].bounds, self.scrollDirection) - CGRectGetLengthInDirection(frame, self.scrollDirection);
|
||||
offset = MIN(MAX(minOffset, offset), maxOffset);
|
||||
}
|
||||
switch (self.scrollDirection) {
|
||||
case UICollectionViewScrollDirectionVertical:
|
||||
frame.origin.y = offset;
|
||||
break;
|
||||
case UICollectionViewScrollDirectionHorizontal:
|
||||
frame.origin.x = offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -231,11 +300,20 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
}
|
||||
|
||||
const IGListSectionEntry section = _sectionData[sectionCount - 1];
|
||||
const CGFloat height = CGRectGetMaxY(section.bounds) + section.insets.bottom;
|
||||
|
||||
UICollectionView *collectionView = self.collectionView;
|
||||
const UIEdgeInsets contentInset = collectionView.contentInset;
|
||||
return CGSizeMake(CGRectGetWidth(collectionView.bounds) - contentInset.left - contentInset.right, height);
|
||||
|
||||
switch (self.scrollDirection) {
|
||||
case UICollectionViewScrollDirectionVertical: {
|
||||
const CGFloat height = CGRectGetMaxY(section.bounds) + section.insets.bottom;
|
||||
return CGSizeMake(CGRectGetWidth(collectionView.bounds) - contentInset.left - contentInset.right, height);
|
||||
}
|
||||
case UICollectionViewScrollDirectionHorizontal: {
|
||||
const CGFloat width = CGRectGetMaxX(section.bounds) + section.insets.right;
|
||||
return CGSizeMake(width, CGRectGetHeight(collectionView.bounds) - contentInset.top - contentInset.bottom);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void)invalidateLayoutWithContext:(IGListCollectionViewLayoutInvalidationContext *)context {
|
||||
|
|
@ -278,7 +356,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
const CGRect oldBounds = self.collectionView.bounds;
|
||||
|
||||
// if the y origin has changed, only invalidate when using sticky headers
|
||||
if (CGRectGetMinY(newBounds) != CGRectGetMinY(oldBounds)) {
|
||||
if (CGRectGetMinInDirection(newBounds, self.scrollDirection) != CGRectGetMinInDirection(oldBounds, self.scrollDirection)) {
|
||||
return self.stickyHeaders;
|
||||
}
|
||||
|
||||
|
|
@ -294,11 +372,11 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
|
||||
#pragma mark - Public API
|
||||
|
||||
- (void)setStickyHeaderOriginYAdjustment:(CGFloat)stickyHeaderOriginYAdjustment {
|
||||
- (void)setStickyHeaderYOffset:(CGFloat)stickyHeaderYOffset {
|
||||
IGAssertMainThread();
|
||||
|
||||
if (_stickyHeaderOriginYAdjustment != stickyHeaderOriginYAdjustment) {
|
||||
_stickyHeaderOriginYAdjustment = stickyHeaderOriginYAdjustment;
|
||||
if (_stickyHeaderYOffset != stickyHeaderYOffset) {
|
||||
_stickyHeaderYOffset = stickyHeaderYOffset;
|
||||
|
||||
IGListCollectionViewLayoutInvalidationContext *invalidationContext = [IGListCollectionViewLayoutInvalidationContext new];
|
||||
invalidationContext.ig_invalidateSupplementaryAttributes = YES;
|
||||
|
|
@ -321,13 +399,13 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
|
||||
const NSInteger sectionCount = [dataSource numberOfSectionsInCollectionView:collectionView];
|
||||
const UIEdgeInsets contentInset = collectionView.contentInset;
|
||||
const CGFloat width = CGRectGetWidth(collectionView.bounds) - contentInset.left - contentInset.right;
|
||||
|
||||
const CGRect contentInsetAdjustedCollectionViewBounds = UIEdgeInsetsInsetRect(collectionView.bounds, contentInset);
|
||||
|
||||
auto sectionData = std::vector<IGListSectionEntry>(sectionCount);
|
||||
|
||||
CGFloat itemY = 0.0;
|
||||
CGFloat itemX = 0.0;
|
||||
CGFloat nextRowY = 0.0;
|
||||
CGFloat itemCoordInScrollDirection = 0.0;
|
||||
CGFloat itemCoordInFixedDirection = 0.0;
|
||||
CGFloat nextRowCoordInScrollDirection = 0.0;
|
||||
|
||||
// union item frames and optionally the header to find a bounding box of the entire section
|
||||
CGRect rollingSectionBounds;
|
||||
|
|
@ -341,63 +419,78 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
const CGFloat lineSpacing = [delegate collectionView:collectionView layout:self minimumLineSpacingForSectionAtIndex:section];
|
||||
const CGFloat interitemSpacing = [delegate collectionView:collectionView layout:self minimumInteritemSpacingForSectionAtIndex:section];
|
||||
|
||||
const CGFloat paddedWidth = width - insets.left - insets.right;
|
||||
const BOOL headerExists = headerSize.height > 0;
|
||||
const CGSize paddedCollectionViewSize = UIEdgeInsetsInsetRect(contentInsetAdjustedCollectionViewBounds, insets).size;
|
||||
const UICollectionViewScrollDirection fixedDirection = self.scrollDirection == UICollectionViewScrollDirectionHorizontal ? UICollectionViewScrollDirectionVertical : UICollectionViewScrollDirectionHorizontal;
|
||||
const CGFloat paddedLengthInFixedDirection = CGSizeGetLengthInDirection(paddedCollectionViewSize, fixedDirection);
|
||||
const CGFloat headerLengthInScrollDirection = CGSizeGetLengthInDirection(headerSize, self.scrollDirection);
|
||||
const BOOL headerExists = headerLengthInScrollDirection > 0;
|
||||
|
||||
// start the section accounting for the header size
|
||||
// header length in scroll direction is subtracted from the sectionBounds when calculating the header bounds after items are done
|
||||
// this bumps the first row of items over enough to make room for the header
|
||||
itemCoordInScrollDirection += headerLengthInScrollDirection;
|
||||
nextRowCoordInScrollDirection += headerLengthInScrollDirection;
|
||||
|
||||
// add the leading inset in fixed direction in case the section falls on the same row as the previous
|
||||
// if the section is newlined then the coord in fixed direction is reset
|
||||
itemCoordInFixedDirection += UIEdgeInsetsLeadingInsetInDirection(insets, fixedDirection);
|
||||
|
||||
// start the section y accounting for the header height
|
||||
// header height is subtracted from the sectionBounds when calculating the header bounds after items are done
|
||||
// this bumps the first row of items down enough to make room for the header
|
||||
itemY += headerSize.height;
|
||||
nextRowY += headerSize.height;
|
||||
|
||||
// add the left inset in case the section falls on the same row as the previous
|
||||
// if the section is newlined then the x is reset
|
||||
itemX += insets.left;
|
||||
|
||||
// the farthest right the frame of an item in this section can go
|
||||
const CGFloat maxX = width - insets.right;
|
||||
// the farthest in the fixed direction the frame of an item in this section can go
|
||||
const CGFloat maxCoordinateInFixedDirection = CGRectGetLengthInDirection(contentInsetAdjustedCollectionViewBounds, fixedDirection) - UIEdgeInsetsTrailingInsetInDirection(insets, fixedDirection);
|
||||
|
||||
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 || fabs(size.width - paddedWidth) < FLT_EPSILON,
|
||||
@"Width of item %zi in section %zi must be less than container %.0f accounting for section insets %@",
|
||||
item, section, width, NSStringFromUIEdgeInsets(insets));
|
||||
CGFloat itemWidth = MIN(size.width, paddedWidth);
|
||||
IGAssert(CGSizeGetLengthInDirection(size, fixedDirection) <= paddedLengthInFixedDirection
|
||||
|| fabs(CGSizeGetLengthInDirection(size, fixedDirection) - paddedLengthInFixedDirection) < FLT_EPSILON,
|
||||
@"%@ of item %zi in section %zi must be less than container %.0f accounting for section insets %@",
|
||||
self.scrollDirection == UICollectionViewScrollDirectionVertical ? @"Width" : @"Height",
|
||||
item, section, CGRectGetLengthInDirection(contentInsetAdjustedCollectionViewBounds, fixedDirection),
|
||||
NSStringFromUIEdgeInsets(insets));
|
||||
|
||||
// if the x + width of the item busts the width of the container
|
||||
CGFloat itemLengthInFixedDirection = MIN(CGSizeGetLengthInDirection(size, fixedDirection), paddedLengthInFixedDirection);
|
||||
|
||||
// if the origin and length in fixed direction of the item busts the size 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
|
||||
// define epsilon to avoid float overflow issue
|
||||
const CGFloat epsilon = 1.0;
|
||||
if (itemX + itemWidth > maxX + epsilon
|
||||
if (itemCoordInFixedDirection + itemLengthInFixedDirection > maxCoordinateInFixedDirection + epsilon
|
||||
|| (item == 0 && headerExists)) {
|
||||
itemY = nextRowY;
|
||||
itemX = insets.left;
|
||||
itemCoordInScrollDirection = nextRowCoordInScrollDirection;
|
||||
itemCoordInFixedDirection = UIEdgeInsetsLeadingInsetInDirection(insets, fixedDirection);
|
||||
|
||||
|
||||
// if newlining, always append line spacing unless its the very first item of the section
|
||||
if (item > 0) {
|
||||
itemY += lineSpacing;
|
||||
itemCoordInScrollDirection += lineSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
const CGFloat distanceToRighEdge = paddedWidth - (itemX + itemWidth);
|
||||
if (self.stretchToEdge && distanceToRighEdge > 0 && distanceToRighEdge <= epsilon) {
|
||||
itemWidth = paddedWidth - itemX;
|
||||
const CGFloat distanceToEdge = paddedLengthInFixedDirection - (itemCoordInFixedDirection + itemLengthInFixedDirection);
|
||||
if (self.stretchToEdge && distanceToEdge > 0 && distanceToEdge <= epsilon) {
|
||||
itemLengthInFixedDirection = paddedLengthInFixedDirection - itemCoordInFixedDirection;
|
||||
}
|
||||
|
||||
const CGRect frame = IGListRectIntegralScaled(CGRectMake(itemX,
|
||||
itemY + insets.top,
|
||||
itemWidth,
|
||||
size.height));
|
||||
const CGRect rawFrame = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ?
|
||||
CGRectMake(itemCoordInFixedDirection,
|
||||
itemCoordInScrollDirection + insets.top,
|
||||
itemLengthInFixedDirection,
|
||||
size.height) :
|
||||
CGRectMake(itemCoordInScrollDirection + insets.left,
|
||||
itemCoordInFixedDirection,
|
||||
size.width,
|
||||
itemLengthInFixedDirection);
|
||||
const CGRect frame = IGListRectIntegralScaled(rawFrame);
|
||||
|
||||
sectionData[section].itemBounds[item] = frame;
|
||||
|
||||
// track the max size of the row to find the y of the next row, adjust for top inset while iterating items
|
||||
nextRowY = MAX(CGRectGetMaxY(frame) - insets.top, nextRowY);
|
||||
// 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);
|
||||
|
||||
// increase the rolling x by the item width and add item spacing for all items on the same row
|
||||
itemX += itemWidth + interitemSpacing;
|
||||
// increase the rolling coord in fixed direction appropriately and add item spacing for all items on the same row
|
||||
itemCoordInFixedDirection += itemLengthInFixedDirection + interitemSpacing;
|
||||
|
||||
// union the rolling section bounds
|
||||
if (item == 0) {
|
||||
|
|
@ -407,10 +500,16 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
}
|
||||
}
|
||||
|
||||
const CGRect headerBounds = CGRectMake(insets.left,
|
||||
CGRectGetMinY(rollingSectionBounds) - headerSize.height,
|
||||
paddedWidth,
|
||||
headerSize.height);
|
||||
const CGRect headerBounds = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ?
|
||||
CGRectMake(insets.left,
|
||||
CGRectGetMinY(rollingSectionBounds) - headerSize.height,
|
||||
paddedLengthInFixedDirection,
|
||||
headerSize.height) :
|
||||
CGRectMake(CGRectGetMinX(rollingSectionBounds) - headerSize.width,
|
||||
insets.top,
|
||||
headerSize.width,
|
||||
paddedLengthInFixedDirection);
|
||||
|
||||
sectionData[section].headerBounds = headerBounds;
|
||||
|
||||
// union the header before setting the bounds of the section
|
||||
|
|
@ -422,11 +521,11 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
|
|||
sectionData[section].bounds = rollingSectionBounds;
|
||||
sectionData[section].insets = insets;
|
||||
|
||||
// bump the x for the next section with the right insets
|
||||
itemX += insets.right;
|
||||
// bump the coord for the next section with the right insets
|
||||
itemCoordInFixedDirection += UIEdgeInsetsTrailingInsetInDirection(insets, fixedDirection);
|
||||
|
||||
// find the lowest point in the section and add the bottom inset to find the next row's Y
|
||||
nextRowY = MAX(nextRowY, CGRectGetMaxY(rollingSectionBounds) + insets.bottom);
|
||||
// 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));
|
||||
}
|
||||
|
||||
_sectionData = sectionData;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,11 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
|
|||
}
|
||||
|
||||
- (void)setUpWithStickyHeaders:(BOOL)sticky topInset:(CGFloat)inset stretchToEdge:(BOOL)stretchToEdge {
|
||||
self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:sticky topContentInset:inset stretchToEdge:stretchToEdge];
|
||||
[self setUpWithStickyHeaders:sticky scrollDirection:UICollectionViewScrollDirectionVertical topInset:inset stretchToEdge:stretchToEdge];
|
||||
}
|
||||
|
||||
- (void)setUpWithStickyHeaders:(BOOL)sticky scrollDirection:(UICollectionViewScrollDirection)scrollDirection topInset:(CGFloat)inset stretchToEdge:(BOOL)stretchToEdge {
|
||||
self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:sticky scrollDirection:scrollDirection topContentInset:inset stretchToEdge:stretchToEdge];
|
||||
self.dataSource = [IGLayoutTestDataSource new];
|
||||
self.collectionView = [[UICollectionView alloc] initWithFrame:kTestFrame collectionViewLayout:self.layout];
|
||||
self.collectionView.dataSource = self.dataSource;
|
||||
|
|
@ -74,7 +78,7 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
|
|||
XCTAssertTrue(CGSizeEqualToSize(CGSizeZero, self.collectionView.contentSize));
|
||||
}
|
||||
|
||||
- (void)test_whenLayingOutCells_withHeaderHeight_withLineSpacing_withInsets_thatFramesCorrect {
|
||||
- (void)test_whenLayingOutCellsVertically_withHeaderHeight_withLineSpacing_withInsets_thatFramesCorrect {
|
||||
[self setUpWithStickyHeaders:NO topInset:0];
|
||||
|
||||
const CGFloat headerHeight = 10;
|
||||
|
|
@ -106,6 +110,39 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
|
|||
IGAssertEqualFrame([self cellForSection:1 item:0].frame, 10, 85, 85, 30);
|
||||
}
|
||||
|
||||
-(void)test_whenLayingOutCellsHorizontally_withHeaderHeight_withLineSpacing_withInsets_thatFramesCorrect {
|
||||
[self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO];
|
||||
|
||||
const CGFloat headerHeight = 10;
|
||||
const CGFloat lineSpacing = 10;
|
||||
const UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 5, 5);
|
||||
|
||||
[self prepareWithData:@[
|
||||
[[IGLayoutTestSection alloc] initWithInsets:insets
|
||||
lineSpacing:lineSpacing
|
||||
interitemSpacing:0
|
||||
headerHeight:headerHeight
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){45,10}],
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){45,20}],
|
||||
]],
|
||||
[[IGLayoutTestSection alloc] initWithInsets:insets
|
||||
lineSpacing:lineSpacing
|
||||
interitemSpacing:0
|
||||
headerHeight:headerHeight
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){45,30}],
|
||||
]],
|
||||
]];
|
||||
XCTAssertEqual(self.collectionView.contentSize.width, 140);
|
||||
IGAssertEqualFrame([self headerForSection:0].frame, 10, 10, 10, 85);
|
||||
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 20, 10, 45, 10);
|
||||
IGAssertEqualFrame([self cellForSection:0 item:1].frame, 20, 20, 45, 20);
|
||||
IGAssertEqualFrame([self headerForSection:1].frame, 80, 10, 10, 85);
|
||||
IGAssertEqualFrame([self cellForSection:1 item:0].frame, 90, 10, 45, 30);
|
||||
}
|
||||
|
||||
|
||||
- (void)test_whenUsingStickyHeaders_withSimulatedScrolling_thatYPositionsAdjusted {
|
||||
[self setUpWithStickyHeaders:YES topInset:10];
|
||||
|
||||
|
|
@ -142,6 +179,42 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
|
|||
IGAssertEqualFrame([self headerForSection:1].frame, 0, 55, 100, 10);
|
||||
}
|
||||
|
||||
- (void)test_whenUsingStickyHeaders_withSimulatedHorizontalScrolling_thatXPositionsAdjusted {
|
||||
[self setUpWithStickyHeaders:YES scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:10 stretchToEdge:NO];
|
||||
|
||||
[self prepareWithData:@[
|
||||
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
|
||||
lineSpacing:0
|
||||
interitemSpacing:0
|
||||
headerHeight:10
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){20,100}],
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){20,100}],
|
||||
]],
|
||||
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
|
||||
lineSpacing:0
|
||||
interitemSpacing:0
|
||||
headerHeight:10
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){30,100}],
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){30,100}],
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){30,100}],
|
||||
]],
|
||||
]];
|
||||
|
||||
// scroll header 0 halfway
|
||||
self.collectionView.contentOffset = CGPointMake(5, 0);
|
||||
[self.collectionView layoutIfNeeded];
|
||||
IGAssertEqualFrame([self headerForSection:0].frame, 15, 0, 10, 100);
|
||||
IGAssertEqualFrame([self headerForSection:1].frame, 50, 0, 10, 100);
|
||||
|
||||
// scroll header 0 off and 1 left
|
||||
self.collectionView.contentOffset = CGPointMake(45, 0);
|
||||
[self.collectionView layoutIfNeeded];
|
||||
IGAssertEqualFrame([self headerForSection:0].frame, 40, 0, 10, 100);
|
||||
IGAssertEqualFrame([self headerForSection:1].frame, 55, 0, 10, 100);
|
||||
}
|
||||
|
||||
- (void)test_whenAdjustingTopYInset_withVaryingHeaderHeights_thatYPositionsUpdated {
|
||||
[self setUpWithStickyHeaders:YES topInset:10];
|
||||
|
||||
|
|
@ -171,12 +244,12 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
|
|||
IGAssertEqualFrame([self headerForSection:0].frame, 0, 30, 100, 10);
|
||||
IGAssertEqualFrame([self headerForSection:1].frame, 0, 45, 100, 10);
|
||||
|
||||
self.layout.stickyHeaderOriginYAdjustment = -10;
|
||||
self.layout.stickyHeaderYOffset = -10;
|
||||
[self.collectionView layoutIfNeeded];
|
||||
IGAssertEqualFrame([self headerForSection:0].frame, 0, 30, 100, 10);
|
||||
IGAssertEqualFrame([self headerForSection:1].frame, 0, 40, 100, 10);
|
||||
|
||||
self.layout.stickyHeaderOriginYAdjustment = 10;
|
||||
self.layout.stickyHeaderYOffset = 10;
|
||||
[self.collectionView layoutIfNeeded];
|
||||
IGAssertEqualFrame([self headerForSection:0].frame, 0, 30, 100, 10);
|
||||
IGAssertEqualFrame([self headerForSection:1].frame, 0, 55, 100, 10);
|
||||
|
|
@ -231,7 +304,27 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
|
|||
IGAssertEqualFrame([self cellForSection:0 item:2].frame, 67, 0, 33, 33);
|
||||
}
|
||||
|
||||
- (void)test_whenSectionsSmallerThanContainerWidth_with0ItemSpacing_with0Insets_with0LineSpacing_thatSectionsFitSameRow {
|
||||
- (void)test_whenItemsLargerThanContainerHeight_withHorizontalScrolling_with5PointItemSpacing_with0Insets_with10PointLineSpacing_thatItemsBumpToNewColumn {
|
||||
[self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO];
|
||||
|
||||
[self prepareWithData:@[
|
||||
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
|
||||
lineSpacing:10
|
||||
interitemSpacing:5
|
||||
headerHeight:0
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}],
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}],
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}],
|
||||
]],
|
||||
]];
|
||||
XCTAssertEqual(self.collectionView.contentSize.width, 76);
|
||||
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33);
|
||||
IGAssertEqualFrame([self cellForSection:0 item:1].frame, 0, 38, 33, 33);
|
||||
IGAssertEqualFrame([self cellForSection:0 item:2].frame, 43, 0, 33, 33);
|
||||
}
|
||||
|
||||
- (void)test_whenSectionsSmallerThanContainerWidth_withVerticalScrolling_with0ItemSpacing_with0Insets_with0LineSpacing_thatSectionsFitSameRow {
|
||||
[self setUpWithStickyHeaders:NO topInset:0];
|
||||
|
||||
[self prepareWithData:@[
|
||||
|
|
@ -263,6 +356,38 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
|
|||
IGAssertEqualFrame([self cellForSection:2 item:0].frame, 66, 0, 33, 33);
|
||||
}
|
||||
|
||||
- (void)test_whenSectionsSmallerThanContainerHeight_withHorizontalScrolling_with0ItemSpacing_with0Insets_with0LineSpacing_thatSectionsFitSameColumn {
|
||||
[self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO];
|
||||
|
||||
[self prepareWithData:@[
|
||||
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
|
||||
lineSpacing:0
|
||||
interitemSpacing:0
|
||||
headerHeight:0
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}],
|
||||
]],
|
||||
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
|
||||
lineSpacing:0
|
||||
interitemSpacing:0
|
||||
headerHeight:0
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}],
|
||||
]],
|
||||
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
|
||||
lineSpacing:0
|
||||
interitemSpacing:0
|
||||
headerHeight:0
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}],
|
||||
]],
|
||||
]];
|
||||
XCTAssertEqual(self.collectionView.contentSize.width, 33);
|
||||
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33);
|
||||
IGAssertEqualFrame([self cellForSection:1 item:0].frame, 0, 33, 33, 33);
|
||||
IGAssertEqualFrame([self cellForSection:2 item:0].frame, 0, 66, 33, 33);
|
||||
}
|
||||
|
||||
- (void)test_whenSectionsSmallerThanContainerWidth_withHalfPointSpacing_with0Insets_with0LineSpacing_thatSectionsFitSameRow {
|
||||
[self setUpWithStickyHeaders:NO topInset:0];
|
||||
|
||||
|
|
@ -384,6 +509,30 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
|
|||
IGAssertEqualFrame([self cellForSection:1 item:0].frame, 0, 43, 33, 33);
|
||||
}
|
||||
|
||||
- (void)test_whenSectionsSmallerThanHeight_withHorizontalScrolling_withSectionHeader_thatHeaderCausesNewline {
|
||||
[self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO];
|
||||
|
||||
[self prepareWithData:@[
|
||||
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
|
||||
lineSpacing:0
|
||||
interitemSpacing:0
|
||||
headerHeight:0
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}],
|
||||
]],
|
||||
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
|
||||
lineSpacing:0
|
||||
interitemSpacing:0
|
||||
headerHeight:10
|
||||
items:@[
|
||||
[[IGLayoutTestItem alloc] initWithSize:(CGSize){33,33}],
|
||||
]],
|
||||
]];
|
||||
XCTAssertEqual(self.collectionView.contentSize.width, 76);
|
||||
IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33);
|
||||
IGAssertEqualFrame([self cellForSection:1 item:0].frame, 43, 0, 33, 33);
|
||||
}
|
||||
|
||||
- (void)test_whenBatchItemUpdates_withHeaderHeight_withLineSpacing_withInsets_thatLayoutCorrectAfterUpdates {
|
||||
[self setUpWithStickyHeaders:NO topInset:0];
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ static NSString * const kHeaderIdentifier = @"header";
|
|||
}
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
|
||||
return CGSizeMake(CGRectGetWidth(collectionView.bounds), self.sections[section].headerHeight);
|
||||
return CGSizeMake(self.sections[section].headerHeight, self.sections[section].headerHeight); // Only the dimension along scrolling direction is used
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
|||
Loading…
Reference in a new issue