当前位置: > > > Swift - 使用网格(UICollectionView)的自定义布局实现复杂页面

Swift - 使用网格(UICollectionView)的自定义布局实现复杂页面

在之前的文章中:Swift - 使用网格(UICollectionView)进行流布局,我简单了介绍了UICollectionView 的使用。
网格UICollectionView除了使用流布局,还可以使用自定义布局。实现自定义布局需要继承UICollectionViewLayout,同时还要重载下面的三个方法:
// 内容区域总大小,不是可见区域
override var collectionViewContentSize: CGSize {
}

// 所有单元格位置属性
override func layoutAttributesForElements(in rect: CGRect)
    -> [UICollectionViewLayoutAttributes]? {
}

// 这个方法返回每个单元格的位置和大小
override func layoutAttributesForItem(at indexPath: IndexPath)
    -> UICollectionViewLayoutAttributes? {
}

下面实现一个自定义布局的例子,单元格有大小两种。网格从上到下,先是左边一个大单元格右边两个小单元格,接着左边两个小单元格右边一个大单元格,依次同上循环排列。 
效果图如下:
  
--- 自定义布局 CustomLayout.swift ---
(本文样例代码已升级到Swift3)
import UIKit

/**
 * 这个类只简单定义了一个section的布局
 */
class CustomLayout : UICollectionViewLayout {
    
    // 内容区域总大小,不是可见区域
    override var collectionViewContentSize: CGSize {
        let width = collectionView!.bounds.size.width - collectionView!.contentInset.left
            - collectionView!.contentInset.right
        let height = CGFloat((collectionView!.numberOfItems(inSection: 0) + 1) / 3)
            * (width / 3 * 2)
        return CGSize(width: width, height: height)
    }
    
    // 所有单元格位置属性
    override func layoutAttributesForElements(in rect: CGRect)
        -> [UICollectionViewLayoutAttributes]? {
            var attributesArray = [UICollectionViewLayoutAttributes]()
            let cellCount = self.collectionView!.numberOfItems(inSection: 0)
            for i in 0..<cellCount {
                let indexPath =  IndexPath(item:i, section:0)
                let attributes =  self.layoutAttributesForItem(at: indexPath)
                attributesArray.append(attributes!)
            }
            return attributesArray
    }
    
    // 这个方法返回每个单元格的位置和大小
    override func layoutAttributesForItem(at indexPath: IndexPath)
        -> UICollectionViewLayoutAttributes? {
            //当前单元格布局属性
            let attribute =  UICollectionViewLayoutAttributes(forCellWith:indexPath)
            
            //单元格边长
            let largeCellSide = collectionViewContentSize.width / 3 * 2
            let smallCellSide = collectionViewContentSize.width / 3
            
            //当前行数,每行显示3个图片,1大2小
            let line:Int =  indexPath.item / 3
            //当前行的Y坐标
            let lineOriginY =  largeCellSide * CGFloat(line)
            //右侧单元格X坐标,这里按左右对齐,所以中间空隙大
            let rightLargeX = collectionViewContentSize.width - largeCellSide
            let rightSmallX = collectionViewContentSize.width - smallCellSide
            
            // 每行2个图片,2行循环一次,一共6种位置
            if (indexPath.item % 6 == 0) {
                attribute.frame = CGRect(x:0, y:lineOriginY, width:largeCellSide,
                                         height:largeCellSide)
            } else if (indexPath.item % 6 == 1) {
                attribute.frame = CGRect(x:rightSmallX, y:lineOriginY, width:smallCellSide,
                                         height:smallCellSide)
            } else if (indexPath.item % 6 == 2) {
                attribute.frame = CGRect(x:rightSmallX,
                                         y:lineOriginY + smallCellSide,
                                         width:smallCellSide, height:smallCellSide)
            } else if (indexPath.item % 6 == 3) {
                attribute.frame = CGRect(x:0, y:lineOriginY, width:smallCellSide,
                                         height:smallCellSide )
            } else if (indexPath.item % 6 == 4) {
                attribute.frame = CGRect(x:0,
                                         y:lineOriginY + smallCellSide,
                                         width:smallCellSide, height:smallCellSide)
            } else if (indexPath.item % 6 == 5) {
                attribute.frame = CGRect(x:rightLargeX, y:lineOriginY,
                                         width:largeCellSide,
                                         height:largeCellSide)
            }
            
            return attribute
    }
    
    /*
     //如果有页眉、页脚或者背景,可以用下面的方法实现更多效果
     func layoutAttributesForSupplementaryViewOfKind(elementKind: String!,
     atIndexPath indexPath: NSIndexPath!) -> UICollectionViewLayoutAttributes!
     func layoutAttributesForDecorationViewOfKind(elementKind: String!,
     atIndexPath indexPath: NSIndexPath!) -> UICollectionViewLayoutAttributes!
     */
}


--- 主页面 ViewController.swift ---
import UIKit

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource
{
    var collectionView:UICollectionView!
    //课程名称和图片,每一门课程用字典来表示
    let courses = [
        ["name":"Swift","pic":"swift.png"],
        ["name":"Xcode","pic":"xcode.png"],
        ["name":"Java","pic":"java.png"],
        ["name":"PHP","pic":"php.png"],
        ["name":"JS","pic":"js.png"],
        ["name":"React","pic":"react.png"],
        ["name":"Ruby","pic":"ruby.png"],
        ["name":"HTML","pic":"html.png"],
        ["name":"C#","pic":"c#.png"]
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let layout = CustomLayout()
        //let layout = UICollectionViewFlowLayout()
        let frame = CGRect(x:0, y:20, width: view.bounds.size.width,
                           height:view.bounds.height-20)
        self.collectionView = UICollectionView(frame: frame, collectionViewLayout:layout)
        self.collectionView.delegate = self
        self.collectionView.dataSource = self
        // 注册CollectionViewCell
        self.collectionView.register(UICollectionViewCell.self,
                                     forCellWithReuseIdentifier: "ViewCell")
        //默认背景是黑色和label一致
        self.collectionView.backgroundColor = UIColor.white
        
        //设置collectionView的内边距
        self.collectionView.contentInset = UIEdgeInsetsMake(0, 5, 0, 5)
        
        self.view.addSubview(collectionView)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    // CollectionView行数
    func collectionView(_ collectionView: UICollectionView,
                        numberOfItemsInSection section: Int) -> Int {
        return courses.count;
    }
    
    // 获取单元格
    func collectionView(_ collectionView: UICollectionView,
                        cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        // storyboard里设计的单元格
        let identify:String = "ViewCell"
        // 获取设计的单元格,不需要再动态添加界面元素
        let cell = self.collectionView.dequeueReusableCell(
            withReuseIdentifier: identify, for: indexPath) as UICollectionViewCell
        //先清空内部原有的元素
        for subview in cell.subviews {
            subview.removeFromSuperview()
        }
        // 添加图片
        let img = UIImageView(image: UIImage(named: courses[indexPath.item]["pic"]!))
        img.frame = cell.bounds
        img.contentMode = .scaleAspectFit
        // 图片上面显示课程名称,居中显示
        let lbl = UILabel(frame:CGRect(x:0, y:0, width:cell.bounds.size.width, height:20))
        lbl.textColor = UIColor.white
        lbl.textAlignment = .center
        lbl.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2)
        lbl.text = courses[indexPath.item]["name"]
        cell.addSubview(img)
        cell.addSubview(lbl)
        return cell
    }
    
    /* 自定义布局不需要调用
     //单元格大小
     func collectionView(collectionView: UICollectionView!,
     layout collectionViewLayout: UICollectionViewLayout!,
     sizeForItemAtIndexPath indexPath: NSIndexPath!) -> CGSize {
     let size:Float = indexPath.item % 3 == 0 ? 200 : 100
     return CGSize(width:size, height:size)
     }
     */
}
源码下载hangge_591.zip
评论14
  • 14楼
    2017-02-03 14:16
    tw

    看了很多篇,收益匪浅!
    想了解一下,网上看到的基本都是通过代码来实现页面布局的,能否出一个在storyboard上设计collectionView并关联代码来实现的案例?新手不太会~~~

    站长回复

    这个上一篇文章就是使用storyboard来自定义collectionView的。Swift - 使用网格(UICollectionView)进行流布局

  • 13楼
    2016-06-22 22:19
    无忧乐活

    有没有纯代码的实现方式?

    站长回复

    这个就是纯代码的啊。

  • 12楼
    2016-05-06 21:48
    一天

    航哥好:信读书,书架--编辑--归档,主要是想实现归档功能。

    站长回复

    明白了,如我要做的话还是只使用一个自定义的cell。cell内部根据传入的数据来显示不同的内容和布局。

    比如,只传入一条数据(单本书),那么这个cell就创建一个100%大小的imageView显示图片。
    如果传入n条数据(多本书归档),那么这个cell内就创建n个1/4大小的imageView显示图片,按九宫格排列(最多显示9个,多处来的忽略)

  • 11楼
    2016-05-01 00:07
    一天

    航哥好:我想实现微信读书,书架的布局方式,想在最后一个cell里再放一个九宫格。

    站长回复

    你指的是这个页面吗?


    我看这个用的也是同一个cell类,只不过根据数据的不同显示不同内容(最后一个由于没有书籍数据,则显示一个加号图片)。

  • 10楼
    2016-04-23 05:19
    一天

    航哥好:如果collectionview下有两个cell:DesignViewCellA,DesignViewCellB,现在怎样获取DesignViewCellB为它赋值,并返回;

    站长回复

    一般collectionview里面都是使用同一个cell类,然后根据indexPath来赋值的啊。不明白你说的collectionview下有两个cell:DesignViewCellA,DesignViewCellB是什么意思。

  • 9楼
    2016-03-22 10:34
    昂科拉

    站长,8 楼的删除方式,有没有更优化的删除方式呢?请问下

    站长回复

    不明白你的意思,是不是评论错文章了?

  • 8楼
    2016-03-21 11:11
    星星

    楼主,请问下是不是写错了,为什么collectionlayout中会就指直接写UIcollectionView。两部分代码基本是一样的啊

    站长回复

    原来代码有bug修改了下。文章编辑的时候更新错了,现已修正。

  • 7楼
    2016-03-16 15:30
    郑郑

    航哥 来回滑动的时候出现了复用错乱问题。cell加载的图片的位置发生了改变然后cell的内容也发生了改变

    站长回复

    测试了下确实,cellForItemAtIndexPath方法中要先清空内部原有的元素。原来漏掉了,现已修复,代码已更新。

  • 6楼
    2016-03-07 15:00
    yesicoo

    您好,如何解决复用导致的错乱问题?

    站长回复

    这个样例应该不存在复用错乱的问题

  • 5楼
    2016-01-12 15:29
    900

    一进去就报错,是因为我的图片大小不对吗

    站长回复

    我这边测试是没问题的,不知道你报的是什么错。我觉得 跟图片大小没什么关系,应该有其它原因。

  • 4楼
    2016-01-08 12:08
    单纯的大白菜

    如果 cell 里的图片自适应大小怎么实现啊

    站长回复

    保持比例:img.contentMode = .ScaleAspectFit

  • 3楼
    2015-12-27 12:47
    Young

    航哥给力,非常感谢

    站长回复

    不客气,谢谢支持。

  • 2楼
    2015-11-24 16:40
    json

    受益不少 谢谢

    站长回复

    不客气,很高兴对你有用

  • 1楼
    2015-11-18 10:01
    Gantry

    给力,谢航哥

    站长回复

    不客气,欢迎常来