设备旋转后的 UICollectionView contentOffset
UICollectionView contentOffset after device rotation
我想做什么:
在 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”?计算offset旋转后offset?但是,当旋转后的 contentSize 可能与对不同单元格大小、单元格间距等的预期不同时,这怎么可能呢?
您可以通过实施 UICollectionViewDelegate method collectionView(_:targetContentOffsetForProposedContentOffset:)
:
During layout updates, or when transitioning between layouts, the collection view calls this method to give you the opportunity to change the proposed content offset to use at the end of the animation. You might return a new value if the layout or animations might cause items to be positioned in a way that is not optimal for your design.
或者,如果您已经继承了 UICollectionViewLayout,您可以在布局子类中实现 targetContentOffset(forProposedContentOffset:)
,这可能更方便。
在该方法的实现中,计算并 return 内容偏移量,这将使中心单元格位于集合视图的中心。如果您的单元格大小是固定的,那么撤消由其他 UI 元素(例如状态栏 and/or 导航栏的消失框架)引起的内容偏移量更改应该是一件简单的事情。
如果您的单元格大小因设备方向而异:
- 在设备旋转之前,通过在集合视图上调用
indexPathForItem(at:)
获取并保存中心单元格的索引路径,将内容偏移量(加上集合视图可见边界高度的一半)作为重点。
- 实施
targetContentOffset(forProposedContentOffset:)
方法之一。通过使用保存的索引路径调用 layoutAttributesForItem(at:)
来检索中心单元格的布局属性。目标内容偏移量是该项目框架的中间(小于集合视图可见边界高度的一半)。
第一步可以在 viewWillTransition(to:with:)
或 scrollViewDidScroll()
中的视图控制器中实现。它也可以在 prepareForAnimatedBoundsChange()
中的布局对象中实现,或者可能在 invalidateLayout(with:)
中检查由设备方向变化引起的边界变化后实现。 (在那种情况下,您还需要确保 shouldInvalidateLayout(forBoundsChange:)
returns true
。)
您可能需要针对内容插入、单元格间距或其他特定于您的应用的事项进行调整。
这是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
}
正如@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
}
}
已接受的答案对我不起作用。
在我的例子中,UICollectionViewDelegate
都不是方法
collectionView(_:targetContentOffsetForProposedContentOffset:)
也没有调用 UICollectionViewLayout
的方法
targetContentOffset(forProposedContentOffset:)
相反,我继承了 UICollectionView
并覆盖了 layoutSubviews
方法:
class CollectionView: UICollectionView {
override func layoutSubviews() {
let old = contentOffset
super.layoutSubviews()
contentOffset = old
}
}
这可能会产生一些副作用,所以在生产中使用前请仔细检查
我想做什么:
在 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”?计算offset旋转后offset?但是,当旋转后的 contentSize 可能与对不同单元格大小、单元格间距等的预期不同时,这怎么可能呢?
您可以通过实施 UICollectionViewDelegate method collectionView(_:targetContentOffsetForProposedContentOffset:)
:
During layout updates, or when transitioning between layouts, the collection view calls this method to give you the opportunity to change the proposed content offset to use at the end of the animation. You might return a new value if the layout or animations might cause items to be positioned in a way that is not optimal for your design.
或者,如果您已经继承了 UICollectionViewLayout,您可以在布局子类中实现 targetContentOffset(forProposedContentOffset:)
,这可能更方便。
在该方法的实现中,计算并 return 内容偏移量,这将使中心单元格位于集合视图的中心。如果您的单元格大小是固定的,那么撤消由其他 UI 元素(例如状态栏 and/or 导航栏的消失框架)引起的内容偏移量更改应该是一件简单的事情。
如果您的单元格大小因设备方向而异:
- 在设备旋转之前,通过在集合视图上调用
indexPathForItem(at:)
获取并保存中心单元格的索引路径,将内容偏移量(加上集合视图可见边界高度的一半)作为重点。 - 实施
targetContentOffset(forProposedContentOffset:)
方法之一。通过使用保存的索引路径调用layoutAttributesForItem(at:)
来检索中心单元格的布局属性。目标内容偏移量是该项目框架的中间(小于集合视图可见边界高度的一半)。
第一步可以在 viewWillTransition(to:with:)
或 scrollViewDidScroll()
中的视图控制器中实现。它也可以在 prepareForAnimatedBoundsChange()
中的布局对象中实现,或者可能在 invalidateLayout(with:)
中检查由设备方向变化引起的边界变化后实现。 (在那种情况下,您还需要确保 shouldInvalidateLayout(forBoundsChange:)
returns true
。)
您可能需要针对内容插入、单元格间距或其他特定于您的应用的事项进行调整。
这是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
}
正如@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
}
}
已接受的答案对我不起作用。
在我的例子中,UICollectionViewDelegate
collectionView(_:targetContentOffsetForProposedContentOffset:)
也没有调用 UICollectionViewLayout
的方法
targetContentOffset(forProposedContentOffset:)
相反,我继承了 UICollectionView
并覆盖了 layoutSubviews
方法:
class CollectionView: UICollectionView {
override func layoutSubviews() {
let old = contentOffset
super.layoutSubviews()
contentOffset = old
}
}
这可能会产生一些副作用,所以在生产中使用前请仔细检查