1
0
mirror of https://github.com/danog/Telegram.git synced 2024-12-03 09:57:46 +01:00
Telegram/Telegraph/TGGifKeyboardBalancedLayout.m
2016-02-25 01:03:51 +01:00

454 lines
17 KiB
Objective-C

#import "TGGifKeyboardBalancedLayout.h"
#import "TGGifKeyboardBalancedPartition.h"
@interface TGGifKeyboardBalancedLayout () {
CGRect **_itemFrameSections;
NSInteger _numberOfItemFrameSections;
}
@property (nonatomic) CGSize contentSize;
@property (nonatomic, strong) NSArray *headerFrames;
@property (nonatomic, strong) NSArray *footerFrames;
@end
@implementation TGGifKeyboardBalancedLayout
#pragma mark - Lifecycle
- (void)clearItemFrames
{
// free all item frame arrays
if (NULL != _itemFrameSections) {
for (NSInteger i = 0; i < _numberOfItemFrameSections; i++) {
CGRect *frames = _itemFrameSections[i];
free(frames);
}
free(_itemFrameSections);
_itemFrameSections = NULL;
}
}
- (void)dealloc
{
[self clearItemFrames];
}
- (id)init
{
self = [super init];
if (self) {
[self initialize];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
[self initialize];
}
return self;
}
- (void)initialize
{
// set to NULL so it is not released by accident in dealloc
_itemFrameSections = NULL;
self.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
self.minimumLineSpacing = 10;
self.minimumInteritemSpacing = 10;
self.headerReferenceSize = CGSizeZero;
self.footerReferenceSize = CGSizeZero;
self.scrollDirection = UICollectionViewScrollDirectionVertical;
}
#pragma mark - Layout
- (void)prepareLayout
{
[super prepareLayout];
NSAssert([self.delegate conformsToProtocol:@protocol(TGGifKeyboardBalancedLayoutDelegate)], @"UICollectionView delegate should conform to BalancedFlowLayout protocol");
CGFloat idealHeight = self.preferredRowSize;
if (idealHeight == 0) {
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
idealHeight = CGRectGetHeight(self.collectionView.bounds) / 3.0;
}
else {
idealHeight = CGRectGetWidth(self.collectionView.bounds) / 3.0;
}
}
NSMutableArray *headerFrames = [NSMutableArray array];
NSMutableArray *footerFrames = [NSMutableArray array];
CGSize contentSize = CGSizeZero;
// first release old item frame sections
[self clearItemFrames];
// create new item frame sections
_numberOfItemFrameSections = [self.collectionView numberOfSections];
_itemFrameSections = (CGRect **)malloc(sizeof(CGRect *) * _numberOfItemFrameSections);
for (int section = 0; section < [self.collectionView numberOfSections]; section++) {
// add new item frames array to sections array
NSInteger numberOfItemsInSections = [self.collectionView numberOfItemsInSection:section];
CGRect *itemFrames = (CGRect *)malloc(sizeof(CGRect) * numberOfItemsInSections);
_itemFrameSections[section] = itemFrames;
CGSize headerSize = [self referenceSizeForHeaderInSection:section];
CGSize sectionSize = CGSizeZero;
CGRect headerFrame;
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
headerFrame = CGRectMake(0, contentSize.height, CGRectGetWidth(self.collectionView.bounds), headerSize.height);
} else {
headerFrame = CGRectMake(contentSize.width, 0, headerSize.width, CGRectGetHeight(self.collectionView.bounds));
}
[headerFrames addObject:[NSValue valueWithCGRect:headerFrame]];
CGFloat totalItemSize = [self totalItemSizeForSection:section preferredRowSize:idealHeight];
NSInteger numberOfRows = MAX((NSInteger)CGRound(totalItemSize / [self viewPortAvailableSize]), 1);
CGPoint sectionOffset;
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
sectionOffset = CGPointMake(0, contentSize.height + headerSize.height);
} else {
sectionOffset = CGPointMake(contentSize.width + headerSize.width, 0);
}
[self setFrames:itemFrames forItemsInSection:section numberOfRows:numberOfRows sectionOffset:sectionOffset sectionSize:&sectionSize];
CGSize footerSize = [self referenceSizeForFooterInSection:section];
CGRect footerFrame;
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
footerFrame = CGRectMake(0, contentSize.height + headerSize.height + sectionSize.height, CGRectGetWidth(self.collectionView.bounds), footerSize.height);
} else {
footerFrame = CGRectMake(contentSize.width + headerSize.width + sectionSize.width, 0, footerSize.width, CGRectGetHeight(self.collectionView.bounds));
}
[footerFrames addObject:[NSValue valueWithCGRect:footerFrame]];
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
contentSize = CGSizeMake(sectionSize.width, contentSize.height + headerSize.height + sectionSize.height + footerSize.height);
}
else {
contentSize = CGSizeMake(contentSize.width + headerSize.width + sectionSize.width + footerSize.width, sectionSize.height);
}
}
self.headerFrames = [headerFrames copy];
self.footerFrames = [footerFrames copy];
self.contentSize = contentSize;
}
- (CGSize)collectionViewContentSize
{
return self.contentSize;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *layoutAttributes = [NSMutableArray array];
for (NSInteger section = 0, n = [self.collectionView numberOfSections]; section < n; section++) {
NSIndexPath *sectionIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
UICollectionViewLayoutAttributes *headerAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
atIndexPath:sectionIndexPath];
if (! CGSizeEqualToSize(headerAttributes.frame.size, CGSizeZero) && CGRectIntersectsRect(headerAttributes.frame, rect)) {
[layoutAttributes addObject:headerAttributes];
}
for (int i = 0; i < [self.collectionView numberOfItemsInSection:section]; i++) {
CGRect itemFrame = _itemFrameSections[section][i];
if (CGRectIntersectsRect(rect, itemFrame)) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section];
[layoutAttributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
}
UICollectionViewLayoutAttributes *footerAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
atIndexPath:sectionIndexPath];
if (! CGSizeEqualToSize(footerAttributes.frame.size, CGSizeZero) && CGRectIntersectsRect(footerAttributes.frame, rect)) {
[layoutAttributes addObject:footerAttributes];
}
}
return layoutAttributes;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attributes.frame = [self itemFrameForIndexPath:indexPath];
return attributes;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:kind withIndexPath:indexPath];
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
attributes.frame = [self headerFrameForSection:indexPath.section];
} else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) {
attributes.frame = [self footerFrameForSection:indexPath.section];
}
// If there is no header or footer, we need to return nil to prevent a crash from UICollectionView private methods.
if(CGRectIsEmpty(attributes.frame)) {
attributes = nil;
}
return attributes;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
CGRect oldBounds = self.collectionView.bounds;
if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds) || CGRectGetHeight(newBounds) != CGRectGetHeight(oldBounds)) {
return YES;
}
return NO;
}
#pragma mark - Layout helpers
- (CGRect)headerFrameForSection:(NSInteger)section
{
return [[self.headerFrames objectAtIndex:section] CGRectValue];
}
- (CGRect)itemFrameForIndexPath:(NSIndexPath *)indexPath
{
return _itemFrameSections[indexPath.section][indexPath.item];
}
- (CGRect)footerFrameForSection:(NSInteger)section
{
return [[self.footerFrames objectAtIndex:section] CGRectValue];
}
- (CGFloat)totalItemSizeForSection:(NSInteger)section preferredRowSize:(CGFloat)preferredRowSize
{
CGFloat totalItemSize = 0;
for (NSInteger i = 0, n = [self.collectionView numberOfItemsInSection:section]; i < n; i++) {
CGSize preferredSize = [self.delegate collectionView:self.collectionView layout:self preferredSizeForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:section]];
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
totalItemSize += (preferredSize.width / preferredSize.height) * preferredRowSize;
}
else {
totalItemSize += (preferredSize.height / preferredSize.width) * preferredRowSize;
}
}
return totalItemSize;
}
- (NSArray *)weightsForItemsInSection:(NSInteger)section
{
NSMutableArray *weights = [NSMutableArray array];
for (NSInteger i = 0, n = [self.collectionView numberOfItemsInSection:section]; i < n; i++) {
CGSize preferredSize = [self.delegate collectionView:self.collectionView layout:self preferredSizeForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:section]];
NSInteger aspectRatio = self.scrollDirection == UICollectionViewScrollDirectionVertical ? (NSInteger)CGRound((preferredSize.width / preferredSize.height) * 100) : (NSInteger)CGRound((preferredSize.height / preferredSize.width) * 100);
[weights addObject:@(aspectRatio)];
}
return [weights copy];
}
- (void)setFrames:(CGRect *)frames forItemsInSection:(NSInteger)section numberOfRows:(NSUInteger)numberOfRows sectionOffset:(CGPoint)sectionOffset sectionSize:(CGSize *)sectionSize
{
NSArray *weights = [self weightsForItemsInSection:section];
NSArray *partition = [TGGifKeyboardBalancedPartition linearPartitionForSequence:weights numberOfPartitions:numberOfRows];
int i = 0;
CGPoint offset = CGPointMake(sectionOffset.x + self.sectionInset.left, sectionOffset.y + self.sectionInset.top);
CGFloat previousItemSize = 0;
CGFloat contentMaxValueInScrollDirection = 0;
CGFloat maxWidth = [self viewPortAvailableSize];
NSInteger rowIndex = -1;
for (NSArray *row in partition) {
rowIndex++;
CGFloat summedRatios = 0;
for (NSInteger j = i, n = i + [row count]; j < n; j++) {
CGSize preferredSize = [self.delegate collectionView:self.collectionView layout:self preferredSizeForItemAtIndexPath:[NSIndexPath indexPathForItem:j inSection:section]];
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
summedRatios += preferredSize.width / preferredSize.height;
}
else {
summedRatios += preferredSize.height / preferredSize.width;
}
}
CGFloat rowSize = [self viewPortAvailableSize] - (([row count] - 1) * self.minimumInteritemSpacing);
if (rowIndex == (NSInteger)[partition count] - 1) {
if ([row count] < 2) {
rowSize = CGFloor([self viewPortAvailableSize] / 3.0f) - (([row count] - 1) * self.minimumInteritemSpacing);
} else if ([row count] < 3) {
rowSize = CGFloor([self viewPortAvailableSize] * 2.0f / 3.0f) - (([row count] - 1) * self.minimumInteritemSpacing);
}
}
for (NSInteger j = i, n = i + [row count]; j < n; j++) {
CGSize preferredSize = [self.delegate collectionView:self.collectionView layout:self preferredSizeForItemAtIndexPath:[NSIndexPath indexPathForItem:j inSection:section]];
CGSize actualSize = CGSizeZero;
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
actualSize = CGSizeMake(CGRound(rowSize / summedRatios * (preferredSize.width / preferredSize.height)), _preferredRowSize /*CGRound(rowSize / summedRatios)*/);
}
else {
actualSize = CGSizeMake(CGRound(rowSize / summedRatios), CGRound(rowSize / summedRatios * (preferredSize.height / preferredSize.width)));
}
CGRect frame = CGRectMake(offset.x, offset.y, actualSize.width, actualSize.height);
if (frame.origin.x + frame.size.width >= maxWidth - 2.0f) {
frame.size.width = MAX(1.0f, maxWidth - frame.origin.x);
}
// copy frame into frames ptr and increment ptr
*frames++ = frame;
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
offset.x += actualSize.width + self.minimumInteritemSpacing;
previousItemSize = actualSize.height;
contentMaxValueInScrollDirection = CGRectGetMaxY(frame);
}
else {
offset.y += actualSize.height + self.minimumInteritemSpacing;
previousItemSize = actualSize.width;
contentMaxValueInScrollDirection = CGRectGetMaxX(frame);
}
}
/**
* Check if row actually contains any items before changing offset,
* because linear partitioning algorithm might return a row with no items.
*/
if ([row count] > 0) {
// move offset to next line
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
offset = CGPointMake(self.sectionInset.left, offset.y + previousItemSize + self.minimumLineSpacing);
}
else {
offset = CGPointMake(offset.x + previousItemSize + self.minimumLineSpacing, self.sectionInset.top);
}
}
i += [row count];
}
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
*sectionSize = CGSizeMake([self viewPortWidth], (contentMaxValueInScrollDirection - sectionOffset.y) + self.sectionInset.bottom);
}
else {
*sectionSize = CGSizeMake((contentMaxValueInScrollDirection - sectionOffset.x) + self.sectionInset.right, [self viewPortHeight]);
}
}
- (CGFloat)viewPortWidth
{
return CGRectGetWidth(self.collectionView.frame) - self.collectionView.contentInset.left - self.collectionView.contentInset.right;
}
- (CGFloat)viewPortHeight
{
return (CGRectGetHeight(self.collectionView.frame) - self.collectionView.contentInset.top - self.collectionView.contentInset.bottom);
}
- (CGFloat)viewPortAvailableSize
{
CGFloat availableSize = 0;
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
availableSize = [self viewPortWidth] - self.sectionInset.left - self.sectionInset.right;
}
else {
availableSize = [self viewPortHeight] - self.sectionInset.top - self.sectionInset.bottom;
}
return availableSize;
}
#pragma mark - Custom setters
- (void)setPreferredRowSize:(CGFloat)preferredRowHeight
{
_preferredRowSize = preferredRowHeight;
[self invalidateLayout];
}
- (void)setSectionInset:(UIEdgeInsets)sectionInset
{
_sectionInset = sectionInset;
[self invalidateLayout];
}
- (void)setMinimumLineSpacing:(CGFloat)minimumLineSpacing
{
_minimumLineSpacing = minimumLineSpacing;
[self invalidateLayout];
}
- (void)setMinimumInteritemSpacing:(CGFloat)minimumInteritemSpacing
{
_minimumInteritemSpacing = minimumInteritemSpacing;
[self invalidateLayout];
}
- (void)setHeaderReferenceSize:(CGSize)headerReferenceSize
{
_headerReferenceSize = headerReferenceSize;
[self invalidateLayout];
}
- (void)setFooterReferenceSize:(CGSize)footerReferenceSize
{
_footerReferenceSize = footerReferenceSize;
[self invalidateLayout];
}
#pragma mark - Delegate
- (id<TGGifKeyboardBalancedLayoutDelegate>)delegate
{
return (id<TGGifKeyboardBalancedLayoutDelegate>)self.collectionView.delegate;
}
#pragma mark - Delegate helpers
- (CGSize)referenceSizeForHeaderInSection:(NSInteger)__unused section
{
return CGSizeZero;
}
- (CGSize)referenceSizeForFooterInSection:(NSInteger)__unused section
{
return CGSizeZero;
}
@end