当前位置:首页 » 编程语言

swift详解之二十三------------UICollectionView基础用法和简单自定义

2015-09-04 15:48 本站整理 浏览(127)

UICollectionView基础用法和简单自定义

注:本文通过几个实例来讲讲UICollectionView基本用法

本次要实现的两个效果。感谢猫神提供的教程 OneV’s Den

第一个界面是一个普通的流布局
UICollectionViewFlowLayout
, 第二个界面是自定义的一个圆形布局。加了点手势操作和动画。老规矩。后面会附上源码
首先来看下基本用法 。

1、UICollectionView基础用法

简单的
UICollectionView
相当于
GridView
,一个多列的
UItableView
,然而
UICollectionView
UItableView
的操作也非常相似 。都是会设置有一个
DataSource
和一个
delegate
标准的UICollectionView包含三个部分,它们都是UIView的子类:
cells 单元格用来展示内容的,可以设置所有的大小 也可以指定不同尺寸和不同的内容
Supplementary Views 追加视图 如果你对UITableView比较熟悉的话,可以理解为每个Section的Header或者Footer,用来标记每个section的view
Decoration Views 装饰视图 这是每个section的背景
UICollectionView和UITableView最大的不同就是UICollectionViewLayout,UICollectionViewLayout可以说是UICollectionView的大脑和中枢,它负责了将各个cell、Supplementary View和Decoration Views进行组织,为它们设定各自的属性。包括位置、尺寸、层级、形状等等 。。
Layout决定了UICollectionView是如何显示在界面上的。在展示之前,一般需要生成合适的UICollectionViewLayout子类对象,并将其赋予CollectionView的collectionViewLayout属性
下面我们实现一个最简单的Demo
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = UICollectionViewScrollDirection.Vertical  //滚动方向
layout.itemSize = CGSizeMake(60, 75) //设置所有cell的size  太重要了 找了半天。(自学就是辛苦呀!!)
layout.minimumLineSpacing = 10.0  //上下间隔
layout.minimumInteritemSpacing = 5.0 //左右间隔
layout.headerReferenceSize = CGSizeMake(20, 20)
layout.footerReferenceSize = CGSizeMake(20, 20)

这里创建了基本的流布局 设置了一些基本属性。
然后其他的设置和UITableView差不多
let collect:UICollectionView = UICollectionView(frame: self.view.frame,collectionViewLayout:layout)
collect.backgroundColor = UIColor.whiteColor()
collect.delegate = self
collect.dataSource = self
self.view.addSubview(collect)

因为初始的背景色是黑色的,这里指定了背景色
然后实现下面三个基本的方法,就能正常跑了 。最要是cell的显示方法
//设置分区个数
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }
//设置每个分区元素个数
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return  10
    }

//设置元素内容
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        //这里创建cell  
        return cell
 }

为了得到高效的View,对于cell的重用是必须的,避免了不断生成和销毁对象的操作,在UICollectionView中使用以下方法进行注册:
registerClass:forCellWithReuseIdentifier:
registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
registerNib:forCellWithReuseIdentifier:
registerNib:forSupplementaryViewOfKind:withReuseIdentifier:

先注册 ,使用一个
Identifier
,加入重用队列。要是在重用队列里没有可用的cell的话,runtime将自动帮我们生成并初始化一个可用的cell。
我们这里是自己用xib 画了个cell

一个很简单的cell ,把它的
class
设置成我们自定义的
MyCellContent
,
MyCellContent
继承自
UICollectionViewCell
,把这两个拖成它的成员属性

import UIKit
class MyCellContent: UICollectionViewCell {
    @IBOutlet  var contentImage: UIImageView!
    @IBOutlet  var contentLabel: UILabel!   
}

然后,在我们的视图控制器中的
viewDidLoad
进行注册
let nib = UINib(nibName: "MyCollectionCell", bundle: NSBundle.mainBundle())
collect.registerNib(nib, forCellWithReuseIdentifier: "DesignViewCell")

然后在
cellForItemAtIndexPath
里面就能这样取了
let identify:String = "DesignViewCell"
 let cell =collectionView.dequeueReusableCellWithReuseIdentifier(identify, forIndexPath: indexPath) as! MyCellContent

我们事先创建了个结构体,用来存放cell的img和name
struct CellContent{
    var img:String
    var name:String
}

然后在控制器中声明了一个
var dic = Array<CellContent>()

viewDidLoad
中初始化。
for i in 1...9{
            dic.append(CellContent(img: "f"+String(i), name: "歪脖子"+String(i)))
}

我图片存放的名字就是
f1-----f9

func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return  self.dic.count
}

这里返回元素个数就可以这么写了
//设置元素内容
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let identify:String = "DesignViewCell"
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(identify, forIndexPath: indexPath) as! MyCellContent
    cell.contentView.backgroundColor = UIColor.grayColor()
    cell.contentView.alpha = 0.2
    let img = UIImage(named: (self.dic[indexPath.row] as CellContent).img)
    cell.contentImage.image = img
    cell.contentLabel.text = (self.dic[indexPath.row] as CellContent).name

    return cell
}

这个就可以很简单的设置了 。现在运行,第一个页面的效果就有了
但让还有向UITableView中样很多的方法去设置别的,比如单个cell的大小
func collectionView(collectionView: UICollectionView!,
        layout collectionViewLayout: UICollectionViewLayout!,
        sizeForItemAtIndexPath indexPath: NSIndexPath!) -> CGSize {
            return CGSizeMake(150, 150)
    }

点击cell
//点击元素
    func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath){
      print("点击了第\(indexPath.section) 分区 ,第\(indexPath.row) 个元素")
    }

还有很多,自己慢慢玩吧 。下面看看自定义的

2、自定义UICollectionViewLayout

UICollectionViewLayoutAttributes
是一个非常重要的类,先来看看property列表:
@property (nonatomic) CGRect frame
@property (nonatomic) CGPoint center
@property (nonatomic) CGSize size
@property (nonatomic) CATransform3D transform3D
@property (nonatomic) CGFloat alpha
@property (nonatomic) NSInteger zIndex
@property (nonatomic, getter=isHidden) BOOL hidden

可以看到,
UICollectionViewLayoutAttributes
的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。和
DataSource
的行为十分类似,当
UICollectionView
在获取布局时将针对每一个
indexPath
的部件(包括
cell
,追加视图和装饰视图),向其上的
UICollectionViewLayout
实例询问该部件的布局信息,这个布局信息,就以
UICollectionViewLayoutAttributes
的实例的方式给出。
UICollectionViewLayout
的功能为向
UICollectionView
提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义
layout
的常规做法是继承
UICollectionViewLayout
类,然后重载下列方法:
-(CGSize)collectionViewContentSize //返回collectionView的内容的尺寸

-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
//返回rect中的所有的元素的布局属性

-(UICollectionViewLayoutAttributes )layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath
//返回对应于indexPath的位置的cell的布局属性

-(UICollectionViewLayoutAttributes )layoutAttributesForSupplementaryViewOfKind:(NSString )kind atIndexPath:(NSIndexPath *)indexPath
//返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载

-(UICollectionViewLayoutAttributes * )layoutAttributesForDecorationViewOfKind:(NSString)decorationViewKind atIndexPath:(NSIndexPath )indexPath
//返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载

-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
//当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。

在初始化一个
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
来生成更新后的布局。
以上都是猫神的巨作,他写的很好直接拿来用了
下面看下demo
首先创建一个类继承自
UICollectionViewLayou
然后声明一些基本的属性
private var _cellCount:Int?
    private var _collectSize:CGSize?
    private var _center:CGPoint?
    private var _radius:CGFloat?

按照上面的步骤
//一般在该方法中设定一些必要的layout的结构和初始需要的参数等
override func prepareLayout() {
    super.prepareLayout()
    _collectSize = self.collectionView?.frame.size
    _cellCount = self.collectionView?.numberOfItemsInSection(0)
    _center = CGPointMake(_collectSize!.width / 2.0, _collectSize!.height / 2.0);
    _radius = min(_collectSize!.width, _collectSize!.height)/2.5
}

这个方法初始化了一些基本信息
//内容区域的总大小 (不是可见区域)
    override func collectionViewContentSize() -> CGSize {
        return _collectSize!  //这里不用可见区域吧
    }

可见区域
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        var attributesArray = [UICollectionViewLayoutAttributes]()
        if let count = self._cellCount {
            for i in 0 ..< count{
                //这里利用了-layoutAttributesForItemAtIndexPath:来获取attributes
                let indexPath = NSIndexPath(forItem: i, inSection: 0)
                let attributes =  self.layoutAttributesForItemAtIndexPath(indexPath)
                attributesArray.append(attributes!)
            }
        }
        return attributesArray
    }

override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attrs = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
        attrs.size = CGSizeMake(ITEM_SIZE, ITEM_SIZE)
        let x = Double(_center!.x) + Double(_radius!) * cos(Double(2 * indexPath.item) * M_PI/Double(_cellCount!))
        let y = Double(_center!.y) + Double(_radius!) * sin(Double(2 * indexPath.item) * M_PI/Double(_cellCount!))
        attrs.center = CGPointMake( CGFloat(x) , CGFloat(y));
        return attrs
    }

这个方法
layoutAttributesForItemAtIndexPath
UICollectionViewLayoutAttributes
的一些属性进行设置 ,前面列出过 ,然后
layoutAttributesForElementsInRect
方法返回所有
UICollectionViewLayoutAttributes
, 以数组的方式
然后再使用的时候把基本用法里的
layout
换掉
layout = MyCollectionViewLayout()

  collect = UICollectionView(frame: self.view.frame,collectionViewLayout:layout)
  collect.backgroundColor = UIColor.whiteColor()
  collect.delegate = self
  collect.dataSource = self

这样运行 , 圆就出现了
if(layout is MyCollectionViewLayout){
            layout = UICollectionViewFlowLayout()
            (layout as! UICollectionViewFlowLayout).scrollDirection = UICollectionViewScrollDirection.Vertical  //滚动方向
            (layout as! UICollectionViewFlowLayout).itemSize = CGSizeMake(60, 75)
        }else{
            layout = MyCollectionViewLayout()
        }
        self.collect.setCollectionViewLayout(layout, animated: true)

可以通过
setCollectionViewLayout
方法来切换
layout


然后给这个界面添加手势
//注册tap手势事件
let tapRecognizer = UITapGestureRecognizer(target: self, action: "handleTap:")
collect.addGestureRecognizer(tapRecognizer)

func handleTap(sender:UITapGestureRecognizer){
        if sender.state == UIGestureRecognizerState.Ended{
            let tapPoint = sender.locationInView(self.collect)
            if let  indexPath = self.collect.indexPathForItemAtPoint(tapPoint)
            {
                //点击了cell
                //这个方法可以用来对collectionView中的元素进行批量的插入,删除,移动等操作,同时将触发collectionView所对应的layout的对应的动画。
                print("------")
                self.collect.performBatchUpdates({ () -> Void in
                    self.collect.deleteItemsAtIndexPaths([indexPath])
                    self.dic.removeAtIndex(indexPath.row)
                }, completion: nil)

            }else{

                let val =  arc4random_uniform(8)+1
                self.dic.append(CellContent(img: "f"+String(val), name: "歪脖子"+String(val)))

                self.collect.insertItemsAtIndexPaths([NSIndexPath(forItem: Int(val) , inSection: 0)])
//                dispatch_async(dispatch_get_global_queue(0, 0), { () -> Void in
//                     let val =  arc4random_uniform(9)
//                    self.dic.append(CellContent(img: "f"+String(val), name: "歪脖子"+String(val)))
//                    dispatch_async(dispatch_get_main_queue()) {
//                            self.collect.reloadData()
//                    
//                    }
//                })
                //点击了不是cell的区域
                print("+++++++")

            }
        }
    }

我注释掉这段GCD的代码也是可以执行的 ,就是没有动画 。
这个方法
performBatchUpdates:completion
可以用来对collectionView中的元素进行批量的插入,删除,移动等操作,同时将触发collectionView所对应的layout的对应的动画。相应的动画由layout中的下列四个方法来定义:
initialLayoutAttributesForAppearingItemAtIndexPath:
initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:

默认的动画是这样的

我们可以自定义动画
每次重新给出layout时都会调用prepareLayout,这样在以后如果有collectionView大小变化的需求时也可以自动适应变化。
override func initialLayoutAttributesForAppearingItemAtIndexPath(itemIndexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {

        // Must call super
        var attributes = super.initialLayoutAttributesForAppearingItemAtIndexPath(itemIndexPath)

        if self.insertIndexPaths.contains(itemIndexPath) {

            if let _ = attributes{
                attributes = self.layoutAttributesForItemAtIndexPath(itemIndexPath)
            }

            // Configure attributes ...
            attributes!.alpha = 0.0;
            attributes!.center =  CGPointMake(_center!.x, _center!.y);
            //attributes?.size = CGSizeMake(1000, 1000)
        }

        return attributes;
    }

    override func prepareForCollectionViewUpdates(updateItems: [UICollectionViewUpdateItem]) {
        super.prepareForCollectionViewUpdates(updateItems)
        self.insertIndexPaths = [NSIndexPath]()
        for update in updateItems{
            if update.updateAction == UICollectionUpdateAction.Insert{
                self.insertIndexPaths.append(update.indexPathAfterUpdate)
            }
    }

首先会调用
prepareForCollectionViewUpdates
,我们在这里拿到那个新增的
NSIndexPath
,然后
initialLayoutAttributesForAppearingItemAtIndexPath
在这个方法中设置一些初始位置。
看下效果

这个是从中间散出去的 ,同理也可以搞一些别的效果 。大概就这些吧。当然UICollectionView可以玩的还很多,期待大家一起探索。多分享哦!
(本实例使用xcode 7 bate , swift 2.0)
最后附上源码:https://github.com/smalldu/SwiftStudy