| // 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 "MDCCollectionViewStyler.h" |
| |
| #import "MDCCollectionViewLayoutAttributes.h" |
| #import "MDCCollectionViewStyling.h" |
| #import "MDCCollectionViewStylingDelegate.h" |
| #import "MDCPalettes.h" |
| #import "UIColor+MaterialDynamic.h" |
| |
| #include <tgmath.h> |
| |
| #if defined(TARGET_OS_VISION) && TARGET_OS_VISION |
| // For code review, use the review queue listed inĀ go/material-visionos-review. |
| #define IS_VISIONOS 1 |
| #else |
| #define IS_VISIONOS 0 |
| #endif |
| |
| typedef NS_OPTIONS(NSUInteger, BackgroundCacheKey) { |
| BackgroundCacheKeyFlat = 0, |
| BackgroundCacheKeyTop = 1 << 0, |
| BackgroundCacheKeyBottom = 1 << 1, |
| BackgroundCacheKeyCard = 1 << 2, |
| BackgroundCacheKeyGrouped = 1 << 3, |
| BackgroundCacheKeyHighlighted = 1 << 4, |
| BackgroundCacheKeyMax = 1 << 5, |
| }; |
| |
| const CGFloat MDCCollectionViewCellStyleCardSectionInset = 8; |
| |
| /** Cell content view insets for card-style cells */ |
| static const CGFloat kFourThirds = (CGFloat)4 / 3; |
| static const UIEdgeInsets kCollectionViewCellContentInsetsRetina3x = {kFourThirds, kFourThirds, |
| kFourThirds, kFourThirds}; |
| static const UIEdgeInsets kCollectionViewCellContentInsetsRetina = {1.5, 1.5, 1.5, 1.5}; |
| static const UIEdgeInsets kCollectionViewCellContentInsets = {1, 2, 1, 2}; |
| |
| /** Default cell separator style settings */ |
| static const CGFloat kCollectionViewCellSeparatorDefaultHeightInPixels = 1; |
| |
| /** Grid layout defaults */ |
| static const NSInteger kCollectionViewGridDefaultColumnCount = 2; |
| static const CGFloat kCollectionViewGridDefaultPadding = 4; |
| |
| /** The drawn cell background */ |
| static const CGSize kCellImageSize = {44, 44}; |
| static const CGFloat kCollectionViewCellDefaultBorderWidth = 1; |
| static const CGFloat kCollectionViewCellDefaultBorderRadius = (CGFloat)1.5; |
| static inline UIColor *kCollectionViewCellDefaultBorderColor(void) { |
| return [UIColor colorWithWhite:0 alpha:(CGFloat)0.05]; |
| } |
| |
| /** Cell shadowing */ |
| static const CGFloat kCollectionViewCellDefaultShadowWidth = 1; |
| static inline CGSize kCollectionViewCellDefaultShadowOffset(void) { return CGSizeMake(0, 1); } |
| static inline UIColor *kCollectionViewCellDefaultShadowColor(void) { |
| return [UIColor colorWithWhite:0 alpha:(CGFloat)0.1]; |
| } |
| |
| /** Animate cell on appearance settings */ |
| static const CGFloat kCollectionViewAnimatedAppearancePadding = 20; |
| static const NSTimeInterval kCollectionViewAnimatedAppearanceDelay = 0.1; |
| static const NSTimeInterval kCollectionViewAnimatedAppearanceDuration = 0.3; |
| |
| /** Modifies only the right and bottom edges of a CGRect. */ |
| NS_INLINE CGRect RectContract(CGRect rect, CGFloat dx, CGFloat dy) { |
| return CGRectMake(rect.origin.x, rect.origin.y, rect.size.width - dx, rect.size.height - dy); |
| } |
| |
| /** Modifies only the top and left edges of a CGRect. */ |
| NS_INLINE CGRect RectShift(CGRect rect, CGFloat dx, CGFloat dy) { |
| return CGRectOffset(RectContract(rect, dx, dy), dx, dy); |
| } |
| |
| @interface MDCCollectionViewStyler () |
| |
| /** |
| A dictionary of NSPointerArray caches, keyed by UIColor, for cached cell background images |
| using that background color. Index into the NSPointerArray using the results of |
| backgroundCacheKeyForCardStyle:isGroupedStyle:isTop:isBottom:isHighlighted: |
| */ |
| @property(nonatomic, readonly) NSMutableDictionary *cellBackgroundCaches; |
| |
| /** An set of index paths for items that are inlaid. */ |
| @property(nonatomic, strong) NSMutableSet *inlaidIndexPathSet; |
| |
| /** The user interface style for the app. */ |
| @property(nonatomic) UIUserInterfaceStyle previousUserInterfaceStyle; |
| |
| @end |
| |
| @implementation MDCCollectionViewStyler |
| |
| @synthesize collectionView = _collectionView; |
| @synthesize delegate = _delegate; |
| @synthesize shouldInvalidateLayout = _shouldInvalidateLayout; |
| @synthesize cellBackgroundColor = _cellBackgroundColor; |
| @synthesize cellLayoutType = _cellLayoutType; |
| @synthesize gridColumnCount = _gridColumnCount; |
| @synthesize gridPadding = _gridPadding; |
| @synthesize cellStyle = _cellStyle; |
| @synthesize cardBorderRadius = _cardBorderRadius; |
| @synthesize separatorColor = _separatorColor; |
| @synthesize separatorInset = _separatorInset; |
| @synthesize separatorLineHeight = _separatorLineHeight; |
| @synthesize shouldHideSeparators = _shouldHideSeparators; |
| @synthesize allowsItemInlay = _allowsItemInlay; |
| @synthesize allowsMultipleItemInlays = _allowsMultipleItemInlays; |
| @synthesize shouldAnimateCellsOnAppearance = _shouldAnimateCellsOnAppearance; |
| @synthesize willAnimateCellsOnAppearance = _willAnimateCellsOnAppearance; |
| @synthesize animateCellsOnAppearancePadding = _animateCellsOnAppearancePadding; |
| @synthesize animateCellsOnAppearanceDuration = _animateCellsOnAppearanceDuration; |
| |
| - (instancetype)initWithCollectionView:(UICollectionView *)collectionView { |
| self = [super init]; |
| if (self) { |
| _collectionView = collectionView; |
| |
| // Cell default style properties. |
| _cellBackgroundColor = [UIColor whiteColor]; |
| _cellStyle = MDCCollectionViewCellStyleDefault; |
| // Background color is 0xEEEEEE |
| _collectionView.backgroundColor = MDCPalette.greyPalette.tint200; |
| _inlaidIndexPathSet = [NSMutableSet set]; |
| _cardBorderRadius = kCollectionViewCellDefaultBorderRadius; |
| |
| // Cell separator defaults. |
| _separatorColor = MDCPalette.greyPalette.tint300; |
| _separatorInset = UIEdgeInsetsZero; |
| |
| #if IS_VISIONOS |
| UITraitCollection *current = [UITraitCollection currentTraitCollection]; |
| CGFloat scale = current ? [current displayScale] : 1.0; |
| _separatorLineHeight = kCollectionViewCellSeparatorDefaultHeightInPixels / scale; |
| #else |
| _separatorLineHeight = |
| kCollectionViewCellSeparatorDefaultHeightInPixels / [[UIScreen mainScreen] scale]; |
| #endif |
| |
| _shouldHideSeparators = NO; |
| |
| // Grid defaults. |
| _cellLayoutType = MDCCollectionViewCellLayoutTypeList; |
| _gridColumnCount = kCollectionViewGridDefaultColumnCount; |
| _gridPadding = kCollectionViewGridDefaultPadding; |
| |
| // Animate cell on appearance settings. |
| _animateCellsOnAppearancePadding = kCollectionViewAnimatedAppearancePadding; |
| _animateCellsOnAppearanceDuration = kCollectionViewAnimatedAppearanceDuration; |
| |
| // Caching. |
| _cellBackgroundCaches = [NSMutableDictionary dictionary]; |
| |
| _previousUserInterfaceStyle = self.collectionView.traitCollection.userInterfaceStyle; |
| } |
| return self; |
| } |
| |
| - (BOOL)isEqual:(id)object { |
| // If shouldInvalidateLayout property is NO, prevent a collection view layout caused when |
| // layout attributes check here if they are equal. |
| return (self == object) && ![self shouldInvalidateLayoutForStyleChange]; |
| } |
| |
| #pragma mark - Cell Appearance Animation |
| |
| - (void)setShouldAnimateCellsOnAppearance:(BOOL)shouldAnimateCellsOnAppearance { |
| _shouldAnimateCellsOnAppearance = shouldAnimateCellsOnAppearance; |
| _willAnimateCellsOnAppearance = shouldAnimateCellsOnAppearance; |
| } |
| |
| - (void)beginCellAppearanceAnimation { |
| if (_shouldAnimateCellsOnAppearance) { |
| _willAnimateCellsOnAppearance = NO; |
| [UIView animateWithDuration:_animateCellsOnAppearanceDuration |
| delay:kCollectionViewAnimatedAppearanceDelay |
| options:UIViewAnimationOptionCurveEaseInOut |
| animations:^{ |
| [self updateLayoutAnimated:YES]; |
| } |
| completion:^(__unused BOOL finished) { |
| [self setShouldAnimateCellsOnAppearance:NO]; |
| }]; |
| } |
| } |
| |
| #pragma mark - Caching |
| |
| - (BackgroundCacheKey)backgroundCacheKeyForCardStyle:(BOOL)isCardStyle |
| isGroupedStyle:(BOOL)isGroupedStyle |
| isTop:(BOOL)isTop |
| isBottom:(BOOL)isBottom |
| isHighlighted:(BOOL)isHighlighted { |
| if (!isCardStyle && !isGroupedStyle) { |
| return BackgroundCacheKeyFlat; |
| } |
| BackgroundCacheKey options = isTop ? BackgroundCacheKeyTop : 0; |
| options |= isBottom ? BackgroundCacheKeyBottom : 0; |
| options |= isCardStyle ? BackgroundCacheKeyCard : 0; |
| options |= isGroupedStyle ? BackgroundCacheKeyGrouped : 0; |
| options |= isHighlighted ? BackgroundCacheKeyHighlighted : 0; |
| NSAssert(isCardStyle != isGroupedStyle, @"Cannot be both card and grouped style"); |
| return options; |
| } |
| |
| - (NSPointerArray *)cellBackgroundCache { |
| NSPointerArray *cache = [NSPointerArray strongObjectsPointerArray]; |
| cache.count = BackgroundCacheKeyMax; |
| return cache; |
| } |
| |
| #pragma mark - Separators |
| |
| - (void)setSeparatorColor:(UIColor *)separatorColor { |
| if (_separatorColor == separatorColor) { |
| return; |
| } |
| [self invalidateLayoutForStyleChange]; |
| _separatorColor = separatorColor; |
| } |
| |
| - (void)setSeparatorInset:(UIEdgeInsets)separatorInset { |
| if (UIEdgeInsetsEqualToEdgeInsets(_separatorInset, separatorInset)) { |
| return; |
| } |
| [self invalidateLayoutForStyleChange]; |
| _separatorInset = separatorInset; |
| } |
| |
| - (void)setSeparatorLineHeight:(CGFloat)separatorLineHeight { |
| if (_separatorLineHeight == separatorLineHeight) { |
| return; |
| } |
| [self invalidateLayoutForStyleChange]; |
| _separatorLineHeight = separatorLineHeight; |
| } |
| |
| - (void)setShouldHideSeparators:(BOOL)shouldHideSeparators { |
| if (_shouldHideSeparators == shouldHideSeparators) { |
| return; |
| } |
| [self invalidateLayoutForStyleChange]; |
| _shouldHideSeparators = shouldHideSeparators; |
| } |
| |
| - (void)setCellStyle:(MDCCollectionViewCellStyle)cellStyle { |
| if (_cellStyle == cellStyle) { |
| return; |
| } |
| [_cellBackgroundCaches removeAllObjects]; |
| [self invalidateLayoutForStyleChange]; |
| _cellStyle = cellStyle; |
| } |
| |
| - (BOOL)shouldHideSeparatorForCellLayoutAttributes:(MDCCollectionViewLayoutAttributes *)attr { |
| BOOL shouldHideSeparator = self.shouldHideSeparators; |
| if (!self.delegate) { |
| return shouldHideSeparator; |
| } |
| |
| NSIndexPath *indexPath = attr.indexPath; |
| BOOL isCell = attr.representedElementCategory == UICollectionElementCategoryCell; |
| BOOL isSectionHeader = |
| [attr.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]; |
| BOOL isSectionFooter = |
| [attr.representedElementKind isEqualToString:UICollectionElementKindSectionFooter]; |
| if (isCell) { |
| if ([self.delegate respondsToSelector:@selector(collectionView: |
| shouldHideItemSeparatorAtIndexPath:)]) { |
| shouldHideSeparator = [self.delegate collectionView:_collectionView |
| shouldHideItemSeparatorAtIndexPath:indexPath]; |
| } |
| } else if (isSectionHeader) { |
| if ([self.delegate respondsToSelector:@selector(collectionView: |
| shouldHideHeaderSeparatorForSection:)]) { |
| shouldHideSeparator = [self.delegate collectionView:_collectionView |
| shouldHideHeaderSeparatorForSection:indexPath.section]; |
| } |
| } else if (isSectionFooter) { |
| if ([self.delegate respondsToSelector:@selector(collectionView: |
| shouldHideFooterSeparatorForSection:)]) { |
| shouldHideSeparator = [self.delegate collectionView:_collectionView |
| shouldHideFooterSeparatorForSection:indexPath.section]; |
| } |
| } |
| return shouldHideSeparator; |
| } |
| |
| #pragma mark - Public |
| |
| - (UIEdgeInsets)backgroundImageViewOutsetsForCellWithAttribute: |
| (MDCCollectionViewLayoutAttributes *)attr { |
| // Inset contentView to allow for shadowed borders in cards. |
| UIEdgeInsets insets = UIEdgeInsetsZero; |
| |
| MDCCollectionViewCellStyle cellStyle = [self cellStyleAtSectionIndex:attr.indexPath.section]; |
| BOOL isCardStyle = cellStyle == MDCCollectionViewCellStyleCard; |
| BOOL isGroupedStyle = cellStyle == MDCCollectionViewCellStyleGrouped; |
| BOOL isHighlighted = NO; |
| |
| MDCCollectionViewOrdinalPosition position = attr.sectionOrdinalPosition; |
| |
| if ([self drawShadowForCellWithIsCardStye:isCardStyle |
| isGroupStyle:isGroupedStyle |
| isHighlighted:isHighlighted]) { |
| #if IS_VISIONOS |
| UITraitCollection *current = [UITraitCollection currentTraitCollection]; |
| CGFloat scale = current ? [current displayScale] : 1.0; |
| CGFloat mainScreenScale = scale; |
| #else |
| CGFloat mainScreenScale = [[UIScreen mainScreen] scale]; |
| #endif |
| if (mainScreenScale > (CGFloat)2.1) { |
| insets = kCollectionViewCellContentInsetsRetina3x; |
| } else if (mainScreenScale > (CGFloat)1.1) { |
| insets = kCollectionViewCellContentInsetsRetina; |
| } else { |
| insets = kCollectionViewCellContentInsets; |
| } |
| |
| if (!isCardStyle) { |
| insets = UIEdgeInsetsMake(insets.top, 0, insets.bottom, 0); |
| } |
| |
| switch (position) { |
| case MDCCollectionViewOrdinalPositionVerticalTop: |
| insets = UIEdgeInsetsMake(insets.top, insets.left, 0, insets.right); |
| break; |
| case MDCCollectionViewOrdinalPositionVerticalBottom: |
| insets = UIEdgeInsetsMake(0, insets.left, insets.bottom, insets.right); |
| break; |
| case MDCCollectionViewOrdinalPositionVerticalCenter: |
| insets = UIEdgeInsetsMake(0, insets.left, 0, insets.right); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| return insets; |
| } |
| |
| - (void)setCellStyle:(MDCCollectionViewCellStyle)cellStyle animated:(BOOL)animated { |
| _cellStyle = cellStyle; |
| [self updateLayoutAnimated:animated]; |
| } |
| |
| - (MDCCollectionViewCellStyle)cellStyleAtSectionIndex:(NSInteger)section { |
| MDCCollectionViewCellStyle cellStyle = self.cellStyle; |
| if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView: |
| cellStyleForSection:)]) { |
| cellStyle = [self.delegate collectionView:_collectionView cellStyleForSection:section]; |
| } |
| return cellStyle; |
| } |
| |
| - (NSArray *)indexPathsForInlaidItems { |
| return [_inlaidIndexPathSet allObjects]; |
| } |
| |
| - (BOOL)isItemInlaidAtIndexPath:(NSIndexPath *)indexPath { |
| return [_inlaidIndexPathSet containsObject:indexPath]; |
| } |
| |
| - (void)applyInlayToItemAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated { |
| if (_allowsItemInlay) { |
| // If necessary, remove any previously inlaid items. |
| if (!_allowsMultipleItemInlays) { |
| for (NSIndexPath *inlaidIndexPath in [self indexPathsForInlaidItems]) { |
| [self removeInlayFromItemAtIndexPath:inlaidIndexPath animated:animated]; |
| } |
| } |
| |
| void (^completionBlock)(BOOL finished) = ^(__unused BOOL finished) { |
| if ([self.delegate respondsToSelector:@selector(collectionView: |
| didApplyInlayToItemAtIndexPaths:)]) { |
| [self.delegate collectionView:self.collectionView |
| didApplyInlayToItemAtIndexPaths:@[ indexPath ]]; |
| } |
| }; |
| |
| // Inlay this item. |
| [_inlaidIndexPathSet addObject:indexPath]; |
| [self updateLayoutAnimated:animated completion:completionBlock]; |
| } |
| } |
| |
| - (void)removeInlayFromItemAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated { |
| [_inlaidIndexPathSet removeObject:indexPath]; |
| |
| void (^completionBlock)(BOOL finished) = ^(__unused BOOL finished) { |
| if ([self.delegate respondsToSelector:@selector(collectionView: |
| didRemoveInlayFromItemAtIndexPaths:)]) { |
| [self.delegate collectionView:self.collectionView |
| didRemoveInlayFromItemAtIndexPaths:@[ indexPath ]]; |
| } |
| }; |
| |
| [self updateLayoutAnimated:animated completion:completionBlock]; |
| } |
| |
| - (void)applyInlayToAllItemsAnimated:(BOOL)animated { |
| if (_allowsItemInlay && _allowsMultipleItemInlays) { |
| // Store all index paths. |
| [_inlaidIndexPathSet removeAllObjects]; |
| NSInteger sections = [_collectionView numberOfSections]; |
| for (NSInteger section = 0; section < sections; section++) { |
| for (NSInteger item = 0; item < [_collectionView numberOfItemsInSection:section]; item++) { |
| [_inlaidIndexPathSet addObject:[NSIndexPath indexPathForItem:item inSection:section]]; |
| } |
| } |
| |
| void (^completionBlock)(BOOL finished) = ^(__unused BOOL finished) { |
| if ([self.delegate respondsToSelector:@selector(collectionView: |
| didApplyInlayToItemAtIndexPaths:)]) { |
| [self.delegate collectionView:self.collectionView |
| didApplyInlayToItemAtIndexPaths:[self.inlaidIndexPathSet allObjects]]; |
| } |
| }; |
| |
| // Inlay all items. |
| [self updateLayoutAnimated:animated completion:completionBlock]; |
| } |
| } |
| |
| - (void)removeInlayFromAllItemsAnimated:(BOOL)animated { |
| NSArray *indexPaths = [_inlaidIndexPathSet allObjects]; |
| [_inlaidIndexPathSet removeAllObjects]; |
| |
| void (^completionBlock)(BOOL finished) = ^(__unused BOOL finished) { |
| if ([self.delegate respondsToSelector:@selector(collectionView: |
| didRemoveInlayFromItemAtIndexPaths:)]) { |
| [self.delegate collectionView:self.collectionView |
| didRemoveInlayFromItemAtIndexPaths:indexPaths]; |
| } |
| }; |
| |
| [self updateLayoutAnimated:animated completion:completionBlock]; |
| } |
| |
| - (void)resetIndexPathsForInlaidItems { |
| [_inlaidIndexPathSet removeAllObjects]; |
| [self applyInlayToAllItemsAnimated:NO]; |
| } |
| |
| - (void)updateLayoutAnimated:(BOOL)animated { |
| [self updateLayoutAnimated:animated completion:nil]; |
| } |
| |
| #pragma mark - Private |
| |
| - (void)updateLayoutAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion { |
| if (animated) { |
| // Invalidate current layout while allowing animation to new layout. |
| [UIView animateWithDuration:0 |
| animations:^{ |
| [self.collectionView.collectionViewLayout invalidateLayout]; |
| } |
| completion:^(BOOL finished) { |
| if (completion) { |
| completion(finished); |
| } |
| }]; |
| } else { |
| _shouldInvalidateLayout = YES; |
| |
| // Create new layout with existing layout properties. |
| NSData *data = |
| [NSKeyedArchiver archivedDataWithRootObject:_collectionView.collectionViewLayout]; |
| UICollectionViewFlowLayout *newLayout = [NSKeyedUnarchiver unarchiveObjectWithData:data]; |
| [_collectionView setCollectionViewLayout:newLayout |
| animated:animated |
| completion:^(BOOL finished) { |
| if (completion) { |
| completion(finished); |
| } |
| }]; |
| } |
| } |
| |
| - (void)invalidateLayoutForStyleChange { |
| _shouldInvalidateLayout = YES; |
| } |
| |
| - (BOOL)shouldInvalidateLayoutForStyleChange { |
| // Whether the collection view layout should be invalidated due to a style property that has |
| // changed value. |
| return _shouldInvalidateLayout; |
| } |
| |
| #pragma mark - Cell Image Background |
| |
| - (BOOL)drawShadowForCellWithIsCardStye:(BOOL)isCardStyle |
| isGroupStyle:(BOOL)isGroupStyle |
| isHighlighted:(BOOL)isHighlighted { |
| return (isCardStyle || isGroupStyle) && kCollectionViewCellDefaultShadowWidth > 0 && |
| !isHighlighted; |
| } |
| |
| - (UIImage *)backgroundImageForCellLayoutAttributes:(MDCCollectionViewLayoutAttributes *)attr { |
| BOOL isSectionHeader = |
| [attr.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]; |
| BOOL isSectionFooter = |
| [attr.representedElementKind isEqualToString:UICollectionElementKindSectionFooter]; |
| BOOL isDecorationView = |
| attr.representedElementCategory == UICollectionElementCategoryDecorationView; |
| BOOL isTop = |
| (attr.sectionOrdinalPosition & MDCCollectionViewOrdinalPositionVerticalTop) ? YES : NO; |
| BOOL isBottom = |
| (attr.sectionOrdinalPosition & MDCCollectionViewOrdinalPositionVerticalBottom) ? YES : NO; |
| |
| MDCCollectionViewCellStyle cellStyle = [self cellStyleAtSectionIndex:attr.indexPath.section]; |
| BOOL isCardStyle = cellStyle == MDCCollectionViewCellStyleCard; |
| BOOL isGroupedStyle = cellStyle == MDCCollectionViewCellStyleGrouped; |
| BOOL isGridLayout = (_cellLayoutType == MDCCollectionViewCellLayoutTypeGrid); |
| if (!isCardStyle && !isGroupedStyle) { |
| // If not card or grouped style, revert @c isBottom to allow drawing separator at bottom. |
| isBottom = NO; |
| } |
| CGFloat borderRadius = (isCardStyle) ? _cardBorderRadius : 0; |
| |
| // Allowance for grid decoration view. |
| if (isGridLayout) { |
| if (!isDecorationView && attr.shouldShowGridBackground) { |
| return nil; |
| } else { |
| isTop = isBottom = YES; |
| } |
| } |
| |
| // If no-background section header, return nil image. |
| BOOL hidesHeaderBackground = NO; |
| if ([_delegate respondsToSelector:@selector(collectionView: |
| shouldHideHeaderBackgroundForSection:)]) { |
| hidesHeaderBackground = [_delegate collectionView:_collectionView |
| shouldHideHeaderBackgroundForSection:attr.indexPath.section]; |
| } |
| if (hidesHeaderBackground && isSectionHeader) { |
| return nil; |
| } |
| |
| // If no-background section footer, return nil image. |
| BOOL hidesFooterBackground = NO; |
| if ([_delegate respondsToSelector:@selector(collectionView: |
| shouldHideFooterBackgroundForSection:)]) { |
| hidesFooterBackground = [_delegate collectionView:_collectionView |
| shouldHideFooterBackgroundForSection:attr.indexPath.section]; |
| } |
| if (hidesFooterBackground && isSectionFooter) { |
| return nil; |
| } |
| |
| // If no-background section item, return nil image. |
| BOOL hidesBackground = NO; |
| if ([_delegate respondsToSelector:@selector(collectionView: |
| shouldHideItemBackgroundAtIndexPath:)]) { |
| hidesBackground = [_delegate collectionView:_collectionView |
| shouldHideItemBackgroundAtIndexPath:attr.indexPath]; |
| } |
| if (hidesBackground && !(isDecorationView || isSectionFooter || isSectionHeader)) { |
| return nil; |
| } |
| |
| BOOL isHighlighted = NO; |
| |
| BackgroundCacheKey backgroundCacheKey = [self backgroundCacheKeyForCardStyle:isCardStyle |
| isGroupedStyle:isGroupedStyle |
| isTop:isTop |
| isBottom:isBottom |
| isHighlighted:isHighlighted]; |
| |
| if (backgroundCacheKey > BackgroundCacheKeyMax) { |
| NSAssert(NO, @"Invalid styler cell background cache key"); |
| return nil; |
| } |
| |
| // Get cell color. |
| UIColor *backgroundColor = _cellBackgroundColor; |
| if ([_delegate respondsToSelector:@selector(collectionView:cellBackgroundColorAtIndexPath:)]) { |
| UIColor *customBackgroundColor = [_delegate collectionView:_collectionView |
| cellBackgroundColorAtIndexPath:attr.indexPath]; |
| if (customBackgroundColor) { |
| backgroundColor = [customBackgroundColor |
| mdc_resolvedColorWithTraitCollection:self.collectionView.traitCollection]; |
| } |
| } |
| |
| NSPointerArray *cellBackgroundCache = _cellBackgroundCaches[backgroundColor]; |
| if (!cellBackgroundCache) { |
| cellBackgroundCache = [self cellBackgroundCache]; |
| _cellBackgroundCaches[backgroundColor] = cellBackgroundCache; |
| } else if ([cellBackgroundCache pointerAtIndex:backgroundCacheKey] && |
| self.previousUserInterfaceStyle == |
| self.collectionView.traitCollection.userInterfaceStyle) { |
| return (__bridge UIImage *)[cellBackgroundCache pointerAtIndex:backgroundCacheKey]; |
| } |
| |
| CGRect imageRect = CGRectMake(0, 0, kCellImageSize.width, kCellImageSize.height); |
| UIGraphicsBeginImageContextWithOptions(imageRect.size, NO, 0); |
| |
| CGContextRef cx = UIGraphicsGetCurrentContext(); |
| |
| // Create a transparent background. |
| CGContextClearRect(cx, imageRect); |
| |
| // Inner background color |
| CGContextSetFillColorWithColor(cx, backgroundColor.CGColor); |
| |
| CGRect contentFrame = imageRect; |
| |
| // Draw the shadow. |
| if ([self drawShadowForCellWithIsCardStye:isCardStyle |
| isGroupStyle:isGroupedStyle |
| isHighlighted:isHighlighted]) { |
| if (isCardStyle) { |
| contentFrame = CGRectInset(imageRect, kCollectionViewCellDefaultShadowWidth, 0); |
| } |
| if (isTop) { |
| contentFrame = RectShift(contentFrame, 0, kCollectionViewCellDefaultShadowWidth); |
| } |
| if (isBottom) { |
| contentFrame = RectContract(contentFrame, 0, kCollectionViewCellDefaultShadowWidth); |
| } |
| |
| CGContextSaveGState(cx); |
| CGRect shadowFrame = contentFrame; |
| |
| // We want the shadow to clip to the top and bottom edges of the image so that when two cells |
| // are next to each other their shadows line up perfectly. |
| if (!isTop) { |
| shadowFrame = RectShift(shadowFrame, 0, -kCollectionViewCellDefaultShadowWidth); |
| } |
| if (!isBottom) { |
| shadowFrame = RectContract(shadowFrame, 0, -kCollectionViewCellDefaultShadowWidth); |
| } |
| |
| [self applyBackgroundPathToContext:cx |
| rect:shadowFrame |
| isTop:isTop |
| isBottom:isBottom |
| isCard:(isCardStyle || isGroupedStyle) |
| borderRadius:borderRadius]; |
| CGContextSetShadowWithColor(cx, kCollectionViewCellDefaultShadowOffset(), |
| kCollectionViewCellDefaultShadowWidth, |
| kCollectionViewCellDefaultShadowColor().CGColor); |
| CGContextDrawPath(cx, kCGPathFill); |
| CGContextRestoreGState(cx); |
| } else { |
| // Draw a flat cell background. |
| CGContextSaveGState(cx); |
| [self applyBackgroundPathToContext:cx |
| rect:contentFrame |
| isTop:isTop |
| isBottom:isBottom |
| isCard:(isCardStyle || isGroupedStyle) |
| borderRadius:borderRadius]; |
| CGContextFillPath(cx); |
| CGContextRestoreGState(cx); |
| } |
| // Draw border paths for cells. We want the cell border to overlap the shadow and the content. |
| if ((isCardStyle || isGroupedStyle) && !isHighlighted) { |
| CGFloat minPixelOffset = [self minPixelOffset]; |
| CGRect borderFrame = CGRectInset(contentFrame, -minPixelOffset, -minPixelOffset); |
| CGContextSaveGState(cx); |
| CGContextSetLineWidth(cx, kCollectionViewCellDefaultBorderWidth); |
| CGContextSetStrokeColorWithColor(cx, kCollectionViewCellDefaultBorderColor().CGColor); |
| [self applyBorderPathToContext:cx |
| rect:borderFrame |
| isTop:isTop |
| isBottom:isBottom |
| isCard:isCardStyle |
| borderRadius:borderRadius]; |
| CGContextStrokePath(cx); |
| CGContextRestoreGState(cx); |
| } |
| |
| UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); |
| UIGraphicsEndImageContext(); |
| UIImage *resizableImage = [self resizableImage:image]; |
| [cellBackgroundCache replacePointerAtIndex:backgroundCacheKey |
| withPointer:(__bridge void *)(resizableImage)]; |
| self.previousUserInterfaceStyle = self.collectionView.traitCollection.userInterfaceStyle; |
| return resizableImage; |
| } |
| |
| #pragma mark - Private Context Paths |
| |
| // We want to draw the borders and shadows on single retina-pixel boundaries if possible, but |
| // we need to avoid doing this on non-retina devices because it'll look blurry. |
| - (CGFloat)minPixelOffset { |
| #if IS_VISIONOS |
| UITraitCollection *current = [UITraitCollection currentTraitCollection]; |
| CGFloat scale = current ? [current displayScale] : 1.0; |
| return 1 / scale; |
| #else |
| return 1 / [[UIScreen mainScreen] scale]; |
| #endif |
| } |
| |
| - (UIImage *)resizableImage:(UIImage *)image { |
| // Returns a resizable version of this image with cap insets equal to center point. |
| CGFloat capWidth = (CGFloat)floor(image.size.width / 2); |
| CGFloat capHeight = (CGFloat)floor(image.size.height / 2); |
| UIEdgeInsets capInsets = UIEdgeInsetsMake(capHeight, capWidth, capHeight, capWidth); |
| return [image resizableImageWithCapInsets:capInsets]; |
| } |
| |
| - (void)applyBackgroundPathToContext:(CGContextRef)c |
| rect:(CGRect)rect |
| isTop:(BOOL)isTop |
| isBottom:(BOOL)isBottom |
| isCard:(BOOL)isCard |
| borderRadius:(CGFloat)borderRadius { |
| // Draw background paths for cell. |
| CGFloat minPixelOffset = (isCard) ? [self minPixelOffset] : 0; |
| CGFloat minX = CGRectGetMinX(rect) + minPixelOffset; |
| CGFloat midX = CGRectGetMidX(rect) + minPixelOffset; |
| CGFloat maxX = CGRectGetMaxX(rect) - minPixelOffset; |
| CGFloat minY = CGRectGetMinY(rect) - minPixelOffset; |
| CGFloat midY = CGRectGetMidY(rect) - minPixelOffset; |
| CGFloat maxY = CGRectGetMaxY(rect) + minPixelOffset; |
| |
| CGContextBeginPath(c); |
| |
| CGContextMoveToPoint(c, minX, midY); |
| if (isTop && isCard) { |
| CGContextAddArcToPoint(c, minX, minY + 1, midX, minY + 1, borderRadius); |
| CGContextAddArcToPoint(c, maxX, minY + 1, maxX, midY, borderRadius); |
| } else { |
| CGContextAddLineToPoint(c, minX, minY); |
| CGContextAddLineToPoint(c, maxX, minY); |
| } |
| |
| CGContextAddLineToPoint(c, maxX, midY); |
| |
| if (isBottom & isCard) { |
| CGContextAddArcToPoint(c, maxX, maxY - 1, midX, maxY - 1, borderRadius); |
| CGContextAddArcToPoint(c, minX, maxY - 1, minX, midY, borderRadius); |
| } else { |
| CGContextAddLineToPoint(c, maxX, maxY); |
| CGContextAddLineToPoint(c, minX, maxY); |
| } |
| CGContextAddLineToPoint(c, minX, midY); |
| |
| CGContextClosePath(c); |
| } |
| |
| - (void)applyBorderPathToContext:(CGContextRef)c |
| rect:(CGRect)rect |
| isTop:(BOOL)isTop |
| isBottom:(BOOL)isBottom |
| isCard:(BOOL)isCard |
| borderRadius:(CGFloat)borderRadius { |
| // Draw border paths for cell. |
| CGFloat minPixelOffset = (isCard) ? [self minPixelOffset] : 0; |
| CGFloat minX = CGRectGetMinX(rect) + minPixelOffset; |
| CGFloat midX = CGRectGetMidX(rect) + minPixelOffset; |
| CGFloat maxX = CGRectGetMaxX(rect) - minPixelOffset; |
| CGFloat minY = CGRectGetMinY(rect) - minPixelOffset; |
| CGFloat midY = CGRectGetMidY(rect) - minPixelOffset; |
| CGFloat maxY = CGRectGetMaxY(rect) + minPixelOffset; |
| |
| CGContextBeginPath(c); |
| |
| if (isTop && isBottom) { |
| CGContextMoveToPoint(c, minX, midY); |
| CGContextAddArcToPoint(c, minX, minY + 1, midX, minY + 1, borderRadius); |
| CGContextAddArcToPoint(c, maxX, minY + 1, maxX, midY, borderRadius); |
| CGContextAddLineToPoint(c, maxX, midY); |
| CGContextAddArcToPoint(c, maxX, maxY - 1, midX, maxY - 1, borderRadius); |
| CGContextAddArcToPoint(c, minX, maxY - 1, minX, midY, borderRadius); |
| CGContextAddLineToPoint(c, minX, midY); |
| } else if (isTop) { |
| CGContextMoveToPoint(c, minX, maxY); |
| CGContextAddLineToPoint(c, minX, midY); |
| CGContextAddArcToPoint(c, minX, minY + 1, midX, minY + 1, borderRadius); |
| CGContextAddArcToPoint(c, maxX, minY + 1, maxX, midY, borderRadius); |
| CGContextAddLineToPoint(c, maxX, maxY); |
| } else if (isBottom) { |
| CGContextMoveToPoint(c, maxX, minY); |
| CGContextAddLineToPoint(c, maxX, midY); |
| CGContextAddArcToPoint(c, maxX, maxY - 1, midX, maxY - 1, borderRadius); |
| CGContextAddArcToPoint(c, minX, maxY - 1, minX, midY, borderRadius); |
| CGContextAddLineToPoint(c, minX, minY); |
| } else { |
| CGContextMoveToPoint(c, minX, minY); |
| CGContextAddLineToPoint(c, minX, maxY); |
| CGContextMoveToPoint(c, maxX, minY); |
| CGContextAddLineToPoint(c, maxX, maxY); |
| } |
| |
| CGContextClosePath(c); |
| } |
| |
| @end |