设备轮换后的UICollectionView contentOffset

时间:2017-01-13 17:12:56

标签: ios swift uiscrollview uicollectionview uicollectionviewlayout

我想做什么:

在Apple的Photo App上,如果在滚动到任意偏移的同时旋转设备,预先在中心的同一个单元将在旋转后终止于中心。

我试图用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. 肖像 - >风景 - >肖像 - >景观:离开!
  4. 说明:

    • 我必须在旋转时使集合视图布局无效 必须重新计算单元大小和间距。
    • self.collectionView?.layoutIfNeeded()在动画内部
      阻止,使过渡顺利
    • 可以在集合视图之前调用 scrollToItem 布局未最终确定,导致滚动偏移不正确?

    替代方法?

    不要使用' indexPathsForVisibleItems '而只使用' contentOffset '?计算偏移后的偏移?但是,当旋转后的contentSize与不同的单元尺寸,单元间距等的预期不同时,这怎么可能呢?

4 个答案:

答案 0 :(得分:26)

您可以通过实施UICollectionViewDelegate方法collectionView(_:targetContentOffsetForProposedContentOffset:)

来实现此目的
  

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

或者,如果您已经将UICollectionViewLayout子类化,则可以在布局子类中实现targetContentOffset(forProposedContentOffset:),这可能更方便。

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

如果您的手机尺寸因设备方向而异:

  1. 在设备轮换之前,通过在集合视图上调用indexPathForItem(at:)来获取并保存中心单元的索引路径,将内容偏移量(加上集合视图的可见边界的高度的一半)作为点传递
  2. 实施targetContentOffset(forProposedContentOffset:)方法之一。通过使用保存的索引路径调用layoutAttributesForItem(at:)来检索中心单元的布局属性。目标内容偏移量是该项目框架的中间位置(减去集合视图可见边界高度的一半)。
  3. 第一步可以在viewWillTransition(to:with:)scrollViewDidScroll()中的视图控制器中实施。在检查由设备方向更改引起的边界更改后,它也可以在prepareForAnimatedBoundsChange()中的布局对象中实现,也可以在invalidateLayout(with:)中实现。 (在这种情况下,您还需要确保shouldInvalidateLayout(forBoundsChange:)返回true。)

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

答案 1 :(得分:3)

这是swift 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
}

答案 2 :(得分:0)

正如@jamesk所说,我们有不同的处理方式,其中之一是:

我在collectionView中有一个ViewController,其默认值为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
    }
}

答案 3 :(得分:0)

接受的答案对我不起作用。 就我而言,{ "data": { "repository": { "issue": { "title": "fix dead path in startover", "projectCards": { "nodes": [ { "createdAt": "2017-08-28T05:47:49Z", "column": { "name": "Done" }, "project": { "name": "Startover", "body": "Show epg / caipy events + startover modes" } } ] } } } } }

的任何一种方法

UICollectionViewDelegate

也没有调用collectionView(_:targetContentOffsetForProposedContentOffset:)的方法

UICollectionViewLayout

相反,我继承了targetContentOffset(forProposedContentOffset:)并取代了UICollectionView方法:

layoutSubviews

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