FSPagerView/Sources/FSPageViewLayout.swift

231 lines
9.2 KiB
Swift

//
// FSPagerViewLayout.swift
// FSPagerView
//
// Created by Wenchao Ding on 20/12/2016.
// Copyright © 2016 Wenchao Ding. All rights reserved.
//
import UIKit
class FSPagerViewLayout: UICollectionViewLayout {
internal var contentSize: CGSize = .zero
internal var leadingSpacing: CGFloat = 0
internal var itemSpan: CGFloat = 0
internal var needsReprepare = true
fileprivate var pagerView: FSPagerView? {
return self.collectionView?.superview?.superview as? FSPagerView
}
fileprivate var layoutAttributes: [IndexPath:UICollectionViewLayoutAttributes] = [:]
fileprivate var isInfinite: Bool = true
fileprivate var collectionViewSize: CGSize = .zero
fileprivate var numberOfSections = 1
fileprivate var numberOfItems = 0
fileprivate var actualInteritemSpacing: CGFloat = 0
fileprivate var actualItemSize: CGSize = .zero
override init() {
super.init()
self.commonInit()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
deinit {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}
override open func prepare() {
guard let collectionView = self.collectionView, let pagerView = self.pagerView else {
return
}
guard self.needsReprepare || self.collectionViewSize != collectionView.frame.size else {
return
}
self.needsReprepare = false
self.collectionViewSize = collectionView.frame.size
self.layoutAttributes.removeAll()
// Calculate basic parameters/variables
self.numberOfSections = pagerView.numberOfSections(in: collectionView)
self.numberOfItems = pagerView.collectionView(collectionView, numberOfItemsInSection: 0)
guard self.numberOfItems > 0 && self.numberOfSections > 0 else {
return
}
self.actualItemSize = {
var size = pagerView.itemSize
if size == .zero {
size = collectionView.frame.size
}
return size
}()
self.actualInteritemSpacing = {
if let transformer = pagerView.transformer {
return transformer.proposedInteritemSpacing()
}
return pagerView.interitemSpacing
}()
self.leadingSpacing = (collectionView.frame.width-self.actualItemSize.width)*0.5
self.itemSpan = self.actualItemSize.width+self.actualInteritemSpacing
// Calculate and cache contentSize, rather than calculating each time
self.contentSize = {
let numberOfItems = self.numberOfItems*self.numberOfSections
var contentSizeWidth: CGFloat = self.leadingSpacing*2 // Leading & trailing spacing
contentSizeWidth += CGFloat(numberOfItems-1)*self.actualInteritemSpacing // Interitem spacing
contentSizeWidth += CGFloat(numberOfItems)*self.actualItemSize.width // Item sizes
let contentSize = CGSize(width: contentSizeWidth, height: collectionView.frame.height)
return contentSize
}()
self.adjustCollectionViewBounds()
}
override open var collectionViewContentSize: CGSize {
return self.contentSize
}
override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
guard self.itemSpan > 0, !rect.isEmpty else {
return layoutAttributes
}
let rect = rect.intersection(CGRect(origin: .zero, size: self.contentSize))
guard !rect.isEmpty else {
return layoutAttributes
}
// Calculate start position and index of certain rects
let numberOfItemsBefore = max(Int((rect.minX-self.leadingSpacing)/self.itemSpan),0)
let startPosition = self.leadingSpacing + CGFloat(numberOfItemsBefore)*self.itemSpan
let startIndex = numberOfItemsBefore
// Create layout attributes
var itemIndex = startIndex
var x = startPosition
let maxPosition = min(rect.maxX,self.contentSize.width-self.actualItemSize.width)
while x <= maxPosition {
let indexPath = IndexPath(item: itemIndex%self.numberOfItems, section: itemIndex/self.numberOfItems)
let attributes = self.layoutAttributesForItem(at: indexPath)!
self.applyTransform(to: attributes)
layoutAttributes.append(attributes)
itemIndex += 1
x += self.itemSpan
}
return layoutAttributes
}
override open func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let collectionView = self.collectionView else {
return UICollectionViewLayoutAttributes(forCellWith: indexPath)
}
if let attributes = self.layoutAttributes[indexPath] {
return attributes
}
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let x = self.frame(for: indexPath).minX
let center = CGPoint(x: x+self.actualItemSize.width*0.5, y: collectionView.frame.height*0.5)
attributes.center = center
attributes.size = self.actualItemSize
self.layoutAttributes[indexPath] = attributes
return attributes
}
override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = self.collectionView else {
return proposedContentOffset
}
var proposedContentOffset = proposedContentOffset
let proposedContentOffsetX: CGFloat = {
let translation = -collectionView.panGestureRecognizer.translation(in: collectionView).x
var offset: CGFloat = round(proposedContentOffset.x/self.itemSpan)*self.itemSpan
let minFlippingDistance = min(0.5 * self.itemSpan,150)
let originalContentOffsetX = collectionView.contentOffset.x - translation
if abs(translation) <= minFlippingDistance {
if abs(velocity.x) >= 0.3 && abs(proposedContentOffset.x-originalContentOffsetX) <= self.itemSpan*0.5 {
offset += self.itemSpan * (velocity.x)/abs(velocity.x)
}
}
return offset
}()
proposedContentOffset = CGPoint(x: proposedContentOffsetX, y: proposedContentOffset.y)
return proposedContentOffset
}
// MARK:- Internal functions
internal func forceInvalidate() {
self.needsReprepare = true
self.invalidateLayout()
}
internal func contentOffset(for indexPath: IndexPath) -> CGPoint {
let origin = self.frame(for: indexPath).origin
guard let collectionView = self.collectionView else {
return origin
}
let contentOffset = CGPoint(x: origin.x - (collectionView.frame.width*0.5-self.actualItemSize.width*0.5), y: collectionView.contentOffset.y)
return contentOffset
}
internal func frame(for indexPath: IndexPath) -> CGRect {
let numberOfItems = self.numberOfItems*indexPath.section + indexPath.item
let originX = self.leadingSpacing + CGFloat(numberOfItems)*self.itemSpan
let originY = (self.collectionView!.frame.height-self.actualItemSize.height)
let origin = CGPoint(x: originX, y: originY)
let frame = CGRect(origin: origin, size: self.actualItemSize)
return frame
}
// MARK:- Notification
@objc
fileprivate func didReceiveNotification(notification: Notification) {
if self.pagerView?.itemSize == .zero {
self.adjustCollectionViewBounds()
}
}
// MARK:- Private functions
fileprivate func commonInit() {
NotificationCenter.default.addObserver(self, selector: #selector(didReceiveNotification(notification:)), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}
fileprivate func adjustCollectionViewBounds() {
guard let collectionView = self.collectionView, let pagerView = self.pagerView else {
return
}
let currentIndex = pagerView.currentIndex
let newIndexPath = IndexPath(item: currentIndex, section: self.isInfinite ? self.numberOfSections/2 : 0)
let contentOffsetX = self.contentOffset(for: newIndexPath).x
let contentOffset = CGPoint(x: contentOffsetX, y: 0)
let newBounds = CGRect(origin: contentOffset, size: collectionView.frame.size)
collectionView.bounds = newBounds
}
fileprivate func applyTransform(to attributes: UICollectionViewLayoutAttributes) {
guard let collectionView = self.collectionView else {
return
}
guard let transformer = self.pagerView?.transformer else {
return
}
let ruler = collectionView.bounds.midX
let position = (attributes.center.x-ruler)/self.itemSpan
transformer.applyTransform(to: attributes, for: position)
}
}