Last active
April 19, 2019 23:45
-
-
Save bgayman/ccab09987129a0f0a723bffbf93fa6c1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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