UICollectionView 自定制布局: 顶部黏住效果

微信扫一扫,分享到朋友圈

UICollectionView 自定制布局: 顶部黏住效果

要实现的布局效果

UICollectionView 自定制布局,就是自个来实现 UICollectionViewLayout,

而不是使用,系统给的 UICollectionViewFlowLayout

上图所示的布局效果:

往下拉动,顶部两块都固定黏住,格子也都黏住了

往上拉动,顶部第二块固定黏住

布局效果的实现思路

实现效果: 往下拉动,顶部两块都固定黏住,格子也都黏住了

常规思路是

控制手势的方向,只能向下,不可向上

这里采用布局控制

UICollectionView 是 UIScrollView 的子类,

UIScrollView 滚动的时候,有一个 contentOffset,

contentOffset 是一个点, CGPoint

下拉的时候,给每一个视图 (两个顶部与格子视图)加一个平移变换,CGAffineTransform.translation ,就好了

手指往下拉,界面看上去没有动,实际上是两个顶部与格子视图,和 scrollView 的容器视图,同步滚动

实现效果: 往上拉动,顶部第二块固定黏住

常规思路是

顶部第二块有两个,一个是 UICollectionView 的补充视图 SupplementaryView,一个是独立的控件,

往上拉动的时候,补充视图顶部第二块拉出界面,独立的顶部第二块默认隐藏,这时候显示

这里直接采用布局控制

顶部第二块只有一份,作为补充视图 SupplementaryView

往上拉动的时候,使用 UICollectionView 的 contentOffset,得到目前的位置,

再添加一个平移放射变换,来保持位置

顶部第二块要在格子视图的上方,顶部第二块 zIndex 大于格子视图们的 zIndex, OK

布局效果的具体实现

自定制 layout ,系统给了我们三个有用的入口

  • 给补充视图的布局信息

func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

  • 给格子视图的布局信息

func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

  • 布局信息汇总

func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?

一般的布局很简单,搞定这三个方法,收工

该布局效果的特别之处是:滚动起来,有不同的表现形式,上面说了两种

常规思路是

使用 func scrollViewDidScroll(_ scrollView: UIScrollView) 监听 contentOffset ,来触发

这里当然布局控制

重写系统的 func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool ,

因为,用户一滚动界面的时候,collectionView 的 bounds 属性就会改变

shouldInvalidateLayout(forBoundsChange:) 这个方法, 里面可以写判断规则,决定去刷新布局的时机。

格子视图 collectionView 的 bounds 属性改变, shouldInvalidateLayout(forBoundsChange:) 判断通过,再次进入 prepare() 方法,计算位置。

override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if oldBounds.size != newBounds.size {
cache.removeAll(keepingCapacity: true)
}
return true
}
复制代码

这酱紫,实现了滚动刷新布局

布局信息,重新计算,又调用 func prepare()

func prepare() 中,布局信息初始化

override public func prepare() {
guard let collectionView = collectionView, collectionView.numberOfItems(inSection: 0) > 0 else {
return
}
// 细节方法,见 github
prepareCache()
contentHeight = 0
zIndex = 0
oldBounds = collectionView.bounds
let itemSize = CGSize(width: collectionViewWidth, height: cellHeight)
// 顶部第一块的布局信息
let headerAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: Element.header.kind,
with: IndexPath(item: 0, section: 0)
)
prepareElement(size: headerSize, type: .header, attributes: headerAttributes)
// 顶部第 2 块的布局信息
let menuAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: Element.menu.kind,
with: IndexPath(item: 0, section: 0))
prepareElement(size: menuSize, type: .menu, attributes: menuAttributes)
// 格子视图的布局信息
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let cellIndexPath = IndexPath(item: item, section: 0)
let attributes = CustomLayoutAttributes(forCellWith: cellIndexPath)
let lineInterSpace = settings.minimumLineSpacing
attributes.frame = CGRect(
x: settings.minimumInteritemSpacing,
y: contentHeight + lineInterSpace,
width: itemSize.width,
height: itemSize.height
)
attributes.zIndex = zIndex
contentHeight = attributes.frame.maxY
cache[.cell]?[cellIndexPath] = attributes
zIndex += 1
}
// 保证顶部第 2 块的布局 zIndex, > 格子视图的
cache[.menu]?.first?.value.zIndex = zIndex
}
复制代码

布局信息汇总,并根据偏移 contentOffset 动态调节

override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
visibleLayoutAttributes.removeAll(keepingCapacity: true)
for (type, elementInfos) in cache {
for (indexPath, attributes) in elementInfos {
attributes.transform = .identity
//  动态调节, 顶部两块的布局信息
updateSupplementaryViews(type, attributes: attributes, collectionView: collectionView, indexPath: indexPath)
// 只关心屏幕上显示了的,布局信息
// 不在屏幕上的布局信息 attributes ,不用管
if attributes.frame.intersects(rect) {
if type == .cell{
// 动态调节, 格子视图的布局信息
updateCells(attributes, collectionView: collectionView, indexPath: indexPath)
}
visibleLayoutAttributes.append(attributes)
}
}
}
return visibleLayoutAttributes
}
复制代码

动态调节, 顶部两块 SupplementaryView 的布局信息

即根据偏移 contentOffset.y,设置相应的平移仿射变换

顶部第一块,还加了一个 alpha 控制

private func updateSupplementaryViews(_ type: Element, attributes: CustomLayoutAttributes, collectionView: UICollectionView, indexPath: IndexPath) {
switch type {
case .header:
attributes.transform = CGAffineTransform(translationX: 0, y: contentOffset.y)
attributes.headerOverlayAlpha = min(settings.headerOverlayMaxAlphaValue, contentOffset.y / headerSize.height)
case .menu:
print(contentOffset.y)
if contentOffset.y < 0{
attributes.transform = CGAffineTransform(translationX: 0, y: attributes.initialOrigin.y - headerSize.height + contentOffset.y)
}
else{
attributes.transform = CGAffineTransform(translationX: 0, y: max(attributes.initialOrigin.y, contentOffset.y) - headerSize.height)
}
default:
break
}
}
复制代码

动态调节, 格子视图的布局信息

即根据偏移 contentOffset.y,设置相应的平移仿射变换

private func updateCells(_ attributes: CustomLayoutAttributes, collectionView: UICollectionView, indexPath: IndexPath) {
if contentOffset.y < 0{
attributes.transform = CGAffineTransform(translationX: 0, y: attributes.initialOrigin.y + contentOffset.y)
}
}
复制代码

最后补充,

系统的, UICollectionViewFlowLayout ,标准的书架,

每一个 section 只能搭配一个有效的补充视图 header 和 footer

( 因为 header 和 footer 的 size ,是按 section 来的 )

这里的布局效果,一个 section 搭配了两个 header

实际上,自己写布局 UICollectionViewFlow,

不但每一个补充视图和格子视图的位置,随意摆放

补充视图的数量也可以任意分配了,

可这么理解,补充视图的最大数量 = Kind * IndexPath

该布局中,这么处理的

先初始化,

override public func prepare() {
// ...
let headerAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: Element.header.kind,
with: IndexPath(item: 0, section: 0)
)
// ...
let menuAttributes = CustomLayoutAttributes(
forSupplementaryViewOfKind: Element.menu.kind,
with: IndexPath(item: 0, section: 0))
prepareElement(size: menuSize, type: .menu, attributes: menuAttributes)
// ...
}
复制代码

再注册进去,

public override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
switch elementKind {
case Element.header.kind:
return cache[.header]?[indexPath]
case CustomLayout.Element.menu.kind:
return cache[.menu]?[indexPath]
default:
return nil
}
}
复制代码

自定制布局,套路明确,更加灵活

github 链接

微信扫一扫,分享到朋友圈

UICollectionView 自定制布局: 顶部黏住效果

Qt QTableWidget及基本操作

上一篇

三个技巧帮助Docker镜像瘦身

下一篇

你也可能喜欢

UICollectionView 自定制布局: 顶部黏住效果

长按储存图像,分享给朋友