滚动双向滚动具有大量单元格(250,000或更多)的UICollectionView时可见滞后

时间:2016-05-30 08:15:27

标签: ios swift uicollectionview uicollectionviewlayout

我是UICollectionViewFlowLayout的子类,以便在UICollectionView中进行双向滚动。滚动对于较少数量的行和节数(100-200行和节)可以正常工作,但是当我增加行和节数超过500,即UICollectionView中的250,000或更多单元格时,滚动时会出现明显滞后。我已经跟踪了layoutAttributesForElementsInRect中的滞后来源。我使用Dictionary来保存每个单元格的UICollectionViewLayoutAttributes,以避免重新计算并循环遍历它以返回layoutAttributesForElementsInRect

中单元格的属性
import UIKit

class LuckGameCollectionViewLayout: UICollectionViewFlowLayout {

    // Used for calculating each cells CGRect on screen.
    // CGRect will define the Origin and Size of the cell.
    let CELL_HEIGHT = 70.0
    let CELL_WIDTH = 70.0

    // Dictionary to hold the UICollectionViewLayoutAttributes for
    // each cell. The layout attribtues will define the cell's size
    // and position (x, y, and z index). I have found this process
    // to be one of the heavier parts of the layout. I recommend
    // holding onto this data after it has been calculated in either
    // a dictionary or data store of some kind for a smooth performance.
    var cellAttrsDictionary = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>()
    // Defines the size of the area the user can move around in
    // within the collection view.
    var contentSize = CGSize.zero

    override func collectionViewContentSize() -> CGSize {
        return self.contentSize
    }

    override func prepareLayout() {

        // Cycle through each section of the data source.
        if collectionView?.numberOfSections() > 0 {
            for section in 0...collectionView!.numberOfSections()-1 {

                // Cycle through each item in the section.
                if collectionView?.numberOfItemsInSection(section) > 0 {
                    for item in 0...collectionView!.numberOfItemsInSection(section)-1 {

                        // Build the UICollectionVieLayoutAttributes for the cell.
                        let cellIndex = NSIndexPath(forItem: item, inSection: section)
                        let xPos = Double(item) * CELL_WIDTH
                        let yPos = Double(section) * CELL_HEIGHT

                        let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: cellIndex)
                        cellAttributes.frame = CGRect(x: xPos, y: yPos, width: CELL_WIDTH, height: CELL_HEIGHT)

                        // Save the attributes.
                        cellAttrsDictionary[cellIndex] = cellAttributes
                    }
                }

            }
        }

        // Update content size.
        let contentWidth = Double(collectionView!.numberOfItemsInSection(0)) * CELL_WIDTH
        let contentHeight = Double(collectionView!.numberOfSections()) * CELL_HEIGHT
        self.contentSize = CGSize(width: contentWidth, height: contentHeight)

    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        // Create an array to hold all elements found in our current view.
        var attributesInRect = [UICollectionViewLayoutAttributes]()

        // Check each element to see if it should be returned.
        for (_,cellAttributes) in cellAttrsDictionary {
            if CGRectIntersectsRect(rect, cellAttributes.frame) {
                attributesInRect.append(cellAttributes)
            }
        }

        // Return list of elements.
        return attributesInRect
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        return cellAttrsDictionary[indexPath]!
    }

    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        return false
    }
}

修改 以下是我在layoutAttributesForElementsInRect方法中提出的更改。

override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {        
    // Create an array to hold all elements found in our current view.
    var attributesInRect = [UICollectionViewLayoutAttributes]()

    let xOffSet = self.collectionView?.contentOffset.x
    let yOffSet = self.collectionView?.contentOffset.y
    let totalColumnCount = self.collectionView?.numberOfSections()
    let totalRowCount = self.collectionView?.numberOfItemsInSection(0)

    let startRow = Int(Double(xOffSet!)/CELL_WIDTH) - 10    //include 10 rows towards left
    let endRow = Int(Double(xOffSet!)/CELL_WIDTH + Double(Utils.getScreenWidth())/CELL_WIDTH) + 10 //include 10 rows towards right
    let startCol = Int(Double(yOffSet!)/CELL_HEIGHT) - 10 //include 10 rows towards top
    let endCol = Int(Double(yOffSet!)/CELL_HEIGHT + Double(Utils.getScreenHeight())/CELL_HEIGHT) + 10 //include 10 rows towards bottom

    for(var i = startRow ; i <= endRow; i = i + 1){
        for (var j = startCol ; j <= endCol; j = j + 1){
            if (i < 0 || i > (totalRowCount! - 1) || j < 0 || j > (totalColumnCount! - 1)){
                continue
            }

            let indexPath: NSIndexPath = NSIndexPath(forRow: i, inSection: j)
            attributesInRect.append(cellAttrsDictionary[indexPath]!)
        }
    }

    // Return list of elements.
    return attributesInRect
}

我计算了collectionView的偏移量,并用它来计算屏幕上可见的单元格(使用每个单元格的高度/宽度)。我不得不在每一侧添加额外的单元格,以便在用户滚动时没有丢失的单元格。我测试了这个并且性能很好。

3 个答案:

答案 0 :(得分:4)

利用已知单元格大小的layoutAttributesForElementsInRect(rect: CGRect),您不需要缓存属性,只需在collectionView请求它们时为给定的rect计算它们。您仍然需要检查0的边界情况和最大的部分/行计数,以避免计算不需要的或无效的属性,但可以在循环周围的where子句中轻松完成。这是一个工作示例,我已经测试了1000个部分x 1000行,它工作得很好,没有滞后于设备:

编辑:我添加了更大的Result,以便在滚动到达之前可以预先计算属性。从您的编辑看起来,您仍然在缓存我认为性能不需要的属性。此外,它会带来更大的内存占用,滚动更多。还有一个原因是你不想使用回调中提供的CGRect而不是手动计算contentOffset中的一个吗?

class LuckGameCollectionViewLayout: UICollectionViewFlowLayout {

let CELL_HEIGHT = 50.0
let CELL_WIDTH = 50.0

override func collectionViewContentSize() -> CGSize {
    let contentWidth = Double(collectionView!.numberOfItemsInSection(0)) * CELL_WIDTH
    let contentHeight = Double(collectionView!.numberOfSections()) * CELL_HEIGHT
    return CGSize(width: contentWidth, height: contentHeight)
}

override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let biggerRect = rect.insetBy(dx: -2048, dy: -2048)
    let startIndexY = Int(Double(biggerRect.origin.y) / CELL_HEIGHT)
    let startIndexX = Int(Double(biggerRect.origin.x) / CELL_WIDTH)
    let numberOfVisibleCellsInRectY = Int(Double(biggerRect.height) / CELL_HEIGHT) + startIndexY
    let numberOfVisibleCellsInRectX = Int(Double(biggerRect.width) / CELL_WIDTH) + startIndexX
    var attributes: [UICollectionViewLayoutAttributes] = []

    for section in startIndexY..<numberOfVisibleCellsInRectY
        where section >= 0 && section < self.collectionView!.numberOfSections() {
            for item in startIndexX..<numberOfVisibleCellsInRectX
                where item >= 0 && item < self.collectionView!.numberOfItemsInSection(section) {
                    let cellIndex = NSIndexPath(forItem: item, inSection: section)
                    if let attrs = self.layoutAttributesForItemAtIndexPath(cellIndex) {
                        attributes.append(attrs)
                    }
            }
    }
    return attributes
}

override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
    let xPos = Double(indexPath.row) * CELL_WIDTH
    let yPos = Double(indexPath.section) * CELL_HEIGHT
    let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
    cellAttributes.frame = CGRect(x: xPos, y: yPos, width: CELL_WIDTH, height: CELL_HEIGHT)
    return cellAttributes
}
}

答案 1 :(得分:1)

你需要为你的单元格提供一个按维度排序的数据结构,以便使用这些维度来缩小搜索范围。

让我们以一个表为例,其中单元格的视图宽度为100像素高,250_000个元素,要求单元格与{0,top,320,bottom}相交。然后,您的数据结构将是按顶部坐标排序的数组,并且伴随的算法将类似于

lfs

添加实际布局所需的复杂程度。

答案 2 :(得分:1)

UICollectionViewLayoutAttributes不适合存储NSIndexPath。它被layoutAttributesForElementsInRect编入索引但在cellAttrsDictionary中您正在搜索某个区域。如果您需要var allCellAttributes = [[UICollectionViewLayoutAttributes]]() 来获取其他内容,那么您可以保留它,但将每个cellAttribute 另外存储在另一个搜索速度更快的数据结构中。

例如嵌套数组:

allCellAttributes[0].append(cellAttributes)

第一级数组描述了一个区域,假设它是1000像素高和全屏宽度的块。因此,与矩形{0,0,screenwidth,1000}相交的所有cellAttribute都会进入:

allCellAttributes[1].append(cellAttributes)

与矩形{0,1000,screenwidth,2000}相交的所有cellAttribute都进入:

layoutAttributesForElementsInRect

等等......

然后,在allCellAttributes[5] allCellAttributes[6] 中,您可以根据给定的CGRect直接跳转到数组中来搜索此数据结构。如果矩形是例如{0,5500,100,6700}然后你只需要在这个范围内搜索:

.parent()

这应该给你基本的想法,我希望你明白我的意思。