swift:自定义UICollectionViewFlowLayout

swift:自定义UICollectionViewFlowLayout

创作目标

UICollectionView是ios中一个非常无敌的控件,利用它亦可非凡简短的实现部分很难堪的效果。UICollectionView的效用又凭借于UICollectionViewLayout要么它的子类UICollectionViewFlowLayout。而有关自定义UICollectionViewFlowLayout网上介绍的可比少。出于这一目标,写下这边文章,希望可以帮助初学者(我也是)实现部分简易的流水布局效果。下边的言传身教就是本篇作品的目的。最后版代码和有着图片素材(图片名和档次中有点不相同)已经上传至Github,大家可以下载学习。

图片 1

多少个简单的定义

  • UICollectionViewLayout与UICollectionViewFlowLayout

UICollectionView的显得效果几乎任何由UICollectionViewLayout担当(甚至是cell的大大小小)。所以,一般开发中所说的自定义UICollectionView也就是自定义UICollectionViewLayout。而UICollectionViewFlowLayout是持续自UICollectionViewLayout的,由苹果官方实现的水流布局成效。倘若想协调实现部分流水布局功能可以继承自最原始UICollectionViewLayout从头写,也足以继续自UICollectionViewFlowLayout进展改动。文本是连续自UICollectionViewFlowLayt*

  • UICollectionViewLayoutAttributes

第二点就说了UICollectionView的来得效果几乎百分之百由UICollectionViewLayout担负,而实在存储着每一个cell的地方、大小等属性的是UICollectionViewLayoutAttributes。每一个cell对应着一个属于自己的UICollectionViewLayoutAttributes,而UICollectionViewLayout好在利用UICollectionViewLayoutAttributes里存在的音信对每一个cel举办布局。

  • 流水布局

所谓流水布局就是:就是cell以一定的法则举办如同流水一般的有规律的一个跟着一个的排列。�最经典的水流布局便是九宫格布局,绝大部分的图样接纳器也是流水布局。

未雨绸缪工作

  • xcode7.0
  • swift2.0
  • 温馨本身提供的材料并在控制器中添加如下代码

class ViewController: UIViewController,UICollectionViewDelegate, UICollectionViewDataSource {

    lazy var imageArray: [String] = {

        var array: [String] = []

        for i in 1...20 {
            array.append("\(i)-1")
        }

        return array
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        let collectionView =  UICollectionView(frame: CGRectMake(0, 100, self.view.bounds.width, 200), collectionViewLayout: UICollectionViewFlowLayout())
        collectionView.backgroundColor = UIColor.blackColor()
        collectionView.dataSource  = self
        collectionView.delegate = self

        collectionView.registerClass(ImageTextCell.self, forCellWithReuseIdentifier: "ImageTextCell")
        self.view.addSubview(collectionView)
    }

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

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("ImageTextCell", forIndexPath: indexPath) as! ImageTextCell
        cell.imageStr = self.imageArray[indexPath.item]

        return cell
    }

}
//这里是自定义cell的代码
class ImageTextCell: UICollectionViewCell {

    var imageView: UIImageView?
    var imageStr: NSString? {

        didSet {
            self.imageView!.image = UIImage(named: self.imageStr as! String)
        }

    }

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.imageView = UIImageView()
        self.addSubview(self.imageView!)

    }

    override func layoutSubviews() {
        super.layoutSubviews()
        self.imageView?.frame = self.bounds
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

效果应该是这么的

图片 2

编码

水平排列

创设一个名为LineLayout.swift的文书(继承自UICollectionViewFlowLayout)。添加如下几行代码

    var itemW: CGFloat = 100
    var itemH: CGFloat = 100

    override init() {
        super.init()

        //设置每一个元素的大小
        self.itemSize = CGSizeMake(itemW, itemH)
        //设置滚动方向
        self.scrollDirection = .Horizontal
        //设置间距
        self.minimumLineSpacing = 0.7 * itemW
    }

    //苹果推荐,对一些布局的准备操作放在这里
    override func prepareLayout() {
        //设置边距(让第一张图片与最后一张图片出现在最中央)ps:这里可以进行优化
        let inset = (self.collectionView?.bounds.width ?? 0)  * 0.5 - self.itemSize.width * 0.5
        self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset)
    }

效率就成了这样

图片 3

shouldInvalidateLayoutForBoundsChange方法与layoutAttributesForElementsInRect方法关系

题目所写出的是可怜重大的两主意,先看我添加的如下测试代码

    /**
    返回true只要显示的边界发生改变就重新布局:(默认是false)
    内部会重新调用prepareLayout和调用
    layoutAttributesForElementsInRect方法获得部分cell的布局属性
    */
    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        print(newBounds)
        return true
    }

    /**
    用来计算出rect这个范围内所有cell的UICollectionViewLayoutAttributes,
    并返回。
    */
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        print("layoutAttributesForElementsInRect==\(rect)")
        let ret = super.layoutAttributesForElementsInRect(rect)
//        print(ret?.count)
        return ret
    }

为了诠释,我添加了多少个打印语句,在shouldInvalidateLayoutForBoundsChange重回值设置为true后,会发现layoutAttributesForElementsInRect措施调用分外频繁,几乎是每滑动一点就会调用三回。观望打印音讯能够窥见众多秘密

  • 开行程序有如下打印

layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)

类似看不太懂,没事,尝试滑动。

  • 滑动

(0.5, 0.0, 320.0, 200.0) //这个是shouldInvalidateLayoutForBoundsChange方法的打印的newBounds
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)//这个是layoutAttributesForElementsInRect打印的rect
(1.5, 0.0, 320.0, 200.0) 
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)
(3.5, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)
...

一拍即合发现,shouldInvalidateLayoutForBoundsChange的参数newBounds的意思是UICollectionView的可见矩形。什么叫可见矩阵?,因为UICollectionView也是UIScrollView的子类,所以它的确的“内容”远远不止大家屏幕上来看的那么多(这里不再话时间持续解释可见矩阵)。这好像layoutAttributesForElementsInRect打印出来的东西从来不啥变化是怎么回事?不急继续滑动。

  • 解密

延续滑动后有这几个消息,经过删除一些无用消息,彰显如下。(注意看有注释的行)

...
(248.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)
(249.0, 0.0, 320.0, 200.0)  //这里是可见矩阵
layoutAttributesForElementsInRect==(0.0, 0.0, 1136.0, 568.0)  //这里变化了1136.0是568.0的2倍(1136代表的是宽度的意思应该知道不需要解释吧)
(250.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 1136.0, 568.0)
...
(567.5, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 1136.0, 568.0)
(568.5, 0.0, 320.0, 200.0)//这里是可见矩阵
layoutAttributesForElementsInRect==(568.0, 0.0, 568.0, 568.0)  // 这里又变化了,x变成了568,宽度变成了568
(571.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 568.0, 568.0)
...
(815.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 568.0, 568.0)
(817.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 1136.0, 568.0) //还有这里
...
(1135.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 1136.0, 568.0)
(1136.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(1136.0, 0.0, 568.0, 568.0)  //还有这里

地点的的多少呈现莫过于已经充裕解释一切了。读到这里,推荐您自己去找找规律,通过协调发现的奥秘相相比直接看我写出答案有含义的多!上面这张图例已经认证了全套

图片 4

有关为啥会是568的翻番。。因为我是用的5s模拟器。你换成4s就成为480了。至于这样设计的说辞,我怀疑是为着便于举行限定的规定。

缩放效果

刺探了地点shouldInvalidateLayoutForBoundsChange方法与layoutAttributesForElementsInRect方法关系后,可以连续开展编码了。因为根本的内容已经讲解截止,剩下的就只是有些卡通的计量,所以不再接续助教,直接贴出代码。

class LineLayout: UICollectionViewFlowLayout {

    var itemW: CGFloat = 100
    var itemH: CGFloat = 100

    lazy var inset: CGFloat = {
        //这样设置,inset就只会被计算一次,减少了prepareLayout的计算步骤
        return  (self.collectionView?.bounds.width ?? 0)  * 0.5 - self.itemSize.width * 0.5
        }()

    override init() {
        super.init()

        //设置每一个元素的大小
        self.itemSize = CGSizeMake(itemW, itemH)
        //设置滚动方向
        self.scrollDirection = .Horizontal
        //设置间距
        self.minimumLineSpacing = 0.7 * itemW
    }

    //苹果推荐,对一些布局的准备操作放在这里
    override func prepareLayout() {

        //设置边距(让第一张图片与最后一张图片出现在最中央)
        self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    /**
    返回true只要显示的边界发生改变就重新布局:(默认是false)
    内部会重新调用prepareLayout和调用
    layoutAttributesForElementsInRect方法获得部分cell的布局属性
    */
    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        return true
    }

    /**
    用来计算出rect这个范围内所有cell的UICollectionViewLayoutAttributes,
    并返回。
    */
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        //取出rect范围内所有的UICollectionViewLayoutAttributes,然而
        //我们并不关心这个范围内所有的cell的布局,我们做动画是做给人看的,
        //所以我们只需要取出屏幕上可见的那些cell的rect即可
        let array = super.layoutAttributesForElementsInRect(rect)

        //可见矩阵
        let visiableRect = CGRectMake(self.collectionView!.contentOffset.x, self.collectionView!.contentOffset.y, self.collectionView!.frame.width, self.collectionView!.frame.height)

        //接下来的计算是为了动画效果
        let maxCenterMargin = self.collectionView!.bounds.width * 0.5 + itemW * 0.5;
        //获得collectionVIew中央的X值(即显示在屏幕中央的X)
        let centerX = self.collectionView!.contentOffset.x + self.collectionView!.frame.size.width * 0.5;
        for attributes in array! {
            //如果不在屏幕上,直接跳过
            if !CGRectIntersectsRect(visiableRect, attributes.frame) {continue}
            let scale = 1 + (0.8 - abs(centerX - attributes.center.x) / maxCenterMargin)
            attributes.transform = CGAffineTransformMakeScale(scale, scale)
        }

        return array
    }

    /**
    用来设置collectionView停止滚动那一刻的位置

    - parameter proposedContentOffset: 原本collectionView停止滚动那一刻的位置
    - parameter velocity:              滚动速度

    - returns: 最终停留的位置
    */
    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        //实现这个方法的目的是:当停止滑动,时刻有一张图片是位于屏幕最中央的。

        let lastRect = CGRectMake(proposedContentOffset.x, proposedContentOffset.y, self.collectionView!.frame.width, self.collectionView!.frame.height)
        //获得collectionVIew中央的X值(即显示在屏幕中央的X)
        let centerX = proposedContentOffset.x + self.collectionView!.frame.width * 0.5;
        //这个范围内所有的属性
        let array = self.layoutAttributesForElementsInRect(lastRect)

        //需要移动的距离
        var adjustOffsetX = CGFloat(MAXFLOAT);
        for attri in array! {
            if abs(attri.center.x - centerX) < abs(adjustOffsetX) {
                adjustOffsetX = attri.center.x - centerX;
            }
        }

        return CGPointMake(proposedContentOffset.x + adjustOffsetX, proposedContentOffset.y)
    }
}

如若在控制器中投入下边六个方法,在你点击控制器,或者点击某个cell会有很炫的卡通暴发,这都是苹果帮大家做好的。

    func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {

        self.imageArray.removeAtIndex(indexPath.item)

        collectionView.deleteItemsAtIndexPaths([indexPath])
    }

        override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {

        if self.collectionView!.collectionViewLayout.isKindOfClass(LineLayout.self) {
            self.collectionView!.setCollectionViewLayout(UICollectionViewFlowLayout(), animated: true)
        }else {
            self.collectionView!.setCollectionViewLayout(LineLayout(), animated: true)
        }

    }

总结

本篇小说记录了本人在自定义UICollectionViewFlowLayout过程中相遇的一些题目和化解形式(其实有局部坑爹的问题我没有列出,怕误导我们)。下面的全体都是基于UICollectionViewFlowLayout拓展的变动。而自己在GitHub下面上传的也有一份继承自UICollectionViewLayout的非流水布局。效果如下,因为原理性的东西都差不多,就不再举行分析(代码也有注释)。感兴趣的可以这Github下边下载。假使小说中有哪些错误或者更好的章程、提议等等,感谢您的指出。我们联合学习!O(∩_∩)O!

图片 5

后记

自我不会告诉你,介绍UICollectionView的自定义布局这篇随笔,是自身下一个试行的前传。然而近年来被助教强迫帮他们去写文档,估计进度得放缓。

发表评论

电子邮件地址不会被公开。 必填项已用*标注