瀑布流(UIScrollView实现)

本文主要描述如何写出类似蘑菇街的瀑布流

瀑布流(UIScrollView实现)_第1张图片

瀑布流可以在保证图片原始比例的情况下,灵活的展现内容,相对于传统的使用相同大小的网格展现大量图片,要好上很多,而实现瀑布流的方式有很多种,网上比较流行的有三种实现方式。
1,使用UIScrollView(也就是本文所要写的),主要技术点在于视图的重用。(偏难)
2,使用UITableView,这种方式应该是最易想到的,因为需要展现几列就用几个tabelview就ok了,而且不需要考虑重用,应为苹果已经做好了,只需要考虑如何在几列tabelView滑动的时候,保持同步不出现BUG。

3,使用UICollectionView,UICollectionView在iOS6中第一次被介绍,它与UITableView有许多相似点,但它多了一个布局类,而实现瀑布流,就与这个布局类有关。此种方式实现,也不需要考虑视图重用。(最简单)
参考链接:collection实现瀑布流

一、写接口

1、首先创建一个自定义View叫DSWaterFlowView,继承自UIScrollView(因为瀑布流必须滚动),然后自定义cell叫DSWaterFlowViewCell,继承自UIView
2.分别写出dataSource协议和delegate协议中的接口

#pragma mark - 数据源方法
@protocol DSWaterFlowViewDataSource <NSObject>
@required
/** * cell的总数 */
- (NSInteger)numberOfcellsInWaterFlowView:(DSWaterFlowView *)waterFlowView;

/** * index所对应的cell */
- (DSWaterFlowViewCell *)waterFlowView:(DSWaterFlowView *)waterFlowView cellAtindex:(NSInteger)index;

@optional
/** * 有多少列 */
- (NSInteger)numberOfColumnsInWaterFlowView:(DSWaterFlowView *)waterFlowView;

@end
#pragma mark - 代理方法
@protocol DSWaterFlowViewDelegate <UIScrollViewDelegate>
@optional
/** * index所对应cell的行高 */
- (CGFloat)waterFlowView:(DSWaterFlowView *)waterFlowView heigthAtIndex:(NSInteger)index;

/** * 选中index位置的cell */
- (void)waterFlowView:(DSWaterFlowView *)waterFlowView didSelectAtIndex:(NSInteger)index;

/** * 间距 */
- (CGFloat)waterFlowView:(DSWaterFlowView *)waterFlowView marginType:(DSWaterFlowViewMarginType)type;

@end

@interface里面写下delegatedatasource

/** * 数据源 */
@property (nonatomic,weak) id<DSWaterFlowViewDataSource>  datasource;

/** * 代理 */
@property (nonatomic,weak) id<DSWaterFlowViewDelegate> delegate;

注意:
1>间距可以用枚举DSWaterFlowViewMarginType

typedef enum {
    DSWaterFlowViewMarginTypeTop, 
    DSWaterFlowViewMarginTypeLeft,
    DSWaterFlowViewMarginTypeBottom,
    DSWaterFlowViewMarginTypeRight,
    DSWaterFlowViewMarginTypeColumn, // 列间距
    DSWaterFlowViewMarginTypeRow, // 行间距
} DSWaterFlowViewMarginType;

2>代理协议应该继承自UIScrollViewDelegate(不然会出现警告),因为scrollView里面也有一个代理,所以瀑布流的delegate也应该同时遵守DSWaterFlowViewDelegateUIScrollViewDelegate

二、cell位置的计算

根据tableView的原理,cell的显示是通过reloadData方法去通知代理和数据源,所以我们可以在.h文件也写出reloadData方法来刷新数据

/** * 刷新数据 */
- (void)reloadData;

注意:当控制器实现了数据源和代理方法后,第一次就应该调用一次reloadData方法,可以在DSWaterFlowView的.m文件中实现willMoveToSuperview方法

/** * 当DSWaterFlowView进入即将进入父控件的时候就刷新数据(第一次) */
- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [self reloadData];
}

接下来就应该实现reloadData方法,计算每个cell的位置


/** * 刷新数据 * 计算每一个cell的位置 */
- (void)reloadData
{
    /** cell的总数 */
    NSInteger numberOfCells = [self.datasource numberOfcellsInWaterFlowView:self];

    /** 总列数 */
    NSInteger numberOfColumns = [self numberOfColumns];

    /** 间距 */
    CGFloat topM = [self marginForType:(DSWaterFlowViewMarginTypeTop)];
    CGFloat bottomM = [self marginForType:(DSWaterFlowViewMarginTypeBottom)];
    CGFloat leftM = [self marginForType:(DSWaterFlowViewMarginTypeLeft)];
    CGFloat rightM = [self marginForType:(DSWaterFlowViewMarginTypeRight)];
    CGFloat columnM = [self marginForType:(DSWaterFlowViewMarginTypeColumn)];
    CGFloat rowM = [self marginForType:(DSWaterFlowViewMarginTypeRow)];
    /** cell的宽度 */
    CGFloat cellW = (self.width - leftM - rightM - (numberOfColumns - 1) * columnM) / numberOfColumns;

    /** 所有列最大Y值的数组 */
    CGFloat maxOfColumns[numberOfColumns];
    for (int i = 0; i < numberOfColumns; i++) {
        maxOfColumns[i] = 0.0;
    }

    // 计算所有cell的frame
    for (int i = 0; i < numberOfCells; i++) {
        /** 最小Y值cell的列数(最短的一列) */
        NSInteger cellColumn = 0;

        /** cell所处那列的最大Y值(最短那一列的最大Y值) */
        CGFloat maxOfCellColumn = maxOfColumns[cellColumn];

        // 求出最短的一列
        for (int j = 1; j < numberOfColumns; j++) {
            if (maxOfColumns[j] < maxOfCellColumn) {
                cellColumn = j;
                maxOfCellColumn = maxOfColumns[j];
            }
        }

        // 询问代理位置的高度
        CGFloat cellH = [self heightAtidnex:i];

        // cell的位置
        CGFloat cellX = leftM + cellColumn * (cellW + columnM);
        CGFloat cellY = 0;
        if (maxOfCellColumn == 0.0) { // 首行
            cellY = topM;
        }else {
            cellY = maxOfCellColumn + rowM;
        }

        // 添加frame到数组中
        CGRect cellFrame = CGRectMake(cellX, cellY, cellW, cellH);
        [self.cellFrames addObject:[NSValue valueWithCGRect:cellFrame]];

        // 更新最短那一列的Y值
        maxOfColumns[cellColumn] = CGRectGetMaxY(cellFrame);

        // 整个瀑布流的高度
        CGFloat contentH = maxOfColumns[0];
        for (int i = 1; i < numberOfColumns; i++) {
            if (maxOfColumns[i] > contentH) {
                contentH = maxOfColumns[i];
            }
        }
        contentH += bottomM;
        // 设置scrollView可滚动的范围
        self.contentSize = CGSizeMake(0, contentH);
    }
}

#pragma mark - 私有方法
/** * 获取间距 */
- (CGFloat)marginForType:(DSWaterFlowViewMarginType)type
{
    if ([self.delegate respondsToSelector:@selector(waterFlowView:marginType:)]) {
       return [self.delegate waterFlowView:self marginType:type];
    }else {
        return DSWaterFlowViewDefaultMargin;
    }
}

/** * 返回多少列 */
- (NSInteger)numberOfColumns
{
    if ([self.datasource respondsToSelector:@selector(numberOfColumnsInWaterFlowView:)]) {
       return [self.datasource numberOfColumnsInWaterFlowView:self];
    }else {
        return DSWaterFlowViewDefaultNumberOfColumns;
    }
}

/** * 返回index对应的cell的高度 */
- (CGFloat)heightAtidnex:(NSInteger)index
{
    if ([self.delegate respondsToSelector:@selector(waterFlowView:heigthAtIndex:)]) {
        return [self.delegate waterFlowView:self heigthAtIndex:index];
    }else {
        return DSWaterFlowViewDefaultCellH;
    }
}

在私有扩展里面添加cellFrames属性

/** * 所有cell的frame */
@property (nonatomic, strong) NSMutableArray *cellFrames;

并且设置默认的参数(列数,间距,高度等)

/** 默认cell的高度 */
#define DSWaterFlowViewDefaultCellH 70
/** 默认间距 */
#define DSWaterFlowViewDefaultMargin 10
/** 默认的列数 */
#define DSWaterFlowViewDefaultNumberOfColumns 3

这个时候瀑布流的基本框架差不多搭建完成了,但是有两点瑕疵:
1>不具备像tableview那样的重用机制
2>不应该在reloadData方法向数据源索要cell,而是应该像tableview一样,在滚动的时候来索要cell

/** * 当UIScrollView滚动的时候也会调用这个方法 */
- (void)layoutSubviews
{
    [super layoutSubviews];

    // 向数据源索要对应位置的cell
    NSUInteger numberOfCells = self.cellFrames.count;

    for (int i = 0; i < numberOfCells; i++) {
        // 取出i位置的cell
        CGRect cellFrame = [self.cellFrames[i] CGRectValue];

        // 优先从字典中取
        DSWaterFlowViewCell *cell = self.displayingCells[@(i)];

        // 判断i位置对应的frame在不在屏幕上(能否看见)
        if ([self isInScreen:cellFrame]) { // 在屏幕上

            if (cell == nil) {
                // 向数据源索要cell,并添加进来
                cell = [self.datasource waterFlowView:self cellAtindex:i];
                cell.frame = cellFrame;
                [self addSubview:cell];

                // 存放到字典中
                self.displayingCells[@(i)] = cell;
            }
        }else { // 不在屏幕上
            if (cell) {
                // 从瀑布流和字典中删除掉
                [cell removeFromSuperview];
                [self.displayingCells removeObjectForKey:@(i)];

                // 存进缓存池
                [self.reusableCells addObject:cell];
            }
        }
    }

}

/** * 根据缓存池标识从缓存池中取出可循环利用的cell */
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
    // 从缓存池中取出可重用cell
    __block DSWaterFlowViewCell *reusbaleCell = [self.reusableCells anyObject];

    // 遍历缓存池
    [self.reusableCells enumerateObjectsUsingBlock:^(DSWaterFlowViewCell *cell, BOOL * _Nonnull stop) {
        if ([reusbaleCell.identifier isEqualToString:cell.identifier]) {
            reusbaleCell = cell;
            *stop = YES;
        }
    }];

    if (reusbaleCell) { // 从缓存池中移除cell
        [self.reusableCells removeObject:reusbaleCell];
    }

    // 返回可重用cell
    return reusbaleCell;


}

/** * 判断一个frame有没有在屏幕上 */
- (BOOL)isInScreen:(CGRect)frame
{
    return (CGRectGetMaxY(frame) > self.contentOffset.y) && (CGRectGetMinY(frame) < self.contentOffset.y + self.height);
}

私有扩展添加如下属性:

/** * 正在展示的cell */
@property (nonatomic, strong) NSMutableDictionary *displayingCells;

/** * 缓存池是随机的,随机从缓存池去数据(用set,存放离开屏幕的cell) */
@property (nonatomic, strong) NSMutableSet *reusableCells;

注意点:
1>当UIScrollView滚动的时候就会调用layoutSubviews方法,因此可以在这个方法中向数据源索要cell
2>displayingCells属性用来保存目前显示在屏幕上的cell(用字典是因为一个位置可以对应一个cell),判断当前位置上的cell在不在屏幕,可以防止出现在原地小范围滚动也会不停的向数据源索要cell并且重复添加进DSWaterFlowView
3>reusableCells属性就是缓存池(用NSSet是因为从缓存池中取cell是随机的),用来装离开屏幕的cell
4>提供dequeueReusableCellWithIdentifier给外面,让其从缓存池中取cell,所以应该在.h文件提供该方法

/** * 根据缓存池标识从缓存池中取出可循环利用的cell */
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;

同时为DSWaterFlowViewCell提供identifier属性

@property (nonatomic, copy) NSString *identifier;

对于如何用这个控件,tableView怎么用,这个就怎么用!
1>将该控件添加到控制器View上,设置代理和数据源,通知遵守协议

// 创建瀑布流控件
    DSWaterFlowView *waterFlowView = [[DSWaterFlowView alloc] init];
    waterFlowView.frame = self.view.bounds;
    waterFlowView.delegate = self;
    waterFlowView.datasource = self;
    [self.view addSubview:waterFlowView];

2>然后实现对应的方法就OK了

好了,文章到这里,整个瀑布流控件就完成了!

你可能感兴趣的:(ios,瀑布流)