FSPageViewLayout.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. //
  2. // FSPagerViewLayout.swift
  3. // FSPagerView
  4. //
  5. // Created by Wenchao Ding on 20/12/2016.
  6. // Copyright © 2016 Wenchao Ding. All rights reserved.
  7. //
  8. import UIKit
  9. class FSPagerViewLayout: UICollectionViewLayout {
  10. internal var contentSize: CGSize = .zero
  11. internal var leadingSpacing: CGFloat = 0
  12. internal var itemSpacing: CGFloat = 0
  13. internal var needsReprepare = true
  14. internal var scrollDirection: FSPagerView.ScrollDirection = .horizontal
  15. open override class var layoutAttributesClass: AnyClass {
  16. return FSPagerViewLayoutAttributes.self
  17. }
  18. fileprivate var pagerView: FSPagerView? {
  19. return self.collectionView?.superview?.superview as? FSPagerView
  20. }
  21. fileprivate var collectionViewSize: CGSize = .zero
  22. fileprivate var numberOfSections = 1
  23. fileprivate var numberOfItems = 0
  24. fileprivate var actualInteritemSpacing: CGFloat = 0
  25. fileprivate var actualItemSize: CGSize = .zero
  26. override init() {
  27. super.init()
  28. self.commonInit()
  29. }
  30. required public init?(coder aDecoder: NSCoder) {
  31. super.init(coder: aDecoder)
  32. self.commonInit()
  33. }
  34. deinit {
  35. #if !os(tvOS)
  36. NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
  37. #endif
  38. }
  39. override open func prepare() {
  40. guard let collectionView = self.collectionView, let pagerView = self.pagerView else {
  41. return
  42. }
  43. guard self.needsReprepare || self.collectionViewSize != collectionView.frame.size else {
  44. return
  45. }
  46. self.needsReprepare = false
  47. self.collectionViewSize = collectionView.frame.size
  48. // Calculate basic parameters/variables
  49. self.numberOfSections = pagerView.numberOfSections(in: collectionView)
  50. self.numberOfItems = pagerView.collectionView(collectionView, numberOfItemsInSection: 0)
  51. self.actualItemSize = {
  52. var size = pagerView.itemSize
  53. if size == .zero {
  54. size = collectionView.frame.size
  55. }
  56. return size
  57. }()
  58. self.actualInteritemSpacing = {
  59. if let transformer = pagerView.transformer {
  60. return transformer.proposedInteritemSpacing()
  61. }
  62. return pagerView.interitemSpacing
  63. }()
  64. self.scrollDirection = pagerView.scrollDirection
  65. self.leadingSpacing = self.scrollDirection == .horizontal ? (collectionView.frame.width-self.actualItemSize.width)*0.5 : (collectionView.frame.height-self.actualItemSize.height)*0.5
  66. self.itemSpacing = (self.scrollDirection == .horizontal ? self.actualItemSize.width : self.actualItemSize.height) + self.actualInteritemSpacing
  67. // Calculate and cache contentSize, rather than calculating each time
  68. self.contentSize = {
  69. let numberOfItems = self.numberOfItems*self.numberOfSections
  70. switch self.scrollDirection {
  71. case .horizontal:
  72. var contentSizeWidth: CGFloat = self.leadingSpacing*2 // Leading & trailing spacing
  73. contentSizeWidth += CGFloat(numberOfItems-1)*self.actualInteritemSpacing // Interitem spacing
  74. contentSizeWidth += CGFloat(numberOfItems)*self.actualItemSize.width // Item sizes
  75. let contentSize = CGSize(width: contentSizeWidth, height: collectionView.frame.height)
  76. return contentSize
  77. case .vertical:
  78. var contentSizeHeight: CGFloat = self.leadingSpacing*2 // Leading & trailing spacing
  79. contentSizeHeight += CGFloat(numberOfItems-1)*self.actualInteritemSpacing // Interitem spacing
  80. contentSizeHeight += CGFloat(numberOfItems)*self.actualItemSize.height // Item sizes
  81. let contentSize = CGSize(width: collectionView.frame.width, height: contentSizeHeight)
  82. return contentSize
  83. }
  84. }()
  85. self.adjustCollectionViewBounds()
  86. }
  87. override open var collectionViewContentSize: CGSize {
  88. return self.contentSize
  89. }
  90. override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
  91. return true
  92. }
  93. override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
  94. var layoutAttributes = [UICollectionViewLayoutAttributes]()
  95. guard self.itemSpacing > 0, !rect.isEmpty else {
  96. return layoutAttributes
  97. }
  98. let rect = rect.intersection(CGRect(origin: .zero, size: self.contentSize))
  99. guard !rect.isEmpty else {
  100. return layoutAttributes
  101. }
  102. // Calculate start position and index of certain rects
  103. let numberOfItemsBefore = self.scrollDirection == .horizontal ? max(Int((rect.minX-self.leadingSpacing)/self.itemSpacing),0) : max(Int((rect.minY-self.leadingSpacing)/self.itemSpacing),0)
  104. let startPosition = self.leadingSpacing + CGFloat(numberOfItemsBefore)*self.itemSpacing
  105. let startIndex = numberOfItemsBefore
  106. // Create layout attributes
  107. var itemIndex = startIndex
  108. var origin = startPosition
  109. let maxPosition = self.scrollDirection == .horizontal ? min(rect.maxX,self.contentSize.width-self.actualItemSize.width-self.leadingSpacing) : min(rect.maxY,self.contentSize.height-self.actualItemSize.height-self.leadingSpacing)
  110. // https://stackoverflow.com/a/10335601/2398107
  111. while origin-maxPosition <= max(CGFloat(100.0) * .ulpOfOne * abs(origin+maxPosition), .leastNonzeroMagnitude) {
  112. let indexPath = IndexPath(item: itemIndex%self.numberOfItems, section: itemIndex/self.numberOfItems)
  113. let attributes = self.layoutAttributesForItem(at: indexPath) as! FSPagerViewLayoutAttributes
  114. self.applyTransform(to: attributes, with: self.pagerView?.transformer)
  115. layoutAttributes.append(attributes)
  116. itemIndex += 1
  117. origin += self.itemSpacing
  118. }
  119. return layoutAttributes
  120. }
  121. override open func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
  122. let attributes = FSPagerViewLayoutAttributes(forCellWith: indexPath)
  123. attributes.indexPath = indexPath
  124. let frame = self.frame(for: indexPath)
  125. let center = CGPoint(x: frame.midX, y: frame.midY)
  126. attributes.center = center
  127. attributes.size = self.actualItemSize
  128. return attributes
  129. }
  130. override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
  131. guard let collectionView = self.collectionView, let pagerView = self.pagerView else {
  132. return proposedContentOffset
  133. }
  134. var proposedContentOffset = proposedContentOffset
  135. func calculateTargetOffset(by proposedOffset: CGFloat, boundedOffset: CGFloat) -> CGFloat {
  136. var targetOffset: CGFloat
  137. if pagerView.decelerationDistance == FSPagerView.automaticDistance {
  138. if abs(velocity.x) >= 0.3 {
  139. let vector: CGFloat = velocity.x >= 0 ? 1.0 : -1.0
  140. targetOffset = round(proposedOffset/self.itemSpacing+0.35*vector) * self.itemSpacing // Ceil by 0.15, rather than 0.5
  141. } else {
  142. targetOffset = round(proposedOffset/self.itemSpacing) * self.itemSpacing
  143. }
  144. } else {
  145. let extraDistance = max(pagerView.decelerationDistance-1, 0)
  146. switch velocity.x {
  147. case 0.3 ... CGFloat.greatestFiniteMagnitude:
  148. targetOffset = ceil(collectionView.contentOffset.x/self.itemSpacing+CGFloat(extraDistance)) * self.itemSpacing
  149. case -CGFloat.greatestFiniteMagnitude ... -0.3:
  150. targetOffset = floor(collectionView.contentOffset.x/self.itemSpacing-CGFloat(extraDistance)) * self.itemSpacing
  151. default:
  152. targetOffset = round(proposedOffset/self.itemSpacing) * self.itemSpacing
  153. }
  154. }
  155. targetOffset = max(0, targetOffset)
  156. targetOffset = min(boundedOffset, targetOffset)
  157. return targetOffset
  158. }
  159. let proposedContentOffsetX: CGFloat = {
  160. if self.scrollDirection == .vertical {
  161. return proposedContentOffset.x
  162. }
  163. let boundedOffset = collectionView.contentSize.width-self.itemSpacing
  164. return calculateTargetOffset(by: proposedContentOffset.x, boundedOffset: boundedOffset)
  165. }()
  166. let proposedContentOffsetY: CGFloat = {
  167. if self.scrollDirection == .horizontal {
  168. return proposedContentOffset.y
  169. }
  170. let boundedOffset = collectionView.contentSize.height-self.itemSpacing
  171. return calculateTargetOffset(by: proposedContentOffset.y, boundedOffset: boundedOffset)
  172. }()
  173. proposedContentOffset = CGPoint(x: proposedContentOffsetX, y: proposedContentOffsetY)
  174. return proposedContentOffset
  175. }
  176. // MARK:- Internal functions
  177. internal func forceInvalidate() {
  178. self.needsReprepare = true
  179. self.invalidateLayout()
  180. }
  181. internal func contentOffset(for indexPath: IndexPath) -> CGPoint {
  182. let origin = self.frame(for: indexPath).origin
  183. guard let collectionView = self.collectionView else {
  184. return origin
  185. }
  186. let contentOffsetX: CGFloat = {
  187. if self.scrollDirection == .vertical {
  188. return 0
  189. }
  190. let contentOffsetX = origin.x - (collectionView.frame.width*0.5-self.actualItemSize.width*0.5)
  191. return contentOffsetX
  192. }()
  193. let contentOffsetY: CGFloat = {
  194. if self.scrollDirection == .horizontal {
  195. return 0
  196. }
  197. let contentOffsetY = origin.y - (collectionView.frame.height*0.5-self.actualItemSize.height*0.5)
  198. return contentOffsetY
  199. }()
  200. let contentOffset = CGPoint(x: contentOffsetX, y: contentOffsetY)
  201. return contentOffset
  202. }
  203. internal func frame(for indexPath: IndexPath) -> CGRect {
  204. let numberOfItems = self.numberOfItems*indexPath.section + indexPath.item
  205. let originX: CGFloat = {
  206. if self.scrollDirection == .vertical {
  207. return (self.collectionView!.frame.width-self.actualItemSize.width)*0.5
  208. }
  209. return self.leadingSpacing + CGFloat(numberOfItems)*self.itemSpacing
  210. }()
  211. let originY: CGFloat = {
  212. if self.scrollDirection == .horizontal {
  213. return (self.collectionView!.frame.height-self.actualItemSize.height)*0.5
  214. }
  215. return self.leadingSpacing + CGFloat(numberOfItems)*self.itemSpacing
  216. }()
  217. let origin = CGPoint(x: originX, y: originY)
  218. let frame = CGRect(origin: origin, size: self.actualItemSize)
  219. return frame
  220. }
  221. // MARK:- Notification
  222. @objc
  223. fileprivate func didReceiveNotification(notification: Notification) {
  224. if self.pagerView?.itemSize == .zero {
  225. self.adjustCollectionViewBounds()
  226. }
  227. }
  228. // MARK:- Private functions
  229. fileprivate func commonInit() {
  230. #if !os(tvOS)
  231. NotificationCenter.default.addObserver(self, selector: #selector(didReceiveNotification(notification:)), name: UIDevice.orientationDidChangeNotification, object: nil)
  232. #endif
  233. }
  234. fileprivate func adjustCollectionViewBounds() {
  235. guard let collectionView = self.collectionView, let pagerView = self.pagerView else {
  236. return
  237. }
  238. let currentIndex = pagerView.currentIndex
  239. let newIndexPath = IndexPath(item: currentIndex, section: pagerView.isInfinite ? self.numberOfSections/2 : 0)
  240. let contentOffset = self.contentOffset(for: newIndexPath)
  241. let newBounds = CGRect(origin: contentOffset, size: collectionView.frame.size)
  242. collectionView.bounds = newBounds
  243. }
  244. fileprivate func applyTransform(to attributes: FSPagerViewLayoutAttributes, with transformer: FSPagerViewTransformer?) {
  245. guard let collectionView = self.collectionView else {
  246. return
  247. }
  248. guard let transformer = transformer else {
  249. return
  250. }
  251. switch self.scrollDirection {
  252. case .horizontal:
  253. let ruler = collectionView.bounds.midX
  254. attributes.position = (attributes.center.x-ruler)/self.itemSpacing
  255. case .vertical:
  256. let ruler = collectionView.bounds.midY
  257. attributes.position = (attributes.center.y-ruler)/self.itemSpacing
  258. }
  259. attributes.zIndex = Int(self.numberOfItems)-Int(attributes.position)
  260. transformer.applyTransform(to: attributes)
  261. }
  262. }