blob: 6e4432aa4bf708c8d443e71c9beda19860fe1795 [file] [log] [blame]
/*
Copyright 2016-present the Material Components for iOS authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MDCCollectionViewFlowLayout.h"
#import "MDCCollectionViewController.h"
#import "MDCCollectionViewEditingDelegate.h"
#import "MDCCollectionViewStyling.h"
#import "MaterialCollectionLayoutAttributes.h"
#import "private/MDCCollectionGridBackgroundView.h"
#import "private/MDCCollectionInfoBarView.h"
#import "private/MDCCollectionViewEditor.h"
#include <tgmath.h>
/** The grid background decoration view kind. */
NSString *const kCollectionGridDecorationView = @"MDCCollectionGridDecorationView";
static const NSInteger kSupplementaryViewZIndex = 99;
@implementation MDCCollectionViewFlowLayout {
NSMutableArray<NSIndexPath *> *_deletedIndexPaths;
NSMutableArray<NSIndexPath *> *_insertedIndexPaths;
NSMutableIndexSet *_deletedSections;
NSMutableIndexSet *_insertedSections;
NSMutableIndexSet *_headerSections;
NSMutableIndexSet *_footerSections;
NSMutableDictionary *_decorationViewAttributeCache;
}
- (instancetype)init {
self = [super init];
if (self != nil) {
[self commonMDCCollectionViewFlowLayoutInit];
}
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self != nil) {
// TODO(#): Use values from decoder, don't overwrite in commonInit
[self commonMDCCollectionViewFlowLayoutInit];
}
return self;
}
- (void)commonMDCCollectionViewFlowLayoutInit {
// Defaults.
self.minimumLineSpacing = 0;
self.minimumInteritemSpacing = 0;
self.scrollDirection = UICollectionViewScrollDirectionVertical;
self.sectionInset = UIEdgeInsetsZero;
// Register decoration view for grid background.
_decorationViewAttributeCache = [NSMutableDictionary dictionary];
[self registerClass:[MDCCollectionGridBackgroundView class]
forDecorationViewOfKind:kCollectionGridDecorationView];
}
- (id<MDCCollectionViewEditing>)editor {
if ([self.collectionView.delegate isKindOfClass:[MDCCollectionViewController class]]) {
MDCCollectionViewController *controller =
(MDCCollectionViewController *)self.collectionView.delegate;
return controller.editor;
}
return nil;
}
- (id<MDCCollectionViewStyling>)styler {
if ([self.collectionView.delegate isKindOfClass:[MDCCollectionViewController class]]) {
MDCCollectionViewController *controller =
(MDCCollectionViewController *)self.collectionView.delegate;
return controller.styler;
}
return nil;
}
#pragma mark - UICollectionViewLayout (SubclassingHooks)
- (NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:
(CGRect)rect {
// If performing appearance animation, increase bounds height in order to retrieve additional
// offscreen attributes needed during animation.
rect = [self boundsForAppearanceAnimationWithInitialBounds:rect];
NSMutableArray<__kindof UICollectionViewLayoutAttributes *> *attributes =
[[NSMutableArray alloc] initWithArray:[super layoutAttributesForElementsInRect:rect]
copyItems:YES];
// Store index path sections of any headers/footers within these attributes.
[self storeSupplementaryViewsWithAttributes:attributes];
// Set layout attributes.
for (MDCCollectionViewLayoutAttributes *attr in attributes) {
[self updateAttribute:attr];
}
// Add info bar header/footer supplementary view if necessary.
[self addInfoBarAttributesIfNecessary:attributes];
// Begin cell appearance animation if necessary.
[self beginCellAppearanceAnimationIfNecessary:attributes];
// Add a grid background decoration view for each section if necessary.
[self addDecorationViewIfNecessary:attributes];
return attributes;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
if (!CGSizeEqualToSize(self.collectionView.bounds.size, newBounds.size) ||
self.editor.isEditing) {
// Invalidate the layout to force cells to respect the new collection view bounds. Doing here
// removes necessity to implement methods -willRotateToInterfaceOrientation:duration: and/or
// -viewWillTransitionToSize:withTransitionCoordinator: on the collection view controller.
[self invalidateLayout];
return YES;
}
return NO;
}
- (void)invalidateLayout {
[super invalidateLayout];
// Clear decoration attribute cache.
[_decorationViewAttributeCache removeAllObjects];
}
#pragma mark - UICollectionViewLayout (UISubclassingHooks)
+ (Class)layoutAttributesClass {
return [MDCCollectionViewLayoutAttributes class];
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attr =
[[super layoutAttributesForItemAtIndexPath:indexPath] copy];
return [self updateAttribute:(MDCCollectionViewLayoutAttributes *)attr];
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind
atIndexPath:
(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attr;
if ([kind isEqualToString:UICollectionElementKindSectionHeader] ||
[kind isEqualToString:UICollectionElementKindSectionFooter]) {
// Update section headers/Footers attributes.
attr = [[super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath] copy];
if (!attr) {
attr =
[MDCCollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:kind
withIndexPath:indexPath];
}
[self updateAttribute:(MDCCollectionViewLayoutAttributes *)attr];
} else {
// Update editing info bar attributes.
attr = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:kind
withIndexPath:indexPath];
// Force the info bar supplementary views to stay fixed to their respective positions
// at top/bottom of the collectionView bounds.
CGFloat offsetY = 0;
CGRect currentBounds = self.collectionView.bounds;
attr.zIndex = kSupplementaryViewZIndex;
if ([kind isEqualToString:MDCCollectionInfoBarKindHeader]) {
attr.size = CGSizeMake(CGRectGetWidth(currentBounds), MDCCollectionInfoBarHeaderHeight);
// Allow header to move upwards with scroll, but prevent from moving downwards with scroll.
CGFloat insetTop = self.collectionView.contentInset.top;
#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
if (@available(iOS 11.0, *)) {
insetTop = self.collectionView.adjustedContentInset.top;
}
#endif
CGFloat boundsY = currentBounds.origin.y;
CGFloat maxOffsetY = MAX(boundsY + insetTop, 0);
offsetY = boundsY + (attr.size.height / 2) + insetTop - maxOffsetY;
} else if ([kind isEqualToString:MDCCollectionInfoBarKindFooter]) {
CGFloat height = MDCCollectionInfoBarFooterHeight;
#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
if (@available(iOS 11.0, *)) {
height += self.collectionView.safeAreaInsets.bottom;
}
#endif
attr.size = CGSizeMake(CGRectGetWidth(currentBounds), height);
offsetY = currentBounds.origin.y + currentBounds.size.height - (attr.size.height / 2);
}
attr.center = CGPointMake(CGRectGetMidX(currentBounds), offsetY);
}
return attr;
}
- (UICollectionViewLayoutAttributes *)
layoutAttributesForDecorationViewOfKind:(NSString *)elementKind
atIndexPath:(NSIndexPath *)indexPath {
// Check cache for decoration view attributes, and add to cache if they don't exist.
MDCCollectionViewLayoutAttributes *decorationAttr =
[_decorationViewAttributeCache objectForKey:indexPath];
if (!decorationAttr) {
decorationAttr =
[MDCCollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:elementKind
withIndexPath:indexPath];
[_decorationViewAttributeCache setObject:decorationAttr forKey:indexPath];
}
// Determine section frame by summing all of its item frames.
CGRect sectionFrame = CGRectNull;
for (NSInteger i = 0; i < [self numberOfItemsInSection:indexPath.section]; ++i) {
indexPath = [NSIndexPath indexPathForItem:i inSection:indexPath.section];
UICollectionViewLayoutAttributes *attribute =
[self layoutAttributesForItemAtIndexPath:indexPath];
if (!CGRectIsNull(attribute.frame)) {
sectionFrame = CGRectUnion(sectionFrame, attribute.frame);
}
}
if (!CGRectIsNull(sectionFrame)) {
decorationAttr.frame = sectionFrame;
}
decorationAttr.zIndex = -1;
return decorationAttr;
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(__unused CGPoint)proposedContentOffset {
// Return current contentOffset to prevent any layout animations from jumping to new offset.
return [super targetContentOffsetForProposedContentOffset:self.collectionView.contentOffset];
}
#pragma mark - UICollectionViewLayout (UIUpdateSupportHooks)
- (void)prepareForCollectionViewUpdates:(NSArray<UICollectionViewUpdateItem *> *)updateItems {
[super prepareForCollectionViewUpdates:updateItems];
_deletedIndexPaths = [NSMutableArray array];
_insertedIndexPaths = [NSMutableArray array];
_deletedSections = [NSMutableIndexSet indexSet];
_insertedSections = [NSMutableIndexSet indexSet];
for (UICollectionViewUpdateItem *item in updateItems) {
if (item.updateAction == UICollectionUpdateActionDelete) {
// Store deleted sections or indexPaths.
if (item.indexPathBeforeUpdate.item == NSNotFound) {
[_deletedSections addIndex:item.indexPathBeforeUpdate.section];
} else {
[_deletedIndexPaths addObject:item.indexPathBeforeUpdate];
}
} else if (item.updateAction == UICollectionUpdateActionInsert) {
// Store inserted sections or indexPaths.
if (item.indexPathAfterUpdate.item == NSNotFound) {
[_insertedSections addIndex:item.indexPathAfterUpdate.section];
} else {
[_insertedIndexPaths addObject:item.indexPathAfterUpdate];
}
}
}
}
- (void)finalizeCollectionViewUpdates {
[super finalizeCollectionViewUpdates];
_deletedIndexPaths = nil;
_insertedIndexPaths = nil;
_deletedSections = nil;
_insertedSections = nil;
}
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:
(NSIndexPath *)itemIndexPath {
UICollectionViewLayoutAttributes *attr =
[[super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath] copy];
// Adding new section or item.
if ([_insertedSections containsIndex:itemIndexPath.section] ||
[_insertedIndexPaths containsObject:itemIndexPath]) {
attr.transform = CGAffineTransformMakeTranslation(0, -CGRectGetHeight(attr.bounds) / 2);
} else {
attr.alpha = 1;
}
return attr;
}
- (UICollectionViewLayoutAttributes *)
initialLayoutAttributesForAppearingSupplementaryElementOfKind:(NSString *)elementKind
atIndexPath:(NSIndexPath *)elementIndexPath {
UICollectionViewLayoutAttributes *attr =
[[super initialLayoutAttributesForAppearingSupplementaryElementOfKind:elementKind
atIndexPath:elementIndexPath] copy];
if ([elementKind isEqualToString:UICollectionElementKindSectionHeader] ||
[elementKind isEqualToString:UICollectionElementKindSectionFooter]) {
// Adding new section header or footer.
if ([_insertedSections containsIndex:elementIndexPath.section]) {
attr.transform = CGAffineTransformMakeTranslation(0, -CGRectGetHeight(attr.bounds) / 2);
} else {
attr.alpha = 1;
}
}
return attr;
}
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:
(NSIndexPath *)itemIndexPath {
UICollectionViewLayoutAttributes *attr =
[[super finalLayoutAttributesForDisappearingItemAtIndexPath:itemIndexPath] copy];
// Deleting section or item.
if ([_deletedSections containsIndex:itemIndexPath.section] ||
[_deletedIndexPaths containsObject:itemIndexPath]) {
attr.transform = CGAffineTransformMakeTranslation(0, -CGRectGetHeight(attr.bounds) / 2);
} else {
attr.alpha = 1;
}
return attr;
}
- (UICollectionViewLayoutAttributes *)
finalLayoutAttributesForDisappearingSupplementaryElementOfKind:(NSString *)elementKind
atIndexPath:(NSIndexPath *)elementIndexPath {
UICollectionViewLayoutAttributes *attr = [[super
finalLayoutAttributesForDisappearingSupplementaryElementOfKind:elementKind
atIndexPath:elementIndexPath] copy];
if ([elementKind isEqualToString:UICollectionElementKindSectionHeader] ||
[elementKind isEqualToString:UICollectionElementKindSectionFooter]) {
// Deleting section header or footer.
if ([_deletedSections containsIndex:elementIndexPath.section]) {
attr.transform = CGAffineTransformMakeTranslation(0, -CGRectGetHeight(attr.bounds) / 2);
} else {
attr.alpha = 1;
}
}
return attr;
}
#pragma mark - Header/Footer Caching
- (void)storeSupplementaryViewsWithAttributes:
(NSArray<__kindof UICollectionViewLayoutAttributes *> *)attributes {
_headerSections = [NSMutableIndexSet indexSet];
_footerSections = [NSMutableIndexSet indexSet];
// Store index path sections for headers/footers.
for (MDCCollectionViewLayoutAttributes *attr in attributes) {
if ([attr.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
[_headerSections addIndex:attr.indexPath.section];
} else if ([attr.representedElementKind isEqualToString:UICollectionElementKindSectionFooter]) {
[_footerSections addIndex:attr.indexPath.section];
}
}
}
#pragma mark - Private
- (MDCCollectionViewLayoutAttributes *)updateAttribute:(MDCCollectionViewLayoutAttributes *)attr {
if (attr.representedElementCategory == UICollectionElementCategoryCell) {
attr.editing = self.editor.isEditing;
}
attr.isGridLayout = NO;
if (self.styler.cellLayoutType == MDCCollectionViewCellLayoutTypeList) {
attr.sectionOrdinalPosition = [self ordinalPositionForListElementWithAttribute:attr];
} else if (self.styler.cellLayoutType == MDCCollectionViewCellLayoutTypeGrid) {
attr.sectionOrdinalPosition = [self ordinalPositionForGridElementWithAttribute:attr];
attr.isGridLayout = YES;
}
[self updateCellStateMaskWithAttribute:attr];
if (attr.representedElementCategory == UICollectionElementCategorySupplementaryView) {
attr = [self updateSupplementaryViewAttribute:attr];
}
// Set cell background.
attr.backgroundImage = [self.styler backgroundImageForCellLayoutAttributes:attr];
attr.backgroundImageViewInsets =
[self.styler backgroundImageViewOutsetsForCellWithAttribute:attr];
// Set separator styling.
attr.separatorColor = self.styler.separatorColor;
attr.separatorInset = self.styler.separatorInset;
attr.separatorLineHeight = self.styler.separatorLineHeight;
attr.shouldHideSeparators = [self.styler shouldHideSeparatorForCellLayoutAttributes:attr];
// Set inlay and hidden state if necessary.
[self inlayAttributeIfNecessary:attr];
[self hideAttributeIfNecessary:attr];
return attr;
}
- (MDCCollectionViewLayoutAttributes *)updateSupplementaryViewAttribute:
(MDCCollectionViewLayoutAttributes *)attr {
// In vertical scrolling, supplementary views only respect their height and ignore their width
// value. The opposite is true for horizontal scrolling. Therefore we must manually set insets
// on both the backgroundView and contentView in order to match the insets of the collection
// view rows.
CGRect insetFrame = attr.frame;
if (!CGRectIsEmpty(insetFrame)) {
UIEdgeInsets insets;
// Retrieve the insets from the Flow Layout delegate to maintain consistency with the CVC
if ([self.collectionView.delegate
respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)]) {
id<UICollectionViewDelegateFlowLayout> flowLayoutDelegate =
(id<UICollectionViewDelegateFlowLayout>)self.collectionView.delegate;
insets = [flowLayoutDelegate collectionView:self.collectionView
layout:self.collectionView.collectionViewLayout
insetForSectionAtIndex:attr.indexPath.section];
} else {
insets = [self insetsAtSectionIndex:attr.indexPath.section];
}
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
insetFrame = CGRectInset(insetFrame, insets.left / 2 + insets.right / 2, 0);
if ([attr.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
insetFrame.origin.y += insets.top;
} else if ([attr.representedElementKind
isEqualToString:UICollectionElementKindSectionFooter]) {
insetFrame.origin.y -= insets.bottom;
}
attr.frame = insetFrame;
}
}
return attr;
}
- (UIEdgeInsets)insetsAtSectionIndex:(NSInteger)section {
// Determine insets based on cell style.
CGFloat inset = (CGFloat)floor(MDCCollectionViewCellStyleCardSectionInset);
UIEdgeInsets insets = UIEdgeInsetsZero;
NSInteger numberOfSections = self.collectionView.numberOfSections;
BOOL isTop = (section == 0);
BOOL isBottom = (section == numberOfSections - 1);
MDCCollectionViewCellStyle cellStyle = [self.styler cellStyleAtSectionIndex:section];
BOOL isCardStyle = cellStyle == MDCCollectionViewCellStyleCard;
BOOL isGroupedStyle = cellStyle == MDCCollectionViewCellStyleGrouped;
// Set left/right insets.
if (isCardStyle) {
insets.left = inset;
insets.right = inset;
}
// Set top/bottom insets.
if (isCardStyle || isGroupedStyle) {
insets.top = (CGFloat)floor((isTop) ? inset : inset / 2.0f);
insets.bottom = (CGFloat)floor((isBottom) ? inset : inset / 2.0f);
}
return insets;
}
- (MDCCollectionViewOrdinalPosition)ordinalPositionForListElementWithAttribute:
(MDCCollectionViewLayoutAttributes *)attr {
// Returns the ordinal position of cells and supplementary views within a list layout. This is
// used to determine the layout attributes applied to their styling.
MDCCollectionViewOrdinalPosition position = 0;
NSIndexPath *indexPath = attr.indexPath;
NSInteger numberOfItemsInSection = [self numberOfItemsInSection:indexPath.section];
BOOL isTop = NO;
BOOL isBottom = NO;
BOOL hasSectionHeader = [_headerSections containsIndex:indexPath.section];
BOOL hasSectionFooter = [_footerSections containsIndex:indexPath.section];
BOOL hasSectionItems = [self numberOfItemsInSection:indexPath.section] > 0;
BOOL hidesHeaderBackground = NO;
if ([self.styler.delegate
respondsToSelector:@selector(collectionView:shouldHideHeaderBackgroundForSection:)]) {
hidesHeaderBackground = [self.styler.delegate collectionView:self.styler.collectionView
shouldHideHeaderBackgroundForSection:indexPath.section];
}
BOOL hidesFooterBackground = NO;
if ([self.styler.delegate
respondsToSelector:@selector(collectionView:shouldHideFooterBackgroundForSection:)]) {
hidesFooterBackground = [self.styler.delegate collectionView:self.styler.collectionView
shouldHideFooterBackgroundForSection:indexPath.section];
}
if (attr.representedElementCategory == UICollectionElementCategoryCell) {
isTop = (indexPath.item == 0) && (!hasSectionHeader || hidesHeaderBackground);
isBottom = (indexPath.item == numberOfItemsInSection - 1) &&
(!hasSectionFooter || hidesFooterBackground);
} else if (attr.representedElementCategory == UICollectionElementCategorySupplementaryView) {
NSString *kind = attr.representedElementKind;
BOOL isElementHeader = ([kind isEqualToString:UICollectionElementKindSectionHeader]);
BOOL isElementFooter = ([kind isEqualToString:UICollectionElementKindSectionFooter]);
isTop = (isElementFooter && !hasSectionItems && !hasSectionHeader) || isElementHeader;
isBottom = (isElementHeader && !hasSectionItems && !hasSectionFooter) || isElementFooter;
}
if (attr.editing || [self.styler isItemInlaidAtIndexPath:attr.indexPath]) {
isTop = YES;
isBottom = YES;
}
if (!isTop && !isBottom) {
position |= MDCCollectionViewOrdinalPositionVerticalCenter;
} else {
position |= isTop ? MDCCollectionViewOrdinalPositionVerticalTop : position;
position |= isBottom ? MDCCollectionViewOrdinalPositionVerticalBottom : position;
}
return position;
}
- (MDCCollectionViewOrdinalPosition)ordinalPositionForGridElementWithAttribute:
(MDCCollectionViewLayoutAttributes *)attr {
// Returns the ordinal position of cells and supplementary views within a grid layout. This is
// used to determine the layout attributes applied to their styling.
MDCCollectionViewOrdinalPosition position = 0;
NSIndexPath *indexPath = attr.indexPath;
NSInteger numberOfItemsInSection = [self numberOfItemsInSection:indexPath.section];
NSInteger gridColumnCount = self.styler.gridColumnCount;
NSInteger maxRowIndex = (NSInteger)(floor(numberOfItemsInSection / gridColumnCount) - 1);
NSInteger maxColumnIndex = gridColumnCount - 1;
NSInteger ordinalRow = (NSInteger)(floor(indexPath.item / gridColumnCount));
NSInteger ordinalColumn = (NSInteger)(floor(indexPath.item % gridColumnCount));
// Set vertical ordinal position.
if (ordinalRow > 0 && ordinalRow < maxRowIndex) {
position = position | MDCCollectionViewOrdinalPositionVerticalCenter;
} else {
position =
(ordinalRow == 0) ? position | MDCCollectionViewOrdinalPositionVerticalTop : position;
position = (ordinalRow == maxRowIndex)
? position | MDCCollectionViewOrdinalPositionVerticalBottom
: position;
}
// Set horizontal ordinal position.
if (ordinalColumn > 0 && ordinalColumn < maxColumnIndex) {
position = position | MDCCollectionViewOrdinalPositionHorizontalCenter;
} else {
position =
(ordinalColumn == 0) ? position | MDCCollectionViewOrdinalPositionHorizontalLeft : position;
position = (ordinalColumn == maxColumnIndex)
? position | MDCCollectionViewOrdinalPositionHorizontalRight
: position;
}
return position;
}
- (void)updateCellStateMaskWithAttribute:(MDCCollectionViewLayoutAttributes *)attr {
attr.shouldShowSelectorStateMask = NO;
attr.shouldShowReorderStateMask = NO;
// Determine proper state to show cell if editing.
if (attr.editing) {
if ([self.collectionView.dataSource
conformsToProtocol:@protocol(MDCCollectionViewEditingDelegate)]) {
id<MDCCollectionViewEditingDelegate> editingDelegate =
(id<MDCCollectionViewEditingDelegate>)self.collectionView.dataSource;
// Check if delegate can select during editing.
if ([editingDelegate respondsToSelector:@selector
(collectionView:canSelectItemDuringEditingAtIndexPath:)]) {
attr.shouldShowSelectorStateMask = [editingDelegate collectionView:self.collectionView
canSelectItemDuringEditingAtIndexPath:attr.indexPath];
}
// Check if delegate can reorder.
if ([editingDelegate respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:)]) {
attr.shouldShowReorderStateMask = [editingDelegate collectionView:self.collectionView
canMoveItemAtIndexPath:attr.indexPath];
}
}
}
}
- (void)inlayAttributeIfNecessary:(MDCCollectionViewLayoutAttributes *)attr {
// Inlay this attribute if necessary.
CGFloat inset = MDCCollectionViewCellStyleCardSectionInset;
UIEdgeInsets inlayInsets = UIEdgeInsetsZero;
NSInteger item = attr.indexPath.item;
NSArray<NSIndexPath *> *inlaidIndexPaths = [self.styler indexPathsForInlaidItems];
// Update ordinal position for index paths adjacent to inlaid index path.
for (NSIndexPath *inlaidIndexPath in inlaidIndexPaths) {
if (inlaidIndexPath.section == attr.indexPath.section) {
NSInteger numberOfItemsInSection = [self numberOfItemsInSection:inlaidIndexPath.section];
if (attr.representedElementCategory == UICollectionElementCategoryCell) {
if (item == inlaidIndexPath.item) {
// Get previous and next index paths to the inlaid index path.
BOOL prevAttrIsInlaid = NO;
BOOL nextAttrIsInlaid = NO;
BOOL hasSectionHeader = [_headerSections containsIndex:inlaidIndexPath.section];
BOOL hasSectionFooter = [_footerSections containsIndex:inlaidIndexPath.section];
if (inlaidIndexPath.item > 0 || hasSectionHeader) {
NSIndexPath *prevIndexPath = [NSIndexPath indexPathForItem:(inlaidIndexPath.item - 1)
inSection:inlaidIndexPath.section];
prevAttrIsInlaid = [self.styler isItemInlaidAtIndexPath:prevIndexPath];
inlayInsets.top = prevAttrIsInlaid ? inset / 2 : inset;
}
if (inlaidIndexPath.item < numberOfItemsInSection - 1 || hasSectionFooter) {
NSIndexPath *nextIndexPath = [NSIndexPath indexPathForItem:(inlaidIndexPath.item + 1)
inSection:inlaidIndexPath.section];
nextAttrIsInlaid = [self.styler isItemInlaidAtIndexPath:nextIndexPath];
inlayInsets.bottom = nextAttrIsInlaid ? inset / 2 : inset;
}
// Is attribute to be inlaid.
attr.frame = UIEdgeInsetsInsetRect(attr.frame, inlayInsets);
attr.sectionOrdinalPosition = MDCCollectionViewOrdinalPositionVerticalTopBottom;
} else if (item == inlaidIndexPath.item - 1) {
// Is previous to inlaid attribute.
if (attr.sectionOrdinalPosition & MDCCollectionViewOrdinalPositionVerticalTop) {
attr.sectionOrdinalPosition = MDCCollectionViewOrdinalPositionVerticalTopBottom;
} else if (attr.sectionOrdinalPosition & MDCCollectionViewOrdinalPositionVerticalCenter) {
attr.sectionOrdinalPosition = MDCCollectionViewOrdinalPositionVerticalBottom;
}
} else if (item == inlaidIndexPath.item + 1) {
// Is next to inlaid attribute.
if (attr.sectionOrdinalPosition & MDCCollectionViewOrdinalPositionVerticalCenter) {
attr.sectionOrdinalPosition = MDCCollectionViewOrdinalPositionVerticalTop;
} else if (attr.sectionOrdinalPosition & MDCCollectionViewOrdinalPositionVerticalBottom) {
attr.sectionOrdinalPosition = MDCCollectionViewOrdinalPositionVerticalBottom;
}
}
} else if (attr.representedElementCategory == UICollectionElementCategorySupplementaryView) {
// If header/footer attribute, update if adjacent to inlaid index path.
NSString *kind = attr.representedElementKind;
BOOL isElementHeader = ([kind isEqualToString:UICollectionElementKindSectionHeader]);
BOOL isElementFooter = ([kind isEqualToString:UICollectionElementKindSectionFooter]);
if (isElementHeader && inlaidIndexPath.item == 0) {
attr.sectionOrdinalPosition = MDCCollectionViewOrdinalPositionVerticalTopBottom;
} else if (isElementFooter && inlaidIndexPath.item == numberOfItemsInSection - 1) {
attr.sectionOrdinalPosition = MDCCollectionViewOrdinalPositionVerticalTopBottom;
}
}
}
}
}
- (void)hideAttributeIfNecessary:(MDCCollectionViewLayoutAttributes *)attr {
if (self.editor) {
// Hide the attribute if the editor is either currently handling a cell item or section swipe
// for dismissal, or is reordering a cell item.
BOOL isCell = attr.representedElementCategory == UICollectionElementCategoryCell;
if (attr.indexPath.section == self.editor.dismissingSection ||
([attr.indexPath isEqual:self.editor.dismissingCellIndexPath] && isCell) ||
([attr.indexPath isEqual:self.editor.reorderingCellIndexPath] && isCell)) {
attr.hidden = YES;
}
}
}
- (void)addInfoBarAttributesIfNecessary:
(NSMutableArray<__kindof UICollectionViewLayoutAttributes *> *)attributes {
if (self.editor.isEditing && [attributes count] > 0) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
// Add header info bar if editing.
[attributes
addObject:[self layoutAttributesForSupplementaryViewOfKind:MDCCollectionInfoBarKindHeader
atIndexPath:indexPath]];
// Add footer info bar if editing and item(s) are selected.
NSInteger selectedItemCount = [self.collectionView.indexPathsForSelectedItems count];
if (selectedItemCount > 0) {
[attributes
addObject:[self layoutAttributesForSupplementaryViewOfKind:MDCCollectionInfoBarKindFooter
atIndexPath:indexPath]];
}
}
}
- (void)addDecorationViewIfNecessary:
(NSMutableArray<__kindof UICollectionViewLayoutAttributes *> *)attributes {
// If necessary, adds a decoration view to a section drawn below its items. This will only happen
// for a grid layout when it is either A) grouped-style or B) card-style with zero padding. When
// this happens, the background for those items will not be drawn, and instead this decoration
// view will extend to the bounds of the sum of its respective section item frames. Shadowing and
// border will be applied to this decoration view as per the styler settings.
if (self.styler.cellLayoutType == MDCCollectionViewCellLayoutTypeGrid) {
NSMutableSet *sectionSet = [NSMutableSet set];
BOOL shouldShowGridBackground = NO;
NSMutableArray<__kindof UICollectionViewLayoutAttributes *> *decorationAttributes =
[NSMutableArray array];
for (MDCCollectionViewLayoutAttributes *attr in attributes) {
NSInteger section = attr.indexPath.section;
// Only add one decoration view per section.
if (![sectionSet containsObject:@(section)]) {
NSIndexPath *decorationIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
MDCCollectionViewLayoutAttributes *decorationAttr =
(MDCCollectionViewLayoutAttributes *)[self
layoutAttributesForDecorationViewOfKind:kCollectionGridDecorationView
atIndexPath:decorationIndexPath];
shouldShowGridBackground = [self shouldShowGridBackgroundWithAttribute:decorationAttr];
decorationAttr.shouldShowGridBackground = shouldShowGridBackground;
decorationAttr.backgroundImage =
shouldShowGridBackground
? [self.styler backgroundImageForCellLayoutAttributes:decorationAttr]
: nil;
[decorationAttributes addObject:decorationAttr];
[sectionSet addObject:@(section)];
}
if (shouldShowGridBackground) {
attr.backgroundImage = nil;
}
}
[attributes addObjectsFromArray:decorationAttributes];
}
}
- (BOOL)shouldShowGridBackgroundWithAttribute:(__unused MDCCollectionViewLayoutAttributes *)attr {
// Determine whether to show grid background.
if (self.styler.cellLayoutType == MDCCollectionViewCellLayoutTypeGrid) {
if (self.styler.cellStyle == MDCCollectionViewCellStyleGrouped ||
(self.styler.cellStyle == MDCCollectionViewCellStyleCard && self.styler.gridPadding == 0)) {
return YES;
}
}
return NO;
}
- (NSInteger)numberOfItemsInSection:(NSInteger)section {
return [self.collectionView numberOfItemsInSection:section];
}
#pragma mark - Cell Appearance Animation
- (CGRect)boundsForAppearanceAnimationWithInitialBounds:(CGRect)initialBounds {
// Increase initial bounds by 25% allowing offscreen attributes to be included in the
// appearance animation.
if (self.styler.shouldAnimateCellsOnAppearance && self.styler.willAnimateCellsOnAppearance) {
CGRect newBounds = initialBounds;
newBounds.size.height += (newBounds.size.height / 4);
return newBounds;
}
return initialBounds;
}
- (void)beginCellAppearanceAnimationIfNecessary:
(NSMutableArray<__kindof UICollectionViewLayoutAttributes *> *)attributes {
// Here we want to assign a delay to each attribute such that the animation will fade-in from the
// top downwards in a staggered manner. However, the array of attributes we receive here are not
// in the correct order and must be sorted and re-ordered to properly assign these delays.
//
// First we will sort the array of attributes by index path to ensure proper ordering. Secondly
// we will manipulate the array to bring any headers before their first respective cell items.
//
// When completed, we will end up with an array of attributes in the form of
// header -> item -> footer ... repeated for each section. Now we can use this ordered array
// to assign delays based on their proper ordinal position from top down.
__block NSInteger attributeCount = attributes.count;
NSTimeInterval duration = self.styler.animateCellsOnAppearanceDuration;
if (self.styler.shouldAnimateCellsOnAppearance && attributeCount > 0) {
// First sort by index path.
NSArray<__kindof UICollectionViewLayoutAttributes *> *sortedByIndexPath = [attributes
sortedArrayUsingComparator:^NSComparisonResult(MDCCollectionViewLayoutAttributes *attr1,
MDCCollectionViewLayoutAttributes *attr2) {
return [attr1.indexPath compare:attr2.indexPath];
}];
// Next create new array containing attributes in the form of header -> item -> footer.
NSMutableArray<__kindof UICollectionViewLayoutAttributes *> *sortedAttributes =
[NSMutableArray array];
[sortedByIndexPath enumerateObjectsUsingBlock:^(MDCCollectionViewLayoutAttributes *attr,
__unused NSUInteger idx, __unused BOOL *stop) {
if (sortedAttributes.count > 0) {
// Check if current attribute is a header and previous attribute is an item. If so,
// insert the current header attribute before the cell.
MDCCollectionViewLayoutAttributes *prevAttr =
[sortedAttributes objectAtIndex:sortedAttributes.count - 1];
BOOL prevAttrIsItem =
prevAttr.representedElementCategory == UICollectionElementCategoryCell;
BOOL attrIsHeader =
[attr.representedElementKind isEqualToString:UICollectionElementKindSectionHeader];
if (attrIsHeader && prevAttrIsItem) {
[sortedAttributes insertObject:attr atIndex:sortedAttributes.count - 1];
return;
}
if ([attr.representedElementKind isEqualToString:MDCCollectionInfoBarKindHeader] ||
[attr.representedElementKind isEqualToString:MDCCollectionInfoBarKindFooter]) {
// Reduce the attributeCount here to reflect only attributes that can be animated.
attributeCount--;
return;
}
}
[sortedAttributes addObject:attr];
}];
// Now assign delays and add padding to frame Y coordinate which gets removed during animation.
[sortedAttributes enumerateObjectsUsingBlock:^(MDCCollectionViewLayoutAttributes *attr,
NSUInteger idx, __unused BOOL *stop) {
// If the element is an info bar header, then don't do anything.
attr.willAnimateCellsOnAppearance = self.styler.willAnimateCellsOnAppearance;
attr.animateCellsOnAppearanceDuration = self.styler.animateCellsOnAppearanceDuration;
attr.animateCellsOnAppearanceDelay =
(attributeCount > 0) ? ((CGFloat)idx / attributeCount) * duration : 0;
if (self.styler.willAnimateCellsOnAppearance) {
CGRect frame = attr.frame;
frame.origin.y += self.styler.animateCellsOnAppearancePadding;
attr.frame = frame;
}
}];
// Call asynchronously to allow the current layout cycle to complete before issuing animations.
if (self.styler.willAnimateCellsOnAppearance) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.styler beginCellAppearanceAnimation];
});
}
}
}
@end