- Github地址:-CollectionViewLayout-CollectionViewFlowLayout-
- 這里詳解了三個demo去幫助大家更好的了解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
- 1.繼承UICollectionViewLayout
**四 **
- 所以我們自定義流水布局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最后停留在哪兒)
- 那我們怎么知道手松開的那一刻最終的偏移量X的值呢?
- 同上面可知矢沿,我們最后拿到的矩形框也是不能亂傳的,也是要拿到最終的哪一個矩形框(不明白酸纲,就想像一下捣鲸,你往左邊或者右邊用手指一甩的時候,手離開的時候是一個值闽坡,最終停下來是一個值栽惶,而現在我們需要的是最終的值)
// 計算出最終顯示的矩形框
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 ,這本來就是來控制內邊距的轻绞,控制整個布局的采记。而且這個屬性只需要設置一次
- 這就是讓我們把所有的cell拔稳,讓它們往右邊或者左邊挪一段距離葛峻,所以就增加內邊距就可以了。怎么添加內邊距呢巴比?
- 這里有一個?給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