|
|
|
|
//
|
|
|
|
|
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
#import "ConversationViewLayout.h"
|
|
|
|
|
#import "UIView+OWS.h"
|
|
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
|
|
|
|
|
@interface ConversationViewLayout ()
|
|
|
|
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
@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) {
|
|
|
|
|
OWSFail(@"%@ Missing delegate", self.tag);
|
|
|
|
|
[self clearState];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) {
|
|
|
|
|
OWSFail(@"%@ Collection view has invalid size: %@", self.tag, NSStringFromCGRect(self.collectionView.bounds));
|
|
|
|
|
[self clearState];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (self.hasLayout) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
self.hasLayout = YES;
|
|
|
|
|
|
|
|
|
|
// TODO: Remove this log statement after we've reduced the invalidation churn.
|
|
|
|
|
DDLogVerbose(@"%@ prepareLayout", self.tag);
|
|
|
|
|
|
|
|
|
|
const int vInset = 15;
|
|
|
|
|
const int hInset = 10;
|
|
|
|
|
const int vSpacing = 5;
|
|
|
|
|
const int viewWidth = (int)floor(self.collectionView.bounds.size.width);
|
|
|
|
|
const int contentWidth = (int)floor(viewWidth - 2 * hInset);
|
|
|
|
|
|
|
|
|
|
NSArray<id<ConversationViewLayoutItem>> *layoutItems = self.delegate.layoutItems;
|
|
|
|
|
|
|
|
|
|
CGFloat y = vInset;
|
|
|
|
|
CGFloat contentBottom = y;
|
|
|
|
|
BOOL isRTL = self.collectionView.isRTL;
|
|
|
|
|
|
|
|
|
|
NSInteger row = 0;
|
|
|
|
|
for (id<ConversationViewLayoutItem> layoutItem in layoutItems) {
|
|
|
|
|
CGSize layoutSize = [layoutItem cellSizeForViewWidth:viewWidth contentWidth:contentWidth];
|
|
|
|
|
|
|
|
|
|
layoutSize.width = MIN(viewWidth, floor(layoutSize.width));
|
|
|
|
|
layoutSize.height = floor(layoutSize.height);
|
|
|
|
|
CGRect itemFrame;
|
|
|
|
|
switch (layoutItem.layoutAlignment) {
|
|
|
|
|
case ConversationViewLayoutAlignment_Incoming:
|
|
|
|
|
case ConversationViewLayoutAlignment_Outgoing: {
|
|
|
|
|
BOOL isLeft = ((layoutItem.layoutAlignment == ConversationViewLayoutAlignment_Incoming && !isRTL)
|
|
|
|
|
|| (layoutItem.layoutAlignment == ConversationViewLayoutAlignment_Outgoing && isRTL));
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - Logging
|
|
|
|
|
|
|
|
|
|
+ (NSString *)tag
|
|
|
|
|
{
|
|
|
|
|
return [NSString stringWithFormat:@"[%@]", self.class];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSString *)tag
|
|
|
|
|
{
|
|
|
|
|
return self.class.tag;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|