從UICollectionView線性布局說起

如果要實現這樣的布局,使用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來生成更新后的布局。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容