UICollectionView

作者: Saxon_Geoffrey | 来源:发表于2015-03-12 01:07 被阅读10932次

    UICollectionView和UITableView很类似,不过对于我个人来讲,UITableView是经常用到的东西,UICollectionView使用较少,所以这篇文章讲UICollectionView。

    1.类和协议

    1).UICollectionViewController:与UITableViewController功能类似
    2).UICollectionViewCell:与UITableViewCell功能类似,同样有ReuseIdentifier,所以它也有复用机制。

    从storyBoard中出列:

    MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"myCell" forIndexPath:indexPath];
    cell.cellLabel.text = [NSString stringWithFormat:@"%ld",(long)indexPath.item];
    

    从nib中注册:

    [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseIdentifier];
    

    3).UICollectionViewDataSource:数据源协议

    4).UICollectionViewDelegate:处理包含选中事件的各种方法的协议

    5).UICollectionViewDelegateFlowLayout:这是UICollectionView和UITableView不同的地方,它可以用来定制一些布局。

    2.例子

    1).初始化

    新建一个工程,删除ViewController类,将storyBoard中的ViewController替换为UICollectionViewController。
    像往常一样,你的主要内容显示在 cell 中,cell 可以被任意分组到 section 中。Collection view 的 cell 必须是 UICollectionViewCell 的子类。所以我们新建UICollectionViewController与UICollectionViewCell的子类,将storyBoard中UICollectionViewController的custom class设置为MyCollectionViewController,将UICollectionViewCell的custom class设置为MyCollectionViewCell。

    在UICollectionViewCell中新增如下图两个控件,UIImageView和UILabel

    不要忘记设置cell的Identifier:

    建立两个IBOutlet:

    2).实现数据源方法

    MyCollectionViewController.m:

    - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
        return 1;
    }
    
    - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
        return 20;
    }
    
    - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
        MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"myCell" forIndexPath:indexPath];
        cell.cellLabel.text = [NSString stringWithFormat:@"%ld",(long)indexPath.item];
        
        return cell;
    }
    

    配置cell,MyCollectionViewCell.m

    -(void)awakeFromNib{
        [super awakeFromNib];
        self.backgroundColor = [UIColor randomColor];
    
    }
    

    现在运行,如下图:

    IMG_0939.PNG

    旋转屏幕后栅格会自动旋转并对齐:

    IMG_0940.PNG

    2).实现委托方法

    a.高亮

    在cell中添加一个selectedBackgroundView视图:

    -(void)awakeFromNib{
        [super awakeFromNib];
        self.selectedBackgroundView = [[UIView alloc]initWithFrame:self.frame];
        self.selectedBackgroundView.backgroundColor = [UIColor blackColor];
        
        self.backgroundColor = [UIColor randomColor];
    }
    

    实现以下代理方法:

    -(BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath{
        return YES;
    }
    
    //放大缩小效果
    -(void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath{
        UICollectionViewCell *selectedCell = [collectionView cellForItemAtIndexPath:indexPath];
        [UIView animateWithDuration:kAnimationDuration animations:^{
            selectedCell.transform = CGAffineTransformMakeScale(2.0f, 2.0f);
        }];
    }
    
    -(void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath{
        UICollectionViewCell *selectedCell = [collectionView cellForItemAtIndexPath:indexPath];
        [UIView animateWithDuration:kAnimationDuration animations:^{
            selectedCell.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
        }];
    }
    

    现在按下collectionCell会显示高亮状态:背景颜色变黑色,且有一个弹跳的放大缩小效果。

    b.选中

    如上右边新建一个MyDetailsViewController,并且从左边控制器中segue到MyDetailsViewController。

    MyDetailsViewController.m

    -(IBAction) doneTapped:(id) sender {
      [self dismissViewControllerAnimated:YES completion:nil];
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.imageView.image = [UIImage imageNamed:@"image"];
    }
    

    实现以下代理方法:
    MyCollectionViewController.m

    -(BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath{
        return YES;
    }
    
    -(void) collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
        dispatch_time_t delayInNanoSeconds = dispatch_time(DISPATCH_TIME_NOW, (int64_t)1*NSEC_PER_SEC);
        dispatch_after(delayInNanoSeconds, dispatch_get_main_queue(), ^{
            [self performSegueWithIdentifier:@"MainSegue" sender:indexPath];
        });
    }
    

    这样在高亮效果1秒后会进行视图切换。

    4).添加头部和尾部视图

    collection view 额外管理着两种视图:supplementary views , Supplementary views 相当于 table view 的 section header 和 footer views。像 cells 一样,他们的内容都由数据源对象驱动。然而和 table view 中用法不一样,supplementary view 并不一定会作为 header 或 footer view;他们的数量和放置的位置完全由布局控制。

    Supplementary views必须是 UICollectionReusableView的子类。布局使用的每个视图类都需要在 collection view 中注册,这样当 data source 让它们从 reuse pool 中出列时,它们才能够创建新的实例。首先我们需要在storyBoard中启用"Section Header"和"Section Footer"

    之后XCode会自动生成两个UICollectionResuableView到视图中:

    然后同样的你可以设置Identifier,然后在以下代理方法中dequeue即可,确实很分别,相比UITableView又进一步封装。

    -(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
              viewForSupplementaryElementOfKind:(NSString *)kind
                                    atIndexPath:(NSIndexPath *)indexPath{
        NSString *resueIndentifier = kCollectionViewHeaderIndentifier;
        UICollectionReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:resueIndentifier forIndexPath:indexPath];
        return  [collectionView dequeueReusableSupplementaryViewOfKind: UICollectionElementKindSectionFooter
                                                   withReuseIdentifier:SupplementaryViewIdentifier
                                                          forIndexPath:indexPath];
    }
    

    在这个demo,我演示下如何通过加载自定义的nib控件来添加头部和尾部视图,如下我们新建两个自定义nib控件:

    MyCollectionViewController中加载并注册nib:

    -(void)awakeFromNib{
        UINib *headerNib = [UINib nibWithNibName:NSStringFromClass([Header class]) bundle:[NSBundle mainBundle]];
        [self.collectionView registerNib:headerNib forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kCollectionViewHeaderIndentifier];
        
        UINib *footerNib = [UINib nibWithNibName:NSStringFromClass([Footer class]) bundle:[NSBundle mainBundle]];
        [self.collectionView registerNib:footerNib forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:kCollectionViewFooterIndentifier];
    }
    

    代理方法类似:

    -(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
              viewForSupplementaryElementOfKind:(NSString *)kind
                                    atIndexPath:(NSIndexPath *)indexPath{
        NSString *resueIndentifier = kCollectionViewHeaderIndentifier;
        if ([kind isEqualToString:UICollectionElementKindSectionFooter]) {
            resueIndentifier = kCollectionViewFooterIndentifier;
        }
        
        UICollectionReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:resueIndentifier forIndexPath:indexPath];
        if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
            Header *header = (Header *)view;
            header.label.text = [NSString stringWithFormat:@"Section Header %lu",(unsigned long)indexPath.section+1];
        }else if ([kind isEqualToString:UICollectionElementKindSectionFooter]){
            Footer *footer = (Footer *)view;
            NSString *title = [NSString stringWithFormat:@"Section Footer %lu",(unsigned long)indexPath.section+1];
            [footer.button setTitle:title forState:UIControlStateNormal];
        }
        return  view;    
    }
    

    UICollectionView和UITableView最重要的区别就是UICollectionView并不知道如何布局,它把布局机制委托给了UICollectionViewLayout子类,默认的布局方式是UICollectionFlowViewLayout类提供的流式布局(flow layout),也就是上面例子显示的那样子。这个类允许你通过UICollectionDelegateViewFlowLayout协议调整各自属性。

    不过你也可以创建自己的布局方式,通过继承UICollectionViewLayout,现在是一个例子。

    3.UICollectionViewLayout子类

    上面的例子中,我们所有cell的大小都是一样的,那如果我们的cell大小不一样呢?我们需要实现UICollectionViewDelegateFlowLayout的协议方法collectionView:layout:sizeForItemAtIndexPath:,但这会使得效果就像下面左边那张图。它会计算每一排中的最大高度,这样会让效果看起来不怎么样。我们可以继承UICollectionViewLayout来实现右图中的效果。

    我们新建一个UICollectionViewController,并把程序运行开始移到改控制器。

    像上面的例子那样,显示50个同样大小的单元,具体上面已经介绍了,之后它看起来像这样:

    现在实现UICollectionViewDelegateFlowLayout的协议方法随机改变cell大小的高度:

    - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {    
        CGFloat randomHeight = 80 + (arc4random() % 150);
        return CGSizeMake(80, randomHeight);
    }
    

    现在效果是这样:

    现在创建一个UICollectionViewLayout的子类:CustomCollectionViewLayout.首先我们需要像UICollectionViewDelegateFlowLayout一样通过代理的方式来获取特定indexPath上cell的高度。

    @class CustomCollectionViewLayout;
    
    @protocol CustomCollectionViewLayoutDelegate <NSObject>
    @required
    - (CGFloat) collectionView:(UICollectionView*) collectionView
                        layout:(CustomCollectionViewLayout*) layout
      heightForItemAtIndexPath:(NSIndexPath*) indexPath;
    @end
    

    子类需要覆盖父类以下3个方法:

    -(void) prepareLayout;
    -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
    -(CGSize) collectionViewContentSize;
    

    prepareLayout在布局开始之前会被调用,我们需要在这个方法中计算边框,所以我们引入numberOfColumns 和 interItemSpacing两个变量。分别是item每行的数目和item间的间距,所以头文件如下:

    @interface CustomCollectionViewLayout : UICollectionViewLayout
    
    @property (nonatomic, assign) NSUInteger numberOfColumns;
    @property (nonatomic, assign) CGFloat interItemSpacing;
    @property (weak, nonatomic) id<CustomCollectionViewLayoutDelegate> delegate;
    
    @end
    

    在开始布局前会执行的方法prepareLayout中,我们需要计算每个item的frame值,并把它存入字典layoutInfo中,然后,在我们覆盖父类的方法layoutAttributesForElementsInRect中,可以返回这个字典中的全部frame总值的数组。

    在prepareLayout中,frame的height可以通过代理传入:

    CGFloat height = [((id<CustomCollectionViewLayoutDelegate>)self.collectionView.delegate)
                                  collectionView:self.collectionView
                                  layout:self
                                  heightForItemAtIndexPath:indexPath];
    

    frame的width则和numberOfColumns 和 interItemSpacing有关,如下:

    //计算Item的宽度
        CGFloat fullWidth = self.collectionView.frame.size.width;
        CGFloat availableSpaceExcludingPadding = fullWidth - (self.interItemSpacing * (self.numberOfColumns + 1));
        CGFloat itemWidth = availableSpaceExcludingPadding / self.numberOfColumns;
    

    x轴和y轴则和当前的indexPath有关,所以我们遍历section和item,得到x轴和y轴,并将之前的高度和宽度加起来得到frame值。

        NSIndexPath *indexPath;
        NSInteger numSections = [self.collectionView numberOfSections];
        //遍历section
        for(NSInteger section = 0; section < numSections; section++)  {
            NSInteger numItems = [self.collectionView numberOfItemsInSection:section];
            //遍历item
            for(NSInteger item = 0; item < numItems; item++){
                indexPath = [NSIndexPath indexPathForItem:item inSection:section];
                UICollectionViewLayoutAttributes *itemAttributes =
                [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];  
                //计算x轴
                CGFloat x = self.interItemSpacing + (self.interItemSpacing + itemWidth) * currentColumn;
                //计算y轴
                CGFloat y = [self.lastYValueForColumn[@(currentColumn)] doubleValue];
                //通过协议回传高度值
                CGFloat height = [((id<CustomCollectionViewLayoutDelegate>)self.collectionView.delegate)
                                  collectionView:self.collectionView
                                  layout:self
                                  heightForItemAtIndexPath:indexPath];
                itemAttributes.frame = CGRectMake(x, y, itemWidth, height);
                //下一个item的y轴是当前y轴加上item高度,并且加上间距
                y += height;
                y += self.interItemSpacing;
                
                //把下一个item的y轴记入到字典中
                self.lastYValueForColumn[@(currentColumn)] = @(y);
                
                currentColumn ++;
                if(currentColumn == self.numberOfColumns) currentColumn = 0;
                //将item的属性记录到字典中
                self.layoutInfo[indexPath] = itemAttributes;
            }
        }
    

    然后在我们需要覆盖的第二个方法中,使用enumerateKeysAndObjectsUsingBlock遍历prepareLayout中的layoutInfo加入一个数组中:

    - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
        NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:self.layoutInfo.count];
        [self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath,
                                                             UICollectionViewLayoutAttributes *attributes,
                                                             BOOL *stop) {
            if (CGRectIntersectsRect(rect, attributes.frame)) {
                [allAttributes addObject:attributes];
            }
        }];
        return allAttributes;
    }
    

    最后一个方法是计算collectionView的内容大小,在第一个方法中,我们已经把下每个item的y轴记入到字典lastYValueForColumn中,所以我们通过do-while循环把这个最大的y值给取出来,加上宽度值即可返回collectionView的内容大小。

    -(CGSize) collectionViewContentSize {
        NSUInteger currentColumn = 0;
        CGFloat maxHeight = 0;
        do {
            //最大高度就是之前字典中的y轴
            CGFloat height = [self.lastYValueForColumn[@(currentColumn)] doubleValue];
            if(height > maxHeight)
                maxHeight = height;
            currentColumn ++;
        } while (currentColumn < self.numberOfColumns);
        
        return CGSizeMake(self.collectionView.frame.size.width, maxHeight);
    }
    

    Done!运行下效果如何:

    你可以在这里下载完整的代码。如果你觉得对你有帮助,希望你不吝啬你的star:)

    相关文章

      网友评论

      本文标题:UICollectionView

      本文链接:https://www.haomeiwen.com/subject/ugfbxttx.html