|
|
//
|
|
|
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
|
|
//
|
|
|
|
|
|
#import "ConversationViewLayout.h"
|
|
|
#import "Session-Swift.h"
|
|
|
#import "UIView+OWS.h"
|
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
|
@interface ConversationViewLayout ()
|
|
|
|
|
|
@property (nonatomic) CGFloat lastViewWidth;
|
|
|
@property (nonatomic) CGSize contentSize;
|
|
|
|
|
|
@property (nonatomic, readonly) NSMutableDictionary<NSNumber *, UICollectionViewLayoutAttributes *> *itemAttributesMap;
|
|
|
|
|
|
// 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.
|
|
|
@property (nonatomic) BOOL hasLayout;
|
|
|
@property (nonatomic) BOOL hasEverHadLayout;
|
|
|
|
|
|
@end
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
@implementation ConversationViewLayout
|
|
|
|
|
|
- (instancetype)initWithConversationStyle:(ConversationStyle *)conversationStyle
|
|
|
{
|
|
|
if (self = [super init]) {
|
|
|
_itemAttributesMap = [NSMutableDictionary new];
|
|
|
_conversationStyle = conversationStyle;
|
|
|
}
|
|
|
|
|
|
return self;
|
|
|
}
|
|
|
|
|
|
- (void)setHasLayout:(BOOL)hasLayout
|
|
|
{
|
|
|
_hasLayout = hasLayout;
|
|
|
|
|
|
if (hasLayout) {
|
|
|
self.hasEverHadLayout = YES;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- (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;
|
|
|
}
|
|
|
|
|
|
- (void)prepareLayout
|
|
|
{
|
|
|
[super prepareLayout];
|
|
|
|
|
|
id<ConversationViewLayoutDelegate> delegate = self.delegate;
|
|
|
if (!delegate) {
|
|
|
OWSFailDebug(@"Missing delegate");
|
|
|
[self clearState];
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
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));
|
|
|
[self clearState];
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (self.hasLayout) {
|
|
|
return;
|
|
|
}
|
|
|
self.hasLayout = YES;
|
|
|
|
|
|
[self prepareLayoutOfItems];
|
|
|
}
|
|
|
|
|
|
- (void)prepareLayoutOfItems
|
|
|
{
|
|
|
const CGFloat viewWidth = self.conversationStyle.viewWidth;
|
|
|
|
|
|
NSArray<id<ConversationViewLayoutItem>> *layoutItems = self.delegate.layoutItems;
|
|
|
|
|
|
CGFloat y = self.conversationStyle.contentMarginTop + self.delegate.layoutHeaderHeight;
|
|
|
CGFloat contentBottom = y;
|
|
|
|
|
|
NSInteger row = 0;
|
|
|
id<ConversationViewLayoutItem> _Nullable previousLayoutItem = nil;
|
|
|
for (id<ConversationViewLayoutItem> layoutItem in layoutItems) {
|
|
|
if (previousLayoutItem) {
|
|
|
y += [layoutItem vSpacingWithPreviousLayoutItem:previousLayoutItem];
|
|
|
}
|
|
|
|
|
|
CGSize layoutSize = CGSizeCeil([layoutItem cellSize]);
|
|
|
|
|
|
// Ensure cell fits within view.
|
|
|
OWSAssertDebug(layoutSize.width <= viewWidth);
|
|
|
layoutSize.width = MIN(viewWidth, layoutSize.width);
|
|
|
|
|
|
// All cells are "full width" and are responsible for aligning their own content.
|
|
|
CGRect itemFrame = CGRectMake(0, y, viewWidth, layoutSize.height);
|
|
|
|
|
|
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;
|
|
|
y = contentBottom;
|
|
|
row++;
|
|
|
previousLayoutItem = layoutItem;
|
|
|
}
|
|
|
|
|
|
contentBottom += self.conversationStyle.contentMarginBottom;
|
|
|
self.contentSize = CGSizeMake(viewWidth, contentBottom);
|
|
|
self.lastViewWidth = viewWidth;
|
|
|
}
|
|
|
|
|
|
- (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;
|
|
|
}
|
|
|
|
|
|
@end
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|