iOS瀑布流布局

前言

瀑布流布局是比较流行的一种网站页面和手机App布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。最早采用此布局的网站是Pinterest,逐渐在国内流行开来,目前很多小清新网站和手机App基本都为此类风格。瀑布流对于图片的展现,是高效而具有吸引力的,它有如下优点

  • 有效的降低了界面复杂度,节省了空间。
  • 对触屏设备来说,交互方式更符合直觉,在移动应用的交互环境当中,通过向上滑动进行滚屏的操作已经成为最基本的用户习惯,而且所需要的操作精准程度远远低于点击链接或按钮。
  • 有更高的参与度,以上两点所带来的交互便捷性可以使用户将注意力更多的集中在内容而不是操作上,从而让他们更乐于沉浸在探索与浏览当中。

下面我们说说瀑布流在iPhone手机上的实现过程。

UICollectionView

在iOS6以前,iOS的布局只有UITableView可以用,一些复杂的排版布局需要自己组装UITableView或者甚至搭配UIScrollView来实现,既麻烦而且一定程度上影响流畅度和性能。然而从iOS6开始,UICollectionView出现了,它是一种新的数据展示方式,简单来说可以把他理解成多列的UITableView(请一定注意这是UICollectionView的最最简单的形式)。如果你用过iBooks的话,可能你还对书架布局有一定印象:一个虚拟书架上放着你下载和购买的各类图书,整齐排列。其实这就是一个UICollectionView的表现形式,或者iPad中的原生时钟应用中的各个时钟,也是UICollectionView的最简单的一个布局。

主要接口

如果你用过UITableView,那么其实UICollectionView最简单的用法也差不多。

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return 1;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return _dataSource.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {

    CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"UICollectionViewCell" forIndexPath:indexPath];

    cell.backgroundColor = [UIColor yellowColor];
    cell.label.frame = cell.bounds;
    cell.label.text = [self.dataSource objectAtIndex:indexPath.row];
    return cell;
}

UITableView则为

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return _dataSource.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MyUITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"myCell"];
    if (!cell) {
        cell = [[MyUITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"myCell"];
    }
    cell.backgroundColor = [UIColor yellowColor];
    cell.label.frame = cell.bounds;
    cell.label.text = [self.dataSource objectAtIndex:indexPath.row];
    return cell;
}

可以发现UICollectionView比UITableView简洁不少,但是需要在使用前注册

    [_collectionView registerClass:[CollectionViewCell class] forCellWithReuseIdentifier:@"UICollectionViewCell"];

UICollectionView还提供了强大的Supplementary View和Decoration Views(本文不做阐述),Supplementary View可以理解为补充的View,业务中使用最广泛的是将其加到整个View的头部和尾部,类似于UITableView的headerView和footerView。同样它也需要使用前注册

//HintCell 继承 UICollectionReusableView
    [_collectionView registerClass:[HintCell class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"Hint"];

    - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
HintCell *HintView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:@"Hint" forIndexPath:indexPath];
return HintView;
}

需要注意的是,由于collection views没有被归入任何特定结构,”header” 和 “footer”视图的约定不是很适用。所以在它这个地方,collection views拥有可以与每个cell关联的 supplementary views。每个cell可以有多个与之关联的supplementary views–每个命名为”kind”。正因如此,headers和footers仅仅是supplementary views一部分功能而已。

强大的UICollectionViewLayout

上面说了那么多,其实UICollectionView的真正精髓在于UICollectionViewLayout,这也是UICollectionView和UITableView最大的不同。UICollectionViewLayout可以说是UICollectionView的大脑和中枢,它负责了将各个cell、Supplementary View和Decoration Views进行组织,为它们设定各自的属性,包括但不限于:

  • 位置
  • 尺寸
  • 透明度
  • 层级关系
  • 形状

Layout决定了UICollectionView是如何显示在界面上的。在展示之前,一般需要生成合适的UICollectionViewLayout子类对象,并将其赋予CollectionView的collectionViewLayout属性。

iOS SDK为我们提供了一个最简单可能也是最常用的默认layout对象,UICollectionViewFlowLayout。Flow Layout简单说是一个直线对齐的layout,最常见的Grid View形式比如九宫格即为一种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:

自定义UICollectionViewLayout

我们先来看看系统的UICollectionViewFlowLayout头文件定义

NS_CLASS_AVAILABLE_IOS(6_0) @interface UICollectionViewFlowLayout : UICollectionViewLayout

//最小的行间距
@property (nonatomic) CGFloat minimumLineSpacing;
//最小的列间距
@property (nonatomic) CGFloat minimumInteritemSpacing;
//cell的size
@property (nonatomic) CGSize itemSize;
// 滚动方向 default is UICollectionViewScrollDirectionVertical
@property (nonatomic) UICollectionViewScrollDirection scrollDirection; 
//header区域size
@property (nonatomic) CGSize headerReferenceSize;
//footer区域size
@property (nonatomic) CGSize footerReferenceSize;
//每个section的inset
@property (nonatomic) UIEdgeInsets sectionInset;

@end

以上都是针对UICollectionView全局属性设置,但其实大多情况下我们会根据不同section和itemcell做定制,这里需要实现UICollectionViewDelegateFlowLayout这个delegate,具体解释就不多说了,看方法名便知

@protocol UICollectionViewDelegateFlowLayout <UICollectionViewDelegate>
@optional

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section;

@end

参考系统的FlowLayout定义,其实大致我们已经知道自定义布局需要哪些内容,剩下就是看怎么实现了,先来看看UICollectionViewLayout到底有些啥东西

  • -(void)prepareLayout

这是最早被调用的方法,默认该方法什么没做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。

  • -(CGSize)collectionViewContentSize

    返回collectionView的内容的尺寸

  • -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

返回rect中的所有的元素的布局属性

  • -(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath

当然不止这些,还有更多高级和复杂的方法,但是这几个已经足够我们实现自定义的布局了,下面用包含2列的瀑布流布局来做示例。

首先自定义一个layout继承于系统的UICollectionViewLayout,并定义delegate,用来获取每个cell的size

#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height

@class CustomCollectionViewLayout;

@protocol CustomCollectionViewLayoutDelegate <UICollectionViewDelegate>

@required
- (CGSize)collectionView:(UICollectionView *)collectionView collectionViewLayout:(CustomCollectionViewLayout *)collectionViewLayout sizeOfItemAtIndexPath:(NSIndexPath *)indexPath;

@end

@interface CustomCollectionViewLayout : UICollectionViewLayout

@property (weak, nonatomic) id layoutDelegate;

@end

然后定义必要的属性

@interface CustomCollectionViewLayout ()

@property (assign, nonatomic) CGFloat leftY; // 左侧起始Y轴
@property (assign, nonatomic) CGFloat rightY; // 右侧起始Y轴
@property (assign, nonatomic) NSInteger cellCount; // cell个数
@property (assign, nonatomic) CGFloat itemWidth; // cell宽度
@property (assign, nonatomic) CGFloat insert; // 间距

@end

接着重载实现初始化方法

/**
 *  初始化layout后自动调动,可以在该方法中初始化一些自定义的变量参数
 */
- (void)prepareLayout {

    [super prepareLayout];

    // 初始化参数
    _cellCount = [self.collectionView numberOfItemsInSection:0]; // cell个数,直接从collectionView中获得
    _insert = 10; // 设置间距为10
    _itemWidth = (SCREEN_WIDTH - 3 * _insert) / 2; // cell宽度
}

然后实现返回UICollectionView的contentSize的方法,显然易见,宽为屏幕宽度,高为2列瀑布流中Y轴值较大的那一列的(top+height)值

/**
 *  设置UICollectionView的内容大小,道理与UIScrollView的contentSize类似
 *
 *  @return 返回设置的UICollectionView的内容大小
 */
- (CGSize)collectionViewContentSize {

    return CGSizeMake(SCREEN_WIDTH, MAX(_leftY, _rightY));
}

再实现layoutAttributesForElementsInRect方法

/**
 *  初始Layout外观
 *
 *  @param rect 所有元素的布局属性
 *
 *  @return 所有元素的布局
 */
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    _leftY = _insert; // 左边起始Y轴
    _rightY = _insert; // 右边起始Y轴

    NSMutableArray *attributes = [[NSMutableArray alloc] init];

    for (int i = 0; i < self.cellCount; i ++) {

        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
        //layoutAttributesForItemAtIndexPath这个才是最核心的实现
        [attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
    }

    return attributes;
}

最后实现最重要的layoutAttributesForItemAtIndexPath方法,它直接决定每个cell的布局和大小

/**
 *  根据不同的indexPath,给出布局
 *
 *  @param indexPath
 *
 *  @return 布局
 */

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {

    // 获取代理中返回的每一个cell的大小
    CGSize itemSize = [self.layoutDelegate collectionView:self.collectionView collectionViewLayout:self sizeOfItemAtIndexPath:indexPath];
    CGFloat itemHeight;
    // 防止代理中给的size.width大于(或小于)layout中定义的width,所以等比例缩放size
    itemHeight = floorf(itemSize.height * self.itemWidth / itemSize.width);


    UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    // 判断当前的item应该在左侧还是右侧
    BOOL isLeft = _leftY <= _rightY;

    if (isLeft) {

        CGFloat x = _insert; 
        CGFloat w = _itemWidth;
        CGFloat y = _leftY;
        attributes.frame = CGRectMake(x, y, w, itemHeight);
        _leftY = y + itemHeight + _insert; // 设置左列的新的Y起点

    }

    if (!isLeft) {

        CGFloat x = _itemWidth + 2 * _insert;
        attributes.frame = CGRectMake(x, _rightY, _itemWidth, itemHeight);
        _rightY += itemHeight + _insert; //设置右列的新的Y起点
    }

    return attributes;
}

可见以上是直接通过计算每个attributes的frame值,最终确定cell的位置和大小,那么如何使用它呢,其实也很简单。

CustomCollectionViewLayout *customlayout = [[CustomCollectionViewLayout alloc] init];
layout.layoutDelegate = self;
//使用customlayout来直接初始化UICollectionView
_collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:customlayout];

//数据源初始化
_dataSource = [[NSMutableArray new];
_itemHeights = [[NSMutableArray new];

for (int i = 0; i < 30; i ++) {

    [_dataSource addObject:[NSString stringWithFormat:@"%d",i]];    
    CGFloat itemHeight = arc4random() % 150 + 20;
    [_itemHeights addObject:@(itemHeight)];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return _dataSource.count;
}

- (CGSize)collectionView:(UICollectionView *)collectionView collectionViewLayout:(CustomCollectionViewLayout *)collectionViewLayout sizeOfItemAtIndexPath:(NSIndexPath *)indexPath {
    //其实这里的100并非实际值,建议自己按照屏幕尺寸算好,否则CustomCollectionViewLayout里会根据屏幕和设定的间距进行强制缩放
    return CGSizeMake(100, [_itemHeights[indexPath.row] floatValue]);

}

到这里,最简单的2列瀑布流就完成了

以上的例子只是个简单的示范,很多属性是写死的,但其实你的layout可以写更多的代码来实现复杂的布局,包含但不限于

  • 自定义列数
  • 自定义lineSpace,columnSpace,Inset,
  • 自定义每个attributes的布局方式,比如是否等比缩放,固定宽高等
  • 列排版方式,比如从左往右,或者从右往左
  • 自定义layoutAttributesForSupplementaryViewOfKind
  • 支持多个section,每个section有不同的列数
  • 自定义每个section的以上所有功能

扩展瀑布流布局

在业务中,我们也许会遇到这样的问题,就是在瀑布流布局下,其中某一个元素我可能不希望只展示屏幕的一半或者三分之一,而是整行展示,将上下内容做个明显的区分

肯定有人会马上想到,用section来解决!当然section无疑是一个强大的功能,不管是UICollectionView还是UITableView都提供了,但是section可能更多用于对平行内容的区分,比如iPhone相册的照片,就是默认按照日期来展示,日期作为每个section的headerView,照片作为contentView,上图的例子不太合适,有点杀鸡用牛刀的感觉,而且会增加代码量。因为插入的区块更像是插入在宝贝里的一个特殊元素,只需要用不同的cell来展示而已。

其实解决这个问题的最核心问题在于,layout布局能否自己指定宽度,答案当然是可以的。其实只要修改下上面的布局实现方法即可

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {

    // 获取代理中返回的每一个cell的大小
    CGSize itemSize = [self.layoutDelegate collectionView:self.collectionView collectionViewLayout:self sizeOfItemAtIndexPath:indexPath];
    CGFloat itemHeight;
    //判断是否整行展示
    BOOL span = itemSize.width == CGFLOAT_MAX;
    if(span){
        itemHeight = itemSize.height;
    }else{
        // 防止代理中给的size.width大于(或小于)layout中定义的width,所以等比例缩放size
        itemHeight = floorf(itemSize.height * self.itemWidth / itemSize.width);
    }

    UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    // 判断当前的item应该在左侧还是右侧
    BOOL isLeft = _leftY <= _rightY;
    //整行显示就直接从左边开始排版了
    if(span)
        isLeft = YES;

    if (isLeft) {

        CGFloat x = _insert; 
        CGFloat w = span ? SCREEN_WIDTH- 2*_insert:_itemWidth;
        //从Y轴较大的那一列开始往下排
        CGFloat y = span ? MAX(_leftY, _rightY) : _leftY;
        attributes.frame = CGRectMake(x, y, w, itemHeight);
        _leftY = y + itemHeight + _insert; // 设置新的Y起点
        //如果整行显示了,那么两列的Y轴重新保持一致
        if(span){
            _rightY = _leftY;
        }
    }

    if (!isLeft) {

        CGFloat x = _itemWidth + 2 * _insert;
        attributes.frame = CGRectMake(x, _rightY, _itemWidth, itemHeight);
        _rightY += itemHeight + _insert;
    }

    return attributes;
}

使用也很简单,只要指定需要整行展示的cell的宽度为CGFLOAT_MAX即可,当然这个只是告诉layout的一个标识而已,你也可以用其他更好的方法实现。

- (CGSize)collectionView:(UICollectionView *)collectionView collectionViewLayout:(CustomCollectionViewLayout *)collectionViewLayout sizeOfItemAtIndexPath:(NSIndexPath *)indexPath {
    //这里假设第五行整行显示
    if(indexPath.row == 5){
        return CGSizeMake(CGFLOAT_MAX, 50);
    }
    return CGSizeMake(100, [_itemHeights[indexPath.row] floatValue]);
}

总结

UICollectionView的强大远不止以上说的这些内容,总之有了UICollectionViewLayout,你的布局会变的非常随心所欲,github上也有非常优秀的开源layout供你参考。

你可能感兴趣的:(移动开发)