Hướng dẫn custom hiển thị card với UICollectionViewLayout
Bài đăng này đã không được cập nhật trong 3 năm
1. Giới thiệu về UICollectionViewLayout
UICollectionViewLayout
là lớp trừu tượng cơ bản. Bạn có thể kế thừa từ nó để tạo ra và bố cục layout cho UICollectionView
. Công việc bố cục tạo ra chủ yếu cho vị trí của cell, supplementary views và decoration views trong UICollectionView
. Khi đó UICollectionView
sẽ sử dụng những thông tin trong đó để thể hiện nội dụng lên màn hình cho chung ta thấy.
Những func cần thiết để override
collectionViewContentSize
: Dùng để trả về content size cho UICollectionView sau khi đã bố cục layout.layoutAttributesForElements(in:)
: Trả về các element sẽ hiện thị trong khung hình.layoutAttributesForItem(at:)
: Trả về các item (cell) ứng với indexPath.layoutAttributesForSupplementaryView(ofKind:at:)
: Trả về các item SupplementaryView (Header hoặc Footer) ứng với indexPath.prepare()
: Sử dụng func này để tính toán và thiết lập các item cho collection view. Apple khuyến cáo nếu muốn cache lại thì nên sử dụng func nàyshouldInvalidateLayout(forBoundsChange:)
: Check sự thay đổi layout cho collection view.invalidateLayout()
: Huỷ bỏ bố cục hiện tại và thiết lập lại cho bố cục mới.
2. Bắt đầu custom cho UICollectionView
1. Định nghĩa delegate cho collection view layout
Bước đầu, cần định nghĩa delegate cho collection view layout để có thể nhận được các thông tin cho việc custom collection view layout như size items , size header, section insets
protocol ATMCollectionViewLayoutDelegate: class {
func collectionViewLayout(collecitionViewLayout: CardCollectionViewLayout, sizeForItem indexPath: IndexPath) -> CGSize
func collectionViewLayout(colletionViewLayout: CardCollectionViewLayout, insetForSection section: Int) -> UIEdgeInsets
func collectionViewLayout(collectionViewLayout: CardCollectionViewLayout, sizeForHeaderSection section: Int) -> CGSize
}
Sử dụng extension
để thiết lập các giá trị ban đầu cho delegate. Cách này có thể giúp khi controller conform từ delegate không cần phải implement hết toàn bộ function trong delegate. Có thể sử dụng cách này thay có optional func trong protocol của Objective C
extension ATMCollectionViewLayoutDelegate {
func collectionViewLayout(collecitionViewLayout: ATMCollectionViewLayout, sizeForItem indexPath: IndexPath) -> CGSize {
return CGSize.zero
}
func collectionViewLayout(colletionViewLayout: ATMCollectionViewLayout, insetForSection section: Int) -> UIEdgeInsets {
return UIEdgeInsets.zero
}
func collectionViewLayout(collectionViewLayout: ATMCollectionViewLayout, sizeForHeaderSection section: Int) -> CGSize {
return CGSize.zero
}
}
2.Tạo subclass cho UICollectionViewLayout
Đầu tiên, cần tạo 1 subclass từ UICollectionViewLayout
có tên là CardCollectionViewLayout
. Trong này, sẽ dùng để override các function phía trên để custom lại layout cho collection giống như trong demo.
weak var delegate: ATMCollectionViewLayoutDelegate?
//Khai báo chiều cao của thẻ
var itemHeight: CGFloat = 40
//Xác định hướng scroll cho collection view
var scrollDirection: UICollectionViewScrollDirection = .vertical
var contentWidth: CGFloat = 0
var contentHeight: CGFloat = 0
// Định nghĩa 1 mảng itemAttributes dùng để lưu lại các item là cell
private var itemAttributesCache: Array<UICollectionViewLayoutAttributes> = []
// Định nghĩa 1 mảng headerAttributes dùng để lưu lại các item là header
private var headerAttributesCache: Array<UICollectionViewLayoutAttributes> = []
Chuẩn bị tính toán và bố cục layout cho collection view. Ta override lại function prepare()
và tính toán layout ở trong đó
override func prepare() {
super.prepare()
// Kiểm tra đã khởi tạo layout chưa, nếu có rồi thì không tính toán lại layout
guard itemAttributesCache.isEmpty, headerAttributesCache.isEmpty, let collectionView = collectionView else {
return
}
// Tính toán phần dimension theo vertical hoặc horizontal
let fixedDimension: CGFloat
if scrollDirection == .vertical {
fixedDimension = collectionView.frame.width - (collectionView.contentInset.left + collectionView.contentInset.right)
contentWidth = fixedDimension
} else {
fixedDimension = collectionView.frame.height - (collectionView.contentInset.top + collectionView.contentInset.bottom)
contentHeight = fixedDimension
}
//Sử dụng để lưu lại khoảng cách của các item và header
var additionalSectionSpacing: CGFloat = 0
for section in 0..<collectionView.numberOfSections {
//Lấy size của header view nếu có
let sizeHeaderSection = (delegate ?? self).collectionViewLayout(collectionViewLayout: self, sizeForHeaderSection: section)
//Số lượng item của section
let itemCount = collectionView.numberOfItems(inSection: section)
//Lấy content inset của section nếu có
let sectionInset = (delegate ?? self).collectionViewLayout(colletionViewLayout: self, insetForSection: section)
//Kiểm tra điều kiện để tính toán layout cho header section
if sizeHeaderSection.width > 0 && sizeHeaderSection.height > 0 && itemCount > 0 {
let frame: CGRect
//Tính toán frame cho header section
if scrollDirection == .vertical {
frame = CGRect(x: 0, y: additionalSectionSpacing, width: sizeHeaderSection.width, height: sizeHeaderSection.height)
} else {
frame = CGRect(x: additionalSectionSpacing, y: 0, width: sizeHeaderSection.height, height: sizeHeaderSection.width)
}
//Khởi tạo layout attrubute của header và set frame
let headerLayoutAttribute = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, with: IndexPath(item: 0, section: section))
headerLayoutAttribute.frame = frame
headerLayoutAttribute.zIndex = section * 1000
// Đưa header vào mảng để sử dụng cho việc cache dữ liệu
headerAttributesCache.append(headerLayoutAttribute)
// Tính lại khoảng cách với
additionalSectionSpacing += frame.height
}
//Tính lại khoảng cách với content inset của section
if sizeHeaderSection.width > 0 && sizeHeaderSection.height > 0 {
additionalSectionSpacing += scrollDirection == .vertical ? sectionInset.top : sectionInset.left
}
for item in 0..<itemCount {
let indexPath = IndexPath(item: item, section: section)
//Lấy item size của collection
let itemSize = (delegate ?? self).collectionViewLayout(collecitionViewLayout: self, sizeForItem: indexPath)
let frame: CGRect
//Tính toán frame của item
if scrollDirection == .vertical {
let widthItem = itemSize.width - (sectionInset.left + sectionInset.top)
frame = CGRect(x: sectionInset.left, y: additionalSectionSpacing, width: widthItem, height: itemSize.height)
} else {
let heightItem = itemSize.height - (sectionInset.top + sectionInset.bottom)
frame = CGRect(x: additionalSectionSpacing, y:sectionInset.top , width: itemSize.width, height: heightItem)
}
//Khởi tạo layout attrubute của cell và set frame cho nó
let itemLayoutAttribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
itemLayoutAttribute.frame = frame
//Set zIndex để xác định thứ tự layout
itemLayoutAttribute.zIndex = section * 1000 + item
//Add layout vào mảng item cho việc cache
itemAttributesCache.append(itemLayoutAttribute)
//Tính toán lại khoảng cách
if item == itemCount - 1 {
additionalSectionSpacing += scrollDirection == .vertical ? frame.height + sectionInset.bottom : frame.width + sectionInset.right
} else {
additionalSectionSpacing += itemHeight
}
if scrollDirection == .vertical {
contentHeight = additionalSectionSpacing
} else {
contentWidth = additionalSectionSpacing
}
}
}
}
Xong phần chuẩn bị layout thì chuyển tiếp qua công việc kiểm tra và đưa layout ra ngoài. Lúc này ta sẽ cần override 3 function chính là :
layoutAttributesForElements(in:)
: Trả về các element sẽ hiện thị trong khung hình.layoutAttributesForItem(at:)
: Trả về các item (cell) ứng với indexPath.layoutAttributesForSupplementaryView(ofKind:at:)
: Trả về các item SupplementaryView (Header hoặc Footer) ứng với indexPath. Đây là 3 function sẽ nhận về các layout và hiện nó ra ngoài collection view
//Trả về các layout atrribute sẽ được hiện thị ở trong khung
//Trả về các layout atrribute sẽ được hiện thị ở trong khung
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let headerInRect = headerAttributesCache.filter { (header) -> Bool in
//Kiểm tra frame của header có giao với khung cần hiển thị
header.frame.intersects(rect)
}
let itemInRect = itemAttributesCache.filter { (item) -> Bool in
//Kiểm tra frame của item có giao với khung cần hiển thị
return item.frame.intersects(rect)
}
return headerInRect + itemInRect
}
//Trả về item layout atrribute với vị trí là indexPath
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return itemAttributesCache.first {
return $0.indexPath == indexPath
}
}
//Trả về header hoặc footer layout atrribute với vị trí là indexPath
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if elementKind == UICollectionElementKindSectionHeader {
return headerAttributesCache.first{ $0.indexPath == indexPath }
}
return nil
}
Kiểm tra khung hiển thị có bị thay đổi và yêu cầu update lại thông tin. Nó sẽ được gọi khi có sự thay đổi về frame của collection view hoặc do thây đổi hướng của thiết bị
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if scrollDirection == .vertical, let oldWidth = collectionView?.bounds.width {
return oldWidth != newBounds.width
}
if scrollDirection == .horizontal, let oldHeight = collectionView?.bounds.height {
return oldHeight != newBounds.height
}
return false
}
Cuối cùng là thiết lập lại dữ liệu nếu có sự thay đổi về bố cục hiển thị ở function phía trên.Lúc này funtion invalidateLayout()
sẽ được gọi
override func invalidateLayout() {
super.invalidateLayout()
itemAttributesCache = []
headerAttributesCache = []
contentWidth = 0
contentHeight = 0
}
3.Kết thúc
Mình đã hướng dẫn cơ bản cho các bạn việc custom collection view sử dụng UICollectionViewLayout
.Các bạn có thể tại class custom về tại đây. Cảm ơn các bạn đã đọc bài viết của mình !
All rights reserved