Created
May 5, 2021 10:05
-
-
Save trilliwon/9c13d55b348961fb0f5f4b556f080c8a 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
class SpringEffectFlowLayout: UICollectionViewFlowLayout { | |
var dynamicAnimator: UIDynamicAnimator! | |
var visibleIndexPathsSet = Set<IndexPath>() | |
var visibleHeaderAndFooterSet = Set<IndexPath>() | |
private var latestDelta: CGFloat = 0 | |
var scrollResistanceFactor: CGFloat? | |
override init() { | |
super.init() | |
dynamicAnimator = UIDynamicAnimator(collectionViewLayout: self) | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func prepare() { | |
super.prepare() | |
guard let collectionView = collectionView else { return } | |
let visibleRect = CGRect(origin: collectionView.bounds.origin, size: collectionView.frame.size).insetBy(dx: -100, dy: -100) | |
guard let itemsInVisibleRectArray = super.layoutAttributesForElements(in: visibleRect) else { return } | |
let itemsIndexPathsInVisibleRectSet = Set<IndexPath>(itemsInVisibleRectArray.map(\.indexPath)) | |
/// Step 1: Remove any behaviours that are no longer visible. | |
let noLongerVisibleBehaviours = self.dynamicAnimator.behaviors.filter { behavior in | |
if let indexPath = ((behavior as? UIAttachmentBehavior)?.items.first as? UICollectionViewLayoutAttributes)?.indexPath { | |
return itemsIndexPathsInVisibleRectSet.contains(indexPath) == false | |
} | |
return false | |
} | |
noLongerVisibleBehaviours | |
.forEach { behavior in | |
dynamicAnimator.removeBehavior(behavior) | |
if let indexPath = ((behavior as? UIAttachmentBehavior)?.items.first as? UICollectionViewLayoutAttributes)?.indexPath { | |
visibleIndexPathsSet.remove(indexPath) | |
visibleHeaderAndFooterSet.remove(indexPath) | |
} | |
} | |
/// Step 2: Add any newly visible behaviours. | |
/// A "newly visible" item is one that is in the itemsInVisibleRect(Set|Array) but not in the visibleIndexPathsSet | |
let newlyVisibleItems = itemsInVisibleRectArray.filter { item in | |
if item.representedElementCategory == .cell { | |
return self.visibleIndexPathsSet.contains(item.indexPath) == false | |
} else { | |
return self.visibleHeaderAndFooterSet.contains(item.indexPath) == false | |
} | |
} | |
let touchLocation = collectionView.panGestureRecognizer.location(in: collectionView) | |
newlyVisibleItems.forEach { item in | |
var center = item.center | |
let springBehaviour = UIAttachmentBehavior(item: item, attachedToAnchor: center) | |
springBehaviour.length = 1.0 | |
springBehaviour.damping = 0.8 | |
springBehaviour.frequency = 1.0 | |
/// If our touchLocation is not (0,0), we'll need to adjust our item's center "in flight" | |
if touchLocation != .zero { | |
if (self.scrollDirection == .vertical) { | |
let distanceFromTouch = abs(touchLocation.y - springBehaviour.anchorPoint.y) | |
var scrollResistance: CGFloat | |
if let scrollResistanceFactor = self.scrollResistanceFactor { | |
scrollResistance = distanceFromTouch / scrollResistanceFactor | |
} else { | |
scrollResistance = distanceFromTouch / 900.0 | |
} | |
if latestDelta < 0 { | |
center.y += max(latestDelta, latestDelta * scrollResistance) | |
} else { | |
center.y += min(latestDelta, latestDelta * scrollResistance) | |
} | |
item.center = center | |
} | |
} | |
self.dynamicAnimator.addBehavior(springBehaviour) | |
if item.representedElementCategory == .cell { | |
visibleIndexPathsSet.insert(item.indexPath) | |
} else { | |
visibleHeaderAndFooterSet.insert(item.indexPath) | |
} | |
} | |
} | |
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { | |
let attributes = dynamicAnimator.items(in: rect) as? [UICollectionViewLayoutAttributes] | |
return attributes | |
} | |
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { | |
let dynamicLayoutAttributes = dynamicAnimator.layoutAttributesForCell(at: indexPath) | |
/// Check if dynamic animator has layout attributes for a layout, otherwise use the flow layouts properties. | |
/// This will prevent crashing when you add items later in a performBatchUpdates block (e.g. triggered by NSFetchedResultsController update) | |
return dynamicLayoutAttributes ?? super.layoutAttributesForItem(at: indexPath) | |
} | |
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { | |
guard let scrollView = self.collectionView else { return false } | |
var delta: CGFloat | |
if scrollDirection == .vertical { | |
delta = newBounds.origin.y - scrollView.bounds.origin.y | |
} else { | |
delta = newBounds.origin.x - scrollView.bounds.origin.x | |
} | |
latestDelta = delta | |
let touchLocation = scrollView.panGestureRecognizer.location(in: collectionView) | |
dynamicAnimator.behaviors.forEach { behaviour in | |
guard let springBehaviour = behaviour as? UIAttachmentBehavior else { return } | |
if scrollDirection == .vertical { | |
let distanceFromTouch = abs(touchLocation.y - springBehaviour.anchorPoint.y) | |
var scrollResistance: CGFloat | |
if let scrollResistanceFactor = scrollResistanceFactor { | |
scrollResistance = distanceFromTouch / scrollResistanceFactor | |
} else { | |
scrollResistance = distanceFromTouch / 900.0 | |
} | |
guard let item = springBehaviour.items.first as? UICollectionViewLayoutAttributes else { return } | |
var center = item.center | |
if delta < 0 { | |
center.y += max(delta, delta * scrollResistance) | |
} else { | |
center.y += min(delta, delta * scrollResistance) | |
} | |
item.center = center | |
dynamicAnimator.updateItem(usingCurrentState: item) | |
} | |
} | |
return false | |
} | |
override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) { | |
super.prepare(forCollectionViewUpdates: updateItems) | |
updateItems.enumerated().forEach { index, item in | |
if item.updateAction == .insert { | |
guard let indexPath = item.indexPathAfterUpdate else { return } | |
if self.dynamicAnimator.layoutAttributesForCell(at: indexPath) != nil { | |
return | |
} | |
if let indexPathAfterUpdate = item.indexPathAfterUpdate { | |
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPathAfterUpdate) | |
let springBehaviour = UIAttachmentBehavior(item: attributes, attachedToAnchor: attributes.center) | |
springBehaviour.length = 1.0 | |
springBehaviour.damping = 0.8 | |
springBehaviour.frequency = 1.0 | |
dynamicAnimator.addBehavior(springBehaviour) | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment