UICollectionViewFlowLayout `targetContentOffset` - 如何在方向改变后保持单元格居中?

UICollectionViewFlowLayout `targetContentOffset` - how to keep cells centered after orientation change?

我有一个自定义 UICollectionViewFlowLayout that centers each cell. To do this, I overrode targetContentOffset,只要用户抬起手指,集合视图就会调用它。然而,一旦我旋转设备,细胞就会偏离中心——targetContentOffset 没有被调用。

Normally, cells are centered After rotating the device, cells are no longer centered

注1:旋转后,轻轻划动单元格,它们会弹回中心...
注意 2: 这会打印在我的控制台中:

2021-11-16 21:37:54.979021-0800 TargetContentOffsetTest[30817:356789] [UICollectionViewRecursion] cv == 0x12f02d400 Disabling recursion trigger logging

这是我的代码 (demo repo):

class PagingFlowLayout: UICollectionViewFlowLayout {
    var layoutAttributes = [UICollectionViewLayoutAttributes]() /// custom attributes
    var contentSize = CGSize.zero /// the scrollable content size of the collection view
    override var collectionViewContentSize: CGSize { return contentSize } /// pass scrollable content size back to the collection view
    
    /// pass attributes to the collection view flow layout
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return layoutAttributes[indexPath.item] }
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return layoutAttributes.filter { rect.intersects([=11=].frame) } }
    override func prepare() {
        super.prepare()
        
        guard let collectionView = collectionView else { return }
        let cellWidth = collectionView.bounds.width
        let cellHeight = collectionView.bounds.height
        
        var layoutAttributes = [UICollectionViewLayoutAttributes]()
        var currentCellOrigin = CGFloat(0) /// used for each cell's origin
        
        for index in 0..<3 { /// hardcoded, but only for now
            let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: index, section: 0))
            attributes.frame = CGRect(x: currentCellOrigin, y: 0, width: cellWidth, height: cellHeight)
            layoutAttributes.append(attributes)
            currentCellOrigin += cellWidth
        }
        
        self.contentSize = CGSize(width: currentCellOrigin, height: cellHeight)
        self.layoutAttributes = layoutAttributes
    }
    
    /// center the cell
    /// this is called when the finger lifts, but NOT when the device rotates!
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        let contentOffset = collectionView?.contentOffset.x ?? 0
        let closestPoint = layoutAttributes.min { abs([=11=].frame.origin.x - contentOffset) < abs(.frame.origin.x - contentOffset) }
        return closestPoint?.frame.origin ?? proposedContentOffset
    }
}

class ViewController: UIViewController, UICollectionViewDataSource {
    lazy var collectionView: UICollectionView = {
        let flowLayout = PagingFlowLayout()
        flowLayout.scrollDirection = .horizontal
        
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.decelerationRate = .fast
        collectionView.dataSource = self
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
        view.addSubview(collectionView)
        
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.leftAnchor.constraint(equalTo: view.leftAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            collectionView.rightAnchor.constraint(equalTo: view.rightAnchor)
        ])
        
        return collectionView
    }()
    
    let colors: [UIColor] = [.red, .green, .blue]
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 3 }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
        cell.contentView.backgroundColor = colors[indexPath.item]
        return cell
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        _ = collectionView /// setup
    }
}

如何在边界 change/rotation 事件后调用集合视图 targetContentOffset?确保我的单元格始终居中的正确方法是什么——有没有我可以使用的自动功能,或者我应该订阅 viewDidLayoutSubviews back in my view controller and manually call setContentOffset(_:animated:)

据我所知,该方法不是在旋转设备时调用,而是在布局更改时调用。意思是,如果您从 Landscape Left 更改为 Landscape Right,则不会调用委托方法 — 但是,如果您从任何 Landscape 更改为 Portrait 或其他方式,它工作正常.

重要!

为了维护居中的集合视图单元格,添加 targetContentOffset(forProposedContentOffset:) method. This is called after a rotation, and is not the same as your current targetContentOffset(forProposedContentOffset:withScrollingVelocity:) 方法。在您的最终代码中,您应该使用这些方法中的 both。总结:

  • targetContentOffset(forProposedContentOffset:) 在布局更改后调用
  • targetContentOffset(forProposedContentOffset:withScrollingVelocity:) 在用户抬起手指时调用

因此,在您的 PagingFlowLayout class 中粘贴以下代码:

private var focusedIndexPath: IndexPath?

override func prepare(forAnimatedBoundsChange oldBounds: CGRect) {
    super.prepare(forAnimatedBoundsChange: oldBounds)
    focusedIndexPath = collectionView?.indexPathsForVisibleItems.first
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
    guard
        let indexPath = focusedIndexPath,
        let attributes = layoutAttributesForItem(at: indexPath),
        let collectionView = collectionView
    else {
        return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
    }
    return CGPoint(
        x: attributes.frame.origin.x - collectionView.contentInset.left,
        y: attributes.frame.origin.y - collectionView.contentInset.top
    )
}
override func finalizeAnimatedBoundsChange() {
    super.finalizeAnimatedBoundsChange()
    focusedIndexPath = nil
}

代码来自:.