session-ios/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m

173 lines
5.3 KiB
Mathematica
Raw Normal View History

2017-10-10 22:13:54 +02:00
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
2017-10-10 22:13:54 +02:00
//
#import "ConversationViewLayout.h"
#import "UIView+OWS.h"
NS_ASSUME_NONNULL_BEGIN
@interface ConversationViewLayout ()
@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) int contentWidth;
2017-10-10 22:13:54 +02:00
@end
#pragma mark -
@implementation ConversationViewLayout
- (instancetype)init
{
if (self = [super init]) {
_itemAttributesMap = [NSMutableDictionary new];
}
return self;
}
- (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;
}
- (void)prepareLayout
{
[super prepareLayout];
id<ConversationViewLayoutDelegate> delegate = self.delegate;
if (!delegate) {
2017-11-08 20:04:51 +01:00
OWSFail(@"%@ Missing delegate", self.logTag);
2017-10-10 22:13:54 +02:00
[self clearState];
return;
}
2017-10-10 22:13:54 +02:00
if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) {
2017-11-08 20:04:51 +01:00
OWSFail(
@"%@ Collection view has invalid size: %@", self.logTag, NSStringFromCGRect(self.collectionView.bounds));
2017-10-10 22:13:54 +02:00
[self clearState];
return;
}
if (self.hasLayout) {
return;
}
self.hasLayout = YES;
// TODO: Remove this log statement after we've reduced the invalidation churn.
2017-11-08 20:04:51 +01:00
DDLogVerbose(@"%@ prepareLayout", self.logTag);
2017-10-10 22:13:54 +02:00
const int vInset = 8;
2017-10-10 22:13:54 +02:00
const int hInset = 10;
const int vSpacing = 5;
const int viewWidth = (int)floor(self.collectionView.bounds.size.width);
2017-10-11 15:44:10 +02:00
const int contentWidth = (int)floor(viewWidth - 2 * hInset);
self.contentWidth = contentWidth;
2017-10-10 22:13:54 +02:00
NSArray<id<ConversationViewLayoutItem>> *layoutItems = self.delegate.layoutItems;
CGFloat y = vInset + self.delegate.layoutHeaderHeight;
2017-10-10 22:13:54 +02:00
CGFloat contentBottom = y;
BOOL isRTL = self.collectionView.isRTL;
NSInteger row = 0;
for (id<ConversationViewLayoutItem> layoutItem in layoutItems) {
2017-10-11 15:44:10 +02:00
CGSize layoutSize = [layoutItem cellSizeForViewWidth:viewWidth contentWidth:contentWidth];
2017-10-10 22:13:54 +02:00
layoutSize.width = MIN(viewWidth, floor(layoutSize.width));
layoutSize.height = floor(layoutSize.height);
CGRect itemFrame;
switch (layoutItem.layoutAlignment) {
case ConversationViewLayoutAlignment_Incoming:
case ConversationViewLayoutAlignment_Outgoing: {
BOOL isIncoming = layoutItem.layoutAlignment == ConversationViewLayoutAlignment_Incoming;
BOOL isLeft = isIncoming ^ isRTL;
2017-10-10 22:13:54 +02:00
if (isLeft) {
itemFrame = CGRectMake(hInset, y, layoutSize.width, layoutSize.height);
} else {
itemFrame
= CGRectMake(viewWidth - (hInset + layoutSize.width), y, layoutSize.width, layoutSize.height);
}
break;
}
case ConversationViewLayoutAlignment_FullWidth:
itemFrame = CGRectMake(0, y, viewWidth, layoutSize.height);
break;
case ConversationViewLayoutAlignment_Center:
itemFrame = CGRectMake(
hInset + round((viewWidth - layoutSize.width) * 0.5f), y, layoutSize.width, layoutSize.height);
break;
}
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 + vSpacing;
row++;
}
contentBottom += vInset;
self.contentSize = CGSizeMake(viewWidth, contentBottom);
}
- (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.collectionView.bounds.size.width != newBounds.size.width;
}
@end
NS_ASSUME_NONNULL_END