使用自定义 collectionView 布局更改单元格大小时如何使用 ScrollToItem(at:)

Posted

技术标签:

【中文标题】使用自定义 collectionView 布局更改单元格大小时如何使用 ScrollToItem(at:)【英文标题】:How to use ScrollToItem(at:) when using a custom collectionView layout to alter cell sizes 【发布时间】:2018-12-13 22:09:17 【问题描述】:

我有一个 collectionView 的自定义布局。此自定义布局增加了中心单元格的宽度。这是执行此操作的自定义布局类。查看 shiftAttributes 函数,看看它是如何完成的

class CustomCollectionViewLayout: UICollectionViewLayout 

    private var cache = [IndexPath: UICollectionViewLayoutAttributes]()
    private var contentWidth = CGFloat()
    private var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
    private var oldBounds = CGRect.zero
    private var cellWidth: CGFloat = 5
    private var collectionViewStartY: CGFloat 
        guard let collectionView = collectionView else 
            return 0
        
        return collectionView.bounds.minY
    
    private var collectionViewHeight: CGFloat 
        return collectionView!.frame.height
    
    override public var collectionViewContentSize: CGSize 
        return CGSize(width: contentWidth, height: collectionViewHeight)
    

    override public func prepare() 
        print("calling prepare")
        guard let collectionView = collectionView,
            cache.isEmpty else 
                return
        

        updateInsets()
        collectionView.decelerationRate = .fast
        cache.removeAll(keepingCapacity: true)
        cache = [IndexPath: UICollectionViewLayoutAttributes]()
        oldBounds = collectionView.bounds
        var xOffset: CGFloat = 0
        var cellWidth: CGFloat = 5

        for item in 0 ..< collectionView.numberOfItems(inSection: 0) 
            let cellIndexPath = IndexPath(item: item, section: 0)
            let cellattributes = UICollectionViewLayoutAttributes(forCellWith: cellIndexPath)
            cellattributes.frame = CGRect(x: xOffset, y: 0, width: cellWidth, height: collectionViewHeight)
            xOffset = xOffset + cellWidth
            contentWidth = max(contentWidth,xOffset)
            cache[cellIndexPath] = cellattributes
        
    

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? 
        visibleLayoutAttributes.removeAll(keepingCapacity: true)

        for (_, attributes) in cache 
            visibleLayoutAttributes.append(self.shiftedAttributes(from: attributes))
        
        return visibleLayoutAttributes
    

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? 
        guard let attributes = cache[indexPath] else  fatalError("No attributes cached") 
        return shiftedAttributes(from: attributes)
    
    override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool 
        if oldBounds.size != newBounds.size 
            cache.removeAll(keepingCapacity: true)
        
        return true
    

    override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) 
        if context.invalidateDataSourceCounts  cache.removeAll(keepingCapacity: true) 
        super.invalidateLayout(with: context)
    

extension CustomCollectionViewLayout 

    func updateInsets() 
        guard let collectionView = collectionView else  return 
        collectionView.contentInset.left = (collectionView.bounds.size.width - cellWidth) / 2
        collectionView.contentInset.right = (collectionView.bounds.size.width - cellWidth) / 2
    
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint 
        guard let collectionView = collectionView else  return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) 
        let midX: CGFloat = collectionView.bounds.size.width / 2
        guard let closestAttribute = findClosestAttributes(toXPosition: proposedContentOffset.x + midX) else  return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) 
        return CGPoint(x: closestAttribute.center.x - midX, y: proposedContentOffset.y)
    

    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
        )
        let closestAttributes = layoutAttributesForElements(in: searchRect)?.min(by:  abs($0.center.x - xPosition) < abs($1.center.x - xPosition) )
        return closestAttributes
    
    private var continuousFocusedIndex: CGFloat 
        guard let collectionView = collectionView else  return 0 
        let offset = collectionView.bounds.width / 2 + collectionView.contentOffset.x - cellWidth / 2
        return offset / cellWidth
    
    private func shiftedAttributes(from attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes 
        guard let attributes = attributes.copy() as? UICollectionViewLayoutAttributes else  fatalError("Couldn't copy attributes") 
        let roundedFocusedIndex = round(continuousFocusedIndex)
        let focusedItemWidth = CGFloat(20)
        if attributes.indexPath.item == Int(roundedFocusedIndex)
            attributes.transform = CGAffineTransform(scaleX: 10, y: 1)
         else 
            let translationDirection: CGFloat = attributes.indexPath.item < Int(roundedFocusedIndex) ? -1 : 1
            attributes.transform = CGAffineTransform(translationX: translationDirection * 20, y: 0)
        
        return attributes
    

这是包含使用此布局的 collectionView 的 View Controller:

class ViewController: UIViewController, UICollectionViewDelegate,UICollectionViewDataSource 

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
        return 10
    

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
        let cell = customCollectionView.dequeueReusableCell(withReuseIdentifier: "singleCell", for: indexPath)
        cell.backgroundColor = UIColor.random()
        return cell
    
    func numberOfSections(in collectionView: UICollectionView) -> Int 
        return 1
    
    @IBOutlet weak var picker: UIPickerView!
    @IBOutlet weak var customCollectionView: UICollectionView!

    override func viewDidLoad() 
        super.viewDidLoad()
        customCollectionView.delegate = self
        customCollectionView.dataSource = self
        picker.delegate = self
        picker.dataSource = self
        // Do any additional setup after loading the view, typically from a nib.
    

    @IBAction func goTo(_ sender: Any) 
        let indexPath = IndexPath(item: picker.selectedRow(inComponent: 0), section: 0)
        customCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
    



extension ViewController: UIPickerViewDelegate, UIPickerViewDataSource 
    func numberOfComponents(in pickerView: UIPickerView) -> Int 
        return 1
    

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int 
        return 10
    

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? 
        return String(row)
           

注意pickerView,您可以在其中选择goTo按钮中使用的索引以滚动到该索引处的项目。它在行动中:

看看即使我停留在同一个索引上,它仍然会不断滚动,并且无论如何都不会真正滚动到那个索引。当我不移动属性(使用shiftedAttributes)并在自定义布局中正常返回它们时,scrollTo 工作正常。

所以在执行 scrollToItem(at:) 时似乎使用了每个单元格的位置,这被移位的属性弄糊涂了?当单元格的大小可能发生变化时,如何滚动到特定索引?

编辑:here 是整个项目代码,如果您想自己尝试一下:

【问题讨论】:

【参考方案1】:

scrollToItem() 似乎使用了固定的布局大小。

我认为您将不得不手动计算偏移量并使用setContentOffset()

//To do: Calculate widths of cells up to the cell you want to scroll to
var calculatedOffset: CGFloat 

//Then scroll to the offset calculated
customCollectionView.setContentOffset(CGPoint(x: calculatedOffset, y: 0.0), animated: true)

【讨论】:

但是,如果您正在滚动到尚未加载的 collectionView 中非常远的地方怎么办? 只有前几个单元格大,其余的都是标准大小,对吧?如果是这样,只需确定特殊的单元格大小(您的自定义布局正在执行的操作)并将它们添加到标准宽度乘以您要滚动到的行号。

以上是关于使用自定义 collectionView 布局更改单元格大小时如何使用 ScrollToItem(at:)的主要内容,如果未能解决你的问题,请参考以下文章

在我的自定义 collectionView 布局中未调用 prepare()

自定义 CollectionView 布局重新加载数据不起作用

iOS 6 CollectionView 动态改变布局

如何在collectionView自定义布局中的其他补充标题单元格上方添加标题补充单元格

text CollectionView自定义布局

可重新排序的自定义 collectionview 布局