// // 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 *itemAttributesMap; // This 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 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> *layoutItems = self.delegate.layoutItems; CGFloat y = vInset; CGFloat contentBottom = y; BOOL isRTL = self.collectionView.isRTL; NSInteger row = 0; for (id 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 *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