UICollectionView scrollToItem() 滚动到上一个单元格时无法正常工作

7
我已经在过去几周里一直苦思冥想,但现在已经没有更多的想法了...

简要背景

我使用自己构建的UICollectionView框架和自定义的UICollectionViewFlowLayout实现了垂直卡片分页和创建卡片堆叠效果。

这是一个GIF演示:

framework gif example

问题

我正在尝试实现一个功能,允许用户 滚动到特定索引的某个卡片(例如使用scrollToItem函数)。

问题在于,由于某种原因,当尝试滚动回到前一个卡片时,它无法正常工作。

下面是发生的情况的录像,我已将tapGestureRecognizer附加到其中,当我点击焦点(当前居中)卡片时,它应该滚动到该索引-1(即上一个卡片)。
由于底部卡片的frame.origin.y高于聚焦(当前居中)卡片的frame.origin.maxY,所以出现了这样的情况。

Problem demo gif

请注意,我在卡片上轻敲两次才能使其工作,因为某种原因底部卡片的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问题

感谢您的时间,任何对此问题的帮助或信息都将不胜感激!此外,请让我知道是否有任何进一步的问题或是否缺少任何关键信息

编辑1: 注意:当滚动到currentIndex-1时才会出现奇怪的效果,currentIndex + 1没有任何问题,我认为这在一定程度上解释了为什么会在let finalY = max(cvMinY,cardMinY)上发生,但我还没有找到合适的解决方案。

1个回答

2

我已经找到了另一种方法使它工作,通过使用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会让事情变得混乱。

如果您确实知道导致此问题的原因或如何更好地解决它,我仍然很乐意了解 :)


2
你是一个传奇和学者。 - andrewcar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接