開始前的準備
- 先看下效果,這個效果是使用UICollectionView實現的,通過自定義繼承自系統的流水布局
kobe.gif
- 如果你對上面效果感興趣,那非常歡迎你繼續往下看,首先我需要先說明幾個關于自定義繼承自系統的流水布局的幾個關鍵方法,實現這個效果的基礎就是先清楚這幾個方法哦
實現這個效果,就默認大家已經掌握了collectionView的最基本使用了哦,所以這里就不在詳細說明collectionView的一些基本屬性和方法啦
基本結構
實現該效果其實也不是很復雜的哦
- 創建一個UICollectionView,尺寸和屏幕一樣大
- 自定義Cell繼承自UICollectionViewCell --- LBPhotoCell
- 自定義布局,繼承自系統的UICollectionViewFlowLayout --- LBVerLayout
關鍵方法說明
可能會枯燥,但是這個是理解這個效果的前提哦,我也會非常用心的講解
1)重寫 - (void)prepareLayout
- 該方法是準備布局,會在cell顯示之前調用,可以在該方法中設置布局的一些屬性,比如滾動方向,cell之間的水平間距,以及行間距等
- 也建議在這個方法中做布局的初始化操作,不建議在init方法中初始化,這個時候可能CollectionView還沒有創建,官方文檔也有明確說明哦
- 如果重寫了該方法,一定要調用父類的prepareLayout
2) 重寫 - (NSArray *)layoutAttributesForElementsInRect:(CGRect):rect
- 該方法的返回值是一個存放著rect范圍內所有元素的布局屬性的數組
- 數組里面的對象決定了rect范圍內所有元素的排布(frame)
- 里面存放的都是UICollectionViewLayoutAttributes對象,該對象決定了cell的排布樣式
- 一個cell就對應一個UICollectionViewLayoutAttributes對象
- UICollectionViewLayoutAttributes對象決定了cell的frame
3) 重寫 - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
- 是否允許在里面cell位置改變的時候重新布局
- 默認是NO,返回YES的話,該方法內部重新會按順序調用以下2個方法
**- (void)prepareLayout
**- (NSArray *)layoutAttributesForElementsInRect:(CGRect):rect
4)重寫 - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
- proposedContentOffset:原本情況下,collectionview停止滾動時最終的偏移量
**滑動的時候手松開因為慣性并不會立即停止,會再滾動一會才會真正停止,這個屬性就是記錄這個真正停止這一刻的偏移量
**我們這個效果是手指松開,完全停止滾動的時候,離屏幕中間y值最近的cell自動滾動到屏幕的中間
**所以我們需要利用該方法的返回值,這個返回值就是需要我們給一個偏移量,這個collectionview在它由于慣性滾動結束后,再去多滾動我們給的這一部分偏移量
- velocity:滾動速率,可以根據velocity的x或y判斷它是向上/向下/向右/向左滑動
**這個參數在這里沒有什么用,但是這個參數本身還是非常有用的,我之前使用過它來判斷當前tabbleview是向上滑還是向下滑,這個時候可以通過這個判斷很簡單的就控制是隱藏tabBar或者顯示tabBar,或者是隱藏顯示導航條,使用很爽
具體實現
復雜的方法說明后,終于迎來了更為枯燥的擼碼時刻~~~
1)在ViewController.m文件中
static NSString * const LBPhoto = @"kobe";
- (void)viewDidLoad {
[super viewDidLoad];
// 創建布局
LBVerLayout *layout = [[LBVerLayout alloc] init];
layout.itemSize = CGSizeMake(150, 150);
// 創建collectionView
CGFloat collectionW = self.view.frame.size.width;
CGFloat collectionH = self.view.frame.size.height;
CGRect frame = CGRectMake(0, 0, collectionW , collectionH);
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
collectionView.dataSource = self;
collectionView.delegate = self;
collectionView.backgroundColor = [UIColor clearColor];
[self.view addSubview:collectionView];
// 注冊cell,我這里是使用的xib
[collectionView registerNib:[UINib nibWithNibName:NSStringFromClass([LBPhotoCell class]) bundle:nil] forCellWithReuseIdentifier:LBPhoto];
}
#pragma mark - <UICollectionViewDataSource>
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 20;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//創建cell
LBPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:LBPhoto forIndexPath:indexPath];
//重寫imageName的set方法,內部其實就是給cell的imageView賦值(imageView是我們自己給cell添加的子控件)
cell.imageName = [NSString stringWithFormat:@"kobe_%zd", indexPath.item ];
}
2)在繼承自UICollectionViewCell的自定義cell的.m文件中
@interface LBPhotoCell()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation LBPhotoCell
- (void)awakeFromNib {
//給imageView的圖層設置邊框寬度以及邊框顏色
self.imageView.layer.borderWidth = 10;
self.imageView.layer.borderColor = [UIColor blackColor].CGColor;
}
//重寫imageName的set方法,外界傳一個圖片名給我們,我們在cell內部給cell的子控件賦值
- (void)setImageName:(NSString *)imageName
{
_imageName = [imageName copy];
self.imageView.image = [UIImage imageNamed:imageName];
}
@end
3)最后一步,最為關鍵的一步,賦值的計算都是在這個類里面實現的
在繼承自系統的UICollectionViewFlowLayout的類的.m文件中
- (void)prepareLayout
{
[super prepareLayout];
// 垂直滾動
self.scrollDirection = UICollectionViewScrollDirectionVertical;
self.minimumInteritemSpacing = 20;
// 設置collectionView里面內容的內邊距(上、左、下、右)
CGFloat inset = (self.collectionView.frame.size.width - 2*self.itemSize.width) /3;
self.sectionInset = UIEdgeInsetsMake(inset, inset, inset, inset);
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
// 拿到系統已經幫我們計算好的布局屬性數組,然后對其進行拷貝一份,后續用這個新拷貝的數組去操作
NSArray * originalArray = [super layoutAttributesForElementsInRect:rect];
NSArray * curArray = [[NSArray alloc] initWithArray:originalArray copyItems:YES];
// 計算collectionView中心點的y值(這個中心點可不是屏幕的中線點哦,是整個collectionView的,所以是包含在屏幕之外的偏移量的哦)
CGFloat centerY = self.collectionView.contentOffset.y + self.collectionView.frame.size.height * 0.5;
// 拿到每一個cell的布局屬性,在原有布局屬性的基礎上,進行調整
for (UICollectionViewLayoutAttributes *attrs in curArray) {
// cell的中心點y 和 collectionView最中心點的y值 的間距的絕對值
CGFloat space = ABS(attrs.center.y - centerY);
// 根據間距值 計算 cell的縮放比例
// 間距越大,cell離屏幕中心點越遠,那么縮放的scale值就小
CGFloat scale = 1 - space / self.collectionView.frame.size.height;
// 設置縮放比例
attrs.transform = CGAffineTransformMakeScale(scale, scale);
}
return curArray;
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
// 計算出停止滾動時(不是松手時)最終顯示的矩形框
CGRect rect;
rect.origin.y = proposedContentOffset.y;
rect.origin.x = 0;
rect.size = self.collectionView.frame.size;
// 獲得系統已經幫我們計算好的布局屬性數組
NSArray *array = [super layoutAttributesForElementsInRect:rect];
// 計算collectionView最中心點的y值
// 再啰嗦一下,這個proposedContentOffset是系統幫我們已經計算好的,當我們松手后它慣性完全停止后的偏移量
CGFloat centerY = proposedContentOffset.y + self.collectionView.frame.size.height * 0.5;
// 當完全停止滾動后,離中點Y值最近的那個cell會通過我們多給出的偏移量回到屏幕最中間
// 存放最小的間距值
// 先將間距賦值為最大值,這樣可以保證第一次一定可以進入這個if條件,這樣可以保證一定能鬧到最小間距
CGFloat minSpace = MAXFLOAT;
for (UICollectionViewLayoutAttributes *attrs in array) {
if (ABS(minSpace) > ABS(attrs.center.y - centerY)) {
minSpace = attrs.center.y - centerY;
}
}
// 修改原有的偏移量
proposedContentOffset.y += minSpace;
return proposedContentOffset;
}
點擊這里下載代碼
(ps:感謝@予人與人,親自敲了代碼發現了我漏掉的一個問題,非常感謝)
結束語
- OK,這個效果也基本實現了,忙碌的周末也快結束了,明天公司會有新人過來面試,一些不錯的面試題有機會的話會繼續跟大家分享的
- 慢慢發現把自己稍微了解的東西分享出去這樣非常有助于自己的提升,感覺自己會不是牛逼的,最牛逼的是把自己會的能分享給別人,過程中會發現很多細節
- 今后會經常性的和大家分享實用有趣的東西,我們共同提升