• swift:自定义UICollectionViewFlowLayout


    UICollectionView是ios中一个十分强大的控件,利用它能够十分简单的实现一些很好看的效果。UICollectionView的效果又依赖于UICollectionViewLayout或者它的子类UICollectionViewFlowLayout。而关于自定义UICollectionViewFlowLayout网上介绍的比较少。出于这一目的,写下这边文章,希望能够帮助初学者(我也是)实现一些简单的流水布局效果。下面的演示就是本篇文章的目标。最终版代码和所有图片素材(图片名和项目中有点不一样)已经上传至Github,大家可以下载学习。


    几个简单的概念

    • 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")
        }
    
    }

    效果应该是这样的


    编码

    水平排列

    创建一个名为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)
        }

    效果就成了这样


    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)  //还有这里

    上面的的数据展示其实已经足够解释一切了。读到这里,推荐你自己去找找规律,通过自己发现的奥秘绝对比直接看我写出答案有意义的多!下面这张图例已经说明了一切


    至于为什么会是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!


    后记

    我不会告诉你,介绍UICollectionView的自定义布局这篇文章,是我下一个实验的前传。不过最近被老师强迫帮他们去写文档,估计进度得缓缓。



    文/疯狂的剁椒鱼头(简书作者)
    原文链接:http://www.jianshu.com/p/f99d59837921
    著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
  • 相关阅读:
    项目部署
    nginx
    IDEA中Lombok插件的安装与使用
    Git常用命令总结
    CentOS 7 NAT模式上网配置
    一名3年工作经验的java程序员应该具备的技能
    maven 项目加载本地JAR
    linux压缩(解压缩)命令详解
    jdk7与jdk8环境共存与切换
    linux服务器卸载本机默认安装的jdk
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/5386364.html
Copyright © 2020-2023  润新知