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.
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.
I achieve this by invalidating the layout every time the pan gesture reconizer
changes. It's working that way, but when I simultaneously scroll while panning over the collectionView
, the cells seem to glitch a little bit. 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:
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):
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).
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)
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.