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