具有组合布局的 UICollectionView 消失,框架仍在屏幕上

UICollectionView with Compositional Layout disappears cells with frame still on screen

尝试获得一个“粘性 header”,它将与该部分正交滚动,但只是部分滚动,使尾端暴露直到向后滚动。

Headers 不工作,因为它们不滚动,所以我决定让它作为该部分中的另一个单元格工作。

演示代码: https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views(下载项目)

打开:现代 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 来确定可见性。