UICollectionview 自定义布局:某些索引比其他索引具有更多可见单元格?
UICollectionview custom layout: some indexes have more visible cells than others?
我遇到了一个奇怪的问题,我似乎无法弄清楚或无法在线找到任何信息。
所以我试图用 UICollectionView
和自定义 UICollectionViewFlowlayout
.
复制 Shazam 发现 UI
到目前为止一切都运行良好,但是 在添加 "card stack" 效果时我(或者更确切地说是实施它的人)注意到似乎有一个奇怪的问题在某些情况下(或者更确切地说,当特定索引可见时,在示例中是第 5、9 行)将有 4 个可见单元格而不是 3 个。 我的猜测是这与单元格重用有关,但是我不确定为什么要这样做。我查看了各个单元格的尺寸,它们似乎都相同,所以并不是单元格的大小不同。
有人知道为什么会发生这种情况吗?非常感谢任何帮助或建议。
我将在下面添加自定义流程布局的代码片段和屏幕截图。
你可以 download the full project here, or alternatively, check out the PR on Github.
这是视觉比较:
自定义流程布局的源代码:
import UIKit
/// Custom `UICollectionViewFlowLayout` that provides the flowlayout information like paging and `CardCell` movements.
internal class VerticalCardSwiperFlowLayout: UICollectionViewFlowLayout {
/// This property sets the amount of scaling for the first item.
internal var firstItemTransform: CGFloat?
/// This property enables paging per card. The default value is true.
internal var isPagingEnabled: Bool = true
/// Stores the height of a CardCell.
internal var cellHeight: CGFloat!
internal override func prepare() {
super.prepare()
assert(collectionView!.numberOfSections == 1, "Number of sections should always be 1.")
assert(collectionView!.isPagingEnabled == false, "Paging on the collectionview itself should never be enabled. To enable cell paging, use the isPagingEnabled property of the VerticalCardSwiperFlowLayout instead.")
}
internal override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let items = NSMutableArray (array: super.layoutAttributesForElements(in: rect)!, copyItems: true)
items.enumerateObjects(using: { (object, index, stop) -> Void in
let attributes = object as! UICollectionViewLayoutAttributes
self.updateCellAttributes(attributes)
})
return items as? [UICollectionViewLayoutAttributes]
}
// We invalidate the layout when a "bounds change" happens, for example when we scale the top cell. This forces a layout update on the flowlayout.
internal override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
// Cell paging
internal override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// If the property `isPagingEnabled` is set to false, we don't enable paging and thus return the current contentoffset.
guard isPagingEnabled else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}
// Page height used for estimating and calculating paging.
let pageHeight = cellHeight + self.minimumLineSpacing
// Make an estimation of the current page position.
let approximatePage = self.collectionView!.contentOffset.y/pageHeight
// Determine the current page based on velocity.
let currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage)
// Create custom flickVelocity.
let flickVelocity = velocity.y * 0.4
// Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
// Calculate newVerticalOffset.
let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - self.collectionView!.contentInset.top
return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}
internal override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// make sure the zIndex of the next card is higher than the one we're swiping away.
let nextIndexPath = IndexPath(row: itemIndexPath.row + 1, section: itemIndexPath.section)
let nextAttr = self.layoutAttributesForItem(at: nextIndexPath)
nextAttr?.zIndex = nextIndexPath.row
// attributes for swiping card away
let attr = self.layoutAttributesForItem(at: itemIndexPath)
return attr
}
/**
Updates the attributes.
Here manipulate the zIndex of the cards here, calculate the positions and do the animations.
- parameter attributes: The attributes we're updating.
*/
fileprivate func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes) {
let minY = collectionView!.bounds.minY + collectionView!.contentInset.top
let maxY = attributes.frame.origin.y
let finalY = max(minY, maxY)
var origin = attributes.frame.origin
let deltaY = (finalY - origin.y) / attributes.frame.height
let translationScale = CGFloat((attributes.zIndex + 1) * 10)
// create stacked effect (cards visible at bottom
if let itemTransform = firstItemTransform {
let scale = 1 - deltaY * itemTransform
var t = CGAffineTransform.identity
t = t.scaledBy(x: scale, y: 1)
t = t.translatedBy(x: 0, y: (translationScale + deltaY * translationScale))
attributes.transform = t
}
origin.x = (self.collectionView?.frame.width)! / 2 - attributes.frame.width / 2 - (self.collectionView?.contentInset.left)!
origin.y = finalY
attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
attributes.zIndex = attributes.indexPath.row
}
}
编辑 1: 作为额外说明,最终结果应该如下所示:
编辑 2:
似乎每滚动 4-5 张卡片就会发生一次测试。
您有一个继承自流式布局的布局。您覆盖了 layoutAttributesForElements(in rect:)
,从 super.layoutAttributesForElements
中获取所有元素,然后为每个元素修改方法 updateCellAttributes
.
中的属性
这通常是创建流布局子类的好方法。 UICollectionViewFlowLayout
正在做大部分艰苦的工作——弄清楚每个元素应该在哪里,哪些元素在 rect 中,它们的基本属性是什么,它们应该如何填充等等,你可以只修改一些"hard" 工作完成后的属性。当您添加旋转或不透明度或其他一些不会更改项目位置的功能时,这会很好地工作。
当您使用 updateCellAttributes
更改项目框架时,您会遇到麻烦。然后,您可能会遇到这样一种情况,即您的单元格对于常规流布局根本不会出现在框架中,但现在由于您的修改应该出现。因此 super.layoutAttributesForElements(in rect: CGRect) 根本没有返回该属性,因此它们根本不显示。您还可能遇到相反的问题,即根本不应该在框架中的单元格在视图中,但以用户无法看到的方式转换。
你没有充分解释你想要做什么效果,以及为什么你认为从 UIFlowLayout 继承对我来说是正确的,无法具体帮助你。但我希望我已经给了你足够的信息,你可以自己找到问题所在。
错误在于您如何为每个属性定义 frame.origin.y
。更具体地说,您在 minY
中保留的值决定了您保留在屏幕上的单元格数量。 (我将编辑此答案并稍后解释更多,但现在,请尝试替换以下代码)
var minY = collectionView!.bounds.minY + collectionView!.contentInset.top
let maxY = attributes.frame.origin.y
if minY > attributes.frame.origin.y + attributes.bounds.height + minimumLineSpacing + collectionView!.contentInset.top {
minY = 0
}
我遇到了一个奇怪的问题,我似乎无法弄清楚或无法在线找到任何信息。
所以我试图用 UICollectionView
和自定义 UICollectionViewFlowlayout
.
到目前为止一切都运行良好,但是 在添加 "card stack" 效果时我(或者更确切地说是实施它的人)注意到似乎有一个奇怪的问题在某些情况下(或者更确切地说,当特定索引可见时,在示例中是第 5、9 行)将有 4 个可见单元格而不是 3 个。 我的猜测是这与单元格重用有关,但是我不确定为什么要这样做。我查看了各个单元格的尺寸,它们似乎都相同,所以并不是单元格的大小不同。
有人知道为什么会发生这种情况吗?非常感谢任何帮助或建议。
我将在下面添加自定义流程布局的代码片段和屏幕截图。 你可以 download the full project here, or alternatively, check out the PR on Github.
这是视觉比较:
自定义流程布局的源代码:
import UIKit
/// Custom `UICollectionViewFlowLayout` that provides the flowlayout information like paging and `CardCell` movements.
internal class VerticalCardSwiperFlowLayout: UICollectionViewFlowLayout {
/// This property sets the amount of scaling for the first item.
internal var firstItemTransform: CGFloat?
/// This property enables paging per card. The default value is true.
internal var isPagingEnabled: Bool = true
/// Stores the height of a CardCell.
internal var cellHeight: CGFloat!
internal override func prepare() {
super.prepare()
assert(collectionView!.numberOfSections == 1, "Number of sections should always be 1.")
assert(collectionView!.isPagingEnabled == false, "Paging on the collectionview itself should never be enabled. To enable cell paging, use the isPagingEnabled property of the VerticalCardSwiperFlowLayout instead.")
}
internal override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let items = NSMutableArray (array: super.layoutAttributesForElements(in: rect)!, copyItems: true)
items.enumerateObjects(using: { (object, index, stop) -> Void in
let attributes = object as! UICollectionViewLayoutAttributes
self.updateCellAttributes(attributes)
})
return items as? [UICollectionViewLayoutAttributes]
}
// We invalidate the layout when a "bounds change" happens, for example when we scale the top cell. This forces a layout update on the flowlayout.
internal override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
// Cell paging
internal override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// If the property `isPagingEnabled` is set to false, we don't enable paging and thus return the current contentoffset.
guard isPagingEnabled else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}
// Page height used for estimating and calculating paging.
let pageHeight = cellHeight + self.minimumLineSpacing
// Make an estimation of the current page position.
let approximatePage = self.collectionView!.contentOffset.y/pageHeight
// Determine the current page based on velocity.
let currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage)
// Create custom flickVelocity.
let flickVelocity = velocity.y * 0.4
// Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
// Calculate newVerticalOffset.
let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - self.collectionView!.contentInset.top
return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}
internal override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// make sure the zIndex of the next card is higher than the one we're swiping away.
let nextIndexPath = IndexPath(row: itemIndexPath.row + 1, section: itemIndexPath.section)
let nextAttr = self.layoutAttributesForItem(at: nextIndexPath)
nextAttr?.zIndex = nextIndexPath.row
// attributes for swiping card away
let attr = self.layoutAttributesForItem(at: itemIndexPath)
return attr
}
/**
Updates the attributes.
Here manipulate the zIndex of the cards here, calculate the positions and do the animations.
- parameter attributes: The attributes we're updating.
*/
fileprivate func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes) {
let minY = collectionView!.bounds.minY + collectionView!.contentInset.top
let maxY = attributes.frame.origin.y
let finalY = max(minY, maxY)
var origin = attributes.frame.origin
let deltaY = (finalY - origin.y) / attributes.frame.height
let translationScale = CGFloat((attributes.zIndex + 1) * 10)
// create stacked effect (cards visible at bottom
if let itemTransform = firstItemTransform {
let scale = 1 - deltaY * itemTransform
var t = CGAffineTransform.identity
t = t.scaledBy(x: scale, y: 1)
t = t.translatedBy(x: 0, y: (translationScale + deltaY * translationScale))
attributes.transform = t
}
origin.x = (self.collectionView?.frame.width)! / 2 - attributes.frame.width / 2 - (self.collectionView?.contentInset.left)!
origin.y = finalY
attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
attributes.zIndex = attributes.indexPath.row
}
}
编辑 1: 作为额外说明,最终结果应该如下所示:
编辑 2: 似乎每滚动 4-5 张卡片就会发生一次测试。
您有一个继承自流式布局的布局。您覆盖了 layoutAttributesForElements(in rect:)
,从 super.layoutAttributesForElements
中获取所有元素,然后为每个元素修改方法 updateCellAttributes
.
这通常是创建流布局子类的好方法。 UICollectionViewFlowLayout
正在做大部分艰苦的工作——弄清楚每个元素应该在哪里,哪些元素在 rect 中,它们的基本属性是什么,它们应该如何填充等等,你可以只修改一些"hard" 工作完成后的属性。当您添加旋转或不透明度或其他一些不会更改项目位置的功能时,这会很好地工作。
当您使用 updateCellAttributes
更改项目框架时,您会遇到麻烦。然后,您可能会遇到这样一种情况,即您的单元格对于常规流布局根本不会出现在框架中,但现在由于您的修改应该出现。因此 super.layoutAttributesForElements(in rect: CGRect) 根本没有返回该属性,因此它们根本不显示。您还可能遇到相反的问题,即根本不应该在框架中的单元格在视图中,但以用户无法看到的方式转换。
你没有充分解释你想要做什么效果,以及为什么你认为从 UIFlowLayout 继承对我来说是正确的,无法具体帮助你。但我希望我已经给了你足够的信息,你可以自己找到问题所在。
错误在于您如何为每个属性定义 frame.origin.y
。更具体地说,您在 minY
中保留的值决定了您保留在屏幕上的单元格数量。 (我将编辑此答案并稍后解释更多,但现在,请尝试替换以下代码)
var minY = collectionView!.bounds.minY + collectionView!.contentInset.top
let maxY = attributes.frame.origin.y
if minY > attributes.frame.origin.y + attributes.bounds.height + minimumLineSpacing + collectionView!.contentInset.top {
minY = 0
}