设备旋转后的 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布局