PSTCollectionViewFlowLayout.m 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. //
  2. // PSTCollectionViewFlowLayout.m
  3. // PSPDFKit
  4. //
  5. // Copyright (c) 2012-2013 Peter Steinberger. All rights reserved.
  6. //
  7. #import "PSTCollectionViewFlowLayout.h"
  8. #import "PSTCollectionView.h"
  9. #import "PSTGridLayoutItem.h"
  10. #import "PSTGridLayoutInfo.h"
  11. #import "PSTGridLayoutRow.h"
  12. #import "PSTGridLayoutSection.h"
  13. #import <objc/runtime.h>
  14. NSString *const PSTCollectionElementKindSectionHeader = @"UICollectionElementKindSectionHeader";
  15. NSString *const PSTCollectionElementKindSectionFooter = @"UICollectionElementKindSectionFooter";
  16. // this is not exposed in UICollectionViewFlowLayout
  17. NSString *const PSTFlowLayoutCommonRowHorizontalAlignmentKey = @"UIFlowLayoutCommonRowHorizontalAlignmentKey";
  18. NSString *const PSTFlowLayoutLastRowHorizontalAlignmentKey = @"UIFlowLayoutLastRowHorizontalAlignmentKey";
  19. NSString *const PSTFlowLayoutRowVerticalAlignmentKey = @"UIFlowLayoutRowVerticalAlignmentKey";
  20. @implementation PSTCollectionViewFlowLayout {
  21. // class needs to have same iVar layout as UICollectionViewLayout
  22. struct {
  23. unsigned int delegateSizeForItem : 1;
  24. unsigned int delegateReferenceSizeForHeader : 1;
  25. unsigned int delegateReferenceSizeForFooter : 1;
  26. unsigned int delegateInsetForSection : 1;
  27. unsigned int delegateInteritemSpacingForSection : 1;
  28. unsigned int delegateLineSpacingForSection : 1;
  29. unsigned int delegateAlignmentOptions : 1;
  30. unsigned int keepDelegateInfoWhileInvalidating : 1;
  31. unsigned int keepAllDataWhileInvalidating : 1;
  32. unsigned int layoutDataIsValid : 1;
  33. unsigned int delegateInfoIsValid : 1;
  34. }_gridLayoutFlags;
  35. CGFloat _interitemSpacing;
  36. CGFloat _lineSpacing;
  37. CGSize _itemSize;
  38. CGSize _headerReferenceSize;
  39. CGSize _footerReferenceSize;
  40. UIEdgeInsets _sectionInset;
  41. PSTGridLayoutInfo *_data;
  42. CGSize _currentLayoutSize;
  43. NSMutableDictionary *_insertedItemsAttributesDict;
  44. NSMutableDictionary *_insertedSectionHeadersAttributesDict;
  45. NSMutableDictionary *_insertedSectionFootersAttributesDict;
  46. NSMutableDictionary *_deletedItemsAttributesDict;
  47. NSMutableDictionary *_deletedSectionHeadersAttributesDict;
  48. NSMutableDictionary *_deletedSectionFootersAttributesDict;
  49. PSTCollectionViewScrollDirection _scrollDirection;
  50. NSDictionary *_rowAlignmentsOptionsDictionary;
  51. CGRect _visibleBounds;
  52. char filler[200]; // [HACK] Our class needs to be larger than Apple's class for the superclass change to work.
  53. }
  54. @synthesize rowAlignmentOptions = _rowAlignmentsOptionsDictionary;
  55. @synthesize minimumLineSpacing = _lineSpacing;
  56. @synthesize minimumInteritemSpacing = _interitemSpacing;
  57. ///////////////////////////////////////////////////////////////////////////////////////////
  58. #pragma mark - NSObject
  59. - (void)commonInit {
  60. _itemSize = CGSizeMake(50.f, 50.f);
  61. _lineSpacing = 10.f;
  62. _interitemSpacing = 10.f;
  63. _sectionInset = UIEdgeInsetsZero;
  64. _scrollDirection = PSTCollectionViewScrollDirectionVertical;
  65. _headerReferenceSize = CGSizeZero;
  66. _footerReferenceSize = CGSizeZero;
  67. }
  68. - (id)init {
  69. if ((self = [super init])) {
  70. [self commonInit];
  71. // set default values for row alignment.
  72. _rowAlignmentsOptionsDictionary = @{
  73. PSTFlowLayoutCommonRowHorizontalAlignmentKey : @(PSTFlowLayoutHorizontalAlignmentJustify),
  74. PSTFlowLayoutLastRowHorizontalAlignmentKey : @(PSTFlowLayoutHorizontalAlignmentJustify),
  75. // TODO: those values are some enum. find out what that is.
  76. PSTFlowLayoutRowVerticalAlignmentKey : @(1),
  77. };
  78. }
  79. return self;
  80. }
  81. - (id)initWithCoder:(NSCoder *)decoder {
  82. if ((self = [super initWithCoder:decoder])) {
  83. [self commonInit];
  84. // Some properties are not set if they're default (like minimumInteritemSpacing == 10)
  85. if ([decoder containsValueForKey:@"UIItemSize"])
  86. self.itemSize = [decoder decodeCGSizeForKey:@"UIItemSize"];
  87. if ([decoder containsValueForKey:@"UIInteritemSpacing"])
  88. self.minimumInteritemSpacing = [decoder decodeFloatForKey:@"UIInteritemSpacing"];
  89. if ([decoder containsValueForKey:@"UILineSpacing"])
  90. self.minimumLineSpacing = [decoder decodeFloatForKey:@"UILineSpacing"];
  91. if ([decoder containsValueForKey:@"UIFooterReferenceSize"])
  92. self.footerReferenceSize = [decoder decodeCGSizeForKey:@"UIFooterReferenceSize"];
  93. if ([decoder containsValueForKey:@"UIHeaderReferenceSize"])
  94. self.headerReferenceSize = [decoder decodeCGSizeForKey:@"UIHeaderReferenceSize"];
  95. if ([decoder containsValueForKey:@"UISectionInset"])
  96. self.sectionInset = [decoder decodeUIEdgeInsetsForKey:@"UISectionInset"];
  97. if ([decoder containsValueForKey:@"UIScrollDirection"])
  98. self.scrollDirection = [decoder decodeIntegerForKey:@"UIScrollDirection"];
  99. }
  100. return self;
  101. }
  102. - (void)encodeWithCoder:(NSCoder *)coder {
  103. [super encodeWithCoder:coder];
  104. [coder encodeCGSize:self.itemSize forKey:@"UIItemSize"];
  105. [coder encodeFloat:(float)self.minimumInteritemSpacing forKey:@"UIInteritemSpacing"];
  106. [coder encodeFloat:(float)self.minimumLineSpacing forKey:@"UILineSpacing"];
  107. [coder encodeCGSize:self.footerReferenceSize forKey:@"UIFooterReferenceSize"];
  108. [coder encodeCGSize:self.headerReferenceSize forKey:@"UIHeaderReferenceSize"];
  109. [coder encodeUIEdgeInsets:self.sectionInset forKey:@"UISectionInset"];
  110. [coder encodeInteger:self.scrollDirection forKey:@"UIScrollDirection"];
  111. }
  112. ///////////////////////////////////////////////////////////////////////////////////////////
  113. #pragma mark - PSTCollectionViewLayout
  114. static char kPSTCachedItemRectsKey;
  115. - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
  116. // Apple calls _layoutAttributesForItemsInRect
  117. if (!_data) [self prepareLayout];
  118. NSMutableArray *layoutAttributesArray = [NSMutableArray array];
  119. for (PSTGridLayoutSection *section in _data.sections) {
  120. if (CGRectIntersectsRect(section.frame, rect)) {
  121. // if we have fixed size, calculate item frames only once.
  122. // this also uses the default PSTFlowLayoutCommonRowHorizontalAlignmentKey alignment
  123. // for the last row. (we want this effect!)
  124. NSMutableDictionary *rectCache = objc_getAssociatedObject(self, &kPSTCachedItemRectsKey);
  125. NSUInteger sectionIndex = [_data.sections indexOfObjectIdenticalTo:section];
  126. CGRect normalizedHeaderFrame = section.headerFrame;
  127. normalizedHeaderFrame.origin.x += section.frame.origin.x;
  128. normalizedHeaderFrame.origin.y += section.frame.origin.y;
  129. if (!CGRectIsEmpty(normalizedHeaderFrame) && CGRectIntersectsRect(normalizedHeaderFrame, rect)) {
  130. PSTCollectionViewLayoutAttributes *layoutAttributes = [[self.class layoutAttributesClass] layoutAttributesForSupplementaryViewOfKind:PSTCollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:sectionIndex]];
  131. layoutAttributes.frame = normalizedHeaderFrame;
  132. [layoutAttributesArray addObject:layoutAttributes];
  133. }
  134. NSArray *itemRects = rectCache[@(sectionIndex)];
  135. if (!itemRects && section.fixedItemSize && section.rows.count) {
  136. itemRects = [(section.rows)[0] itemRects];
  137. if (itemRects) rectCache[@(sectionIndex)] = itemRects;
  138. }
  139. for (PSTGridLayoutRow *row in section.rows) {
  140. CGRect normalizedRowFrame = row.rowFrame;
  141. normalizedRowFrame.origin.x += section.frame.origin.x;
  142. normalizedRowFrame.origin.y += section.frame.origin.y;
  143. if (CGRectIntersectsRect(normalizedRowFrame, rect)) {
  144. // TODO be more fine-grained for items
  145. for (NSInteger itemIndex = 0; itemIndex < row.itemCount; itemIndex++) {
  146. PSTCollectionViewLayoutAttributes *layoutAttributes;
  147. NSUInteger sectionItemIndex;
  148. CGRect itemFrame;
  149. if (row.fixedItemSize) {
  150. itemFrame = [itemRects[itemIndex] CGRectValue];
  151. sectionItemIndex = row.index * section.itemsByRowCount + itemIndex;
  152. }else {
  153. PSTGridLayoutItem *item = row.items[itemIndex];
  154. sectionItemIndex = [section.items indexOfObjectIdenticalTo:item];
  155. itemFrame = item.itemFrame;
  156. }
  157. layoutAttributes = [[self.class layoutAttributesClass] layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForItem:sectionItemIndex inSection:sectionIndex]];
  158. layoutAttributes.frame = CGRectMake(normalizedRowFrame.origin.x + itemFrame.origin.x, normalizedRowFrame.origin.y + itemFrame.origin.y, itemFrame.size.width, itemFrame.size.height);
  159. [layoutAttributesArray addObject:layoutAttributes];
  160. }
  161. }
  162. }
  163. CGRect normalizedFooterFrame = section.footerFrame;
  164. normalizedFooterFrame.origin.x += section.frame.origin.x;
  165. normalizedFooterFrame.origin.y += section.frame.origin.y;
  166. if (!CGRectIsEmpty(normalizedFooterFrame) && CGRectIntersectsRect(normalizedFooterFrame, rect)) {
  167. PSTCollectionViewLayoutAttributes *layoutAttributes = [[self.class layoutAttributesClass] layoutAttributesForSupplementaryViewOfKind:PSTCollectionElementKindSectionFooter withIndexPath:[NSIndexPath indexPathForItem:0 inSection:sectionIndex]];
  168. layoutAttributes.frame = normalizedFooterFrame;
  169. [layoutAttributesArray addObject:layoutAttributes];
  170. }
  171. }
  172. }
  173. return layoutAttributesArray;
  174. }
  175. - (PSTCollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
  176. if (!_data) [self prepareLayout];
  177. PSTGridLayoutSection *section = _data.sections[indexPath.section];
  178. PSTGridLayoutRow *row = nil;
  179. CGRect itemFrame = CGRectZero;
  180. if (section.fixedItemSize && indexPath.item / section.itemsByRowCount < (NSInteger)section.rows.count) {
  181. row = section.rows[indexPath.item / section.itemsByRowCount];
  182. NSUInteger itemIndex = indexPath.item % section.itemsByRowCount;
  183. NSArray *itemRects = [row itemRects];
  184. itemFrame = [itemRects[itemIndex] CGRectValue];
  185. }else if (indexPath.item < (NSInteger)section.items.count) {
  186. PSTGridLayoutItem *item = section.items[indexPath.item];
  187. row = item.rowObject;
  188. itemFrame = item.itemFrame;
  189. }
  190. PSTCollectionViewLayoutAttributes *layoutAttributes = [[self.class layoutAttributesClass] layoutAttributesForCellWithIndexPath:indexPath];
  191. // calculate item rect
  192. CGRect normalizedRowFrame = row.rowFrame;
  193. normalizedRowFrame.origin.x += section.frame.origin.x;
  194. normalizedRowFrame.origin.y += section.frame.origin.y;
  195. layoutAttributes.frame = CGRectMake(normalizedRowFrame.origin.x + itemFrame.origin.x, normalizedRowFrame.origin.y + itemFrame.origin.y, itemFrame.size.width, itemFrame.size.height);
  196. return layoutAttributes;
  197. }
  198. - (PSTCollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
  199. if (!_data) [self prepareLayout];
  200. NSUInteger sectionIndex = indexPath.section;
  201. PSTCollectionViewLayoutAttributes *layoutAttributes = nil;
  202. if (sectionIndex < _data.sections.count) {
  203. PSTGridLayoutSection *section = _data.sections[sectionIndex];
  204. CGRect normalizedFrame = CGRectZero;
  205. if ([kind isEqualToString:PSTCollectionElementKindSectionHeader]) {
  206. normalizedFrame = section.headerFrame;
  207. }
  208. else if ([kind isEqualToString:PSTCollectionElementKindSectionFooter]) {
  209. normalizedFrame = section.footerFrame;
  210. }
  211. if (!CGRectIsEmpty(normalizedFrame)) {
  212. normalizedFrame.origin.x += section.frame.origin.x;
  213. normalizedFrame.origin.y += section.frame.origin.y;
  214. layoutAttributes = [[self.class layoutAttributesClass] layoutAttributesForSupplementaryViewOfKind:kind withIndexPath:[NSIndexPath indexPathForItem:0 inSection:sectionIndex]];
  215. layoutAttributes.frame = normalizedFrame;
  216. }
  217. }
  218. return layoutAttributes;
  219. }
  220. - (PSTCollectionViewLayoutAttributes *)layoutAttributesForDecorationViewWithReuseIdentifier:(NSString *)identifier atIndexPath:(NSIndexPath *)indexPath {
  221. return nil;
  222. }
  223. - (CGSize)collectionViewContentSize {
  224. if (!_data) [self prepareLayout];
  225. return _data.contentSize;
  226. }
  227. - (void)setSectionInset:(UIEdgeInsets)sectionInset {
  228. if (!UIEdgeInsetsEqualToEdgeInsets(sectionInset, _sectionInset)) {
  229. _sectionInset = sectionInset;
  230. [self invalidateLayout];
  231. }
  232. }
  233. ///////////////////////////////////////////////////////////////////////////////////////////
  234. #pragma mark - Invalidating the Layout
  235. - (void)invalidateLayout {
  236. [super invalidateLayout];
  237. objc_setAssociatedObject(self, &kPSTCachedItemRectsKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  238. _data = nil;
  239. }
  240. - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
  241. // we need to recalculate on width changes
  242. if ((_visibleBounds.size.width != newBounds.size.width && self.scrollDirection == PSTCollectionViewScrollDirectionVertical) || (_visibleBounds.size.height != newBounds.size.height && self.scrollDirection == PSTCollectionViewScrollDirectionHorizontal)) {
  243. _visibleBounds = self.collectionView.bounds;
  244. return YES;
  245. }
  246. return NO;
  247. }
  248. // return a point at which to rest after scrolling - for layouts that want snap-to-point scrolling behavior
  249. - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
  250. return proposedContentOffset;
  251. }
  252. - (void)prepareLayout {
  253. // custom ivars
  254. objc_setAssociatedObject(self, &kPSTCachedItemRectsKey, [NSMutableDictionary dictionary], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  255. _data = [PSTGridLayoutInfo new]; // clear old layout data
  256. _data.horizontal = self.scrollDirection == PSTCollectionViewScrollDirectionHorizontal;
  257. _visibleBounds = self.collectionView.bounds;
  258. CGSize collectionViewSize = _visibleBounds.size;
  259. _data.dimension = _data.horizontal ? collectionViewSize.height : collectionViewSize.width;
  260. _data.rowAlignmentOptions = _rowAlignmentsOptionsDictionary;
  261. [self fetchItemsInfo];
  262. }
  263. ///////////////////////////////////////////////////////////////////////////////////////////
  264. #pragma mark - Private
  265. - (void)fetchItemsInfo {
  266. [self getSizingInfos];
  267. [self updateItemsLayout];
  268. }
  269. // get size of all items (if delegate is implemented)
  270. - (void)getSizingInfos {
  271. // NSAssert(_data.sections.count == 0, @"Grid layout is already populated?");
  272. id<PSTCollectionViewDelegateFlowLayout> flowDataSource = (id<PSTCollectionViewDelegateFlowLayout>)self.collectionView.delegate;
  273. BOOL implementsSizeDelegate = [flowDataSource respondsToSelector:@selector(collectionView:layout:sizeForItemAtIndexPath:)];
  274. BOOL implementsHeaderReferenceDelegate = [flowDataSource respondsToSelector:@selector(collectionView:layout:referenceSizeForHeaderInSection:)];
  275. BOOL implementsFooterReferenceDelegate = [flowDataSource respondsToSelector:@selector(collectionView:layout:referenceSizeForFooterInSection:)];
  276. NSUInteger numberOfSections = [self.collectionView numberOfSections];
  277. for (NSUInteger section = 0; section < numberOfSections; section++) {
  278. PSTGridLayoutSection *layoutSection = [_data addSection];
  279. layoutSection.verticalInterstice = _data.horizontal ? self.minimumInteritemSpacing : self.minimumLineSpacing;
  280. layoutSection.horizontalInterstice = !_data.horizontal ? self.minimumInteritemSpacing : self.minimumLineSpacing;
  281. if ([flowDataSource respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)]) {
  282. layoutSection.sectionMargins = [flowDataSource collectionView:self.collectionView layout:self insetForSectionAtIndex:section];
  283. }else {
  284. layoutSection.sectionMargins = self.sectionInset;
  285. }
  286. if ([flowDataSource respondsToSelector:@selector(collectionView:layout:minimumLineSpacingForSectionAtIndex:)]) {
  287. CGFloat minimumLineSpacing = [flowDataSource collectionView:self.collectionView layout:self minimumLineSpacingForSectionAtIndex:section];
  288. if (_data.horizontal) {
  289. layoutSection.horizontalInterstice = minimumLineSpacing;
  290. }else {
  291. layoutSection.verticalInterstice = minimumLineSpacing;
  292. }
  293. }
  294. if ([flowDataSource respondsToSelector:@selector(collectionView:layout:minimumInteritemSpacingForSectionAtIndex:)]) {
  295. CGFloat minimumInterimSpacing = [flowDataSource collectionView:self.collectionView layout:self minimumInteritemSpacingForSectionAtIndex:section];
  296. if (_data.horizontal) {
  297. layoutSection.verticalInterstice = minimumInterimSpacing;
  298. }else {
  299. layoutSection.horizontalInterstice = minimumInterimSpacing;
  300. }
  301. }
  302. CGSize headerReferenceSize;
  303. if (implementsHeaderReferenceDelegate) {
  304. headerReferenceSize = [flowDataSource collectionView:self.collectionView layout:self referenceSizeForHeaderInSection:section];
  305. }else {
  306. headerReferenceSize = self.headerReferenceSize;
  307. }
  308. layoutSection.headerDimension = _data.horizontal ? headerReferenceSize.width : headerReferenceSize.height;
  309. CGSize footerReferenceSize;
  310. if (implementsFooterReferenceDelegate) {
  311. footerReferenceSize = [flowDataSource collectionView:self.collectionView layout:self referenceSizeForFooterInSection:section];
  312. }else {
  313. footerReferenceSize = self.footerReferenceSize;
  314. }
  315. layoutSection.footerDimension = _data.horizontal ? footerReferenceSize.width : footerReferenceSize.height;
  316. NSUInteger numberOfItems = [self.collectionView numberOfItemsInSection:section];
  317. // if delegate implements size delegate, query it for all items
  318. if (implementsSizeDelegate) {
  319. for (NSUInteger item = 0; item < numberOfItems; item++) {
  320. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];
  321. CGSize itemSize = implementsSizeDelegate ? [flowDataSource collectionView:self.collectionView layout:self sizeForItemAtIndexPath:indexPath] : self.itemSize;
  322. PSTGridLayoutItem *layoutItem = [layoutSection addItem];
  323. layoutItem.itemFrame = (CGRect){.size=itemSize};
  324. }
  325. // if not, go the fast path
  326. }else {
  327. layoutSection.fixedItemSize = YES;
  328. layoutSection.itemSize = self.itemSize;
  329. layoutSection.itemsCount = numberOfItems;
  330. }
  331. }
  332. }
  333. - (void)updateItemsLayout {
  334. CGSize contentSize = CGSizeZero;
  335. for (PSTGridLayoutSection *section in _data.sections) {
  336. [section computeLayout];
  337. // update section offset to make frame absolute (section only calculates relative)
  338. CGRect sectionFrame = section.frame;
  339. if (_data.horizontal) {
  340. sectionFrame.origin.x += contentSize.width;
  341. contentSize.width += section.frame.size.width + section.frame.origin.x;
  342. contentSize.height = fmax(contentSize.height, sectionFrame.size.height + section.frame.origin.y + section.sectionMargins.top + section.sectionMargins.bottom);
  343. }else {
  344. sectionFrame.origin.y += contentSize.height;
  345. contentSize.height += sectionFrame.size.height + section.frame.origin.y;
  346. contentSize.width = fmax(contentSize.width, sectionFrame.size.width + section.frame.origin.x + section.sectionMargins.left + section.sectionMargins.right);
  347. }
  348. section.frame = sectionFrame;
  349. }
  350. _data.contentSize = contentSize;
  351. }
  352. @end