【问题标题】:UICollectionView contentOffset after device rotation设备旋转后的 UICollectionView contentOffset
【发布时间】: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


    })
}

上面的代码有什么问题:

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

注意事项:

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

替代方法?

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

【问题讨论】:

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

标签: ios swift uiscrollview uicollectionview uicollectionviewlayout


【解决方案1】:

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

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

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

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

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

  1. 在设备旋转之前,通过在集合视图上调用 indexPathForItem(at:) 获取并保存中心单元格的索引路径,并将内容偏移量(加上集合视图可见边界高度的一半)作为点传递。
  2. 实现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
        }
    }
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-08-21
      • 2012-11-09
      • 2016-05-15
      • 2023-03-22
      • 1970-01-01
      • 2015-12-20
      • 2012-05-18
      相关资源
      最近更新 更多