简体   繁体   English

具有自定义流布局的UICollectionView-滚动时具有活动的平移手势时,单元会出现毛刺

[英]UICollectionView with custom flow layout - cells glitching when scrolling while having an active pan gesture

so I'm building my own Drag&Drop system. 所以我要建立自己的拖放系统。 For that to work, I need a way to create "gaps" between cells in collectionViews , where the user hovers dragged items. 为此,我需要一种方法在用户将鼠标悬停在其中的collectionViews单元格之间创建“间隙”。

I'm trying out stuff right now and got a basic demo working using a custom flow layout that moves the cells around. 我现在正在尝试一些东西,并使用可移动单元格的自定义流程布局进行了基本演示。

The demo I did consists of a simple collectionView (using IGListKit but that doesn't matter for this problem, I'm pretty sure) and a UIPanGestureRecognizer that allows you to pan over the collectionView to create a gap beneath your finger. 我所做的演示包括一个简单的collectionView (使用IGListKit但这对这个问题无关紧要,我很确定)和一个UIPanGestureRecognizer ,它允许您在collectionView上平移以在手指下创建一个间隙。

I achieve this by invalidating the layout every time the pan gesture reconizer changes. 我通过在每次pan gesture reconizer更改时使布局无效来实现此目的。 It's working that way, but when I simultaneously scroll while panning over the collectionView , the cells seem to glitch a little bit. 它以这种方式工作,但是当我在平移collectionView同时滚动时,单元格似乎会出现一点故障。 It looks like this (it looks as if the rendering of the cells can't keep up): 看起来像这样(看起来单元格的渲染跟不上):

在此处输入图片说明

I'm pretty sure the problem is within the makeAGap function that contains this call: 我很确定问题出在包含此调用的makeAGap函数内:

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

If I don't animate the invalidation, like this 如果我不为无效设置动画,像这样

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

The glitch does not appear at all. 根本不会出现故障。 It has something to do with the animation. 它与动画有关。 Do you have any ideas? 你有什么想法?

Thanks 谢谢

PS: Here's the code (there's more IGListKit stuff but that's not important): 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
    }
}

Okay, this keeps happening. 好的,这一直在发生。 I spend hours and hours trying to fix a problem, give up to ask a question here and minutes later I find the/a solution. 我花了几个小时试图解决问题,放弃了在这里问一个问题,几分钟后,我找到了一个解决方案。

So, I can't entirely explain why this glitch is occurring, but it seems like it's got to do with the drawing of the screen. 因此,我无法完全解释为什么会发生这种故障,但似乎与屏幕绘制有关。 I added a CADisplayLink to synchronize the layout invalidation with the refresh rate of the screen and now the glitch is gone (code snippet below for anyone interested). 我添加了一个CADisplayLink来使布局无效与屏幕的刷新率同步,现在故障已消失(下面的代码段供感兴趣的用户使用)。

However, I would love to know what exactly is happening there and why synchronizing the invalidation fixes the drawing glitch. 但是,我很想知道那里到底发生了什么,以及为什么同步失效可以解决图形故障。 I'm gonna look into it as well but I'm not that experienced so if anyone knows about stuff like this, I'd highly appreciate a new (more detailed) answer to this question :) 我也会去研究它,但是我没有那么丰富的经验,所以如果有人知道这样的事情, 我将非常感谢对此问题提出一个新的(更详细的)答案:)

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)
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM