PSCollectionView瀑布流实现

PSCollectionView是一个实现较简洁的仿Pinterest瀑布流iOS版实现,使用UIScrollView做容器,每列列宽固定,高度可变,使用方式类似UITableView。
其效果如图:

一.基本原理

其基本实现原理为:

  1. 列数固定,根据列数每列存储一个当前列的高度值。
  2. 每次插入数据块时,在当前最小高度的列里插入,然后更新当前列的高度为原有高度加上当前数据模块高度
  3. 重复2直到所有数据模块插入完毕
  4. 调整容器(UIScrollView)的高度为各列最大的高度值。

二.具体实现

1. 相关数据结构

公共属性

@property (nonatomic, retain) UIView *headerView;
@property (nonatomic, retain) UIView *footerView;
@property (nonatomic, retain) UIView *emptyView;
@property (nonatomic, retain) UIView *loadingView;

@property (nonatomic, assign, readonly) CGFloat colWidth;
@property (nonatomic, assign, readonly) NSInteger numCols;
@property (nonatomic, assign) NSInteger numColsLandscape;
@property (nonatomic, assign) NSInteger numColsPortrait;
@property (nonatomic, assign) id  collectionViewDelegate;
@property (nonatomic, assign) id  collectionViewDataSource;
  • headerView,footerView,emptyView,loadingView分别对应列表头部,尾部,空白时,正在加载时要显示的视图。
    numColsLandscape,numColsPortrait为横屏和竖屏时的列数。
  • colWidth,numCols为只读属性,根据当前的视图方向,视图总大小,横屏和竖屏时的列数计算得出。
  • collectionViewDelegate,collectionViewDataSource为Delegate和数据源。

私有属性

@property (nonatomic, assign, readwrite) CGFloat colWidth;
@property (nonatomic, assign, readwrite) NSInteger numCols;
@property (nonatomic, assign) UIInterfaceOrientation orientation;

@property (nonatomic, retain) NSMutableSet *reuseableViews;
@property (nonatomic, retain) NSMutableDictionary *visibleViews;
@property (nonatomic, retain) NSMutableArray *viewKeysToRemove;
@property (nonatomic, retain) NSMutableDictionary *indexToRectMap;
  • 私有属性将colWidth,numCols定义为readwrite,便于内部赋值操作。
  • orientation为当前视图的方向,从UIApplicationstatusBarOrientation属性获取。
  • reuseableViews数据集存储可重用的的数据块视图,在数据块移除可见范围时将其放入reuseableViews中,当DataSource调用dequeueReusableView时,从reuseableViews取出一个返回。
  • visibleViews字典存储当前可见的数据块视图,key为数据块索引,当容器滚动时,将移除可见范围的数据块视图从visibleViews中移除,并放入reuseableViews中;当存在应该显示的数据块视图,但还未放入容器视图时,则从DataSource获取新的数据块视图,加入到容器视图中,同时将其加入到visibleViews中。
  • viewKeysToRemove数组在遍历visibleViews时存储应该移除的数据块视图Key。
  • indexToRectMap数据字典存储每个数据块(不管可不可见)在容器中的位置,将CGRect转换为NSString(NSStringFromCGRect)作为Value存储,Key为数据块的索引。



    2.视图更新方式

    • 在reloadData或视图方向发生变化时,需要重新计算所有数据块的位置并重新加载,见relayoutViews方法。
    • 当滑动容器时,UIScrollView会调用其layoutSubviews方法,若方向未变化,则不需要重新加载所有数据块,仅仅需要移除非可见的数据块,载入进入可见范围的数据块,见removeAndAddCellsIfNecessary方法.
    #pragma mark - DataSource
    
    - (void)reloadData {
        [self relayoutViews];
    }
    
    #pragma mark - View
    
    - (void)layoutSubviews {
        [super layoutSubviews];
    
        UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
        if (self.orientation != orientation) {
            self.orientation = orientation;
            [self relayoutViews];
        } else {
            [self removeAndAddCellsIfNecessary];
        }
    }

    3.relayoutViews方法

    relayoutViews会将现有的所有数据块视图清除,重新从DataSource获取数据,重新计算所有数据块视图的位置,容器的高度等。

    1. 首先遍历可见数据块视图字典visibleViews,将所有数据块视图放入reuseableViews中,并清空visibleViews,indexToRectMap。
    2. 将emptyView,loadingView从容器视图中移除。
    3. 从DataSource获取数据块的个数numViews。
    4. 若headerView不为nil,则将headerView视图加入到容器,更新top索引。
    5. 若numViews不为0,则依次计算每个数据块的位置。使用colOffsets存储每一列的当前高度,每次增加数据块时将其添加到高度最小的列中,所处的列确定后,其orig坐标就确定了,宽度固定,再从DataSource获取此数据块的高度,那么当前数据块的frame位置就确定了,将其转换为NSString(使用setObject:NSStringFromCGRect)存储到indexToRectMap字典中,以数控块索引为key;同时将当前列的高度更新,再继续处理下一数据块,还是加入到高度最小的列中,直至所有数据块处理完毕。
    6. 这时的总高度即最高列的高度。
    7. 若numViews为0,则将emptyView增加到容器中,总高度则为添加emptyView的高度。
    8. 若footerView不为nil,则将footerView加入到容器中.
    9. 这时的总高度totalHeight即为最终容器内容的总高度,将其赋值的UIScrollView的contentSize属性。
    10. 这时headerView和footView已加入到容器中,但所有的数据块只是计算了其应该处于的位置,并未实际放入容器中,调用removeAndAddCellsIfNecessary将当前可见的数据块视图加入到容器中。

    //重置并重新计算所有itemframe

    - (void)relayoutViews {

        //根据屏幕方向设置列数

        self.numCols = UIInterfaceOrientationIsPortrait(self.orientation) ? self.numColsPortrait : self.numColsLandscape;

        

        

        

        //把所有当前可见的item放到reusePool里,并从当前视图移除

        [self.visibleViews enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {

            PSCollectionViewCell *view = (PSCollectionViewCell *)obj;

            [self enqueueReusableView:view];

        }];

        //清空可见item的字典

        [self.visibleViews removeAllObjects];

        //清空要删除的item的数组

        [self.viewKeysToRemove removeAllObjects];

        //清空所有itemframe的字典

        [self.indexToRectMap removeAllObjects];

        

        //从数据源获取item的个数

        NSInteger numViews = [self.collectionViewDataSource numberOfRowsInCollectionView:self];

        

        //总高度

        CGFloat totalHeight = 0.0;

        //CollectionViewtop,如果没headerView就是有个小间隙,有headerView则是headerViewbottom

        CGFloat top = kMargin;

        

        //如果headerView存在则设置headerView

        if (self.headerView) {

            top = self.headerView.top;

            //设置headerView的宽为collectionView的宽,跟tableViewheader一样

            self.headerView.width = self.width;

            [self addSubview:self.headerView];

            top += self.headerView.height;

        }

        

        //如果有item

        if (numViews > 0) {

            

            //根据列数创建一个数组,保存每列的高度即top

            NSMutableArray *colOffsets = [NSMutableArray arrayWithCapacity:self.numCols];

            

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

                //添加top为当前列的高度

                [colOffsets addObject:[NSNumber numberWithFloat:top]];

            }

            

            //计算出每一列的宽,如果有3列,则列宽为(self.width - 间隙 * 4) / 3,其实就是减去间隙然后等分

            self.colWidth = floorf((self.width - kMargin * (self.numCols + 1)) / self.numCols);

            

            /*

             *  1. 遍历所有的item,以indexkey,为每个item创建frame,并且添加到indexToRectMap里,为了布局使用

             *  2. 每次遍历都找出几列中高度(colOffsets中的top)最低的那一列,这样item的位置就确定了,然后更新那一列的总高度(top)

             *  3. 如此循环计算直到遍历完

             */

            for (NSInteger i = 0; i < numViews; i++) {

                //以下标为key

                NSString *key = PSCollectionKeyForIndex(i);

                //默认第0列的当前总高度最低

                NSInteger col = 0;

                CGFloat minHeight = [[colOffsets objectAtIndex:col] floatValue];

                

                //从第一列开始遍历每一列,找出总高度最低的那一列并记录下来

                for (int i = 1; i < [colOffsets count]; i++) {

                    CGFloat colHeight = [[colOffsets objectAtIndex:i] floatValue];

                    

                    if (colHeight < minHeight) {

                        col = i;

                        minHeight = colHeight;

                    }

                }

                //找出当前高度偏移量最低的那列之后,为下标为iitem创建frame

                

                //left:间隙 + 最低的那列(即第几列)* 间隙 + 最低的那列(即第几列)*列宽

                CGFloat left = kMargin + (col * kMargin) + (col * self.colWidth);

                //top: 就是最低的那列的总高度

                CGFloat top = [[colOffsets objectAtIndex:col] floatValue];

                //从数据源获取item高度

                CGFloat colHeight = [self.collectionViewDataSource collectionView:self heightForRowAtIndex:i];

                //创建frame

                CGRect viewRect = CGRectMake(left, top, self.colWidth, colHeight);

                

                //indexkey,把frame转成string存到字典里

                [self.indexToRectMap setObject:NSStringFromCGRect(viewRect) forKey:key];

                

                //更新最低那列的总高度,加上colHeight和间隙,如果colHeight从数据源获取回来是0,则top不变

                CGFloat heightOffset = colHeight > 0 ? top + colHeight + kMargin : top;

                [colOffsets replaceObjectAtIndex:col withObject:[NSNumber numberWithFloat:heightOffset]];

            }

            //把几列中最高的那列的高度作为ContentSize总高度

            for (NSNumber *colHeight in colOffsets) {

                totalHeight = (totalHeight < [colHeight floatValue]) ? [colHeight floatValue] : totalHeight;

            }

        //如果没有item

        } else {

            totalHeight = self.height;

        }

        

        //如果有footer则添加footer

        if (self.footerView) {

            self.footerView.top = totalHeight;

            self.footerView.width = self.width;

            [self addSubview:self.footerView];

            totalHeight += self.footerView.height;

        }

        

        //依据ContentSize总高度设置contentSize

        self.contentSize = CGSizeMake(self.width, totalHeight);

        

        //布局

        [self removeAndAddCellsIfNecessary];

        //布局完成发送通知

        [[NSNotificationCenter defaultCenter] postNotificationName:kPSCollectionViewDidRelayoutNotification object:self];

    }

    4.removeAndAddCellsIfNecessary方法

    removeAndAddCellsIfNecessary根据当前容器UIScrollViewcontentOffset,将用户不可见的数据块视图从容器中移除,将用户可见的数据块视图加入到容器中。

    1. 获得当前容器的可见部分。

      CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height);
    2. 逐个遍历visibleViews中的视图,使用CGRectIntersectsRect方法判断其frame与容器可见部分visibleRect是否有交集,若没有,则将其从visibleViews中去除,并添加到reuseableViews中。
    3. 对visibleViews剩余的数据块视图排序,获得其最小索引(topIndex)和最大索引(bottomIndex)。
    4. 将topIndex和bottomIndex分别向上和向下扩充bufferViewFactor*numCols个数据块索引。
    5. 从topIndex开始到bottomIndex判断索引对应的数据块视图的位置是否在容器的visibleRect范围内,以及其是否在visibleViews中。若其应该显示,而且不在visibleViews中,则向DataSource请求一个新的数据块视图,加到容器视图中,同时添加到visibleViews中。

    这样新的ScrollView可见区域就可以被数据块填充满。

    - (void)removeAndAddCellsIfNecessary {

        //缓冲的itemView的个数

        static NSInteger bufferViewFactor = 8;

        //最上方开始的item下标

        static NSInteger topIndex = 0;

        //最底部结束的item下标

        static NSInteger bottomIndex = 0;

        //获取item个数

        NSInteger numViews = [self.collectionViewDataSource numberOfRowsInCollectionView:self];

        

        //如果item个数为0 没必要布局

        if (numViews == 0) return;

        

        

        /*

         * 以下部分:

         *      计算当前可见区域,并扩充留出一个缓冲区域,把当前不在这个可见区域内的所有item都放到reusePool,并且从可见的item字典里清除

         */

        

        

        //根据当前滚动的位置创建出一个矩形frame,即当前可见区域

        CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height);

        //依据offsetThreshold(缓冲区域大小)再往上和下扩展一部分可见区域

        visibleRect = CGRectInset(visibleRect, 0, -1.0 * self.offsetThreshold);

        //遍历visibleViews(可见的cell的字典),如果cell不在可见区域则放到reusePool

        [self.visibleViews enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {

            PSCollectionViewCell *view = (PSCollectionViewCell *)obj;

            CGRect viewRect = view.frame;

            //如果cellframe与可见区域frame不相交,则cell添加到reusePool,并且添加到viewKeysToRemove

            if (!CGRectIntersectsRect(visibleRect, viewRect)) {

                [self enqueueReusableView:view];

                [self.viewKeysToRemove addObject:key];

            }

        }];

        

        //visibleViews移除viewKeysToRemove所包含的cell,即从可见的字典里移除

        [self.visibleViews removeObjectsForKeys:self.viewKeysToRemove];

        //清空viewKeysToRemove

        [self.viewKeysToRemove removeAllObjects];

        

        

        

        /*

         * 以下部分:

         *      先算出应该从第几个item开始,到第几个item结束(topIndex,bottomIndex),不用每次都循环判断所有的item,判断这些itemframe(relayoutViews计算好的),是否与当前可见区域相交(即在不在可见区域)

             如果相交,并且没有在self.visibleViews里(即没有显示着),则从数据源获取item,并添加到self上,再添加到self.visibleViews

         *

         */

        

        

        //如果当前可见item的字典的个数为0,表示第一次布局,需要判断所有的item

        if ([self.visibleViews count] == 0) {

            topIndex = 0;

            bottomIndex = numViews;

        } else {

        //如果不是第一次布局,在滚动的时候,则从visibleViews排序之后取出topIndexbottomIndex,再分别往上和下扩充 列数 * 8这么多个 用于判断,就是说不用每次都从头到尾都判断,浪费性能

            NSArray *sortedKeys = [[self.visibleViews allKeys] sortedArrayUsingComparator:^(id obj1, id obj2) {

                if ([obj1 integerValue] < [obj2 integerValue]) {

                    return (NSComparisonResult)NSOrderedAscending;

                } else if ([obj1 integerValue] > [obj2 integerValue]) {

                    return (NSComparisonResult)NSOrderedDescending;

                } else {

                    return (NSComparisonResult)NSOrderedSame;

                }

            }];

            topIndex = [[sortedKeys objectAtIndex:0] integerValue];

            bottomIndex = [[sortedKeys lastObject] integerValue];

    //        NSLog(@"!! topIndex %d",topIndex);

    //        NSLog(@"!! bottomIndex %d",bottomIndex);

            topIndex = MAX(0, topIndex - (bufferViewFactor * self.numCols));

            bottomIndex = MIN(numViews, bottomIndex + (bufferViewFactor * self.numCols));

    //        NSLog(@"## topIndex %d",topIndex);

    //        NSLog(@"## bottomIndex %d",bottomIndex);

        }

        //    NSLog(@"topIndex: %d, bottomIndex: %d", topIndex, bottomIndex);

        

        //开始遍历并添加

        for (NSInteger i = topIndex; i < bottomIndex; i++) {

            //依据下标索引为key

            NSString *key = PSCollectionKeyForIndex(i);

            //拿出对应的frame

            CGRect rect = CGRectFromString([self.indexToRectMap objectForKey:key]);

            

            //如果这个item在可见区域,并且没有在显示着(因为可能上面扩展的item包含了其中一部分已经显示着的item,则向数据源请求item并且设置frame并添加

            if (![self.visibleViews objectForKey:key] && CGRectIntersectsRect(visibleRect, rect)) {

                

                PSCollectionViewCell *newCell = [self.collectionViewDataSource collectionView:self cellForRowAtIndex:i];

                newCell.frame = CGRectFromString([self.indexToRectMap objectForKey:key]);

                [self addSubview:newCell];

                

                //为每个item添加一个手势

                if ([newCell.gestureRecognizers count] == 0) {

                    PSCollectionViewTapGestureRecognizer *gr = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)];

                    gr.delegate = self;

                    [newCell addGestureRecognizer:gr];

                    newCell.userInteractionEnabled = YES;

                }

                //以下标为key,添加到可见的字典里(visibleViews

                [self.visibleViews setObject:newCell forKey:key];

            }

        }

    }

    5.select方法

    其定义了一个UITapGestureRecognizer的子类PSCollectionViewTapGestureRecognizer来检测每个数据块的点击操作。
    从DataSource获取到一个新的数据块视图时,会检测里面是否已包含gesture recognizer对象,若没有则新创建一个PSCollectionViewTapGestureRecognizer对象放入,将delegate设为自身。

                // Setup gesture recognizer
                if ([newView.gestureRecognizers count] == 0) {
                    PSCollectionViewTapGestureRecognizer *gr = [[[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)] autorelease];
                    gr.delegate = self;
                    [newView addGestureRecognizer:gr];
                    newView.userInteractionEnabled = YES;
                }

    手势识别检测到点击时会向Delegate询问此点是否可接受(gestureRecognizer:shouldReceiveTouch:),若手势识别对象是PSCollectionViewTapGestureRecognizer类型,则是我们添加进去的。若该点所属的数据块视图可见,则接受此点,若不可见,则忽略。若手势识别对象不是PSCollectionViewTapGestureRecognizer对象,就不是我们放入的,则一直返回YES。

    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
        if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) return YES;
    
        NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame);
        NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString];
        NSString *key = [matchingKeys lastObject];
    
        if ([touch.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) {
            return YES;
        } else {
            return NO;
        }
    }

    当检测到点击操作时,调用didSelectView:方法,在其中调用delegate的collectionView:didSelectView:atIndex:方法,传递参数为self对象,选择的数据块视图以及选择的数据块索引;

    - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer {
        NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame);
        NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString];
        NSString *key = [matchingKeys lastObject];
        if ([gestureRecognizer.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) {
            if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) {
                NSInteger matchingIndex = PSCollectionIndexForKey([matchingKeys lastObject]);
                [self.collectionViewDelegate collectionView:self didSelectView:(PSCollectionViewCell *)gestureRecognizer.view atIndex:matchingIndex];
            }
        }
    }

    这种方式还存在各种问题

    1. 若DataSource返回的数据块视图中已加入自己的UITapGestureRecognizer对象,则[newView.gestureRecognizers count]就不为0,在判断时PSCollectionView内部定义的PSCollectionViewTapGestureRecognizer就不会加入, 这样选择数据块视图的操作就不会触发。
    2. 实现的gestureRecognizer:shouldReceiveTouch:方法对非PSCollectionViewTapGestureRecognizer的对象直接返回YES。这样,如果子类化PSCollectionView重写gestureRecognizer:shouldReceiveTouch:方法时,如果调用super的此方法,则会直接返回,不会执行自己的定制化操作;若不调用super的此方法,则选择功能就会出差错。

    6.重用数据块视图机制

    NSMutableSet *reuseableViews;中存储可复用的数据块视图。dequeueReusableView从reuseableViews中任取一个视图返回,enqueueReusableView将数据块视图放入reuseableViews中。

    #pragma mark - Reusing Views


    - (PSCollectionViewCell *)dequeueReusableViewForClass:(Class)viewClass {

        

        /*

         * 以类名为keyreusePool里找,找到则返回并从reusePool移除

         */

        

        NSString *identifier = NSStringFromClass(viewClass);

        PSCollectionViewCell *view = nil;

        if ([self.reuseableViews objectForKey:identifier]) {

            view = [[self.reuseableViews objectForKey:identifier] anyObject];

            

            if (view) {

                [[self.reuseableViews objectForKey:identifier] removeObject:view];

            }

        }

        

        return view;

    }



    - (void)enqueueReusableView:(PSCollectionViewCell *)view {

        //回调每个cellprepareForReuse方法

        if ([view respondsToSelector:@selector(prepareForReuse)]) {

            [view performSelector:@selector(prepareForReuse)];

        }

        

        view.frame = CGRectZero;

        

        /*

         *  以类名为key,添加到reusePool

         */

        

        //重用标示符就是cell的类名

        NSString *identifier = NSStringFromClass([view class]);


        //如果reuseableViews里没有这个标示符所对应的集合(reusePool),则为这个标识创建一个

        if (![self.reuseableViews objectForKey:identifier]) {

            [self.reuseableViews setObject:[NSMutableSet set] forKey:identifier];

        }

        //添加到对应的标识的集合(reusePool)

        [[self.reuseableViews objectForKey:identifier] addObject:view];

        

        //从父视图移除

        [view removeFromSuperview];

    }


    #pragma mark - Gesture Recognizer

    //触发手势的处理

    - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer {

        NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame);

        NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString];

        NSString *key = [matchingKeys lastObject];

        if ([gestureRecognizer.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) {

            if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectCell:atIndex:)]) {

                NSInteger matchingIndex = PSCollectionIndexForKey([matchingKeys lastObject]);

                [self.collectionViewDelegate collectionView:self didSelectCell:(PSCollectionViewCell *)gestureRecognizer.view atIndex:matchingIndex];

            }

        }

    }



    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {

        if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) return YES;

        

        NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame);

        NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString];

        NSString *key = [matchingKeys lastObject];

        

        if ([touch.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) {

            return YES;

        } else {

            return NO;

        }

    }


    代码:
    PSCollectionView.h
    PSCollectionView.m
    PSCollectionViewCell.h
    PSCollectionViewCell.m

    git工程:
    https://github.com/ptshih/PSCollectionView

    三.使用方法

    创建PSCollectionView对象

    self.collectionView = [[[PSCollectionView alloc] initWithFrame:self.view.bounds] autorelease];
    self.collectionView.delegate = self;
    self.collectionView.collectionViewDelegate = self;
    self.collectionView.collectionViewDataSource = self;
    self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

    设置列数

    // Specify number of columns for both iPhone and iPad
    if (isDeviceIPad()) {
        self.collectionView.numColsPortrait = 4;
        self.collectionView.numColsLandscape = 5;
    } else {
        self.collectionView.numColsPortrait = 2;
        self.collectionView.numColsLandscape = 3;
    }

    添加header,footer,empty,loader等视图

    UIView *loadingLabel = ...
    self.collectionView.loadingView = loadingLabel;
    UIView *emptyView = ...
    self.collectionView.emptyView = emptyView;
    UIView *headerView = ...
    self.collectionView.headerView = headerView;
    UIView *footerView = ...
    self.collectionView.footerView = footerView;

    实现Delegate和DataSource

    - (PSCollectionViewCell *)collectionView:(PSCollectionView *)collectionView viewAtIndex:(NSInteger)index {
        NSDictionary *item = [self.items objectAtIndex:index];
    
        // You should probably subclass PSCollectionViewCell
        PSCollectionViewCell *v = (PSCollectionViewCell *)[self.collectionView dequeueReusableView];
        if (!v) {
            v = [[[PSCollectionViewCell alloc] initWithFrame:CGRectZero] autorelease];
        }
    
        [v fillViewWithObject:item]
    
        return v;
    }
    
    - (CGFloat)heightForViewAtIndex:(NSInteger)index {
        NSDictionary *item = [self.items objectAtIndex:index];
    
        // You should probably subclass PSCollectionViewCell
        return [PSCollectionViewCell heightForViewWithObject:item inColumnWidth:self.collectionView.colWidth];
    }
    
    - (void)collectionView:(PSCollectionView *)collectionView didSelectView:(PSCollectionViewCell *)view atIndex:(NSInteger)index {
        // Do something with the tap
    }

    四.其他瀑布流实现

    1.WaterflowView
    2.上拉刷新瀑布流PSCollectionViewEGOTableViewPullRefresh结合,增加上拉/下拉刷新效果。
    3.瀑布效果,不同的实现方式

    参考:
    PSCollectionView
    When does layoutSubviews get called?
    Overriding layoutSubviews when rotating UIView
    iPhone开发笔记 – 瀑布流布局
    瀑布流布局浅析
    说说瀑布流式网站里那些可人的小细节
    EGOTableViewPullRefresh

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值