如果要實現這樣的布局,使用UICollectionView無疑是最好的一個選擇,在圖片數量很大的情況下,collectionView的cell有重用的機制。通過重寫collectionView的布局可以實現各種五花八門的布局。
一、實現一個簡單的UICollectionView
實現一個簡單的UICollectionView和UITableView非常的類似,同樣是datasource和delegate的設計模式,datasource為View提供數據源,告訴View 顯示什么內容及如何顯示他們,delegate提供一些樣式的細節以及和用戶的交互。
UICollectionViewDataSource
- section的數量(組數量) -numberOfSectionsInCollection:
- 某個section里有多少個item(某一組里面有多少個item) -collectionView:numberOfItemsInSection:
- 對于某個位置應該顯示什么樣的cell -collectionView:cellForItemAtIndexPath:
實現上面的三個數據源方法,基本就可以保證collectionView正常工作。
關于重用
View的渲染是消耗性能的,為了提高效率,所以必須重用item。與tableView一樣在tableView向數據源請求數據之前可以使用
- registerClass:forCellWithReuseIdentifier:
- registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
- registerNib:forCellWithReuseIdentifier:
- registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
這些方法注冊cell,這樣可以省下每次判斷并初始化cell的代碼,要是在重用隊列里沒有可用的cell的話,runtime將自動幫我們生成并初始化一個可用的cell。
UICollectionViewDelegate
數據無關的view的外形啊,用戶交互啊什么的,由UICollectionViewDelegate來負責:
- cell的高亮
- cell的選中狀態
- 可以支持長按后的菜單
每個cell有獨立的高亮事件和選中事件的delegate,用戶點擊cell的時候,現在會按照以下流程向delegate進行詢問:
- collectionView:shouldHighlightItemAtIndexPath: 是否應該高亮?
- collectionView:didHighlightItemAtIndexPath: 如果1回答為是,那么高亮
- collectionView:shouldSelectItemAtIndexPath: 無論1結果如何,都詢問是否可以被選中? - collectionView:didUnhighlightItemAtIndexPath: 如果1回答為是,那么現在取消高亮
- collectionView:didSelectItemAtIndexPath: 如果3回答為是,那么選中cell
對應的高亮和選中狀態分別由highlighted和selected兩個屬性表示。
關于Cell
相對于UITableViewCell來說,UICollectionViewCell比較簡單。首先UICollectionViewCell不存在各式各樣的默認的style,這主要是由于展示對象的性質決定的,因為UICollectionView所用來展示的對象相比UITableView來說要來得靈活,大部分情況下更偏向于圖像而非文字,因此需求將會千奇百怪。因此SDK提供給我們的默認的UICollectionViewCell結構上相對比較簡單,由下至上:
- 首先是cell本身作為容器view
- 然后是一個大小自動適應整個cell的backgroundView,用作cell平時的背景
- 再其上是selectedBackgroundView,是cell被選中時的背景
- 最后是一個contentView,自定義內容應被加在這個view上
被選中cell的自動變化,所有的cell中的子view,也包括contentView中的子view,在當cell被選中時,會自動去查找view是否有被選中狀態下的改變。比如在contentView里加了一個normal和selected指定了不同圖片的imageView,那么選中這個cell的同時這張圖片也會從normal變成selected,而不需要額外的任何代碼。
UICollectionViewLayout
是UICollectionView的精髓…這也是UICollectionView和UITableView最大的不同。UICollectionViewLayout可以說是UICollectionView的大腦和中樞,它負責了將各個cell、Supplementary View和Decoration Views進行組織,為它們設定各自的屬性,包括但不限于:
位置
尺寸
透明度
層級關系
形狀
等等等等…
Layout決定了UICollectionView是如何顯示在界面上的。在展示之前,一般需要生成合適的UICollectionViewLayout子類對象,并將其賦予CollectionView的collectionViewLayout屬性。Apple為我們提供了一個最簡單可能也是最常用的默認layout對象,UICollectionViewFlowLayout。Flow Layout簡單說是一個直線對齊的layout(流水布局),最常見的Grid View形式即為一種Flow Layout配置。上面的照片架界面就是一個典型的Flow Layout。首先一個重要的屬性是itemSize,它定義了每一個item的大小。通過設定itemSize可以全局地改變所有cell的尺寸,如果想要對某個cell制定尺寸,可以使用-collectionView:layout:sizeForItemAtIndexPath:方法。
間隔 可以指定item之間的間隔和每一行之間的間隔,和size類似,有全局屬性,也可以對每一個item和每一個section做出設定:
@property (CGSize) minimumInteritemSpacing
@property (CGSize) minimumLineSpacing
- -collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
- -collectionView:layout:minimumLineSpacingForSectionAtIndex:
- 滾動方向 由屬性scrollDirection確定scroll view的方向,將影響Flow Layout的基本方向和由header及footer確定的section之間的寬度
UICollectionViewScrollDirectionVertical
UICollectionViewScrollDirectionHorizontal
Header和Footer尺寸 同樣地分為全局和部分。需要注意根據滾動方向不同,header和footer的高和寬中只有一個會起作用。垂直滾動時section間寬度為該尺寸的高,而水平滾動時為寬度起作用,如圖。
@property (CGSize) headerReferenceSize
@property (CGSize) footerReferenceSize
--collectionView:layout:referenceSizeForHeaderInSection:
--collectionView:layout:referenceSizeForFooterInSection:
縮進
@property UIEdgeInsets sectionInset;
--collectionView:layout:insetForSectionAtIndex:
總結
一個UICollectionView的實現包括兩個必要部分:UICollectionViewDataSource和UICollectionViewLayout,和一個交互部分:UICollectionViewDelegate。而Apple給出的UICollectionViewFlowLayout已經是一個很強力的layout方案了。
UICollectionViewLayoutAttributes
UICollectionViewLayoutAttributes是一個非常重要的類,先來看看property列表:
//item的frame信息
@property (nonatomic) CGRect frame
//item的中心點信息
@property (nonatomic) CGPoint center
//item的size信息
@property (nonatomic) CGSize size
//item的transfrom信息
@property (nonatomic) CATransform3D transform3D
//item的透明度信息
@property (nonatomic) CGFloat alpha
@property (nonatomic) NSInteger zIndex
//是否隱藏
@property (nonatomic, getter=isHidden) BOOL hidden
可以看到,UICollectionViewLayoutAttributes的實例中包含了諸如邊框,中心點,大小,形狀,透明度,層次關系和是否隱藏等信息。和DataSource的行為十分類似,當UICollectionView在獲取布局時將針對每一個indexPath的部件(包括cell,追加視圖和裝飾視圖),向其上的UICollectionViewLayout實例詢問該部件的布局信息,這個布局信息,就以UICollectionViewLayoutAttributes的實例的方式給出。
自定義UICollectionViewLayout
由于Apple已經給出了一種默認的樣式UICollectionViewFlowLayout,所以可以繼承自UICollectionViewFlowLayout來自定義一種布局,這種方式比較簡單,由于繼承自UICollectionViewFlowLayout所以collectionView的一些信息父類已經計算好了,只需要調用super就可以了。接著上代碼
創建1個空項目在ViewController的.m文件中實現如下代碼
#import "ViewController.h"
//線性布局
#import "GZDLineLayout.h"
//自定義的CollectionViewCell
#import "GZDCollectionViewCell.h"
@interface ViewController ()<UICollectionViewDataSource>
//imageName 數組 (圖片的命名是按鈕1.jpg,2.jpg來命名的)
@property (strong,nonatomic) NSMutableArray * imageNames;
//控制器View里面的CollectionView
@property (weak,nonatomic) UICollectionView *collectionView;
@end
@implementation ViewController
//懶加載圖片數組
- (NSMutableArray *)imageNames {
if (!_imageNames) {
_imageNames = [NSMutableArray array];
for (int i = 0; i < 20; i++ ) {
NSString *name = [NSString stringWithFormat:@"%d",i + 1];
[_imageNames addObject:name];
}
}
return _imageNames;
}
//懶加載CollectionView。。。其實沒必要
- (UICollectionView *)collectionView {
if (!_collectionView) {
//創建自定義的布局【這個是重點】
GZDLineLayout *layout = [[GZDLineLayout alloc] init];
//創建collectionView并且設置frame ,frame是隨便寫的
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 100, self.view.bounds.size.width, 200) collectionViewLayout:layout];
//不顯示水平滾動條
collectionView.showsHorizontalScrollIndicator = NO;
//不顯示垂直滾動條
collectionView.showsVerticalScrollIndicator = NO;
//隨便設置了1個背景顏色
collectionView.backgroundColor = [UIColor darkGrayColor];
//設置數據源
collectionView.dataSource = self;
//注冊Cell
[collectionView registerClass:[GZDCollectionViewCell class] forCellWithReuseIdentifier:ID];
//添加到控制器的View中
[self.view addSubview:collectionView];
_collectionView = collectionView;
}
return _collectionView;
}
//重用ID
static NSString *const ID = @"cell";
- (void)viewDidLoad {
[super viewDidLoad];
//取出layout
GZDLineLayout *layout =(GZDLineLayout *)self.collectionView.collectionViewLayout;
//為了讓第一個Item放在最中間,所以設置了edgeInsets
self.collectionView.contentInset = UIEdgeInsetsMake(0, self.collectionView.bounds.size.width * 0.5 - layout.itemSize.width * 0.5 , 0, self.collectionView.bounds.size.width * 0.5 - layout.itemSize.width * 0.5);
}
//數據源方法返回一共多少個cell
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.imageNames.count;
}
//數據源方法,返回顯示的是什么樣的cell
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
GZDCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath];
cell.imageName = self.imageNames[indexPath.item];
return cell;
}
@end
//自定義cell文件
#import "GZDCollectionViewCell.h"
@interface GZDCollectionViewCell()
@property (weak,nonatomic) UIImageView *photoView;
@end
@implementation GZDCollectionViewCell
- (UIImageView *)photoView {
if (!_photoView) {
UIImageView *photoView = [[UIImageView alloc] init];
photoView.layer.borderColor = [[UIColor whiteColor] CGColor];
photoView.layer.borderWidth = 5;
[self.contentView addSubview:photoView];
_photoView = photoView;
}
return _photoView;
}
- (void)setImageName:(NSString *)imageName {
_imageName = [imageName copy];
self.photoView.image = [UIImage imageNamed:_imageName];
}
- (void)layoutSubviews {
[super layoutSubviews];
self.photoView.frame = self.bounds;
}
@end
//重點布局.m文件
#import "GZDLineLayout.h"
@implementation GZDLineLayout
/**
UICollectionViewLayoutAttributes *attrs;
1.一個cell對應一個UICollectionViewLayoutAttributes對象
2.UICollectionViewLayoutAttributes對象決定了cell的frame
這個方法的返回值是一個數組(數組里面存放著rect范圍內所有元素的布局屬性)
* 這個方法的返回值決定了rect范圍內所有元素的排布(frame)
*/
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *layoutAttrs = [super layoutAttributesForElementsInRect:rect];
//collectionView中心點的位置
CGFloat collectionViewCenterX = self.collectionView.bounds.size.width * 0.5 + self.collectionView.contentOffset.x;
// NSLog(@"%f",self.collectionView.contentOffset.x);
for (UICollectionViewLayoutAttributes *attrs in layoutAttrs) {
//item距離collectionView中點的位置距離
CGFloat delta = ABS(collectionViewCenterX - attrs.center.x);
CGFloat scale = 1 - delta / self.collectionView.bounds.size.width;
attrs.transform = CGAffineTransformMakeScale(scale, scale);
}
return layoutAttrs;
}
/**
* 當collectionView的顯示范圍發生改變的時候,是否需要重新刷新布局
* 一旦重新刷新布局,就會重新調用下面的方法:
1.prepareLayout
2.layoutAttributesForElementsInRect:方法
*/
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
/**
重寫targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法
作用:返回值決定了collectionView停止滾動時最終的偏移量(contentOffset)
參數:
- proposedContentOffset:原本情況下,collectionView停止滾動時最終的偏移量
- velocity:滾動速率,通過這個參數可以了解滾動的方向
*/
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
//目的的位置,然后計算與中心點的距離 最小的那一個就 = 中心點的位置。
NSArray *layoutAttrs = [self layoutAttributesForElementsInRect:CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height)];
CGFloat centerX = self.collectionView.bounds.size.width * 0.5 + proposedContentOffset.x;
CGFloat minDelta = MAXFLOAT;
for (UICollectionViewLayoutAttributes *attrs in layoutAttrs) {
if (!CGRectIntersectsRect(CGRectMake(proposedContentOffset.x, proposedContentOffset.y, self.collectionView.frame.size.width, self.collectionView.frame.size.height), attrs.frame))continue;
CGFloat delta = ABS(attrs.center.x - centerX);
if (delta < ABS(minDelta)) {
minDelta = attrs.center.x - centerX;
}
}
return CGPointMake(proposedContentOffset.x + minDelta, proposedContentOffset.y);
}
/**
* 用來做布局的初始化操作(不建議在init方法中進行布局的初始化操作)
作用:在這個方法中做一些初始化操作
注意:一定要調用[super prepareLayout]
*/
- (void)prepareLayout {
[super prepareLayout];
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.itemSize = CGSizeMake(self.collectionView.bounds.size.height * 0.5, self.collectionView.bounds.size.height * 0.5);
}
@end
在初始化一個UICollectionViewLayout實例后,會有一系列準備方法被自動調用,以保證layout實例的正確。
首先,-(void)prepareLayout將被調用,默認下該方法什么沒做,但是在自己的子類實現中,一般在該方法中設定一些必要的layout的結構和初始需要的參數等。
之后,-(CGSize) collectionViewContentSize將被調用,以確定collection應該占據的尺寸。注意這里的尺寸不是指可視部分的尺寸,而應該是所有內容所占的尺寸。collectionView的本質是一個scrollView,因此需要這個尺寸來配置滾動行為。
接下來-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect被調用,這個沒什么值得多說的。初始的layout的外觀將由該方法返回的UICollectionViewLayoutAttributes來決定。
另外,在需要更新layout時,需要給當前layout發送 -invalidateLayout,該消息會立即返回,并且預約在下一個loop的時候刷新當前layout,這一點和UIView的setNeedsLayout方法十分類似。在-invalidateLayout后的下一個collectionView的刷新loop中,又會從prepareLayout開始,依次再調用-collectionViewContentSize和-layoutAttributesForElementsInRect來生成更新后的布局。