blob: 9ded7247dd0596a082baac263d72b4a037085de1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 "WXMultiColumnLayout.h"
#import "NSArray+Weex.h"
#import "WXUtility.h"
#import "WXAssert.h"
NSString * const kCollectionSupplementaryViewKindHeader = @"WXCollectionSupplementaryViewKindHeader";
NSString * const kMultiColumnLayoutHeader = @"WXMultiColumnLayoutHeader";
NSString * const kMultiColumnLayoutCell = @"WXMultiColumnLayoutCell";
@interface WXMultiColumnLayoutHeaderAttributes : UICollectionViewLayoutAttributes
@property (nonatomic, assign) BOOL isSticky;
@end
@implementation WXMultiColumnLayoutHeaderAttributes
- (id)copyWithZone:(NSZone *)zone
{
WXMultiColumnLayoutHeaderAttributes *copy = [super copyWithZone:zone];
copy.isSticky = self.isSticky;
return copy;
}
@end
@interface WXMultiColumnLayout ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSDictionary<id, UICollectionViewLayoutAttributes *> *> *layoutAttributes;
@property (nonatomic, strong) NSMutableArray<NSNumber *> *columnsMaxHeights;
@end
@implementation WXMultiColumnLayout
- (instancetype)init
{
if (self = [super init]) {
_layoutAttributes = [NSMutableDictionary dictionary];
_columnsMaxHeights = [NSMutableArray array];
}
return self;
}
//Under system version 10.0, UICollectionViewLayout.collectionView seems be unsafe_unretain rather than weak. sometime when the collectionView is released, and the layout is not released, it may crash.
- (UICollectionView *)weakCollectionView
{
if ([[[UIDevice currentDevice] systemVersion] floatValue]<10.0f) {
return self.weak_collectionView;
} else {
return self.collectionView;
}
}
#pragma mark - Public Accessors
- (void)setColumnCount:(WXLength *)columnCount
{
if (!(columnCount.isAuto && _columnCount.isAuto) || _columnCount.intValue != columnCount.intValue) {
_columnCount = columnCount;
[self _cleanComputed];
}
}
- (void)setColumnWidth:(WXLength *)columnWidth
{
if (!(columnWidth.isAuto && _columnWidth.isAuto) || _columnWidth.floatValue != columnWidth.floatValue) {
_columnWidth = columnWidth;
[self _cleanComputed];
}
}
- (void)setColumnGap:(float)columnGap
{
if (_columnGap != columnGap) {
_columnGap = columnGap;
[self _cleanComputed];
}
}
- (void)setLeftGap:(float)leftGap
{
if (_leftGap != leftGap) {
_leftGap = leftGap;
[self _cleanComputed];
}
}
- (void)setRightGap:(float)rightGap
{
if (_rightGap != rightGap) {
_rightGap = rightGap;
[self _cleanComputed];
}
}
- (CGFloat)computedColumnWidth
{
WXAssert([_columnWidth isFixed], @"column width must be calculated by core.");
return _columnWidth.floatValue;
}
- (int)computedColumnCount
{
WXAssert([_columnCount isFixed], @"column count must be calculated by core.");
return _columnCount.intValue;
}
- (CGFloat)computedHeaderWidth
{
UIEdgeInsets insets = [self.delegate collectionView:[self weakCollectionView] insetForLayout:self];
return self.contentWidth - (insets.left + insets.right);
}
#pragma mark - Methods to Override for UICollectionViewLayout
- (void)prepareLayout
{
[super prepareLayout];
[self _cleanup];
NSInteger numberOfSections = [[self weakCollectionView] numberOfSections];
UIEdgeInsets insets = [self.delegate collectionView:[self weakCollectionView] insetForLayout:self];
float columnWidth = self.computedColumnWidth;
int columnCount = self.computedColumnCount;
float columnGap = self.columnGap;
CGFloat currentHeight = insets.top;
NSMutableDictionary *headersAttributes = [NSMutableDictionary dictionaryWithCapacity:numberOfSections];
NSMutableDictionary *cellAttributes = [NSMutableDictionary dictionary];
for (NSInteger i = 0; i < columnCount; i++) {
[self.columnsMaxHeights addObject:@(currentHeight)];
}
for (NSInteger section = 0; section < numberOfSections; section++) {
BOOL hasHeader = [self.delegate collectionView:[self weakCollectionView] layout:self hasHeaderInSection:section];
// header
if (hasHeader) {
CGFloat headerHeight = [self.delegate collectionView:[self weakCollectionView] layout:self heightForHeaderInSection:section];
WXMultiColumnLayoutHeaderAttributes *headerAttributes = [WXMultiColumnLayoutHeaderAttributes layoutAttributesForSupplementaryViewOfKind:kCollectionSupplementaryViewKindHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]];
headerAttributes.frame = CGRectMake(insets.left, currentHeight, self.contentWidth - (insets.left + insets.right), headerHeight);
headerAttributes.isSticky = [self.delegate collectionView:[self weakCollectionView] layout:self isNeedStickyForHeaderInSection:section];
headerAttributes.zIndex = headerAttributes.isSticky ? 1 : 0;
headersAttributes[@(section)] = headerAttributes;
currentHeight = CGRectGetMaxY(headerAttributes.frame);
[self _columnsReachToHeight:currentHeight];
}
// cells
@try {
for (NSInteger item = 0; item < [[self weakCollectionView] numberOfItemsInSection:section]; item++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];
CGFloat itemHeight = [self.delegate collectionView:[self weakCollectionView] layout:self heightForItemAtIndexPath:indexPath];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
NSUInteger column = [self _minHeightColumnForAllColumns];
CGFloat x = insets.left + (columnWidth + columnGap) * column+_leftGap;
if (column >= [self.columnsMaxHeights count]) {
return;
}
CGFloat y = [self.columnsMaxHeights[column] floatValue];
itemAttributes.frame = CGRectMake(x, y, columnWidth, itemHeight);
cellAttributes[indexPath] = itemAttributes;
self.columnsMaxHeights[column] = @(CGRectGetMaxY(itemAttributes.frame));
}
} @catch (NSException *exception) {
WXLog(@"%@", exception);
} @finally {
}
currentHeight = [self _maxHeightForAllColumns];
[self _columnsReachToHeight:currentHeight];
}
currentHeight = currentHeight + insets.bottom;
[self _columnsReachToHeight:currentHeight];
self.layoutAttributes[kMultiColumnLayoutHeader] = headersAttributes;
self.layoutAttributes[kMultiColumnLayoutCell] = cellAttributes;
}
- (CGSize)collectionViewContentSize
{
NSInteger numberOfSections = [[self weakCollectionView] numberOfSections];
if (numberOfSections == 0) {
return CGSizeZero;
}
return CGSizeMake(self.contentWidth, self.contentHeight);
}
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray<WXMultiColumnLayoutHeaderAttributes *> *stickyHeaders = [NSMutableArray array];
NSMutableArray<UICollectionViewLayoutAttributes *> *result = [NSMutableArray array];
[self.layoutAttributes enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull kind, NSDictionary<id,UICollectionViewLayoutAttributes *> * _Nonnull dictionary, BOOL * _Nonnull stop) {
[dictionary enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, UICollectionViewLayoutAttributes * _Nonnull attributes, BOOL * _Nonnull stop) {
if (attributes.representedElementKind == kCollectionSupplementaryViewKindHeader
&& [self.delegate collectionView:[self weakCollectionView] layout:self isNeedStickyForHeaderInSection:attributes.indexPath.section]) {
[stickyHeaders addObject:(WXMultiColumnLayoutHeaderAttributes *)attributes];
} else if (CGRectIntersectsRect(rect, attributes.frame)) {
[result addObject:attributes];
}
}];
}];
[stickyHeaders sortUsingComparator:^NSComparisonResult(WXMultiColumnLayoutHeaderAttributes *obj1, WXMultiColumnLayoutHeaderAttributes *obj2) {
if (obj1.indexPath.section < obj2.indexPath.section) {
return NSOrderedAscending;
} else {
return NSOrderedDescending;
}
}];
for (int i = 0; i < stickyHeaders.count; i++) {
WXMultiColumnLayoutHeaderAttributes *header = stickyHeaders[i];
[self _adjustStickyForHeaderAttributes:header next:(i == stickyHeaders.count - 1) ? nil : stickyHeaders[i + 1]];
[result addObject:header];
}
WXLogDebug(@"return result attributes:%@ for rect:%@", result, NSStringFromCGRect(rect));
return result;
}
- (void)_adjustStickyForHeaderAttributes:(WXMultiColumnLayoutHeaderAttributes *)header
next:(WXMultiColumnLayoutHeaderAttributes *)nextHeader
{
CGRect bounds = [self weakCollectionView].bounds;
CGFloat originY = header.frame.origin.y;
CGFloat maxY = nextHeader ? (nextHeader.frame.origin.y - header.frame.size.height) : (CGRectGetMaxY(bounds) - header.frame.size.height);
CGFloat currentY = CGRectGetMaxY(bounds) - bounds.size.height + [self weakCollectionView].contentInset.top;
CGFloat resultY = MIN(MAX(currentY, originY), maxY);
CGPoint origin = header.frame.origin;
origin.y = resultY;
header.frame = (CGRect){origin, header.frame.size};
header.hidden = NO;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath
{
if ([elementKind isEqualToString:kCollectionSupplementaryViewKindHeader]) {
UICollectionViewLayoutAttributes *attributes = self.layoutAttributes[kMultiColumnLayoutHeader][@(indexPath.section)];
if (!attributes) {
attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:elementKind withIndexPath:indexPath];
attributes.frame = CGRectZero;
attributes.hidden = YES;
}
WXLogDebug(@"return header attributes:%@ for index path:%@", attributes, indexPath);
return attributes;
}
return nil;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
if (self.layoutAttributes.count == 0) {
[self prepareLayout];
}
UICollectionViewLayoutAttributes *attributes = self.layoutAttributes[kMultiColumnLayoutCell][indexPath];
WXLogDebug(@"return item attributes:%@ for index path:%@", attributes, indexPath);
return attributes;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
__block BOOL hasStickyHeader = NO;
[self.layoutAttributes[kMultiColumnLayoutHeader] enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, UICollectionViewLayoutAttributes * _Nonnull obj, BOOL * _Nonnull stop) {
WXMultiColumnLayoutHeaderAttributes *attribute = (WXMultiColumnLayoutHeaderAttributes *)obj;
if (attribute.isSticky) {
hasStickyHeader = YES;
*stop = YES;
}
}];
if (hasStickyHeader) {
// always return yes no trigger resetting sticky header's frame.
return YES;
} else {
CGRect oldBounds = [self weakCollectionView].bounds;
if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds)) {
return YES;
}
}
return NO;
}
#pragma mark - Private
- (CGFloat)contentWidth
{
return [self.delegate collectionView:[self weakCollectionView] contentWidthForLayout:self];
}
- (CGFloat)contentHeight
{
return [self _maxHeightForAllColumns];
}
- (CGFloat)_maxHeightForAllColumns
{
CGFloat maxHeight = 0.0;
for (NSNumber *number in self.columnsMaxHeights) {
CGFloat height = [number floatValue];
if (height > maxHeight) {
maxHeight = height;
}
}
return maxHeight;
}
- (NSUInteger)_minHeightColumnForAllColumns
{
__block NSUInteger index = 0;
__block CGFloat minHeight = FLT_MAX;
[self.columnsMaxHeights enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
CGFloat height = [obj floatValue];
if (height < minHeight) {
minHeight = height;
index = idx;
}
}];
return index;
}
- (void)_columnsReachToHeight:(CGFloat)height
{
for (NSInteger i = 0; i < self.columnsMaxHeights.count; i ++) {
self.columnsMaxHeights[i] = @(height);
}
}
- (void)_cleanup
{
[self.layoutAttributes removeAllObjects];
[self.columnsMaxHeights removeAllObjects];
}
- (void)_cleanComputed
{
}
- (void)invalidateLayout
{
[super invalidateLayout];
[self _cleanComputed];
}
@end