具有自定义流布局的 UICollectionView - 在具有活动平移手势的情况下滚动时单元格出现故障

Posted

技术标签:

【中文标题】具有自定义流布局的 UICollectionView - 在具有活动平移手势的情况下滚动时单元格出现故障【英文标题】:UICollectionView with custom flow layout - cells glitching when scrolling while having an active pan gesture 【发布时间】:2019-05-04 05:30:30 【问题描述】:

所以我正在构建自己的拖放系统。为此,我需要一种方法来在collectionViews 中的单元格之间创建“间隙”,用户悬停拖动的项目。

我现在正在尝试一些东西,并获得了一个基本演示,它使用移动单元格的自定义流布局。

我做的演示包括一个简单的collectionView(使用IGListKit,但这对这个问题无关紧要,我很确定)和一个UIPanGestureRecognizer,它允许你平移collectionView来创建手指下方的空隙。

我通过在每次pan gesture reconizer 更改时使布局无效来实现这一点。它是这样工作的,但是当我在平移collectionView 时同时滚动时,单元格似乎有点小故障。看起来是这样的(看起来单元格的渲染跟不上):

我很确定问题出在包含此调用的 makeAGapfunction 中:

collectionView?.performBatchUpdates(
            self.invalidateLayout()
            self.collectionView?.layoutIfNeeded()
, completion: nil)

如果我不对失效设置动画,像这样

self.invalidateLayout()
self.collectionView?.layoutIfNeeded()

故障根本没有出现。它与动画有关。你有什么想法?

谢谢

PS:这是代码(还有更多 IGListKit 的东西,但这并不重要):

class MyCustomLayout: UICollectionViewFlowLayout 

    fileprivate var cellPadding: CGFloat = 6

    fileprivate var cache = [UICollectionViewLayoutAttributes]()

    fileprivate var contentHeight: CGFloat = 300
    fileprivate var contentWidth: CGFloat = 0

    var gap: IndexPath? = nil
    var gapPosition: CGPoint? = nil

    override var collectionViewContentSize: CGSize 
        return CGSize(width: contentWidth, height: contentHeight)
    

    override func prepare() 
        // cache contains the cached layout attributes
        guard cache.isEmpty, let collectionView = collectionView else 
            return
        

        // I'm using IGListKit, so every cell is in its own section in my case
        for section in 0..<collectionView.numberOfSections 
            let indexPath = IndexPath(item: 0, section: section)
            // If a gap has been set, just make the current offset (contentWidth) bigger to
            // simulate a "missing" item, which creates a gap
            if let gapPosition = self.gapPosition 
                if gapPosition.x >= (contentWidth - 100) && gapPosition.x < (contentWidth + 100) 
                    contentWidth += 100
                
            

            // contentWidth is used as x origin
            let frame = CGRect(x: contentWidth, y: 10, width: 100, height: contentHeight)
            contentWidth += frame.width + cellPadding

            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = frame
            cache.append(attributes)
        
    

    public func makeAGap(at indexPath: IndexPath, position: CGPoint) 
        gap = indexPath
        self.cache = []
        self.contentWidth = 0
        self.gapPosition = position


        collectionView?.performBatchUpdates(
            self.invalidateLayout()
            self.collectionView?.layoutIfNeeded()
        , completion: nil)

        //invalidateLayout() // Using this, the glitch does NOT appear
    

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? 

        var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()

        // Loop through the cache and look for items in the rect
        for attributes in cache 
            if attributes.frame.intersects(rect) 
                visibleLayoutAttributes.append(attributes)
            
        
        return visibleLayoutAttributes
    

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? 
        return cache[indexPath.item]
    


class ViewController: UIViewController 

    /// IGListKit stuff: Data for self.collectionView ("its cells", which represent the rows)
    public var data: [String] 
        return (0...100).compactMap  "\($0)" 
    

    /// This collectionView will consist of cells, that each have their own collectionView.
    private lazy var collectionView: UICollectionView = 
        let layout = MyCustomLayout()
        layout.scrollDirection = .horizontal
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.backgroundColor = UIColor(hex: 0xeeeeee)

        adapter.collectionView = collectionView
        adapter.dataSource = self
        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.frame = CGRect(x: 0, y: 50, width: 1000, height: 300)

        return collectionView
    ()

    /// IGListKit stuff. Data manager for the collectionView
    private lazy var adapter: ListAdapter = 
        let adapter = ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)
        return adapter
    ()

    override func viewDidLoad() 
        super.viewDidLoad()
        _ = collectionView

        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(pan:)))
        pan.maximumNumberOfTouches = 1
        pan.delegate = self
        view.addGestureRecognizer(pan)
    

    @objc private func handlePan(pan: UIPanGestureRecognizer) 
        guard let indexPath = collectionView.indexPathForItem(at: pan.location(in: collectionView)) else 
            return
        

        (collectionView.collectionViewLayout as? MyCustomLayout)?.makeAGap(at: indexPath, position: pan.location(in: collectionView))
    


extension ViewController: ListAdapterDataSource, UIGestureRecognizerDelegate 
    func objects(for listAdapter: ListAdapter) -> [ListDiffable] 
        return data as [ListDiffable]
    

    func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController 
        return RowListSection()
    

    func emptyView(for listAdapter: ListAdapter) -> UIView? 
        return nil
    

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool 
        return true
    

【问题讨论】:

【参考方案1】:

好的,这一直在发生。我花了好几个小时试图解决一个问题,放弃在这里提出问题,几分钟后我找到了解决方案。

所以,我无法完全解释为什么会出现这种故障,但它似乎与屏幕的绘制有关。 我添加了一个CADisplayLink以使布局失效与屏幕的刷新率同步,现在故障已经消失(感兴趣的人可以使用下面的代码 sn-p)。

但是,我很想知道那里到底发生了什么,以及为什么同步失效会修复绘图故障。我也会调查它,但我没有那么有经验,所以如果任何人都知道这样的事情,我非常感谢这个问题的新(更详细)答案:)

override func viewDidLoad() 
        super.viewDidLoad()
        _ = collectionView

        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(pan:)))
        pan.maximumNumberOfTouches = 1
        pan.delegate = self
        view.addGestureRecognizer(pan)

        let displayLink = CADisplayLink(target: self, selector: #selector(update))
        displayLink.add(to: .current, forMode: .common)
    

    var indexPath: IndexPath? = nil
    var position: CGPoint? = nil

    @objc private func update() 
        if let indexPath = self.indexPath, let position = self.position 
            (collectionView.collectionViewLayout as? MyCustomLayout)?.makeAGap(at: indexPath, position: position)
        
    

    @objc private func handlePan(pan: UIPanGestureRecognizer) 
        guard let indexPath = collectionView.indexPathForItem(at: pan.location(in: collectionView)) else 
            return
        

        // buffer the values
        self.indexPath = indexPath
        position = pan.location(in: collectionView)

【讨论】:

以上是关于具有自定义流布局的 UICollectionView - 在具有活动平移手势的情况下滚动时单元格出现故障的主要内容,如果未能解决你的问题,请参考以下文章

前端之瀑布流布局(多种实现方案)

关于waterfall 瀑布流布局出现布局错乱的问题

瀑布流布局的原理及实现

WPF如何实现瀑布流布局?

FlexboxLayout流布局的使用(分割线的妙用)

瀑布流实现思路