ios - 如何使用自定义 UICollectionViewLayout 更改 UICollectionView 上的选定项目
问题描述
我有一个带有自定义 collectionViewLayout 的 UICollectionView,就像卡片轮播一样。
触摸时它可以正常工作,但是在尝试从代码中更改所选卡片时它不会正确对齐。
图片显示了问题:
我首先尝试使用 scrollToItem,以及 setContentOffset 的一些变体,例如:
collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
collectionView.setContentOffset(CGPoint(x: (attri!.frame.origin.x - sectionLeftInset), y: 0), animated: true)
collectionView.setContentOffset(CGPoint(x: CGFloat((213 * currentPage)), y: 0), animated: true)
但它永远不适合。
编辑:添加 UICollectionViewLayout 的代码
import UIKit
class CarouselLayout: UICollectionViewLayout {
// MARK: - Private Properties
private var cachedItemsAttributes: [IndexPath: UICollectionViewLayoutAttributes] = [:]
private let itemSize = CGSize(width: 213, height: 135)
private let spacing: CGFloat = 30
private let spacingWhenFocused: CGFloat = 60
// MARK: - Public Properties
override var collectionViewContentSize: CGSize {
let leftmostEdge = cachedItemsAttributes.values.map { $0.frame.minX }.min() ?? 0
let rightmostEdge = cachedItemsAttributes.values.map { $0.frame.maxX }.max() ?? 0
return CGSize(width: rightmostEdge - leftmostEdge, height: itemSize.height)
}
private var continuousFocusedIndex: CGFloat {
guard let collectionView = collectionView else { return 0 }
let offset = collectionView.bounds.width / 2 + collectionView.contentOffset.x - itemSize.width / 2
return offset / (itemSize.width + spacing)
}
// MARK: - Public Methods
override open func prepare() {
super.prepare()
guard let collectionView = self.collectionView else { return }
updateInsets()
guard cachedItemsAttributes.isEmpty else { return }
collectionView.decelerationRate = UIScrollView.DecelerationRate.fast
let itemsCount = collectionView.numberOfItems(inSection: 0)
for item in 0..<itemsCount {
let indexPath = IndexPath(item: item, section: 0)
cachedItemsAttributes[indexPath] = createAttributesForItem(at: indexPath)
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cachedItemsAttributes
.map { $0.value }
.filter { $0.frame.intersects(rect) }
.map { self.shiftedAttributes(from: $0) }
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let collectionViewMidX: CGFloat = collectionView.bounds.size.width / 2
guard let closestAttribute = findClosestAttributes(toXPosition: proposedContentOffset.x + collectionViewMidX) else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
return CGPoint(x: closestAttribute.center.x - collectionViewMidX, y: proposedContentOffset.y)
}
// MARK: - Invalidate layout
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if newBounds.size != collectionView?.bounds.size { cachedItemsAttributes.removeAll() }
return true
}
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
if context.invalidateDataSourceCounts { cachedItemsAttributes.removeAll() }
super.invalidateLayout(with: context)
}
// MARK: - Items
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let attributes = cachedItemsAttributes[indexPath] else { fatalError("No attributes cached") }
return shiftedAttributes(from: attributes)
}
private func createAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
guard let collectionView = collectionView else { return nil }
attributes.frame.size = itemSize
attributes.frame.origin.y = (collectionView.bounds.height - itemSize.height) / 2
attributes.frame.origin.x = CGFloat(indexPath.item) * (itemSize.width + spacing)
return attributes
}
private func shiftedAttributes(from attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let attributes = attributes.copy() as? UICollectionViewLayoutAttributes else { fatalError("Couldn't copy attributes") }
let roundedFocusedIndex = round(continuousFocusedIndex)
guard attributes.indexPath.item != Int(roundedFocusedIndex) else { return attributes }
let shiftArea = (roundedFocusedIndex - 0.5)...(roundedFocusedIndex + 0.5)
let distanceToClosestIdentityPoint = min(abs(continuousFocusedIndex - shiftArea.lowerBound), abs(continuousFocusedIndex - shiftArea.upperBound))
let normalizedShiftFactor = distanceToClosestIdentityPoint * 2
let translation = (spacingWhenFocused - spacing) * normalizedShiftFactor
let translationDirection: CGFloat = attributes.indexPath.item < Int(roundedFocusedIndex) ? -1 : 1
attributes.transform = CGAffineTransform(translationX: translationDirection * translation, y: 0)
return attributes
}
// MARK: - Private Methods
private func findClosestAttributes(toXPosition xPosition: CGFloat) -> UICollectionViewLayoutAttributes? {
guard let collectionView = collectionView else { return nil }
let searchRect = CGRect(
x: xPosition - collectionView.bounds.width, y: collectionView.bounds.minY,
width: collectionView.bounds.width * 2, height: collectionView.bounds.height
)
return layoutAttributesForElements(in: searchRect)?.min(by: { abs($0.center.x - xPosition) < abs($1.center.x - xPosition) })
}
private func updateInsets() {
guard let collectionView = collectionView else { return }
collectionView.contentInset.left = (collectionView.bounds.size.width - itemSize.width) / 2
collectionView.contentInset.right = (collectionView.bounds.size.width - itemSize.width) / 2
}
}
解决方案
推荐阅读
- javascript - 如何从个人用户那里获取文本
- python - 使用 setattr() 设置 None 或空白值
- ios - NavigationLink 中的弹出框无法正常工作
- sql - 在 group by 中使用 datetime 日期并在单个 SELECT 中使用 order by 与使用子查询
- apache-spark - 读取 orc 文件时,最新版本的 Hudi(0.7.0、0.6.0)是否可以与 Spark 2.3.0 一起使用?
- excel - 图表并非每次都生成并且有额外的图例
- wix - WIX 如何计算安装所需的空间
- python - 将标准输出(进度条)带到 QTextBrowser 的最佳方法
- mysql - Mysql 错误 BLOB、TEXT、GEOMETRY 或 JSON 列不能有默认值
- openstack - kolla-ansible opnestack,cinder.exception.NoValidBackend:找不到有效的后端