自定義布局和自定義流水布局(CollectionViewLayout和CollectionViewFlowLayout)


  • 自定義流水布局--CollectionViewFlowLayout---水平布局實現一個相冊功能


  • 在UIScrollView的基礎上進行循環(huán)利用
    • 那怎么去做循環(huán)利用呢尉咕?
  • 第一種方案:
    • 實時監(jiān)控ScrollView的滾動山孔,一旦有一個家伙離開屏幕涩维,我們就把它放進一個數組或者是集合里面去,到時候我要用浩螺,我就把它拿過去用
    • 但是這個是很麻煩的,因為你總是得判斷它有沒有離開屏幕
  • 第二種方案:
    • 用蘋果自帶的幾個類:TableView或者是CollectionView
    • 因為它們本來就具備循環(huán)利用的功能
    • 但是TableView一看就不符合要求祭陷,因為它默認就是上下豎直滾動峦椰,不是左右水平滾動
      • 當然我們也可以用非主流的方式,讓TableView實現水平滾動
      • 讓TableView的Transform來個90°壁拉,讓它里面所有的cell也翻個90°谬俄,都轉過來。但這種做法有點奇葩弃理,開發(fā)中還是不要這么搞
      • 所以我們可以用CollectionView
    • CollectionView在我們的印象中是展示像那種九宮格的樣子溃论,而且也是上下豎直滾動
    • 但是CollectionView和TableView的區(qū)別就是:
      • CollectionView它默認就支持水平滾動,你只要修改它一個屬性為水平方向就行了痘昌。而TableView默認支持豎直滾動钥勋,沒有屬性去支持它水平滾動,除非你去搞一些非主流的做法

  • CollectionView一定要傳一個不空的Layout那個參數辆苔,因為默認的布局是九宮格算灸,它按這種方式排的原因是它有一個流水布局。正因為給它傳了一個流水布局驻啤,所以它就一行滿了乎婿,就流向下一行,流水一樣流下去流過來
    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:[UICollectionViewlayout alloc] init]];
  • 數據源方法 - <UICollectionViewDataSource>
  • numberOfItemsInSection是告訴它一組有多少個格子
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 50 ;
}
  • cellForItemAtIndexPath告訴它每個格子長出來是怎樣的一個cell街佑,因為每個格子都是一個CollectionViewCell
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    // 先要注冊
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CYCellId forIndexPath:indexPath];
    cell.backgroundColor = [UIColor orangeColor]

    return cell;
}
  • TableView和CollectionView的排布有很大的區(qū)別
    • TableView的排布是一行一行往下排布谢翎,而CollectionView的排布是完全取決于Layout,也就是說沐旨,你傳給它的Layout不一樣森逮,它的排布就不一樣。它的布局決定了cell的排布
    • 也就是說磁携,今后你想要CollectionView的cell排布豐富多彩褒侧,你只需要改變它的布局就行了
  • scrollDirection決定了它的滾動方向,設置它滾動的方向為水平
    // 水平滾動
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
  • itemSize決定了CollectionView布局的里面的cell的大小
    layout.itemSize = CGSizeMake(100, 100);
  • 你將CollectionView高度改小點谊迄,比如200闷供,那么你的高度不夠顯示兩排,就會如下顯示:
  • 而且你會發(fā)現不用擔心循環(huán)利用的問題统诺,CollectionView內部已經幫你做好了

  • 我們現在已經實現流水布局水平滾動歪脏,而且做好了循環(huán)利用。如果要做一層改進粮呢,那么我們就要自定義布局婿失,自己來寫一套布局钞艇,所以現在我們繼承于UICollectionViewFlowLayout
  • 我們要自定義CollectionView的布局有兩種方案
    • 1.繼承UICollectionViewLayout
      • 一般是繼承于UICollectionViewLayout就行了
      • 而且UICollectionViewFlowLayout繼承于UICollectionViewLayout
      • 但是如果你自定義繼承于UICollectionViewLayout,代表著你沒有流水布局功能豪硅,也就是在你不想要流水布局功能的時候就選擇繼承UICollectionViewLayout
    • 2.繼承UICollectionViewFlowLayout

**四 **

  • 所以我們自定義流水布局CYLineLayout
  • 在CYLineLayout.h文件中
#import <UIKit/UIKit.h>

@interface CYLineLayout : UICollectionViewFlowLayout

@end

  • 在CYLineLayout.m文件中重寫某些方法去實現:
    • 1.cell的放大與縮小
    • 2.停止?jié)L動的時候:cell居中
  • 進入頭文件可以發(fā)現要重寫的一些方法

  • UICollectionViewLayoutAttributes
    • 1.它是描述布局屬性的
    • 2.一個cell對應一個UICollectionViewLayoutAttributes對象
    • 3.UICollectionViewLayoutAttributes對象決定了cell的展示樣式(frame)說白了就是決定你的cell擺在哪里哩照,怎么去擺


  • layoutAttributesForElementsInRect這個方法的返回值是一個數組(數組里面存放著rect范圍內所有元素的布局屬性)
  • 這個方法的返回值決定了rect范圍內所有元素的排布(frame)
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    // 獲得super已經計算好的布局屬性(在super已經算好的基礎上,再去做一些改進)
    NSArray *array = [super layoutAttributesForElementsInRect:rect];

    // 計算collectionView最中心點的x值
    CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;

    // 在原有布局屬性的基礎上進行微調
    for (UICollectionViewLayoutAttributes *attrs in array) {
        // cell的中心點x和collectionView最中心點的x值 的間距
        CGFloat delta = ABS(attrs.center.x - centerX);

        // 根據間距值計算cell的縮放比例
        CGFloat scale = 1 - delta / self.collectionView.frame.size.width;

        // 設置縮放比例
        attrs.transform = CGAffineTransformMakeScale(scale, scale);
    }

    return array;
}

  • 計算collectionView中心點的x值
    • 要記住collectionView的坐標原點是以內容contentSize的原點為原點
    • 計算collectionView中心點的x值懒浮,千萬不要用collectionView的寬度除以2飘弧。而是用collectionView的偏移量加上collectionView寬度的一半
    • 坐標原點弄錯了就沒有可比性了,因為后面要判斷cell的中心點與collectionView中心點的差值
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;

  • cell的中心點x 和CollectionView最中心點的x值 的間距
     CGFloat delta = ABS(attrs.center.x - centerX);
     ABS(A)
     // 表示取絕對值 

  • 我們再根據間距值delta去算cell的縮放比例scale
    • 間距值delta和縮放比例scale是成反比的
    • 間距值delta的范圍為0--self.collectionView.frame.size.width * 0.5
CGFloat scale = 1 - delta / self.collectionView.frame.size.width;
// 用1-()砚著,是因為間距值delta和縮放比例scale是成反比的
  • 設置縮放比例
attrs.transform = CGAffineTransformMakeScale(scale, scale);

  • 但是設置后你會發(fā)現基本沒啥反應眯牧,顯示還亂七八糟的,這是什么原因呢赖草?
    • 我們是想要稍微動一下就修改一下学少,但是現在沒法達到我動一下就根據最新的中心點X來再算一遍一邊比例。沒有實現這個代碼
    • 因為這里還需要實現一個方法
  • 這個方法是shouldInvalidateLayoutForBoundsChange: 它的特點是:
    • 默認return NO
    • 當collectionView的顯示范圍發(fā)生改變的時候秧骑,判斷是否需要重新刷新布局
    • 一旦重新刷新布局版确,就會重新調用下面的方法:
      • 1.prepareLayout
      • 2.layoutAttributesForElementsInRect:方法
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    return YES;
  
}
  • 這樣之后,你會發(fā)現乎折,你稍微挪一下绒疗,它就重新算一遍,比例就會縮放骂澄, 達到了我們的要求
  • 而且非常流暢吓蘑,因為它有循環(huán)利用

  • 還要實現一個方法:targetContentOffsetForProposedContentOffset:()方法。它的返回值坟冲,就決定了collectionView停止?jié)L動時的偏移量
  • 這個方法在你手離開屏幕之前會調用磨镶,也就是cell即將停止?jié)L動的時候 (記住這一點)
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    // 計算出最終顯示的矩形框
    CGRect rect;
    rect.origin.y = 0;
    rect.origin.x = proposedContentOffset.x;
    rect.size = self.collectionView.frame.size;

    // 獲得super已經計算好的布局屬性
    NSArray *array = [super layoutAttributesForElementsInRect:rect];

    // 計算collectionView最中心點的x值
    CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;

    // 存放最小的間距值
    CGFloat minDelta = MAXFLOAT;
    for (UICollectionViewLayoutAttributes *attrs in array) {
        if (ABS(minDelta) > ABS(attrs.center.x - centerX)) {
            minDelta = attrs.center.x - centerX;
        }
    }

    // 修改原有的偏移量
    proposedContentOffset.x += minDelta;
    return proposedContentOffset;
}

  • 獲得super已經計算好的布局屬性
    NSArray *array = [super layoutAttributesForElementsInRect:rect];
  • 這里為什么不用self
    • 因為如果調self,又會來到layoutAttributesForElementsInRect:()方法的for循環(huán)中健提, 將transform再算一遍琳猫。而我們只想要拿到中心點X值∷奖裕靠父類就行了
    • 我們調super這個方法脐嫂,因為它當時已經算好了cell的中心點等X的值了。所以這里調super可能更好一點

  • 計算collectionView最中心點的x值
 CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;
- 這里為什么不按前面
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;

來算呢紊遵?

  • 因為targetContentOffsetForProposedContentOffset:()方法在你手離開屏幕之前會調用账千,也就是cell即將停止?jié)L動的時候,這個時候我們要算的是最后停下來偏移量暗膜。
  • 假如我們用力往左邊一甩匀奏,你的手已經離開,算的偏移量是你手離開時候的偏移量桦山,而不是我們最終的偏移量攒射,也就是說這么算的話醋旦,我們就算錯了
  • 你是應該拿到最終停下來的cell和CollectionView的中心點的X值進行比較的恒水。所以你應該最終的值会放,而不是手松開的那一刻的偏移量的值
    • 那我們怎么知道手松開的那一刻最終的偏移量X的值呢?
      • 這個方法返回的參數(CGPoint)proposedContentOffset钉凌,這是它本應該停留的位置咧最,最終停留的的值。而(CGPoint)targetContentOffsetForProposedContentOffset:這個是你最終返回的值御雕,也就是你要它停留到哪兒的值(這個參數決定你要cell最后停留在哪兒)

  • 同上面可知矢沿,我們最后拿到的矩形框也是不能亂傳的,也是要拿到最終的哪一個矩形框(不明白酸纲,就想像一下捣鲸,你往左邊或者右邊用手指一甩的時候,手離開的時候是一個值闽坡,最終停下來是一個值栽惶,而現在我們需要的是最終的值)
    // 計算出最終顯示的矩形框 
    CGRect rect; rect.origin.y = 0; rect.origin.x = proposedContentOffset.x; rect.size = self.collectionView.frame.size;


  • 然后我們要找最短的偏移量,找到它疾嗅,然后就讓他偏移?它的那個值外厂,讓它的中心點回到collectionView的中心點,也就是說重合代承。這樣就實現了不管你怎么去甩汁蝶,等cell停下來的時候。都會有一個cell它會停留在矩形框CollectionView的中心
    // 存放最小的間距值
         CGFloat minDelta = MAXFLOAT; 
         for (UICollectionViewLayoutAttributes *attrs in array) {
               if (ABS(minDelta) > ABS(attrs.center.x - centerX)) {                     
              minDelta = attrs.center.x - centerX; 
              }
       } 
        // 修改原有的偏移量 
        proposedContentOffset.x += minDelta; 
        return proposedContentOffset;
  • 一開始先保證minDelta是最大的论悴,保證誰都比你小掖棉。 第一次算出來的絕對值就肯定比你小,然后把它賦值給你minDelta膀估。?這樣就算出來了最小的間距值
  • 算出來最小間距值后啊片,你通過分析應該會發(fā)現,不管是往左偏還是往右偏玖像,要想讓cell回到中心點紫谷,最后你的偏移量應該是用:你本來應該 的偏移量+(cell的中心點X值—collectionView中心點X值)
  • 所以上面在比較的時候用絕對值,計算的時候不用絕對值捐寥,minDelta最后就有正數也有負數
  • 修改后讓它回到中間
  • 最后不管你怎么滑笤昨,它都會停在中間

  • 有一個小缺陷,你會發(fā)現握恳,一打開程序瞒窒,你往左或往右滑到最左或者最右的時候,cell總是默認粘著邊上乡洼,這個不太和諧崇裁,我們需要它距離左右兩邊都有一個距離匕坯,那我們該怎么做呢?
    • 這就是讓我們把所有的cell拔稳,讓它們往右邊或者左邊挪一段距離葛峻,所以就增加內邊距就可以了。怎么添加內邊距呢巴比?
      • collectionView是繼承ScrollView的术奖,所以設置它的ContentInset就可以了
      • 還一種方法通過這個布局它本來就有一個屬性sectionInset ,這本來就是來控制內邊距的轻绞,控制整個布局的采记。而且這個屬性只需要設置一次
  • 這里有一個?給collectionView專門用來布局的方法---prepareLayout,這里一般是做初始化操作
/**
 * 用來做布局的初始化操作(不建議在init方法中進行布局的初始化操作--可能布局還未加到View中去政勃,就會返回為空)
 */
- (void)prepareLayout
{
    [super prepareLayout];

    // 設置內邊距
    CGFloat inset = (self.collectionView.frame.size.width - self.itemSize.width) * 0.5;
    self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset);
}


  • 總的來說我們若要繼承自這個流水布局來實現這個功能的話唧龄,肯定是要重寫一些方法,告訴它一些內部的行為奸远,它才知道怎么去顯示那個東西既棺,我們用了一下的方法:
    • 我們首先得實現prepareLayout方法,做一些初始化
    • 然后然走,我們實現layoutAttributesForElementsInRect:方法援制。目的是拿出它計算好的布局屬性來做一個微調,這樣可以導致我們的cell可以變大或者變小
    • 然后實現targetContentOffsetForProposedContentOffset:方法芍瑞。它的目的是告訴當我手松開晨仑,cell停止?jié)L動的時候,他應該去哪兒拆檬,所以這個方法就決定了collectionView停止?jié)L動時的偏移量
    • 最后shouldInvalidateLayoutForBoundsChange:這個方法的價值就是告訴它你只要稍微往左或者往右挪一下洪己,你就重新刷新,只要你重新刷新竟贯,它就會重新根據你cell的中心點的X值距離你collectionView中心點的X值來決定你的縮放比例答捕。這樣就保證了我們每動一點點,比例都在變屑那,所以我們要動一下刷新一下拱镐。也就是當collectionView的顯示范圍發(fā)生改變的時候,是否需要重新刷新布局持际,一旦重新刷新布局沃琅,就會重新調用下面的方法:1.prepareLayout2.layoutAttributesForElementsInRect:方法
  • 關于做這個效果有一個挺牛逼的三方框架:iCarousel大家可以參考一下

  • 在CYLineLayout.h文件中
#import <UIKit/UIKit.h>

@interface CYLineLayout : UICollectionViewFlowLayout

@end
  • 在CYLineLayout.h文件中
#import "CYLineLayout.h"

@implementation CYLineLayout

- (instancetype)init
{
    if (self = [super init]) {
    }
    return self;
}

/**
 * 當collectionView的顯示范圍發(fā)生改變的時候,是否需要重新刷新布局
 * 一旦重新刷新布局蜘欲,就會重新調用下面的方法:
 1.prepareLayout
 2.layoutAttributesForElementsInRect:方法
 */
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    return YES;
}

/**
 * 用來做布局的初始化操作(不建議在init方法中進行布局的初始化操作)
 */
- (void)prepareLayout
{
    [super prepareLayout];

    // 水平滾動 
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    // 設置內邊距
    CGFloat inset = (self.collectionView.frame.size.width - self.itemSize.width) * 0.5;
    self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset);
}

/**
 UICollectionViewLayoutAttributes *attrs;
 1.一個cell對應一個UICollectionViewLayoutAttributes對象
 2.UICollectionViewLayoutAttributes對象決定了cell的frame
 */
/**
 * 這個方法的返回值是一個數組(數組里面存放著rect范圍內所有元素的布局屬性)
 * 這個方法的返回值決定了rect范圍內所有元素的排布(frame)
 */
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    // 獲得super已經計算好的布局屬性
    NSArray *array = [super layoutAttributesForElementsInRect:rect] ;

    // 計算collectionView最中心點的x值
    CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;

    // 在原有布局屬性的基礎上益眉,進行微調
    for (UICollectionViewLayoutAttributes *attrs in array) {
        // cell的中心點x 和 collectionView最中心點的x值 的間距
        CGFloat delta = ABS(attrs.center.x - centerX);

        // 根據間距值 計算 cell的縮放比例
        CGFloat scale = 1 - delta / self.collectionView.frame.size.width;

        // 設置縮放比例
        attrs.transform = CGAffineTransformMakeScale(scale, scale);
    }
        return array;
}

/**
 * 這個方法的返回值,就決定了collectionView停止?jié)L動時的偏移量

 */
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    // 計算出最終顯示的矩形框
    CGRect rect;
    rect.origin.y = 0;
    rect.origin.x = proposedContentOffset.x;
    rect.size = self.collectionView.frame.size;

    // 獲得super已經計算好的布局屬性
    NSArray *array = [super layoutAttributesForElementsInRect:rect];

    // 計算collectionView最中心點的x值
    CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;

    // 存放最小的間距值
    CGFloat minDelta = MAXFLOAT;
    for (UICollectionViewLayoutAttributes *attrs in array) {
        if (ABS(minDelta) > ABS(attrs.center.x - centerX)) {
            minDelta = attrs.center.x - centerX;
        }
    }

    // 修改原有的偏移量
    proposedContentOffset.x += minDelta;
    return proposedContentOffset;
}

@end

  • 假如我們要監(jiān)聽cell的點擊,要怎么辦呢郭脂?上面這講的這些都和CollectionViewCell的點擊沒有關系年碘,只是和布局有關。監(jiān)聽CollectionViewCell的點擊和CollectionViewCell的布局沒有任何關系展鸡,布局只負責展示屿衅,格子里面是什么內容,還是取決于cell
  • 布局的作用僅僅是控制cell的排布
    • 控制器先成為CollectionViewCell的代理:UICollectionViewDelegate
  • 現在要把數據填充上去娱颊,讓它顯示相冊了傲诵,所以自定義CollectionViewCell--CYPhotoCell,由于里面是固定死的凯砍,所以加一個Xib文件箱硕,里面加一個ImageView,拖線給一個屬性,給ImageView一個標識photo
  • 給cell里面的相片加上一個相冊相框的效果--兩種方案:
    • 第一種方案:在Xib的ImageView的布局上下左右都給一個10的間距悟衩,給一個white的背景顏色
    • 第二種方案:給我們的ImageView加一個圖層就可以了
- (void)awakeFromNib {
       self.imageView.layer.borderColor = [UIColor whiteColor].CGColor; 
       self.imageView.layer.borderWidth = 10;
 }
  • 在CYPhotoCell.h文件中
#import <UIKit/UIKit.h>

@interface CYPhotoCell : UICollectionViewCell
/** 圖片名 */
@property (nonatomic, copy) NSString *imageName;
@end
  • 在CYPhotoCell.m文件中
#import "CYPhotoCell.h"

@interface CYPhotoCell()

@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end

@implementation CYPhotoCell

- (void)awakeFromNib {
    self.imageView.layer.borderColor = [UIColor whiteColor].CGColor;
    self.imageView.layer.borderWidth = 10;
}

- (void)setImageName:(NSString *)imageName
{
    _imageName = [imageName copy];

    self.imageView.image = [UIImage imageNamed:imageName];
}

@end
  • 在ViewController.m文件中
#import "ViewController.h"
#import "CYLineLayout.h"
#import "CYPhotoCell.h"

@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>
@end

@implementation ViewController

static NSString * const CYPhotoId = @"photo";

- (void)viewDidLoad {
    [super viewDidLoad];

    // 創(chuàng)建布局
    CYLineLayout *layout = [[CYLineLayout alloc] init];
    layout.itemSize = CGSizeMake(100, 100);

    // 創(chuàng)建CollectionView
    CGFloat collectionW = self.view.frame.size.width;
    CGFloat collectionH = 200;
    CGRect frame = CGRectMake(0, 150, collectionW, collectionH);
    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
    collectionView.dataSource = self;
    collectionView.delegate = self;
    [self.view addSubview:collectionView];

    // 注冊
    [collectionView registerNib:[UINib nibWithNibName:NSStringFromClass([CYPhotoCell class]) bundle:nil] forCellWithReuseIdentifier:CYPhotoId];
}

#pragma mark - <UICollectionViewDataSource>
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 20;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    CYPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CYPhotoId forIndexPath:indexPath];

    cell.imageName = [NSString stringWithFormat:@"%zd", indexPath.item + 1];

    return cell;
}

#pragma mark - <UICollectionViewDelegate>
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"------%zd", indexPath.item);
}
@end
  • 最后就實現了:



自定義流水布局

  • 自定義布局 - 繼承UICollectionViewFlowLayout

  • 重寫prepareLayout方法

    • 作用:
      - 在這個方法中做一些初始化操作
    • 注意:
      - 一定要調用[super prepareLayout]
  • 重寫layoutAttributesForElementsInRect:方法

    • 作用:
      - 這個方法的返回值是個數組
      - 這個數組中存放的都是UICollectionViewLayoutAttributes對象
      - UICollectionViewLayoutAttributes對象決定了cell的排布方式(frame等)
  • 重寫shouldInvalidateLayoutForBoundsChange:方法

    • 作用:
      - 如果返回YES剧罩,那么collectionView顯示的范圍發(fā)生改變時,就會重新刷新布局
    • 一旦重新刷新布局座泳,就會按順序調用下面的方法:
      - prepareLayout
      - layoutAttributesForElementsInRect:
  • 重寫targetContentOffsetForProposedContentOffset:方法

    • 作用:
      - 返回值決定了collectionView停止?jié)L動時最終的偏移量(contentOffset)
    • 參數:
      - proposedContentOffset:原本情況下惠昔,collectionView停止?jié)L動時最終的偏移量
      - velocity:滾動速率,通過這個參數可以了解滾動的方向(根據X和Y的正負)





自定義布局--CollectionViewLayout--格子布局

  • 分析一下這個布局的排布是有規(guī)律的:
    • 這里的相冊布局和上面的流水布局不同
    • 我們較上面的不需要更改太多東西挑势,只是修改它的布局方式就行了
    • 六個為一組
    • 對應cell相差兩個高度


  • 一個這樣的布局如何實現镇防?
    • 首先這里不不好用流水布局,流水布局的ItemSize是一樣大的
    • 肯定也牽扯到了循環(huán)利用潮饱,所以仍然用CollectionView来氧,?所以就用一個?最根的布局--CollectionViewLayout
    • CollectionViewLayout它不像流水布局,內部沒有任何方法給你去排香拉,所以你只有繼承自它啦扬,然后自己去寫一套排布方式,排布是由我們來算
    • 將上面文件中的CYLineLayout刪除凫碌,New一個File--CYGridLayout繼承自CollectionViewLayout

    // 創(chuàng)建UICollectionViewLayoutAttributes
    NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
  • 說白了我這個UICollectionViewLayoutAttributes是描述一個cell用的
  • indexPath代表了對應某個位置的cell扑毡,也就是說我這個UICollectionViewLayoutAttributes是描述哪個位置的cell
  • 通過觀察可以發(fā)現規(guī)律


  • 在ViewController.m文件中修改一下collectionView的frame和布局
#import "ViewController.h"
#import "CYGridLayout.h"
#import "CYPhotoCell.h"

@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>
@end

@implementation ViewController

static NSString * const CYPhotoId = @"photo";

- (void)viewDidLoad {
    [super viewDidLoad];

    // 創(chuàng)建布局
    CYGridLayout *layout = [[CYGridLayout alloc] init];

    // 創(chuàng)建CollectionView
    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
    collectionView.dataSource = self;
    collectionView.delegate = self;
    [self.view addSubview:collectionView];

    // 注冊
    [collectionView registerNib:[UINib nibWithNibName:NSStringFromClass([CYPhotoCell class]) bundle:nil] forCellWithReuseIdentifier:CYPhotoId];
}
  • CYGridLayout里面去實現collectionView具體的布局
  • 在CYGridLayout.m文件中
#import "CYGridLayout.h"

@interface CYGridLayout()
/** 所有的布局屬性 */
@property (nonatomic, strong) NSMutableArray *attrsArray;
@end

@implementation CYGridLayout

- (NSMutableArray *)attrsArray
{
    if (!_attrsArray) {
        _attrsArray = [NSMutableArray array];
    }
    return _attrsArray;
}

- (void)prepareLayout
{
    [super prepareLayout];

    [self.attrsArray removeAllObjects];

    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    for (int i = 0; i < count; i++) {
        // 創(chuàng)建UICollectionViewLayoutAttributes
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

        // 設置布局屬性
        CGFloat width = self.collectionView.frame.size.width * 0.5;
        if (i == 0) {
            CGFloat height = width;
            CGFloat x = 0;
            CGFloat y = 0;
            attrs.frame = CGRectMake(x, y, width, height);
        } else if (i == 1) {
            CGFloat height = width * 0.5;
            CGFloat x = width;
            CGFloat y = 0;
            attrs.frame = CGRectMake(x, y, width, height);
        } else if (i == 2) {
            CGFloat height = width * 0.5;
            CGFloat x = width;
            CGFloat y = height;
            attrs.frame = CGRectMake(x, y, width, height);
        } else if (i == 3) {
            CGFloat height = width * 0.5;
            CGFloat x = 0;
            CGFloat y = width;
            attrs.frame = CGRectMake(x, y, width, height);
        } else if (i == 4) {
            CGFloat height = width * 0.5;
            CGFloat x = 0;
            CGFloat y = width + height;
            attrs.frame = CGRectMake(x, y, width, height);
        } else if (i == 5) {
            CGFloat height = width;
            CGFloat x = width;
            CGFloat y = width;
            attrs.frame = CGRectMake(x, y, width, height);
        } else {
            UICollectionViewLayoutAttributes *lastAttrs = self.attrsArray[i - 6];
            CGRect lastFrame = lastAttrs.frame;
            lastFrame.origin.y += 2 * width;
            attrs.frame = lastFrame;
        }

        // 添加UICollectionViewLayoutAttributes
        [self.attrsArray addObject:attrs];
    }
}

  • 運行程序:
  • 你會發(fā)現無法使它往上滾動,這是為啥呢盛险?
    • 因為你現在時繼承自最根本的布局CollectionViewLayout瞄摊,很多東西是得自己去設置了才會有,來到頭文件苦掘,你會發(fā)現
    • 要重寫它的(CGSize)collectionViewContentSize方法换帜,告訴它你這個CollectionView的內容尺寸暮顺,來決定它怎么滾嘹锁。所以你現在無法滾動是因為CollectionView的ContentSize沒有確定
/**
 * 返回collectionView的內容大小
 */
- (CGSize)collectionViewContentSize
{
    int count = (int)[self.collectionView numberOfItemsInSection:0];
    int rows = (count + 3 - 1) / 3;
    CGFloat rowH = self.collectionView.frame.size.width * 0.5;
    return CGSizeMake(0, rows * rowH);
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.attrsArray;
}

  • 這里在性能優(yōu)化上是還有點小問題的,因為我們一口氣把所有東西都算完了伤塌。你如果覺得費時揉忘,完全可以把計算放在子線程中跳座,然后返回到主線程刷新UI(CollectionViewLayout布局中有一個刷新方法端铛,你調一下就行了)
  • 計算不是重點,你是可以總結出計算的規(guī)律的疲眷。重點是:繼承自CollectionViewLayout你需要注意什么禾蚕?
    • 1.一旦你重寫了layoutAttributesForElementsInRect這個方法,就意味著所有東西你得自己寫了狂丝,你的Attributes對象得自己創(chuàng)建了换淆,因為它的父類不會幫你創(chuàng)建
    • 2.一旦你繼承自CollectionViewLayout,意味著你這個collectionViewContentSize都得告訴它了几颜,這個是得你自己去算的
    • 3.如果你是希望一口氣把所有東西算完倍试,不希望它在滾動過程中再算,你可以在prepareLayout方法里面先算清楚蛋哭,算完后盡管它傳的矩形框都不一樣县习,但是我返回的還是同一份。
  • ?這里給?出一個思想:
    • 以后谆趾,你凡事牽扯到內容是很多很多的躁愿,你想做什么循環(huán)利用,而且布局又亂七八糟的沪蓬,我們用CollectionViewLayout就可以了彤钟。我們只有繼承自這個CollectionViewLayout,然后我們實現layoutAttributesForElementsInRect這個方法跷叉,在那里去告訴它逸雹,你的cell怎么去排。并且繼承自CollectionViewLayout性芬,意味著很多東西都要重寫峡眶,如:collectionViewContentSize
  • 這樣就實現了:







自定義布局--CollectionViewLayout--布局之間的切換

  • 要求:
    • 實現一個環(huán)形布局和水平布局的相冊,點擊屏幕能夠進行不同布局之間的切換
    • 點擊cell的時候可以刪除cell
  • 首先通過分析植锉,在上面第一個案例的基礎上辫樱,再添加一個環(huán)形布局--CYCircleLayout,肯定也是只能繼承自CollectionViewLayout
  • 在這里CYCircleLayout里面就只需要實現prepareLayout方法和layoutAttributesForElementsInRect方法俊庇,不需再要重寫實現collectionViewContentSize的方法狮暑,因為它不需要滾動,所以CollectionViewLayout里面所有方法的實現是看你的需求的
#import "CYCircleLayout.h"

@interface CYCircleLayout()
/** 布局屬性 */
@property (nonatomic, strong) NSMutableArray *attrsArray;
@end

@implementation CYCircleLayout

- (NSMutableArray *)attrsArray
{
    if (!_attrsArray) {
        _attrsArray = [NSMutableArray array];
    }
    return _attrsArray;
}

- (void)prepareLayout
{
    [super prepareLayout];

    [self.attrsArray removeAllObjects];

    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    for (int i = 0; i < count; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
        [self.attrsArray addObject:attrs];
    }
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.attrsArray;
}

  • 我們可以看出辉饱,每個相片cell的中心點都在一個圓上搬男,所以我們要將它擺正,肯定不是設置它們的frame彭沼,而是去設置它center這個值缔逛,我只要保證它的center那個值在那個圓上就可以了
  • 也就是說我們要算出每個相片cell的中心點的X和Y值,通過中心點來布局它,而不是通過frame的original的X和Y(這樣太麻煩褐奴,不好算)
  • 這里我們只要確定圓心就好算了
    • 圓心(X和Y值分別是CollectionView寬度和高度的一半)
    • 而且每張相片的中心點距離圓心的距離為半徑
    • 你會發(fā)現每個相片cell的中心點的X按脚,Y和圓心的X,Y之間的差值是有規(guī)律的:
      • Y值--圓心點的Y值-(Y*cosa)= cell的Y值,X值同樣道理去算
      • 角度a的大小取決于cell的個數(假如20個cell--->a = 360° / 20)


  • 所以我們只要算出平分角度就行了
  • 比如說第一個cell為索引0敦冬,角度就是0辅搬,第二個為索引1,角度就是a, 第三個為索引2脖旱,角度就是a2......第i個為索引i-1堪遂,角度就是a(i-1 )
  • 于是乎

  • 這里記住:如果你是繼承自CollectionViewLayout萌庆,如果你要換布局話溶褪,有一個方法是一定得實現的--layoutAttributesForItemAtIndexPath:方法。只有繼承CollectionViewLayout才需要踊兜,流水布局不需要竿滨,因為流水布局內部早已經幫你實現了這個方法
/**
 * 這個方法需要返回indexPath位置對應cell的布局屬性
 */
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    CGFloat radius = 70;
    // 圓心的位置
    CGFloat oX = self.collectionView.frame.size.width * 0.5;
    CGFloat oY = self.collectionView.frame.size.height * 0.5;

    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    attrs.size = CGSizeMake(50, 50);
    if (count == 1) {
        attrs.center = CGPointMake(oX, oY);
    } else {
        CGFloat angle = (2 * M_PI / count) * indexPath.item;
        CGFloat centerX = oX + radius * sin(angle);
        CGFloat centerY = oY + radius * cos(angle);
        attrs.center = CGPointMake(centerX, centerY);
    }

    return attrs;
}
  • 點擊屏幕切換布局
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    if ([self.collectionView.collectionViewLayout isKindOfClass:[CYLineLayout class]]) {
        [self.collectionView setCollectionViewLayout:[[CYCircleLayout alloc] init] animated:YES];
    } else {
        CYLineLayout *layout = [[CYLineLayout alloc] init];
        layout.itemSize = CGSizeMake(100, 100);
        [self.collectionView setCollectionViewLayout:layout animated:YES];
    }
}
  • 點擊cell就把cell刪掉
    • 這里要注意的是:
      • 你要把cell刪掉了佳恬,對應的模型或者說 數據也是得改變的
  • 可變數組捏境,先把所有圖片名放進去
@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>
/** collectionView */
@property (nonatomic, weak) UICollectionView *collectionView;
/** 數據 */
@property (nonatomic, strong) NSMutableArray *imageNames;
@end

@implementation ViewController

static NSString * const CYPhotoId = @"photo";

- (NSMutableArray *)imageNames
{
    if (!_imageNames) {
        _imageNames = [NSMutableArray array];
        for (int i = 0; i<20; i++) {
            [_imageNames addObject:[NSString stringWithFormat:@"%zd", i + 1]];
        }
    }
    return _imageNames;
}

  • 數據源里面的東西也是得改變的
#pragma mark - <UICollectionViewDataSource>
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return self.imageNames.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    CYPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CYPhotoId forIndexPath:indexPath];

    cell.imageName = self.imageNames[indexPath.item];

    return cell;
}
  • 你要把cell刪掉,也得保證把模型也刪掉了(不可能你cell刪掉了毁葱,數據還是這么多垫言,那就出問題了)
#pragma mark - <UICollectionViewDelegate>
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    [self.imageNames removeObjectAtIndex:indexPath.item];
    [self.collectionView deleteItemsAtIndexPaths:@[indexPath]];
}
  • 刪除到最后一個的時候,讓最后一個cell的位置來到圓心
    if (count == 1) {
        attrs.center = CGPointMake(oX, oY);
    } else {
        CGFloat angle = (2 * M_PI / count) * indexPath.item;
        CGFloat centerX = oX + radius * sin(angle);
        CGFloat centerY = oY + radius * cos(angle);
        attrs.center = CGPointMake(centerX, centerY);
    }


  • 這樣所有的邏輯就理清楚了

  • 在CYCircleLayout.m文件中
#import "CYCircleLayout.h"

@interface CYCircleLayout()
/** 布局屬性 */
@property (nonatomic, strong) NSMutableArray *attrsArray;
@end

@implementation CYCircleLayout

- (NSMutableArray *)attrsArray
{
    if (!_attrsArray) {
        _attrsArray = [NSMutableArray array];
    }
    return _attrsArray;
}

- (void)prepareLayout
{
    [super prepareLayout];

    [self.attrsArray removeAllObjects];

    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    for (int i = 0; i < count; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
        [self.attrsArray addObject:attrs];
    }
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.attrsArray;
}

/**
 * 這個方法需要返回indexPath位置對應cell的布局屬性
 */
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    CGFloat radius = 70;
    // 圓心的位置
    CGFloat oX = self.collectionView.frame.size.width * 0.5;
    CGFloat oY = self.collectionView.frame.size.height * 0.5;

    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    attrs.size = CGSizeMake(50, 50);
    if (count == 1) {
        attrs.center = CGPointMake(oX, oY);
    } else {
        CGFloat angle = (2 * M_PI / count) * indexPath.item;
        CGFloat centerX = oX + radius * sin(angle);
        CGFloat centerY = oY + radius * cos(angle);
        attrs.center = CGPointMake(centerX, centerY);
    }

    return attrs;
}
@end
  • 在ViewController.m文件中
#import "ViewController.h"
#import "CYLineLayout.h"
#import "CYCircleLayout.h"
#import "CYPhotoCell.h"

@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>
/** collectionView */
@property (nonatomic, weak) UICollectionView *collectionView;
/** 數據 */
@property (nonatomic, strong) NSMutableArray *imageNames;
@end

@implementation ViewController

static NSString * const CYPhotoId = @"photo";

- (NSMutableArray *)imageNames
{
    if (!_imageNames) {
        _imageNames = [NSMutableArray array];
        for (int i = 0; i<20; i++) {
            [_imageNames addObject:[NSString stringWithFormat:@"%zd", i + 1]];
        }
    }
    return _imageNames;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    // 創(chuàng)建布局
    CYCircleLayout *layout = [[CYCircleLayout alloc] init];

    // 創(chuàng)建CollectionView
    CGFloat collectionW = self.view.frame.size.width;
    CGFloat collectionH = 200;
    CGRect frame = CGRectMake(0, 150, collectionW, collectionH);
    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
    collectionView.dataSource = self;
    collectionView.delegate = self;
    [self.view addSubview:collectionView];
    self.collectionView = collectionView;

    // 注冊
    [collectionView registerNib:[UINib nibWithNibName:NSStringFromClass([CYPhotoCell class]) bundle:nil] forCellWithReuseIdentifier:CYPhotoId];

    // 繼承UICollectionViewLayout
    // 繼承UICollectionViewFlowLayout
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    if ([self.collectionView.collectionViewLayout isKindOfClass:[CYLineLayout class]]) {
        [self.collectionView setCollectionViewLayout:[[CYCircleLayout alloc] init] animated:YES];
    } else {
        CYLineLayout *layout = [[CYLineLayout alloc] init];
        layout.itemSize = CGSizeMake(100, 100);
        [self.collectionView setCollectionViewLayout:layout animated:YES];
    }
}


#pragma mark - <UICollectionViewDataSource>
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return self.imageNames.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    CYPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CYPhotoId forIndexPath:indexPath];

    cell.imageName = self.imageNames[indexPath.item];

    return cell;
}

#pragma mark - <UICollectionViewDelegate>
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    [self.imageNames removeObjectAtIndex:indexPath.item];
    [self.collectionView deleteItemsAtIndexPaths:@[indexPath]];
}
@end


  • 這樣就實現了


  • 如果覺得對你有幫助倾剿,?Give me a star

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末筷频,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子前痘,更是在濱河造成了極大的恐慌凛捏,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芹缔,死亡現場離奇詭異坯癣,居然都是意外死亡,警方通過查閱死者的電腦和手機最欠,發(fā)現死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門示罗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人芝硬,你說我怎么就攤上這事蚜点。” “怎么了拌阴?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵绍绘,是天一觀的道長。 經常有香客問我,道長陪拘,這世上最難降的妖魔是什么实辑? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮藻丢,結果婚禮上剪撬,老公的妹妹穿的比我還像新娘。我一直安慰自己悠反,他們只是感情好残黑,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著斋否,像睡著了一般梨水。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上茵臭,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天疫诽,我揣著相機與錄音,去河邊找鬼旦委。 笑死奇徒,一個胖子當著我的面吹牛,可吹牛的內容都是我干的缨硝。 我是一名探鬼主播摩钙,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼查辩!你這毒婦竟也來了胖笛?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤宜岛,失蹤者是張志新(化名)和其女友劉穎长踊,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體萍倡,經...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡身弊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了遣铝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佑刷。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖酿炸,靈堂內的尸體忽然破棺而出瘫絮,到底是詐尸還是另有隱情,我是刑警寧澤填硕,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布麦萤,位于F島的核電站鹿鳖,受9級特大地震影響,放射性物質發(fā)生泄漏壮莹。R本人自食惡果不足惜翅帜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望命满。 院中可真熱鬧涝滴,春花似錦、人聲如沸胶台。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诈唬。三九已至韩脏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間铸磅,已是汗流浹背赡矢。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留阅仔,地道東北人吹散。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像霎槐,于是被迫代替她去往敵國和親送浊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內容