UICollectionView scrollToItem() 滚动到上一个单元格无法正常工作
UICollectionView scrollToItem() scrolling to previous cell not working correctly
在过去的几周里,我一直在为这个问题伤脑筋,但我没有主意了..
短背景
我构建了自己的 UICollectionView
框架,其中包含用于垂直卡片分页和创建卡片堆叠效果的自定义 UICollectionViewFlowLayout
。
这是它的外观 gif:
问题
我正在尝试实现一项允许用户滚动到特定索引处的特定卡片的功能(使用scrollToItem
函数为例)。
现在 问题 是由于某种原因,当试图滚动回上一张卡片时 它无法正常工作。
下面是发生的事情的记录,我在上面附加了一个 tapGestureRecognizer,当我点击聚焦(当前居中)的卡片时,它应该滚动到该索引 - 1(所以前一张牌)。
出于某种原因,它只会在底部卡片的 frame.origin.y
高于聚焦(当前居中的卡片)的 frame.origin.maxY
上方时执行此操作。
请注意,我在卡片上轻敲两次以使其生效,因为底牌的 frame.origin.y
值需要低于底牌的 frame.origin.maxY
出于某种原因,顶级卡可以正常工作。
到目前为止我已经尝试过的基本答案
collectionView.scrollToItem(at: convertIndexToIndexPath(for: index), at: .top, animated: animated)
和
if let frame = collectionView.layoutAttributesForItem(at: convertIndexToIndexPath(for: index))?.frame {
collectionView.scrollRectToVisible(frame, animated: true)
}
一些重要的知识
我已经知道是哪一行导致了问题,在UICollectionViewFlowLayout
的子类中(称为VerticalCardSwiperFlowLayout
),有一个函数叫做updateCellAttributes
,它修改了一些框架的属性才能达到这种效果。问题出现在以下行:
let finalY = max(cvMinY, cardMinY)
这是有问题的函数,包括在代码注释顶部对其工作原理的非常详尽的解释:
/**
Updates the attributes.
Here manipulate the zIndex of the cards here, calculate the positions and do the animations.
Below we'll briefly explain how the effect of scrolling a card to the background instead of the top is achieved.
Keep in mind that (x,y) coords in views start from the top left (x: 0,y: 0) and increase as you go down/to the right,
so as you go down, the y-value increases, and as you go right, the x value increases.
The two most important variables we use to achieve this effect are cvMinY and cardMinY.
* cvMinY (A): The top position of the collectionView + inset. On the drawings below it's marked as "A".
This position never changes (the value of the variable does, but the position is always at the top where "A" is marked).
* cardMinY (B): The top position of each card. On the drawings below it's marked as "B". As the user scrolls a card,
this position changes with the card position (as it's the top of the card).
When the card is moving down, this will go up, when the card is moving up, this will go down.
We then take the max(cvMinY, cardMinY) to get the highest value of those two and set that as the origin.y of the card.
By doing this, we ensure that the origin.y of a card never goes below cvMinY, thus preventing cards from scrolling upwards.
```
+---------+ +---------+
| | | |
| +-A=B-+ | | +-A-+ | ---> The top line here is the previous card
| | | | | +--B--+ | that's visible when the user starts scrolling.
| | | | | | | |
| | | | | | | | | As the card moves down,
| | | | | | | | v cardMinY ("B") goes up.
| +-----+ | | | | |
| | | +-----+ |
| +--B--+ | | +--B--+ |
| | | | | | | |
+-+-----+-+ +-+-----+-+
```
- parameter attributes: The attributes we're updating.
*/
fileprivate func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes) {
guard let collectionView = collectionView else { return }
var cvMinY = collectionView.bounds.minY + collectionView.contentInset.top
let cardMinY = attributes.frame.minY
var origin = attributes.frame.origin
let cardHeight = attributes.frame.height
if cvMinY > cardMinY + cardHeight + minimumLineSpacing + collectionView.contentInset.top {
cvMinY = 0
}
let finalY = max(cvMinY, cardMinY)
let deltaY = (finalY - cardMinY) / cardHeight
transformAttributes(attributes: attributes, deltaY: deltaY)
// Set the attributes frame position to the values we calculated
origin.x = collectionView.frame.width/2 - attributes.frame.width/2 - collectionView.contentInset.left
origin.y = finalY
attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
attributes.zIndex = attributes.indexPath.row
}
// Creates and applies a CGAffineTransform to the attributes to recreate the effect of the card going to the background.
fileprivate func transformAttributes(attributes: UICollectionViewLayoutAttributes, deltaY: CGFloat) {
let translationScale = CGFloat((attributes.zIndex + 1) * 10)
if let itemTransform = firstItemTransform {
let scale = 1 - deltaY * itemTransform
var t = CGAffineTransform.identity
t = t.scaledBy(x: scale, y: 1)
if isPreviousCardVisible {
t = t.translatedBy(x: 0, y: (deltaY * translationScale))
}
attributes.transform = t
}
}
调用该特定代码的函数如下:
internal override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let items = NSArray(array: super.layoutAttributesForElements(in: rect)!, copyItems: true)
for object in items {
if let attributes = object as? UICollectionViewLayoutAttributes {
self.updateCellAttributes(attributes)
}
}
return items as? [UICollectionViewLayoutAttributes]
}
如果你想亲自看看,可以在这里下载完整的项目:
https://github.com/JoniVR/VerticalCardSwiper/archive/master.zip
(如果您通过 github 结帐,请务必下载开发分支)
特定的 Github 问题 可以找到一些讨论和可能的额外信息 here。
感谢您抽出宝贵时间,我们将不胜感激有关此问题的任何帮助或信息!另外,如果您还有其他问题或缺少任何重要信息,请告诉我
编辑 1:
注意:奇怪的效果只发生在滚动到 currentIndex - 1
时,currentIndex + 1
没有给出任何问题,我认为这在一定程度上解释了为什么它发生在 let finalY = max(cvMinY, cardMinY)
上,但我还没有弄明白一个合适的解决方案。
我设法让它以另一种方式工作,方法是使用 setContentOffset
并手动计算偏移量。
guard index >= 0 && index < verticalCardSwiperView.numberOfItems(inSection: 0) else { return }
let y = CGFloat(index) * (flowLayout.cellHeight + flowLayout.minimumLineSpacing) - topInset
let point = CGPoint(x: verticalCardSwiperView.contentOffset.x, y: y)
verticalCardSwiperView.setContentOffset(point, animated: animated)
verticalCardSwiperView 基本上只是 UICollectionView
的子类,我这样做是因为我想缩小用户可以访问的范围,因为这是一个特定的库并允许完整的 collectionView 访问会让事情变得混乱。
如果您碰巧知道究竟是什么导致了这个问题,或者我该如何更好地解决它,我仍然很乐意找出答案:)
在过去的几周里,我一直在为这个问题伤脑筋,但我没有主意了..
短背景
我构建了自己的 UICollectionView
框架,其中包含用于垂直卡片分页和创建卡片堆叠效果的自定义 UICollectionViewFlowLayout
。
这是它的外观 gif:
问题
我正在尝试实现一项允许用户滚动到特定索引处的特定卡片的功能(使用scrollToItem
函数为例)。
现在 问题 是由于某种原因,当试图滚动回上一张卡片时 它无法正常工作。
下面是发生的事情的记录,我在上面附加了一个 tapGestureRecognizer,当我点击聚焦(当前居中)的卡片时,它应该滚动到该索引 - 1(所以前一张牌)。
出于某种原因,它只会在底部卡片的 frame.origin.y
高于聚焦(当前居中的卡片)的 frame.origin.maxY
上方时执行此操作。
请注意,我在卡片上轻敲两次以使其生效,因为底牌的 frame.origin.y
值需要低于底牌的 frame.origin.maxY
出于某种原因,顶级卡可以正常工作。
到目前为止我已经尝试过的基本答案
collectionView.scrollToItem(at: convertIndexToIndexPath(for: index), at: .top, animated: animated)
和
if let frame = collectionView.layoutAttributesForItem(at: convertIndexToIndexPath(for: index))?.frame {
collectionView.scrollRectToVisible(frame, animated: true)
}
一些重要的知识
我已经知道是哪一行导致了问题,在UICollectionViewFlowLayout
的子类中(称为VerticalCardSwiperFlowLayout
),有一个函数叫做updateCellAttributes
,它修改了一些框架的属性才能达到这种效果。问题出现在以下行:
let finalY = max(cvMinY, cardMinY)
这是有问题的函数,包括在代码注释顶部对其工作原理的非常详尽的解释:
/**
Updates the attributes.
Here manipulate the zIndex of the cards here, calculate the positions and do the animations.
Below we'll briefly explain how the effect of scrolling a card to the background instead of the top is achieved.
Keep in mind that (x,y) coords in views start from the top left (x: 0,y: 0) and increase as you go down/to the right,
so as you go down, the y-value increases, and as you go right, the x value increases.
The two most important variables we use to achieve this effect are cvMinY and cardMinY.
* cvMinY (A): The top position of the collectionView + inset. On the drawings below it's marked as "A".
This position never changes (the value of the variable does, but the position is always at the top where "A" is marked).
* cardMinY (B): The top position of each card. On the drawings below it's marked as "B". As the user scrolls a card,
this position changes with the card position (as it's the top of the card).
When the card is moving down, this will go up, when the card is moving up, this will go down.
We then take the max(cvMinY, cardMinY) to get the highest value of those two and set that as the origin.y of the card.
By doing this, we ensure that the origin.y of a card never goes below cvMinY, thus preventing cards from scrolling upwards.
```
+---------+ +---------+
| | | |
| +-A=B-+ | | +-A-+ | ---> The top line here is the previous card
| | | | | +--B--+ | that's visible when the user starts scrolling.
| | | | | | | |
| | | | | | | | | As the card moves down,
| | | | | | | | v cardMinY ("B") goes up.
| +-----+ | | | | |
| | | +-----+ |
| +--B--+ | | +--B--+ |
| | | | | | | |
+-+-----+-+ +-+-----+-+
```
- parameter attributes: The attributes we're updating.
*/
fileprivate func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes) {
guard let collectionView = collectionView else { return }
var cvMinY = collectionView.bounds.minY + collectionView.contentInset.top
let cardMinY = attributes.frame.minY
var origin = attributes.frame.origin
let cardHeight = attributes.frame.height
if cvMinY > cardMinY + cardHeight + minimumLineSpacing + collectionView.contentInset.top {
cvMinY = 0
}
let finalY = max(cvMinY, cardMinY)
let deltaY = (finalY - cardMinY) / cardHeight
transformAttributes(attributes: attributes, deltaY: deltaY)
// Set the attributes frame position to the values we calculated
origin.x = collectionView.frame.width/2 - attributes.frame.width/2 - collectionView.contentInset.left
origin.y = finalY
attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
attributes.zIndex = attributes.indexPath.row
}
// Creates and applies a CGAffineTransform to the attributes to recreate the effect of the card going to the background.
fileprivate func transformAttributes(attributes: UICollectionViewLayoutAttributes, deltaY: CGFloat) {
let translationScale = CGFloat((attributes.zIndex + 1) * 10)
if let itemTransform = firstItemTransform {
let scale = 1 - deltaY * itemTransform
var t = CGAffineTransform.identity
t = t.scaledBy(x: scale, y: 1)
if isPreviousCardVisible {
t = t.translatedBy(x: 0, y: (deltaY * translationScale))
}
attributes.transform = t
}
}
调用该特定代码的函数如下:
internal override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let items = NSArray(array: super.layoutAttributesForElements(in: rect)!, copyItems: true)
for object in items {
if let attributes = object as? UICollectionViewLayoutAttributes {
self.updateCellAttributes(attributes)
}
}
return items as? [UICollectionViewLayoutAttributes]
}
如果你想亲自看看,可以在这里下载完整的项目: https://github.com/JoniVR/VerticalCardSwiper/archive/master.zip
(如果您通过 github 结帐,请务必下载开发分支)
特定的 Github 问题 可以找到一些讨论和可能的额外信息 here。
感谢您抽出宝贵时间,我们将不胜感激有关此问题的任何帮助或信息!另外,如果您还有其他问题或缺少任何重要信息,请告诉我
编辑 1:
注意:奇怪的效果只发生在滚动到 currentIndex - 1
时,currentIndex + 1
没有给出任何问题,我认为这在一定程度上解释了为什么它发生在 let finalY = max(cvMinY, cardMinY)
上,但我还没有弄明白一个合适的解决方案。
我设法让它以另一种方式工作,方法是使用 setContentOffset
并手动计算偏移量。
guard index >= 0 && index < verticalCardSwiperView.numberOfItems(inSection: 0) else { return }
let y = CGFloat(index) * (flowLayout.cellHeight + flowLayout.minimumLineSpacing) - topInset
let point = CGPoint(x: verticalCardSwiperView.contentOffset.x, y: y)
verticalCardSwiperView.setContentOffset(point, animated: animated)
verticalCardSwiperView 基本上只是 UICollectionView
的子类,我这样做是因为我想缩小用户可以访问的范围,因为这是一个特定的库并允许完整的 collectionView 访问会让事情变得混乱。
如果您碰巧知道究竟是什么导致了这个问题,或者我该如何更好地解决它,我仍然很乐意找出答案:)