Làm thế nào để có thể custom collection view layout ?

  • Đã bao giờ bạn không hài lòng với layout mặc định của collection view (flow layout) hay có bao giờ flow layout không thể đáp ứng được yêu cầu về giao diện của bạn? Khi đó bạn cần phải làm gì? Câu trả lời cho bạn là bạn cần phải custom collection view layout.
  • Đây là một ví dụ cho project của mình, yêu cầu về giao diện khá phức tạp. Bạn hãy nhìn vào hình ảnh sau:

Simulator Screen Shot Nov 27, 2016, 17.12.23.png

  • Một số yêu cầu đặt ra với giao diện như bên:

    • Có thể scroll 2 chiều horizontal và vertical
    • Khi scroll theo chiều ngang thì cột thứ tự kia vẫn keep lại mà không scroll theo, tương tự như khi scroll dọc thì row title cũng keep.
  • Có lẽ bạn đang tưởng tượng nó như một sheet của excel đúng không? Đúng vậy.

  • Tôi đã buộc phải custom collection view layout thay vì sử dụng flow layout mặc định của collection view

  • Chúng ta cùng bắt đầu nhé: Một số method cần phải overwrite khi custom đó là

// Func này sẽ cung cấp các artribute cho mỗi cell khi scroll
override func prepareLayout() {

}
// Return content size của collection view
override func collectionViewContentSize() -> CGSize {

}
// Return attribute đã có được tại prepareLayout()
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {

}
//Trả ra một mảng các attribute của các cell có trong vùng hiển thị (chính là phần được filter cho mục đích reused cell)
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

}

1. Tạo class custom collection view layout

  • Để có thể custom được layout của collection view, bạn hãy tạo một class mới kế thừa lại UICollectionViewLayout, ở đây tôi đặt tên là GanttCollectionViewLayout
  • Ví dụ như sau:
class GanttCollectionViewLayout: UICollectionViewLayout {
    // Lưu attributes của các cell
    private var itemAttributes: NSMutableArray!
    // Compute size collection view
    private var contentSize: CGSize!
    //data source for custom size of cell
    private weak var ganttDataSource: GanttCollectionViewLayoutDelegate?
}

2. Prepare layout

  • Chúng ta coi mỗi row là một section
// Tại mỗi cell tương ứng với một indexPath
let indexPath = NSIndexPath(forItem: index, inSection: section)
if let attributes = layoutAttributesForItemAtIndexPath(indexPath) {
    var frame = attributes.frame
    // Keep first row
    if section == 0 {
        frame.origin.y = collectionView.contentOffset.y
    }
    / Keep first column
    if index == 0 {
        frame.origin.x = collectionView.contentOffset.x
    }
    attributes.frame = frame
}
  • Create attributes cho mỗi row
// Tại cell thứ nhất chúng ta change zIndex lớn nhất
// Những cell thuộc row 0 và column 0 zIndex lớn để cho nó nổi lên
if (section == 0) && (index == 0) {
    attributes.zIndex = 1024
} else if (section == 0) || (index == 0) {
    attributes.zIndex = 1023
}
// Khi scroll theo bất cứ chiều ngang hay dọc chúng ta đều phải set lại origin x và y cho first row và first column
var frame = attributes.frame
if section == 0 {
    frame.origin.y = collectionView.contentOffset.y
}
if index == 0 {
    frame.origin.x = collectionView.contentOffset.x
}
attributes.frame = frame
  • Lặp cho tất cả các cell, tăng origin x và y
sectionAttributes.addObject(attributes)

xOffset = xOffset + itemSize.width
column = column + 1

if column == numOfItem {
    if xOffset > contentWidth {
        contentWidth = xOffset
    }
    column = 0
    xOffset = 0
    yOffset = yOffset + itemSize.height
}
  • Lưu các attributes đó vào mảng
if (itemAttributes == nil) {
    itemAttributes = NSMutableArray(capacity: collectionView.numberOfSections())
}
itemAttributes.addObject(sectionAttributes)

3. Collection view content size

  • Mỗi khi một cell được add vào thì chúng ta cần tính lại content size cho collection view để đảm bảo có thể scroll hết các cell
// Tính lại content size sau mỗi cell
if let attributes = itemAttributes.lastObject?.lastObject as? UICollectionViewLayoutAttributes {
    contentHeight = attributes.frame.origin.y + attributes.frame.size.height
    contentSize = CGSizeMake(contentWidth, contentHeight)
}
  • Chúng ta cũng cần overwrite lại method collectionViewContentSize để return content size của collection view tại mỗi thời điểm scroll
// Overwrite method get content size
override func collectionViewContentSize() -> CGSize {
    if contentSize != nil {
        return contentSize
    }
    return CGSizeZero
}

4. Filter attribute for visible cells

  • Như chúng ta biết collection view có cơ chế reused cell nhằm mục đích tối ưu hiệu năng, tại mỗi thời điểm chỉ hiển thị và khởi tạo một số cell nhất định, vậy khi chúng ta có rất nhiều attributes tương ứng với số phần tử dữ liệu, làm thế nào chúng ta vẫn hiển thị đúng? Câu trả lời là cần phải filter để lấy ra các attributes tương ứng với các cell được hiện thị tại mỗi thời điểm (cells visible)
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    var attributes = [UICollectionViewLayoutAttributes]()
    if itemAttributes != nil {
        for section in itemAttributes {
            let filteredArray = section.filteredArrayUsingPredicate(
                NSPredicate(block: { (evaluatedObject, bindings) -> Bool in
                    return CGRectIntersectsRect(rect, evaluatedObject.frame)
                })
                ) as! [UICollectionViewLayoutAttributes]
            attributes.appendContentsOf(filteredArray)
        }
    }
    return attributes
}

5. Custom size for item

  • Để có thể custom size cho từng cell chúng ta cần tạo một protocol có tên là ganttDataSource
// Khai báo
private weak var ganttDataSource: GanttCollectionViewLayoutDelegate?
  • Tạo protocol như sau
@objc protocol GanttCollectionViewLayoutDelegate: class {
    optional func collectionView(collectionView: UICollectionView, sizeForItem indexPath: NSIndexPath) -> CGSize
}
  • Để sử dụng datasource đó vào mục đích set size cho mỗi cell, chúng ta sử dụng đoạn code sau trong phần tạo attributes frame mỗi cell
let indexPath = NSIndexPath(forItem: index, inSection: section)
var itemSize = CGSizeZero
if let size = ganttDataSource?.collectionView?(collectionView, sizeForItem: indexPath) {
    itemSize = size
}
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
attributes.frame = CGRectIntegral(CGRectMake(xOffset, yOffset, itemSize.width, itemSize.height))
  • Như vậy trên đây là một ví dụ giúp các bạn có thể custom collection view layout. Đây cũng là một bài toán khá cơ bản cho yêu cầu về giao diện, đặc biệt là các giao diện màn hình cho chức năng thống kê.
  • Với các bài toán khác chúng ta cũng có thể custon collection view layout như: hiển thị ảnh nhiều size (float layout), mosac layout, .... Các bài toán đó cũng cùng một yêu cầu đó là bạn cần phải custom layout của collection view.
  • Các bạn hãy cố gắng đọc hiểu để có thể custom theo ý các bạn nhé

All Rights Reserved