从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阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容