具有组合布局的 UICollectionView 消失,框架仍在屏幕上
UICollectionView with Compositional Layout disappears cells with frame still on screen
尝试获得一个“粘性 header”,它将与该部分正交滚动,但只是部分滚动,使尾端暴露直到向后滚动。
Headers 不工作,因为它们不滚动,所以我决定让它作为该部分中的另一个单元格工作。
打开:现代 Collection 视图 > 组合布局 > 高级布局视图控制器 > OrthogonalScrollBehaviorViewController.swift
替换
func createLayout() -> UICollectionViewLayout {
...
}
有
func createLayout() -> UICollectionViewLayout {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 20
let layout = UICollectionViewCompositionalLayout(sectionProvider: {
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionKind = SectionKind(rawValue: sectionIndex) else { fatalError("unknown section kind") }
let leadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(0.5)))
let orthogonallyScrolls = sectionKind.orthogonalScrollingBehavior() != .none
let containerGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.4)),
subitems: [leadingItem])
let section = NSCollectionLayoutSection(group: containerGroup)
section.orthogonalScrollingBehavior = sectionKind.orthogonalScrollingBehavior()
section.visibleItemsInvalidationHandler = { (items, offset, env) in
let buffer: CGFloat = 50
for item in items {
if item.indexPath.item == 0 {
item.zIndex = 25
let w = item.frame.width
if offset.x >= (w - buffer) {
item.transform = CGAffineTransform(translationX: offset.x - (w - buffer), y: 0)
} else {
item.transform = .identity
}
} else {
item.zIndex = -item.indexPath.item
}
}
}
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44)),
elementKind: OrthogonalScrollBehaviorViewController.headerElementKind,
alignment: .top)
section.boundarySupplementaryItems = [sectionHeader]
return section
}, configuration: config)
return layout
}
如您所见,即使在屏幕上仍然清晰可见,它仍然可以完美地工作到单元格现在根据其边界处于“屏幕外”的位置。
我也尝试过使用自定义 UICollectionViewCompositionalLayout 确保 IndexPath 在 layoutAttributesForElements(in rect: CGRect) 中有一个属性,结果完全相同:一旦 'bounds' 在屏幕外,即使框架非常清楚地仍在屏幕上,单元格也会被删除。
此外,行
item.transform = CGAffineTransform(translationX: offset.x - (w - buffer), y: 0)
可以是功能等效的任何东西(我唯一尝试过的另一件事是移动中心)但结果是一样的。
您可以使用 header、header 中的 contentView + 偏移量对其进行破解,覆盖 header 上的 hitTest,section.visibleItemsInvalidationHandler 以更新偏移量,以及在包含 collectionView 的视图上添加一个 tapGesture 以允许点击 header(因为它的 hitTest 需要始终为 nil)
Header 看起来像这样:
class MagicHeader: UICollectionReusableView {
static let reuseIdentifier = "text-cell-reuse-identifier3"
static let elementKind = "magic-header-kind"
var contentView: UIView!
var leadingC: NSLayoutConstraint!
var offset: CGFloat = 0 {
didSet {
leadingC.constant = -offset
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return nil
}
override init(frame: CGRect) {
super.init(frame: frame)
contentView = UIView(frame: frame)
addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = false
leadingC = contentView.leadingAnchor.constraint(equalTo: self.leadingAnchor)
NSLayoutConstraint.activate([
contentView.widthAnchor.constraint(equalTo: self.widthAnchor),
contentView.heightAnchor.constraint(equalTo: self.heightAnchor),
contentView.topAnchor.constraint(equalTo: self.topAnchor),
leadingC
])
//configure() -- This is from the example code above, not 'necessary' for solution
}
// Other code here
}
栏目提供者:
let headerW: CGFloat = 200
let headerSize = NSCollectionLayoutSize(widthDimension: .absolute(headerW), heightDimension: .absolute(80))
let header = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerSize,
elementKind: MagicHeader.elementKind,
alignment: .leading,
absoluteOffset: CGPoint(x: -headerW, y: 0))
header.pinToVisibleBounds = true
header.zIndex = 2
let contribItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(80), heightDimension: .absolute(80)))
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(80),
heightDimension: .absolute(80)),
subitems: [contribItem])
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [header]
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: headerW, bottom: 0, trailing: 0)
section.visibleItemsInvalidationHandler = { (items, offset, env) in
for item in items {
if item.representedElementKind == MagicHeader.elementKind {
guard let headerElement = self.collectionView.supplementaryView(forElementKind: item.representedElementKind!, at: item.indexPath) as? MagicHeader
}
let buffer: CGFloat = 20
if offset.x >= (headerW - buffer) {
headerElement.offset = (headerW - buffer)
} else {
headerElement.offset = offset.x
}
}
}
}
section.orthogonalScrollingBehavior = .continuous
return section
然后最后
class OrthogonalScrollBehaviorViewController {
-- code
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Orthogonal Section Behaviors"
configureHierarchy()
configureDataSource()
kingTap = HeaderTapGR(target: self, action: #selector(OrthogonalScrollBehaviorViewController.handleTap(_:)))
view.addGestureRecognizer(kingTap)
kingTap.delegate = self
}
}
extension OrthogonalScrollBehaviorViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
for subv in collectionView.subviews {
if let header = subv as? MagicHeader {
let pt = gestureRecognizer.location(in: header)
if header.contentView.frame.contains(pt) {
if let headerGR = gestureRecognizer as? HeaderTapGR {
headerGR.header = header
}
return true
}
}
}
return false
}
}
所以基本上 'visibleItems' 部分计算偏移量,更新 header 单元格上的偏移量。 MagicHeader 从未实际移动,它只是移动内部 contentView 使其看起来像在移动。
因为它没有移动,所以它会吞噬触摸事件,所以你必须告诉它“永远不要被触摸”。如果你有 hitTest 'hit' 当触摸事件在 'contentView' 的边界时它不会滚动,所以这取决于你。如果你没有它 return nil,那么敲击可能更容易。
如果它始终为 nil,则它永远不会接收点击事件,因此如果您希望能够点击单元格,则需要在顶部查找触摸事件(VC 包含 collectionV)和然后仅当它位于 contentView 的边界时触发该点击,否则您将阻止 'collectionView(_, didSelectItemAt:...)' 触发。
都是因为有人使用 cell.bounds 而不是 cell.frame 来确定可见性。
尝试获得一个“粘性 header”,它将与该部分正交滚动,但只是部分滚动,使尾端暴露直到向后滚动。
Headers 不工作,因为它们不滚动,所以我决定让它作为该部分中的另一个单元格工作。
打开:现代 Collection 视图 > 组合布局 > 高级布局视图控制器 > OrthogonalScrollBehaviorViewController.swift
替换
func createLayout() -> UICollectionViewLayout {
...
}
有
func createLayout() -> UICollectionViewLayout {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 20
let layout = UICollectionViewCompositionalLayout(sectionProvider: {
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionKind = SectionKind(rawValue: sectionIndex) else { fatalError("unknown section kind") }
let leadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(0.5)))
let orthogonallyScrolls = sectionKind.orthogonalScrollingBehavior() != .none
let containerGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.4)),
subitems: [leadingItem])
let section = NSCollectionLayoutSection(group: containerGroup)
section.orthogonalScrollingBehavior = sectionKind.orthogonalScrollingBehavior()
section.visibleItemsInvalidationHandler = { (items, offset, env) in
let buffer: CGFloat = 50
for item in items {
if item.indexPath.item == 0 {
item.zIndex = 25
let w = item.frame.width
if offset.x >= (w - buffer) {
item.transform = CGAffineTransform(translationX: offset.x - (w - buffer), y: 0)
} else {
item.transform = .identity
}
} else {
item.zIndex = -item.indexPath.item
}
}
}
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44)),
elementKind: OrthogonalScrollBehaviorViewController.headerElementKind,
alignment: .top)
section.boundarySupplementaryItems = [sectionHeader]
return section
}, configuration: config)
return layout
}
如您所见,即使在屏幕上仍然清晰可见,它仍然可以完美地工作到单元格现在根据其边界处于“屏幕外”的位置。
我也尝试过使用自定义 UICollectionViewCompositionalLayout 确保 IndexPath 在 layoutAttributesForElements(in rect: CGRect) 中有一个属性,结果完全相同:一旦 'bounds' 在屏幕外,即使框架非常清楚地仍在屏幕上,单元格也会被删除。
此外,行
item.transform = CGAffineTransform(translationX: offset.x - (w - buffer), y: 0)
可以是功能等效的任何东西(我唯一尝试过的另一件事是移动中心)但结果是一样的。
您可以使用 header、header 中的 contentView + 偏移量对其进行破解,覆盖 header 上的 hitTest,section.visibleItemsInvalidationHandler 以更新偏移量,以及在包含 collectionView 的视图上添加一个 tapGesture 以允许点击 header(因为它的 hitTest 需要始终为 nil)
Header 看起来像这样:
class MagicHeader: UICollectionReusableView {
static let reuseIdentifier = "text-cell-reuse-identifier3"
static let elementKind = "magic-header-kind"
var contentView: UIView!
var leadingC: NSLayoutConstraint!
var offset: CGFloat = 0 {
didSet {
leadingC.constant = -offset
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return nil
}
override init(frame: CGRect) {
super.init(frame: frame)
contentView = UIView(frame: frame)
addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = false
leadingC = contentView.leadingAnchor.constraint(equalTo: self.leadingAnchor)
NSLayoutConstraint.activate([
contentView.widthAnchor.constraint(equalTo: self.widthAnchor),
contentView.heightAnchor.constraint(equalTo: self.heightAnchor),
contentView.topAnchor.constraint(equalTo: self.topAnchor),
leadingC
])
//configure() -- This is from the example code above, not 'necessary' for solution
}
// Other code here
}
栏目提供者:
let headerW: CGFloat = 200
let headerSize = NSCollectionLayoutSize(widthDimension: .absolute(headerW), heightDimension: .absolute(80))
let header = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerSize,
elementKind: MagicHeader.elementKind,
alignment: .leading,
absoluteOffset: CGPoint(x: -headerW, y: 0))
header.pinToVisibleBounds = true
header.zIndex = 2
let contribItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(80), heightDimension: .absolute(80)))
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(80),
heightDimension: .absolute(80)),
subitems: [contribItem])
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [header]
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: headerW, bottom: 0, trailing: 0)
section.visibleItemsInvalidationHandler = { (items, offset, env) in
for item in items {
if item.representedElementKind == MagicHeader.elementKind {
guard let headerElement = self.collectionView.supplementaryView(forElementKind: item.representedElementKind!, at: item.indexPath) as? MagicHeader
}
let buffer: CGFloat = 20
if offset.x >= (headerW - buffer) {
headerElement.offset = (headerW - buffer)
} else {
headerElement.offset = offset.x
}
}
}
}
section.orthogonalScrollingBehavior = .continuous
return section
然后最后
class OrthogonalScrollBehaviorViewController {
-- code
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Orthogonal Section Behaviors"
configureHierarchy()
configureDataSource()
kingTap = HeaderTapGR(target: self, action: #selector(OrthogonalScrollBehaviorViewController.handleTap(_:)))
view.addGestureRecognizer(kingTap)
kingTap.delegate = self
}
}
extension OrthogonalScrollBehaviorViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
for subv in collectionView.subviews {
if let header = subv as? MagicHeader {
let pt = gestureRecognizer.location(in: header)
if header.contentView.frame.contains(pt) {
if let headerGR = gestureRecognizer as? HeaderTapGR {
headerGR.header = header
}
return true
}
}
}
return false
}
}
所以基本上 'visibleItems' 部分计算偏移量,更新 header 单元格上的偏移量。 MagicHeader 从未实际移动,它只是移动内部 contentView 使其看起来像在移动。
因为它没有移动,所以它会吞噬触摸事件,所以你必须告诉它“永远不要被触摸”。如果你有 hitTest 'hit' 当触摸事件在 'contentView' 的边界时它不会滚动,所以这取决于你。如果你没有它 return nil,那么敲击可能更容易。
如果它始终为 nil,则它永远不会接收点击事件,因此如果您希望能够点击单元格,则需要在顶部查找触摸事件(VC 包含 collectionV)和然后仅当它位于 contentView 的边界时触发该点击,否则您将阻止 'collectionView(_, didSelectItemAt:...)' 触发。
都是因为有人使用 cell.bounds 而不是 cell.frame 来确定可见性。