LNCyclePagerTransformLayout.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import UIKit
  2. public enum LNCyclePagerTransformLayoutType {
  3. case normal
  4. case linear
  5. case coverflow
  6. }
  7. public enum LNCyclePagerScrollDirection {
  8. case horizontal
  9. case vertical
  10. }
  11. public final class LNCyclePagerViewLayout {
  12. weak var pageView: UIView?
  13. public var itemSize: CGSize = .zero
  14. public var itemSpacing: CGFloat = 0
  15. public var sectionInset: UIEdgeInsets = .zero
  16. public var layoutType: LNCyclePagerTransformLayoutType = .normal
  17. public var scrollDirection: LNCyclePagerScrollDirection = .horizontal
  18. public var minimumScale: CGFloat = 0.8
  19. public var minimumAlpha: CGFloat = 1.0
  20. public var maximumAngle: CGFloat = 0.2
  21. public var isInfiniteLoop = true
  22. public var rateOfChange: CGFloat = 0.4
  23. public var adjustSpacingWhenScrolling = true
  24. public var itemVerticalCenter = true
  25. public var itemHorizontalCenter = false
  26. public init() {}
  27. public var onlyOneSectionInset: UIEdgeInsets {
  28. if scrollDirection == .horizontal {
  29. let leftSpace = (pageView != nil && !isInfiniteLoop && itemHorizontalCenter) ? ((pageView!.bounds.width - itemSize.width) * 0.5) : sectionInset.left
  30. let rightSpace = (pageView != nil && !isInfiniteLoop && itemHorizontalCenter) ? ((pageView!.bounds.width - itemSize.width) * 0.5) : sectionInset.right
  31. if itemVerticalCenter, let pageView {
  32. let vertical = (pageView.bounds.height - itemSize.height) * 0.5
  33. return UIEdgeInsets(top: vertical, left: leftSpace, bottom: vertical, right: rightSpace)
  34. }
  35. return UIEdgeInsets(top: sectionInset.top, left: leftSpace, bottom: sectionInset.bottom, right: rightSpace)
  36. }
  37. let topSpace = (pageView != nil && !isInfiniteLoop) ? ((pageView!.bounds.height - itemSize.height) * 0.5) : sectionInset.top
  38. let bottomSpace = (pageView != nil && !isInfiniteLoop) ? ((pageView!.bounds.height - itemSize.height) * 0.5) : sectionInset.bottom
  39. let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
  40. return UIEdgeInsets(top: topSpace, left: horizontal, bottom: bottomSpace, right: horizontal)
  41. }
  42. public var firstSectionInset: UIEdgeInsets {
  43. if scrollDirection == .horizontal {
  44. if itemVerticalCenter, let pageView {
  45. let vertical = (pageView.bounds.height - itemSize.height) * 0.5
  46. return UIEdgeInsets(top: vertical, left: sectionInset.left, bottom: vertical, right: itemSpacing)
  47. }
  48. return UIEdgeInsets(top: sectionInset.top, left: sectionInset.left, bottom: sectionInset.bottom, right: itemSpacing)
  49. }
  50. let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
  51. return UIEdgeInsets(top: sectionInset.top, left: horizontal, bottom: itemSpacing, right: horizontal)
  52. }
  53. public var lastSectionInset: UIEdgeInsets {
  54. if scrollDirection == .horizontal {
  55. if itemVerticalCenter, let pageView {
  56. let vertical = (pageView.bounds.height - itemSize.height) * 0.5
  57. return UIEdgeInsets(top: vertical, left: 0, bottom: vertical, right: sectionInset.right)
  58. }
  59. return UIEdgeInsets(top: sectionInset.top, left: 0, bottom: sectionInset.bottom, right: sectionInset.right)
  60. }
  61. let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
  62. return UIEdgeInsets(top: 0, left: horizontal, bottom: sectionInset.bottom, right: horizontal)
  63. }
  64. public var middleSectionInset: UIEdgeInsets {
  65. if scrollDirection == .horizontal {
  66. if itemVerticalCenter, let pageView {
  67. let vertical = (pageView.bounds.height - itemSize.height) * 0.5
  68. return UIEdgeInsets(top: vertical, left: 0, bottom: vertical, right: itemSpacing)
  69. }
  70. return sectionInset
  71. }
  72. let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
  73. return UIEdgeInsets(top: 0, left: horizontal, bottom: itemSpacing, right: horizontal)
  74. }
  75. }
  76. public protocol LNCyclePagerTransformLayoutDelegate: AnyObject {
  77. func pagerTransformLayout(_ layout: LNCyclePagerTransformLayout, initialize attributes: UICollectionViewLayoutAttributes)
  78. func pagerTransformLayout(_ layout: LNCyclePagerTransformLayout, apply attributes: UICollectionViewLayoutAttributes)
  79. }
  80. public final class LNCyclePagerTransformLayout: UICollectionViewFlowLayout {
  81. public weak var transformDelegate: LNCyclePagerTransformLayoutDelegate?
  82. public var layoutConfig: LNCyclePagerViewLayout? {
  83. didSet {
  84. guard let layoutConfig else { return }
  85. layoutConfig.pageView = collectionView
  86. itemSize = layoutConfig.itemSize
  87. minimumInteritemSpacing = layoutConfig.itemSpacing
  88. minimumLineSpacing = layoutConfig.itemSpacing
  89. scrollDirection = layoutConfig.scrollDirection == .horizontal ? .horizontal : .vertical
  90. invalidateLayout()
  91. }
  92. }
  93. private enum ItemDirection {
  94. case leading
  95. case center
  96. case trailing
  97. }
  98. public override init() {
  99. super.init()
  100. scrollDirection = .horizontal
  101. }
  102. public required init?(coder: NSCoder) {
  103. super.init(coder: coder)
  104. scrollDirection = .horizontal
  105. }
  106. public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
  107. guard let layoutConfig else {
  108. return super.shouldInvalidateLayout(forBoundsChange: newBounds)
  109. }
  110. return layoutConfig.layoutType == .normal ? super.shouldInvalidateLayout(forBoundsChange: newBounds) : true
  111. }
  112. public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
  113. guard let attrs = super.layoutAttributesForElements(in: rect), let layoutConfig else {
  114. return super.layoutAttributesForElements(in: rect)
  115. }
  116. if transformDelegate == nil, layoutConfig.layoutType == .normal {
  117. return attrs
  118. }
  119. let copied = attrs.compactMap { $0.copy() as? UICollectionViewLayoutAttributes }
  120. let visibleRect = CGRect(origin: collectionView?.contentOffset ?? .zero, size: collectionView?.bounds.size ?? .zero)
  121. for attr in copied where attr.representedElementCategory == .cell {
  122. guard visibleRect.intersects(attr.frame) else { continue }
  123. if let transformDelegate {
  124. transformDelegate.pagerTransformLayout(self, apply: attr)
  125. } else {
  126. applyTransform(to: attr, type: layoutConfig.layoutType)
  127. }
  128. }
  129. return copied
  130. }
  131. public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
  132. guard let attr = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
  133. return nil
  134. }
  135. guard let layoutConfig else { return attr }
  136. if let transformDelegate {
  137. transformDelegate.pagerTransformLayout(self, initialize: attr)
  138. } else if layoutConfig.layoutType != .normal {
  139. initializeTransform(to: attr, type: layoutConfig.layoutType)
  140. }
  141. return attr
  142. }
  143. private func itemDirection(for center: CGPoint) -> ItemDirection {
  144. guard let collectionView else { return .center }
  145. if scrollDirection == .horizontal {
  146. let contentCenterX = collectionView.contentOffset.x + collectionView.bounds.width * 0.5
  147. if abs(center.x - contentCenterX) < 0.5 { return .center }
  148. return center.x < contentCenterX ? .leading : .trailing
  149. }
  150. let contentCenterY = collectionView.contentOffset.y + collectionView.bounds.height * 0.5
  151. if abs(center.y - contentCenterY) < 0.5 { return .center }
  152. return center.y < contentCenterY ? .leading : .trailing
  153. }
  154. private func initializeTransform(to attributes: UICollectionViewLayoutAttributes, type: LNCyclePagerTransformLayoutType) {
  155. guard let layoutConfig else { return }
  156. switch type {
  157. case .linear:
  158. applyLinear(to: attributes, scale: layoutConfig.minimumScale, alpha: layoutConfig.minimumAlpha)
  159. case .coverflow:
  160. applyCoverflow(to: attributes, angle: layoutConfig.maximumAngle, alpha: layoutConfig.minimumAlpha)
  161. case .normal:
  162. break
  163. }
  164. }
  165. private func applyTransform(to attributes: UICollectionViewLayoutAttributes, type: LNCyclePagerTransformLayoutType) {
  166. switch type {
  167. case .linear:
  168. applyLinear(to: attributes)
  169. case .coverflow:
  170. applyCoverflow(to: attributes)
  171. case .normal:
  172. break
  173. }
  174. }
  175. private func applyLinear(to attributes: UICollectionViewLayoutAttributes) {
  176. guard let layoutConfig, let collectionView else { return }
  177. if scrollDirection == .horizontal {
  178. let width = collectionView.bounds.width
  179. guard width > 0 else { return }
  180. let centerX = collectionView.contentOffset.x + width * 0.5
  181. let delta = abs(attributes.center.x - centerX)
  182. let scale = max(1 - delta / width * layoutConfig.rateOfChange, layoutConfig.minimumScale)
  183. let alpha = max(1 - delta / width, layoutConfig.minimumAlpha)
  184. applyLinear(to: attributes, scale: scale, alpha: alpha)
  185. return
  186. }
  187. let height = collectionView.bounds.height
  188. guard height > 0 else { return }
  189. let centerY = collectionView.contentOffset.y + height * 0.5
  190. let delta = abs(attributes.center.y - centerY)
  191. let scale = max(1 - delta / height * layoutConfig.rateOfChange, layoutConfig.minimumScale)
  192. let alpha = max(1 - delta / height, layoutConfig.minimumAlpha)
  193. applyLinear(to: attributes, scale: scale, alpha: alpha)
  194. }
  195. private func applyLinear(to attributes: UICollectionViewLayoutAttributes, scale: CGFloat, alpha: CGFloat) {
  196. guard let layoutConfig else { return }
  197. var resolvedScale = scale
  198. var resolvedAlpha = alpha
  199. var transform = CGAffineTransform(scaleX: resolvedScale, y: resolvedScale)
  200. if layoutConfig.adjustSpacingWhenScrolling {
  201. let direction = itemDirection(for: attributes.center)
  202. let length = scrollDirection == .horizontal ? attributes.size.width : attributes.size.height
  203. var translate: CGFloat = 0
  204. switch direction {
  205. case .leading:
  206. translate = 1.15 * length * (1 - resolvedScale) * 0.5
  207. case .trailing:
  208. translate = -1.15 * length * (1 - resolvedScale) * 0.5
  209. case .center:
  210. resolvedScale = 1
  211. resolvedAlpha = 1
  212. transform = CGAffineTransform.identity
  213. }
  214. if direction != .center {
  215. if scrollDirection == .horizontal {
  216. transform = transform.translatedBy(x: translate, y: 0)
  217. } else {
  218. transform = transform.translatedBy(x: 0, y: translate)
  219. }
  220. }
  221. }
  222. attributes.transform = transform
  223. attributes.alpha = resolvedAlpha
  224. }
  225. private func applyCoverflow(to attributes: UICollectionViewLayoutAttributes) {
  226. guard let layoutConfig, let collectionView else { return }
  227. if scrollDirection == .horizontal {
  228. let width = collectionView.bounds.width
  229. guard width > 0 else { return }
  230. let centerX = collectionView.contentOffset.x + width * 0.5
  231. let delta = abs(attributes.center.x - centerX)
  232. let angle = min(delta / width * (1 - layoutConfig.rateOfChange), layoutConfig.maximumAngle)
  233. let alpha = max(1 - delta / width, layoutConfig.minimumAlpha)
  234. applyCoverflow(to: attributes, angle: angle, alpha: alpha)
  235. return
  236. }
  237. let height = collectionView.bounds.height
  238. guard height > 0 else { return }
  239. let centerY = collectionView.contentOffset.y + height * 0.5
  240. let delta = abs(attributes.center.y - centerY)
  241. let angle = min(delta / height * (1 - layoutConfig.rateOfChange), layoutConfig.maximumAngle)
  242. let alpha = max(1 - delta / height, layoutConfig.minimumAlpha)
  243. applyCoverflow(to: attributes, angle: angle, alpha: alpha)
  244. }
  245. private func applyCoverflow(to attributes: UICollectionViewLayoutAttributes, angle: CGFloat, alpha: CGFloat) {
  246. guard let layoutConfig else { return }
  247. var resolvedAngle = angle
  248. var resolvedAlpha = alpha
  249. let direction = itemDirection(for: attributes.center)
  250. var transform3D = CATransform3DIdentity
  251. transform3D.m34 = -0.002
  252. var translate: CGFloat = 0
  253. switch direction {
  254. case .leading:
  255. let length = scrollDirection == .horizontal ? attributes.size.width : attributes.size.height
  256. translate = (1 - cos(resolvedAngle * 1.2 * .pi)) * length
  257. case .trailing:
  258. let length = scrollDirection == .horizontal ? attributes.size.width : attributes.size.height
  259. translate = -(1 - cos(resolvedAngle * 1.2 * .pi)) * length
  260. resolvedAngle = -resolvedAngle
  261. case .center:
  262. resolvedAngle = 0
  263. resolvedAlpha = 1
  264. }
  265. if scrollDirection == .horizontal {
  266. transform3D = CATransform3DRotate(transform3D, .pi * resolvedAngle, 0, 1, 0)
  267. } else {
  268. transform3D = CATransform3DRotate(transform3D, .pi * (-resolvedAngle), 1, 0, 0)
  269. }
  270. if layoutConfig.adjustSpacingWhenScrolling {
  271. if scrollDirection == .horizontal {
  272. transform3D = CATransform3DTranslate(transform3D, translate, 0, 0)
  273. } else {
  274. transform3D = CATransform3DTranslate(transform3D, 0, translate, 0)
  275. }
  276. }
  277. attributes.transform3D = transform3D
  278. attributes.alpha = resolvedAlpha
  279. }
  280. }