12/08/2018, 14:44

Tùy biến layout của UICollectionView

UICollectionView là một trong những đối tượng quen thuộc đối với lập trình viên iOS. Trước hết ta tự đặt ra câu hỏi "Vì sao cần phải tùy biến bố cục của UICollectionView". Mặc dù bản thân UIKIT của iOS đã cung cấp sẵn flow layout giúp hiển thị các đối tượng thành phần dưới dạng lưới (grid ...

UICollectionView là một trong những đối tượng quen thuộc đối với lập trình viên iOS.

Trước hết ta tự đặt ra câu hỏi "Vì sao cần phải tùy biến bố cục của UICollectionView". Mặc dù bản thân UIKIT của iOS đã cung cấp sẵn flow layout giúp hiển thị các đối tượng thành phần dưới dạng lưới (grid layout), nhưng trong một số trường hợp cách hiển thị này không còn phù hợp.

Ví dụ: Ta muốn hiện thị một galery ảnh nhưng các ảnh lại có kích thước khác nhau, nếu sử dụng flow layout mặc định ta sẽ có kết quả như dưới đây Giao diện trên trông "lem nhem" và không ổn chút nào.

Mục tiêu của bài viết tùy biến layout của UICollectionView để hiện thị dưới dạng Pinterest layout (một kiểu layout sử dụng phổ biến tại một số ứng dụng di động như Pinterest, Lozi, Foody, Mua chung ....)

Các đối tượng cha

Class / protocol name Mô tả
UICollectionView Main UI component class
UICollectionViewCell UI component để hiển thị thành phần dưới dạng cell
UICollectionViewDelegate Delegate tương tác với CollectionView
UICollectionViewDataSource Protocol định nghĩa cách thức hiển thị dữ liệu trong CollectionView
UICollectionViewLayout Abtract class mô tả cách thức xử lý layout
UICollectionViewLayoutAttributes Đối tượng lưu trữ thông tin layout

Các đối tượng mở rộng

Class / protocol name Lớp cha Mô tả
PinterestViewController UICollectionViewController Lớp quản lý UICollectionView
MyCell UICollectionViewCell Custom Cell
PinterestLayoutDelegate Protocol cho phép lấy một số thông tin của CollectionView
PinterestLayout UICollectionViewLayout Định nghĩa lại cách thức tính toán kích thước, sắp xếp các cell trong collection view

Custom lớp UICollectionViewLayout (PinterestLayout.swift)

Mô tả

  • Kế thừa lớp cha UICollectionViewLayout
  • Các xử lý chính:
    • Tính toán kích thước và vị trí của các cell trong CollectionView và lưu trữ vào cache
    • Sử dụng cache (tọa độ, kích thước của các cell) để xác định các cell sẽ được hiển thị trong khung hình hiển thị
    • Tính toán content size của CollectionView

Hàm prepare

Đây là hàm được gọi đầu tiên khi bắt đầu xử lý layout của CollectionView, hàm sẽ tiến hành xử lý những công việc sau:

  • Tính toán vị trị và kích thước của các cell trong CollectionView
  • Tính toán content size của CollectionView
override func prepare()
{
    super.prepare()
    self.attributeArray.removeAllObjects()
    let numberOfColumn : Int = self.delegate.getNumberOfColumn();
    let padding:CGFloat = 15.0;

    let collectionViewWidth = self.collectionView?.frame.size.awidth
    let itemWidth : CGFloat = (collectionViewWidth! - padding * CGFloat((numberOfColumn + 1))) / CGFloat(numberOfColumn)
    var contentHeight:CGFloat = 0.0;
    var columnArray = [CGFloat](repeating: 0.0, count: numberOfColumn);

    //Tính toán kích thước và vị trí của từng cell trong CollectionView
    for i in 0 ... (self.collectionView?.numberOfItems(inSection: 0))! - 1 {
        var tempX : CGFloat = 0.0
        var tempY : CGFloat = 0.0
        let indexPath = NSIndexPath(item: i, section: 0)
        let itemHeight:CGFloat = delegate.collectionView(collectionView: (self.collectionView)!, heightForPhotoAtIndexPath: indexPath)

        //Tìm cột có độ dài ngắn nhất trong CollectionView
        var minHeight:CGFloat = 0.0;
        var minIndex:Int = 0;

        if (numberOfColumn > 0){
            minHeight = columnArray[0]

        }
        for colIndex in 0..<numberOfColumn {
            if (minHeight > columnArray[colIndex]){
                minHeight = columnArray[colIndex]
                minIndex = colIndex
            }
        }

        //Bổ sung  cell mới vào cột có kích thước ngắn nhất
        tempX = padding + (itemWidth + padding) *  CGFloat(minIndex);
        tempY = minHeight + padding;
        columnArray[minIndex] = tempY + itemHeight;
        let attributes = UICollectionViewLayoutAttributes(forCellWith:indexPath as IndexPath);
        attributes.frame = CGRect(x: tempX, y: tempY, awidth: itemWidth, height: itemHeight);
        self.attributeArray.setObject(attributes, forKey: indexPath)

        //Tính toán lại chiều cao Content Size của CollectionView
        let newContentHeight:CGFloat = tempY + padding + itemHeight + padding;
        if (newContentHeight > contentHeight){
            contentHeight = newContentHeight;
        }
    }

    self.contentSize = CGSize(awidth: (self.collectionView?.frame.size.awidth)!, height: contentHeight);
}

Hàm collectionViewContentSize

Hàm được gọi khi cần lấy content size của CollectionView

override var collectionViewContentSize: CGSize{
        // Trả về ContentSize của CollectionView đã tính được ở hàm prepare
        return self.contentSize
}

layoutAttributesForElementsInRect:

Hàm được sử dụng để xác định các cell sẽ được hiển trên khung nhìn của CollectionView khi được scroll tới. Việc xác định các cell sẽ được hiển thị dựa vào cache đã được tính toán ở hàm prepare

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    var layoutAttributes = [UICollectionViewLayoutAttributes]()

    // Duyệt các đối tượng trong attributeArray để tìm ra các cell nằm trong khung nhìn rect
    for attributes  in self.attributeArray {
        if (attributes.value as! UICollectionViewLayoutAttributes).frame.intersects(rect ) {
            layoutAttributes.append((attributes.value as! UICollectionViewLayoutAttributes))
        }
    }
    return layoutAttributes
}

Khởi tạo Collection View

let pinterestLayout = PinterestLayout()
pinterestLayout.delegate = self
self.collectionView?.collectionViewLayout = pinterestLayout

Implement Delegate

Để sử dụng Pinterest Layout đã xây dựng ở trên ta cần phải implement các delegate sau:

UICollectionViewDataSource

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    //Chỉ ra số cell sẽ có trong CollectionView (quy tắc này do code trong phần layout sử dụng hàm này để lấy ra số cell trong collection view)
    return self.imageList.count
}

PinterestLayoutDelegate

//Tính toán chiều cao của từng cell
func collectionView(collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:NSIndexPath) -> CGFloat
{
    let paddingSpace = self.sectionInsets.left * CGFloat(self.itemsPerRow + 1)
    let availableWidth = self.collectionView.frame.awidth - paddingSpace
    let awidthPerItem = availableWidth / CGFloat(itemsPerRow)

    let boundingRect =  CGRect(x: 0, y: 0, awidth: awidthPerItem, height: CGFloat.greatestFiniteMagnitude);
    let rect = AVMakeRect(aspectRatio: (UIImage(named: imageList[indexPath.item])?.size)!, insideRect: boundingRect);
    return rect.height
}

//Chỉ ra số cột hiện thị trong một cell
func getNumberOfColumn() -> Int {
    return self.itemsPerRow
}

Mã nguồn của chương trình: https://github.com/TuInh/Report_T2_2017 Nguồn tham khảo:

  • http://dev.classmethod.jp/smartphone/iphone/ios-pinterest-layout/ (google translate)
  • https://www.raywenderlich.com/107439/uicollectionview-custom-layout-tutorial-pinterest
0