UICollectionViewFlowLayout `targetContentOffset` - 如何在方向改变后保持单元格居中?
UICollectionViewFlowLayout `targetContentOffset` - how to keep cells centered after orientation change?
我有一个自定义 UICollectionViewFlowLayout that centers each cell. To do this, I overrode targetContentOffset
,只要用户抬起手指,集合视图就会调用它。然而,一旦我旋转设备,细胞就会偏离中心——targetContentOffset
没有被调用。
Normally, cells are centered
After rotating the device, cells are no longer centered
注1:旋转后,轻轻划动单元格,它们会弹回中心...
注意 2: 这会打印在我的控制台中:
2021-11-16 21:37:54.979021-0800 TargetContentOffsetTest[30817:356789] [UICollectionViewRecursion] cv == 0x12f02d400 Disabling recursion trigger logging
这是我的代码 (demo repo):
class PagingFlowLayout: UICollectionViewFlowLayout {
var layoutAttributes = [UICollectionViewLayoutAttributes]() /// custom attributes
var contentSize = CGSize.zero /// the scrollable content size of the collection view
override var collectionViewContentSize: CGSize { return contentSize } /// pass scrollable content size back to the collection view
/// pass attributes to the collection view flow layout
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return layoutAttributes[indexPath.item] }
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return layoutAttributes.filter { rect.intersects([=11=].frame) } }
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
let cellWidth = collectionView.bounds.width
let cellHeight = collectionView.bounds.height
var layoutAttributes = [UICollectionViewLayoutAttributes]()
var currentCellOrigin = CGFloat(0) /// used for each cell's origin
for index in 0..<3 { /// hardcoded, but only for now
let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: index, section: 0))
attributes.frame = CGRect(x: currentCellOrigin, y: 0, width: cellWidth, height: cellHeight)
layoutAttributes.append(attributes)
currentCellOrigin += cellWidth
}
self.contentSize = CGSize(width: currentCellOrigin, height: cellHeight)
self.layoutAttributes = layoutAttributes
}
/// center the cell
/// this is called when the finger lifts, but NOT when the device rotates!
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let contentOffset = collectionView?.contentOffset.x ?? 0
let closestPoint = layoutAttributes.min { abs([=11=].frame.origin.x - contentOffset) < abs(.frame.origin.x - contentOffset) }
return closestPoint?.frame.origin ?? proposedContentOffset
}
}
class ViewController: UIViewController, UICollectionViewDataSource {
lazy var collectionView: UICollectionView = {
let flowLayout = PagingFlowLayout()
flowLayout.scrollDirection = .horizontal
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.decelerationRate = .fast
collectionView.dataSource = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.leftAnchor.constraint(equalTo: view.leftAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.rightAnchor.constraint(equalTo: view.rightAnchor)
])
return collectionView
}()
let colors: [UIColor] = [.red, .green, .blue]
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 3 }
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.contentView.backgroundColor = colors[indexPath.item]
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
_ = collectionView /// setup
}
}
如何在边界 change/rotation 事件后调用集合视图 targetContentOffset
?确保我的单元格始终居中的正确方法是什么——有没有我可以使用的自动功能,或者我应该订阅 viewDidLayoutSubviews
back in my view controller and manually call setContentOffset(_:animated:)
?
据我所知,该方法不是在旋转设备时调用,而是在布局更改时调用。意思是,如果您从 Landscape Left
更改为 Landscape Right
,则不会调用委托方法 — 但是,如果您从任何 Landscape
更改为 Portrait
或其他方式,它工作正常.
重要!
为了维护居中的集合视图单元格,添加 targetContentOffset(forProposedContentOffset:)
method. This is called after a rotation, and is not the same as your current targetContentOffset(forProposedContentOffset:withScrollingVelocity:)
方法。在您的最终代码中,您应该使用这些方法中的 both。总结:
targetContentOffset(forProposedContentOffset:)
在布局更改后调用
targetContentOffset(forProposedContentOffset:withScrollingVelocity:)
在用户抬起手指时调用
因此,在您的 PagingFlowLayout
class 中粘贴以下代码:
private var focusedIndexPath: IndexPath?
override func prepare(forAnimatedBoundsChange oldBounds: CGRect) {
super.prepare(forAnimatedBoundsChange: oldBounds)
focusedIndexPath = collectionView?.indexPathsForVisibleItems.first
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
guard
let indexPath = focusedIndexPath,
let attributes = layoutAttributesForItem(at: indexPath),
let collectionView = collectionView
else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
return CGPoint(
x: attributes.frame.origin.x - collectionView.contentInset.left,
y: attributes.frame.origin.y - collectionView.contentInset.top
)
}
override func finalizeAnimatedBoundsChange() {
super.finalizeAnimatedBoundsChange()
focusedIndexPath = nil
}
代码来自:.
我有一个自定义 UICollectionViewFlowLayout that centers each cell. To do this, I overrode targetContentOffset
,只要用户抬起手指,集合视图就会调用它。然而,一旦我旋转设备,细胞就会偏离中心——targetContentOffset
没有被调用。
Normally, cells are centered | After rotating the device, cells are no longer centered |
---|---|
注1:旋转后,轻轻划动单元格,它们会弹回中心...
注意 2: 这会打印在我的控制台中:
2021-11-16 21:37:54.979021-0800 TargetContentOffsetTest[30817:356789] [UICollectionViewRecursion] cv == 0x12f02d400 Disabling recursion trigger logging
这是我的代码 (demo repo):
class PagingFlowLayout: UICollectionViewFlowLayout {
var layoutAttributes = [UICollectionViewLayoutAttributes]() /// custom attributes
var contentSize = CGSize.zero /// the scrollable content size of the collection view
override var collectionViewContentSize: CGSize { return contentSize } /// pass scrollable content size back to the collection view
/// pass attributes to the collection view flow layout
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return layoutAttributes[indexPath.item] }
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return layoutAttributes.filter { rect.intersects([=11=].frame) } }
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
let cellWidth = collectionView.bounds.width
let cellHeight = collectionView.bounds.height
var layoutAttributes = [UICollectionViewLayoutAttributes]()
var currentCellOrigin = CGFloat(0) /// used for each cell's origin
for index in 0..<3 { /// hardcoded, but only for now
let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: index, section: 0))
attributes.frame = CGRect(x: currentCellOrigin, y: 0, width: cellWidth, height: cellHeight)
layoutAttributes.append(attributes)
currentCellOrigin += cellWidth
}
self.contentSize = CGSize(width: currentCellOrigin, height: cellHeight)
self.layoutAttributes = layoutAttributes
}
/// center the cell
/// this is called when the finger lifts, but NOT when the device rotates!
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let contentOffset = collectionView?.contentOffset.x ?? 0
let closestPoint = layoutAttributes.min { abs([=11=].frame.origin.x - contentOffset) < abs(.frame.origin.x - contentOffset) }
return closestPoint?.frame.origin ?? proposedContentOffset
}
}
class ViewController: UIViewController, UICollectionViewDataSource {
lazy var collectionView: UICollectionView = {
let flowLayout = PagingFlowLayout()
flowLayout.scrollDirection = .horizontal
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.decelerationRate = .fast
collectionView.dataSource = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.leftAnchor.constraint(equalTo: view.leftAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.rightAnchor.constraint(equalTo: view.rightAnchor)
])
return collectionView
}()
let colors: [UIColor] = [.red, .green, .blue]
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 3 }
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.contentView.backgroundColor = colors[indexPath.item]
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
_ = collectionView /// setup
}
}
如何在边界 change/rotation 事件后调用集合视图 targetContentOffset
?确保我的单元格始终居中的正确方法是什么——有没有我可以使用的自动功能,或者我应该订阅 viewDidLayoutSubviews
back in my view controller and manually call setContentOffset(_:animated:)
?
据我所知,该方法不是在旋转设备时调用,而是在布局更改时调用。意思是,如果您从 Landscape Left
更改为 Landscape Right
,则不会调用委托方法 — 但是,如果您从任何 Landscape
更改为 Portrait
或其他方式,它工作正常.
重要!
为了维护居中的集合视图单元格,添加 targetContentOffset(forProposedContentOffset:)
method. This is called after a rotation, and is not the same as your current targetContentOffset(forProposedContentOffset:withScrollingVelocity:)
方法。在您的最终代码中,您应该使用这些方法中的 both。总结:
targetContentOffset(forProposedContentOffset:)
在布局更改后调用targetContentOffset(forProposedContentOffset:withScrollingVelocity:)
在用户抬起手指时调用
因此,在您的 PagingFlowLayout
class 中粘贴以下代码:
private var focusedIndexPath: IndexPath?
override func prepare(forAnimatedBoundsChange oldBounds: CGRect) {
super.prepare(forAnimatedBoundsChange: oldBounds)
focusedIndexPath = collectionView?.indexPathsForVisibleItems.first
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
guard
let indexPath = focusedIndexPath,
let attributes = layoutAttributesForItem(at: indexPath),
let collectionView = collectionView
else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
return CGPoint(
x: attributes.frame.origin.x - collectionView.contentInset.left,
y: attributes.frame.origin.y - collectionView.contentInset.top
)
}
override func finalizeAnimatedBoundsChange() {
super.finalizeAnimatedBoundsChange()
focusedIndexPath = nil
}
代码来自:.