session-ios/Session/Conversations/ConversationViewLayout.m

169 lines
4.7 KiB
Mathematica
Raw Normal View History

2017-10-10 22:13:54 +02:00
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
2017-10-10 22:13:54 +02:00
//
#import "ConversationViewLayout.h"
2019-05-02 23:58:48 +02:00
#import "Session-Swift.h"
2017-10-10 22:13:54 +02:00
#import "UIView+OWS.h"
NS_ASSUME_NONNULL_BEGIN
@interface ConversationViewLayout ()
@property (nonatomic) CGFloat lastViewWidth;
2017-10-10 22:13:54 +02:00
@property (nonatomic) CGSize contentSize;
2017-10-12 22:19:07 +02:00
2017-10-10 22:13:54 +02:00
@property (nonatomic, readonly) NSMutableDictionary<NSNumber *, UICollectionViewLayoutAttributes *> *itemAttributesMap;
2017-10-11 15:58:20 +02:00
// This dirty flag may be redundant with logic in UICollectionViewLayout,
// but it can't hurt and it ensures that we can safely & cheaply call
// prepareLayout from view logic to ensure that we always have a¸valid
// layout without incurring any of the (great) expense of performing an
// unnecessary layout pass.
2017-10-10 22:13:54 +02:00
@property (nonatomic) BOOL hasLayout;
@property (nonatomic) BOOL hasEverHadLayout;
2017-10-10 22:13:54 +02:00
@end
#pragma mark -
@implementation ConversationViewLayout
2018-06-25 21:20:17 +02:00
- (instancetype)initWithConversationStyle:(ConversationStyle *)conversationStyle
2017-10-10 22:13:54 +02:00
{
if (self = [super init]) {
_itemAttributesMap = [NSMutableDictionary new];
2018-06-25 21:20:17 +02:00
_conversationStyle = conversationStyle;
2017-10-10 22:13:54 +02:00
}
return self;
}
- (void)setHasLayout:(BOOL)hasLayout
{
_hasLayout = hasLayout;
if (hasLayout) {
self.hasEverHadLayout = YES;
}
}
2017-10-10 22:13:54 +02:00
- (void)invalidateLayout
{
[super invalidateLayout];
[self clearState];
}
- (void)invalidateLayoutWithContext:(UICollectionViewLayoutInvalidationContext *)context
{
[super invalidateLayoutWithContext:context];
[self clearState];
}
- (void)clearState
{
self.contentSize = CGSizeZero;
[self.itemAttributesMap removeAllObjects];
self.hasLayout = NO;
self.lastViewWidth = 0.f;
2017-10-10 22:13:54 +02:00
}
- (void)prepareLayout
{
[super prepareLayout];
id<ConversationViewLayoutDelegate> delegate = self.delegate;
if (!delegate) {
OWSFailDebug(@"Missing delegate");
2017-10-10 22:13:54 +02:00
[self clearState];
return;
}
2018-01-16 21:27:53 +01:00
2017-10-10 22:13:54 +02:00
if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) {
OWSFailDebug(@"Collection view has invalid size: %@", NSStringFromCGRect(self.collectionView.bounds));
2017-10-10 22:13:54 +02:00
[self clearState];
return;
}
if (self.hasLayout) {
return;
}
self.hasLayout = YES;
2018-08-09 16:47:43 +02:00
[self prepareLayoutOfItems];
}
2018-08-09 16:47:43 +02:00
- (void)prepareLayoutOfItems
{
2018-06-25 21:20:17 +02:00
const CGFloat viewWidth = self.conversationStyle.viewWidth;
2017-10-10 22:13:54 +02:00
NSArray<id<ConversationViewLayoutItem>> *layoutItems = self.delegate.layoutItems;
2018-06-25 21:20:17 +02:00
CGFloat y = self.conversationStyle.contentMarginTop + self.delegate.layoutHeaderHeight;
2017-10-10 22:13:54 +02:00
CGFloat contentBottom = y;
NSInteger row = 0;
2018-06-25 18:43:25 +02:00
id<ConversationViewLayoutItem> _Nullable previousLayoutItem = nil;
2017-10-10 22:13:54 +02:00
for (id<ConversationViewLayoutItem> layoutItem in layoutItems) {
2018-06-25 18:43:25 +02:00
if (previousLayoutItem) {
y += [layoutItem vSpacingWithPreviousLayoutItem:previousLayoutItem];
2017-10-10 22:13:54 +02:00
}
2018-08-09 16:47:43 +02:00
CGSize layoutSize = CGSizeCeil([layoutItem cellSize]);
2018-06-22 19:48:23 +02:00
// Ensure cell fits within view.
OWSAssertDebug(layoutSize.width <= viewWidth);
2018-06-22 19:48:23 +02:00
layoutSize.width = MIN(viewWidth, layoutSize.width);
2018-06-22 20:53:33 +02:00
// All cells are "full width" and are responsible for aligning their own content.
2018-06-22 19:48:23 +02:00
CGRect itemFrame = CGRectMake(0, y, viewWidth, layoutSize.height);
2017-10-10 22:13:54 +02:00
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
UICollectionViewLayoutAttributes *itemAttributes =
[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
itemAttributes.frame = itemFrame;
self.itemAttributesMap[@(row)] = itemAttributes;
contentBottom = itemFrame.origin.y + itemFrame.size.height;
2018-06-22 19:48:23 +02:00
y = contentBottom;
2017-10-10 22:13:54 +02:00
row++;
2018-06-25 18:43:25 +02:00
previousLayoutItem = layoutItem;
2017-10-10 22:13:54 +02:00
}
2018-06-25 21:20:17 +02:00
contentBottom += self.conversationStyle.contentMarginBottom;
2017-10-10 22:13:54 +02:00
self.contentSize = CGSizeMake(viewWidth, contentBottom);
self.lastViewWidth = viewWidth;
2017-10-10 22:13:54 +02:00
}
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray<UICollectionViewLayoutAttributes *> *result = [NSMutableArray new];
for (UICollectionViewLayoutAttributes *itemAttributes in self.itemAttributesMap.allValues) {
if (CGRectIntersectsRect(rect, itemAttributes.frame)) {
[result addObject:itemAttributes];
}
}
return result;
}
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return self.itemAttributesMap[@(indexPath.row)];
}
- (CGSize)collectionViewContentSize
{
return self.contentSize;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return self.lastViewWidth != newBounds.size.width;
2017-10-10 22:13:54 +02:00
}
@end
NS_ASSUME_NONNULL_END