Skip to content

Instantly share code, notes, and snippets.

@bgayman
Last active April 19, 2019 23:45
Show Gist options
  • Save bgayman/ccab09987129a0f0a723bffbf93fa6c1 to your computer and use it in GitHub Desktop.
Save bgayman/ccab09987129a0f0a723bffbf93fa6c1 to your computer and use it in GitHub Desktop.
import UIKit
class SpringyCollectionViewFlowLayout: UICollectionViewFlowLayout
{
lazy var dynamicAnimator: UIDynamicAnimator =
{
return UIDynamicAnimator(collectionViewLayout: self)
}()
var visibleIndexPathsSet = Set<IndexPath>()
var latestDelta: CGFloat = 0
override init()
{
super.init()
self.minimumInteritemSpacing = 10
self.minimumLineSpacing = 10
self.itemSize = CGSize(width: 44, height: 44)
self.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
}
required init?(coder aDecoder: NSCoder)
{
fatalError("Don't use a coder")
}
override func prepare()
{
super.prepare()
let visibleRect = CGRect(origin: self.collectionView?.bounds.origin ?? CGPoint.zero, size: self.collectionView?.frame.size ?? CGSize.zero).insetBy(dx: -100, dy: -100)
let itemsInVisibleRectArray = super.layoutAttributesForElements(in: visibleRect) ?? []
let itemsIndexPathsInVisibleRectSet = Set(itemsInVisibleRectArray.map{ $0.indexPath })
let noLongerVisibleBehaviors = self.dynamicAnimator.behaviors.filter
{ behavior in
guard let behavior = behavior as? UIAttachmentBehavior else { return false }
guard let attribute = behavior.items.first as? UICollectionViewLayoutAttributes else { return false }
let currentlyVisible = itemsIndexPathsInVisibleRectSet.contains(attribute.indexPath)
return !currentlyVisible
}
noLongerVisibleBehaviors.forEach
{ behavior in
self.dynamicAnimator.removeBehavior(behavior)
guard let behavior = behavior as? UIAttachmentBehavior else { return }
guard let attribute = behavior.items.first as? UICollectionViewLayoutAttributes else { return }
self.visibleIndexPathsSet.remove(attribute.indexPath)
}
let newlyVisibleItems = itemsInVisibleRectArray.filter
{ item in
let currentlyVisible = self.visibleIndexPathsSet.contains(item.indexPath)
return !currentlyVisible
}
let touchLocation = self.collectionView?.panGestureRecognizer.location(in: self.collectionView)
for item in newlyVisibleItems
{
var center = item.center
let springBehavior = UIAttachmentBehavior(item: item, attachedToAnchor: center)
springBehavior.length = 0.0
springBehavior.damping = 0.8
springBehavior.frequency = 1.0
if let touchLocation = touchLocation, CGPoint.zero != touchLocation
{
let yDistanceFromTouch = fabs(touchLocation.y - springBehavior.anchorPoint.y)
let xDistanceFromTouch = fabs(touchLocation.x - springBehavior.anchorPoint.x)
let scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0
if self.latestDelta < 0.0
{
center.y += max(self.latestDelta, self.latestDelta * scrollResistance)
}
else
{
center.y += min(self.latestDelta, self.latestDelta * scrollResistance)
}
item.center = center
}
self.dynamicAnimator.addBehavior(springBehavior)
self.visibleIndexPathsSet.insert(item.indexPath)
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
guard let attributes = self.dynamicAnimator.items(in: rect) as? [UICollectionViewLayoutAttributes] else { return nil }
return attributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
{
return self.dynamicAnimator.layoutAttributesForCell(at: indexPath)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool
{
let scrollView = self.collectionView
let delta = newBounds.origin.y - (scrollView?.bounds.origin.y ?? 0)
self.latestDelta = delta
let touchLocation = self.collectionView?.panGestureRecognizer.location(in: self.collectionView)
for springBehavior in self.dynamicAnimator.behaviors
{
guard let springBehavior = springBehavior as? UIAttachmentBehavior, let touchLocation = touchLocation else { continue }
let yDistanceFromTouch = fabs(touchLocation.y - springBehavior.anchorPoint.y)
let xDistanceFromTouch = fabs(touchLocation.x - springBehavior.anchorPoint.x)
let scrollResistance: CGFloat = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0
guard let item = springBehavior.items.first as? UICollectionViewLayoutAttributes else { continue }
var center = item.center
if self.latestDelta < 0.0
{
center.y += max(self.latestDelta, self.latestDelta * scrollResistance)
}
else
{
center.y += min(self.latestDelta, self.latestDelta * scrollResistance)
}
item.center = center
print("\(item.center) \(item.indexPath)")
self.dynamicAnimator.updateItem(usingCurrentState: item)
}
return false
}
}
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource
{
let cellIdentifier = "Cell"
var collectionView: UICollectionView!
override func viewDidLoad()
{
super.viewDidLoad()
let flowLayout = SpringyCollectionViewFlowLayout()
self.collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: flowLayout)
self.view.addSubview(collectionView)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
self.collectionView.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.collectionView.reloadData()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
self.collectionView.frame = self.view.bounds
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return 10000
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
cell.contentView.backgroundColor = .orange
return cell
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment