设备旋转后的 UICollectionView contentOffset

Posted

技术标签:

【中文标题】设备旋转后的 UICollectionView contentOffset【英文标题】:UICollectionView contentOffset after device rotation 【发布时间】:2017-05-29 03:24:05 【问题描述】:

我想做什么:

在 Apple 的 Photo 应用程序中,如果您在滚动到任意偏移量的同时旋转设备,则之前位于中心的相同单元格将在旋转后最终位于中心。

我正在尝试使用 UICollectionView 实现相同的行为。 'indexPathsForVisibleItems' 似乎是这样做的方法......

到目前为止我得到了什么:

以下代码提供了旋转之间的平滑操作,但中心项目的计算似乎已关闭:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 
    super.viewWillTransition(to: size, with: coordinator)

    self.collectionView?.collectionViewLayout.invalidateLayout()

    let middleItem = (self.collectionView?.indexPathsForVisibleItems.count / 2) - 1
    let indexPath = self.collectionView?.indexPathsForVisibleItems[middleItem]

    coordinator.animate(alongsideTransition:  ctx in

        self.collectionView?.layoutIfNeeded()
        self.collectionView?.scrollToItem(at: indexPath!, at: .centeredVertically, animated: false)

    , completion:  _ in


    )

上面的代码有什么问题:

    纵向 -> 横向:与最终位于中心的单元格有些偏离。 人像 -> 风景 -> 人像:完全偏离了人像最初的偏移量。 人像 -> 风景 -> 人像 -> 风景:离得远!

注意事项:

我必须在旋转时使集合视图布局无效,因为 必须重新计算像元大小和间距。 self.collectionView?.layoutIfNeeded() 在动画中 块,使过渡平滑 可能是 scrollToItem 在集合视图之前被调用 布局未最终确定,导致滚动偏移不正确?

替代方法?

不使用“indexPathsForVisibleItems”,而只使用“contentOffset”?计算旋转后偏移量?但是,当旋转后的 contentSize 可能与对不同单元格大小、单元格间距等的预期不同时,这怎么可能呢?

【问题讨论】:

我在尝试使用 scrollToItem 制作自定义动画时总是遇到麻烦。也许尝试将该行放在完成块中,看看您是否真的针对正确的单元格和位置。如果这行得通,我认为您将不得不做一些巧妙的计算来为contentOffset 设置动画,而不是使用scrollToItem 如果我在完成后放置scrollToItem,它会在设备旋转后创建一个辅助滚动动画。当它不仅仅是一个简单的动画时,看起来就感觉不对...... 我只是建议将其作为调试的中间步骤 - 检查问题是否在于您如何计算中心单元格以及 scrollToItem 将如何定位它,或者是否与动画。 我认为 scrollToItem 发生在新布局未完成时。 @Gizmodo 我遇到了同样的问题。如果你能分享一些对你有用的代码,那就太好了。 【参考方案1】:

你可以通过实现UICollectionViewDelegate方法collectionView(_:targetContentOffsetForProposedContentOffset:)来做到这一点:

在布局更新期间,或在布局之间转换时,集合视图会调用此方法,让您有机会更改提议的内容偏移以在动画结束时使用。如果布局或动画可能导致项目以不适合您的设计的方式定位,您可能会返回一个新值。

或者,如果你已经继承了 UICollectionViewLayout,你可以在你的布局子类中实现targetContentOffset(forProposedContentOffset:),这样可能更方便。

在该方法的实现中,计算并返回将导致中心单元格位于集合视图中心的内容偏移量。如果您的单元格大小是固定的,那么撤消由其他 UI 元素(例如状态栏和/或导航栏的消失框架)引起的内容偏移更改应该是一件简单的事情。

如果您的单元格大小因设备方向而异:

    在设备旋转之前,通过在集合视图上调用 indexPathForItem(at:) 获取并保存中心单元格的索引路径,并将内容偏移量(加上集合视图可见边界高度的一半)作为点传递。 实现targetContentOffset(forProposedContentOffset:) 方法之一。通过使用保存的索引路径调用 layoutAttributesForItem(at:) 来检索中心单元格的布局属性。目标内容偏移量是该项目的框架的中间(小于集合视图可见边界高度的一半)。

第一步可以在viewWillTransition(to:with:)scrollViewDidScroll() 中的视图控制器中实现。它也可以在prepareForAnimatedBoundsChange() 中的布局对象中实现,或者在检查由设备方向变化引起的边界变化后可能在invalidateLayout(with:) 中实现。 (您还需要确保 shouldInvalidateLayout(forBoundsChange:) 在这些情况下返回 true。)

您可能需要对内容插入、单元格间距或其他特定于您的应用的事项进行调整。

【讨论】:

很好的答案,很好的解释!它完美无缺。将CGPoints从一个视图转换到另一个视图确实没有什么麻烦,但设法让一切都解决了。干得好! 遗憾的是,当拆分视图更改时,iPad 上不会调用 targetContentOffsetForProposedContentOffset。这意味着无法普遍处理旋转和宽度变化。 "目标内容偏移量是该项目的框架的中间(小于集合视图可见边界高度的一半)。" -- 我在targetContentOffset(forProposedContentOffset:) 中使用attr.frame.origin 代替了水平布局和分页的集合视图。【参考方案2】:

这是jamesk解决方案的实现:

在你的ViewController

fileprivate var prevIndexPathAtCenter: IndexPath?

fileprivate var currentIndexPath: IndexPath? 
    let center = view.convert(collectionView.center, to: collectionView)
    return collectionView.indexPathForItem(at: center)


override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) 
    super.willTransition(to: newCollection, with: coordinator)

    if let indexAtCenter = currentIndexPath 
        prevIndexPathAtCenter = indexAtCenter
    
    collectionView.collectionViewLayout.invalidateLayout()

UICollectionViewDelegateFlowLayout:

func collectionView(_ collectionView: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint 

    guard let oldCenter = prevIndexPathAtCenter else 
        return proposedContentOffset
    

    let attrs =  collectionView.layoutAttributesForItem(at: oldCenter)

    let newOriginForOldIndex = attrs?.frame.origin

    return newOriginForOldIndex ?? proposedContentOffset

【讨论】:

试过了,发现你想使用viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)而不是willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator)(正如最初在jamesk的解决方案中提到的那样),因为有时旋转不会改变特征集合,但会改变大小(例如。 iPad) collectionView.layoutAttributesForItem 仍在使用旧集合视图的布局属性。相反,let attributes = layoutAttributes[prevIndexPathAtCenter.item] 为我工作(其中layoutAttributes 来自this code)。【参考方案3】:

正如@jamesk 所说,我们有不同的方法来做到这一点,其中之一是:

我在ViewController 中有一个collectionView,其默认为UICollectionViewDelegateFlowLayout。 (我不会覆盖它)。

我只有一节。

我想提前向 @jamesk@0rt 的坦克寻求他们的答案。

class ViewController: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout,  UICollectionViewDataSource 

    var currentVisibleIndexPath: IndexPath = IndexPath(row: 0, section: 0)



    // 1. get the current Index of item
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) 

        let center = CGPoint(x: scrollView.contentOffset.x + (scrollView.frame.width / 2), y: (scrollView.frame.height / 2))
        if let ip = self.screenView.indexPathForItem(at: center) 
            currentVisibleIndexPath = ip
        
    

    // 2. if transition happen, invalidateLayout of collectionView, to recalculate layout positions, but this does not change its offset

    override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) 
        super.willTransition(to: newCollection, with: coordinator)

        collectionView.collectionViewLayout.invalidateLayout()
    

    // 3. change offset to make current index to center of collection view
    // copy of @0rt answer
    func collectionView(_ collectionView: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint 

        let attrs =  collectionView.layoutAttributesForItem(at: currentVisibleIndexPath)

        let newOriginForOldIndex = attrs?.frame.origin

        return newOriginForOldIndex ?? proposedContentOffset
    


【讨论】:

【参考方案4】:

接受的答案对我不起作用。 在我的情况下UICollectionViewDelegate

collectionView(_:targetContentOffsetForProposedContentOffset:)

也没有调用UICollectionViewLayout的方法

targetContentOffset(forProposedContentOffset:)

相反,我继承自 UICollectionView 并覆盖了 layoutSubviews 方法:

class CollectionView: UICollectionView 
    override func layoutSubviews() 
        let old = contentOffset
        super.layoutSubviews()
        contentOffset = old
    

这可能会导致一些副作用,所以在生产中使用前请仔细检查

【讨论】:

以上是关于设备旋转后的 UICollectionView contentOffset的主要内容,如果未能解决你的问题,请参考以下文章

UICollectionView - 在设备旋转上调整单元格大小 - Swift

Swift:如何在设备旋转后刷新UICollectionView布局

UICollectionView 在旋转时重新排列单元格

旋转后 UICollectionView 单元格不水平

如何在自定义 UICollectionView 的旋转后触发 drawrect?

屏幕旋转后如何在 UICollectionView 中跟踪单元格的移动