| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- import UIKit
- public enum LNCyclePagerTransformLayoutType {
- case normal
- case linear
- case coverflow
- }
- public enum LNCyclePagerScrollDirection {
- case horizontal
- case vertical
- }
- public final class LNCyclePagerViewLayout {
- weak var pageView: UIView?
- public var itemSize: CGSize = .zero
- public var itemSpacing: CGFloat = 0
- public var sectionInset: UIEdgeInsets = .zero
- public var layoutType: LNCyclePagerTransformLayoutType = .normal
- public var scrollDirection: LNCyclePagerScrollDirection = .horizontal
- public var minimumScale: CGFloat = 0.8
- public var minimumAlpha: CGFloat = 1.0
- public var maximumAngle: CGFloat = 0.2
- public var isInfiniteLoop = true
- public var rateOfChange: CGFloat = 0.4
- public var adjustSpacingWhenScrolling = true
- public var itemVerticalCenter = true
- public var itemHorizontalCenter = false
- public init() {}
- public var onlyOneSectionInset: UIEdgeInsets {
- if scrollDirection == .horizontal {
- let leftSpace = (pageView != nil && !isInfiniteLoop && itemHorizontalCenter) ? ((pageView!.bounds.width - itemSize.width) * 0.5) : sectionInset.left
- let rightSpace = (pageView != nil && !isInfiniteLoop && itemHorizontalCenter) ? ((pageView!.bounds.width - itemSize.width) * 0.5) : sectionInset.right
- if itemVerticalCenter, let pageView {
- let vertical = (pageView.bounds.height - itemSize.height) * 0.5
- return UIEdgeInsets(top: vertical, left: leftSpace, bottom: vertical, right: rightSpace)
- }
- return UIEdgeInsets(top: sectionInset.top, left: leftSpace, bottom: sectionInset.bottom, right: rightSpace)
- }
- let topSpace = (pageView != nil && !isInfiniteLoop) ? ((pageView!.bounds.height - itemSize.height) * 0.5) : sectionInset.top
- let bottomSpace = (pageView != nil && !isInfiniteLoop) ? ((pageView!.bounds.height - itemSize.height) * 0.5) : sectionInset.bottom
- let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
- return UIEdgeInsets(top: topSpace, left: horizontal, bottom: bottomSpace, right: horizontal)
- }
- public var firstSectionInset: UIEdgeInsets {
- if scrollDirection == .horizontal {
- if itemVerticalCenter, let pageView {
- let vertical = (pageView.bounds.height - itemSize.height) * 0.5
- return UIEdgeInsets(top: vertical, left: sectionInset.left, bottom: vertical, right: itemSpacing)
- }
- return UIEdgeInsets(top: sectionInset.top, left: sectionInset.left, bottom: sectionInset.bottom, right: itemSpacing)
- }
- let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
- return UIEdgeInsets(top: sectionInset.top, left: horizontal, bottom: itemSpacing, right: horizontal)
- }
- public var lastSectionInset: UIEdgeInsets {
- if scrollDirection == .horizontal {
- if itemVerticalCenter, let pageView {
- let vertical = (pageView.bounds.height - itemSize.height) * 0.5
- return UIEdgeInsets(top: vertical, left: 0, bottom: vertical, right: sectionInset.right)
- }
- return UIEdgeInsets(top: sectionInset.top, left: 0, bottom: sectionInset.bottom, right: sectionInset.right)
- }
- let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
- return UIEdgeInsets(top: 0, left: horizontal, bottom: sectionInset.bottom, right: horizontal)
- }
- public var middleSectionInset: UIEdgeInsets {
- if scrollDirection == .horizontal {
- if itemVerticalCenter, let pageView {
- let vertical = (pageView.bounds.height - itemSize.height) * 0.5
- return UIEdgeInsets(top: vertical, left: 0, bottom: vertical, right: itemSpacing)
- }
- return sectionInset
- }
- let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
- return UIEdgeInsets(top: 0, left: horizontal, bottom: itemSpacing, right: horizontal)
- }
- }
- public protocol LNCyclePagerTransformLayoutDelegate: AnyObject {
- func pagerTransformLayout(_ layout: LNCyclePagerTransformLayout, initialize attributes: UICollectionViewLayoutAttributes)
- func pagerTransformLayout(_ layout: LNCyclePagerTransformLayout, apply attributes: UICollectionViewLayoutAttributes)
- }
- public final class LNCyclePagerTransformLayout: UICollectionViewFlowLayout {
- public weak var transformDelegate: LNCyclePagerTransformLayoutDelegate?
- public var layoutConfig: LNCyclePagerViewLayout? {
- didSet {
- guard let layoutConfig else { return }
- layoutConfig.pageView = collectionView
- itemSize = layoutConfig.itemSize
- minimumInteritemSpacing = layoutConfig.itemSpacing
- minimumLineSpacing = layoutConfig.itemSpacing
- scrollDirection = layoutConfig.scrollDirection == .horizontal ? .horizontal : .vertical
- invalidateLayout()
- }
- }
- private enum ItemDirection {
- case leading
- case center
- case trailing
- }
- public override init() {
- super.init()
- scrollDirection = .horizontal
- }
- public required init?(coder: NSCoder) {
- super.init(coder: coder)
- scrollDirection = .horizontal
- }
- public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
- guard let layoutConfig else {
- return super.shouldInvalidateLayout(forBoundsChange: newBounds)
- }
- return layoutConfig.layoutType == .normal ? super.shouldInvalidateLayout(forBoundsChange: newBounds) : true
- }
- public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
- guard let attrs = super.layoutAttributesForElements(in: rect), let layoutConfig else {
- return super.layoutAttributesForElements(in: rect)
- }
- if transformDelegate == nil, layoutConfig.layoutType == .normal {
- return attrs
- }
- let copied = attrs.compactMap { $0.copy() as? UICollectionViewLayoutAttributes }
- let visibleRect = CGRect(origin: collectionView?.contentOffset ?? .zero, size: collectionView?.bounds.size ?? .zero)
- for attr in copied where attr.representedElementCategory == .cell {
- guard visibleRect.intersects(attr.frame) else { continue }
- if let transformDelegate {
- transformDelegate.pagerTransformLayout(self, apply: attr)
- } else {
- applyTransform(to: attr, type: layoutConfig.layoutType)
- }
- }
- return copied
- }
- public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
- guard let attr = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
- return nil
- }
- guard let layoutConfig else { return attr }
- if let transformDelegate {
- transformDelegate.pagerTransformLayout(self, initialize: attr)
- } else if layoutConfig.layoutType != .normal {
- initializeTransform(to: attr, type: layoutConfig.layoutType)
- }
- return attr
- }
- private func itemDirection(for center: CGPoint) -> ItemDirection {
- guard let collectionView else { return .center }
- if scrollDirection == .horizontal {
- let contentCenterX = collectionView.contentOffset.x + collectionView.bounds.width * 0.5
- if abs(center.x - contentCenterX) < 0.5 { return .center }
- return center.x < contentCenterX ? .leading : .trailing
- }
- let contentCenterY = collectionView.contentOffset.y + collectionView.bounds.height * 0.5
- if abs(center.y - contentCenterY) < 0.5 { return .center }
- return center.y < contentCenterY ? .leading : .trailing
- }
- private func initializeTransform(to attributes: UICollectionViewLayoutAttributes, type: LNCyclePagerTransformLayoutType) {
- guard let layoutConfig else { return }
- switch type {
- case .linear:
- applyLinear(to: attributes, scale: layoutConfig.minimumScale, alpha: layoutConfig.minimumAlpha)
- case .coverflow:
- applyCoverflow(to: attributes, angle: layoutConfig.maximumAngle, alpha: layoutConfig.minimumAlpha)
- case .normal:
- break
- }
- }
- private func applyTransform(to attributes: UICollectionViewLayoutAttributes, type: LNCyclePagerTransformLayoutType) {
- switch type {
- case .linear:
- applyLinear(to: attributes)
- case .coverflow:
- applyCoverflow(to: attributes)
- case .normal:
- break
- }
- }
- private func applyLinear(to attributes: UICollectionViewLayoutAttributes) {
- guard let layoutConfig, let collectionView else { return }
- if scrollDirection == .horizontal {
- let width = collectionView.bounds.width
- guard width > 0 else { return }
- let centerX = collectionView.contentOffset.x + width * 0.5
- let delta = abs(attributes.center.x - centerX)
- let scale = max(1 - delta / width * layoutConfig.rateOfChange, layoutConfig.minimumScale)
- let alpha = max(1 - delta / width, layoutConfig.minimumAlpha)
- applyLinear(to: attributes, scale: scale, alpha: alpha)
- return
- }
- let height = collectionView.bounds.height
- guard height > 0 else { return }
- let centerY = collectionView.contentOffset.y + height * 0.5
- let delta = abs(attributes.center.y - centerY)
- let scale = max(1 - delta / height * layoutConfig.rateOfChange, layoutConfig.minimumScale)
- let alpha = max(1 - delta / height, layoutConfig.minimumAlpha)
- applyLinear(to: attributes, scale: scale, alpha: alpha)
- }
- private func applyLinear(to attributes: UICollectionViewLayoutAttributes, scale: CGFloat, alpha: CGFloat) {
- guard let layoutConfig else { return }
- var resolvedScale = scale
- var resolvedAlpha = alpha
- var transform = CGAffineTransform(scaleX: resolvedScale, y: resolvedScale)
- if layoutConfig.adjustSpacingWhenScrolling {
- let direction = itemDirection(for: attributes.center)
- let length = scrollDirection == .horizontal ? attributes.size.width : attributes.size.height
- var translate: CGFloat = 0
- switch direction {
- case .leading:
- translate = 1.15 * length * (1 - resolvedScale) * 0.5
- case .trailing:
- translate = -1.15 * length * (1 - resolvedScale) * 0.5
- case .center:
- resolvedScale = 1
- resolvedAlpha = 1
- transform = CGAffineTransform.identity
- }
- if direction != .center {
- if scrollDirection == .horizontal {
- transform = transform.translatedBy(x: translate, y: 0)
- } else {
- transform = transform.translatedBy(x: 0, y: translate)
- }
- }
- }
- attributes.transform = transform
- attributes.alpha = resolvedAlpha
- }
- private func applyCoverflow(to attributes: UICollectionViewLayoutAttributes) {
- guard let layoutConfig, let collectionView else { return }
- if scrollDirection == .horizontal {
- let width = collectionView.bounds.width
- guard width > 0 else { return }
- let centerX = collectionView.contentOffset.x + width * 0.5
- let delta = abs(attributes.center.x - centerX)
- let angle = min(delta / width * (1 - layoutConfig.rateOfChange), layoutConfig.maximumAngle)
- let alpha = max(1 - delta / width, layoutConfig.minimumAlpha)
- applyCoverflow(to: attributes, angle: angle, alpha: alpha)
- return
- }
- let height = collectionView.bounds.height
- guard height > 0 else { return }
- let centerY = collectionView.contentOffset.y + height * 0.5
- let delta = abs(attributes.center.y - centerY)
- let angle = min(delta / height * (1 - layoutConfig.rateOfChange), layoutConfig.maximumAngle)
- let alpha = max(1 - delta / height, layoutConfig.minimumAlpha)
- applyCoverflow(to: attributes, angle: angle, alpha: alpha)
- }
- private func applyCoverflow(to attributes: UICollectionViewLayoutAttributes, angle: CGFloat, alpha: CGFloat) {
- guard let layoutConfig else { return }
- var resolvedAngle = angle
- var resolvedAlpha = alpha
- let direction = itemDirection(for: attributes.center)
- var transform3D = CATransform3DIdentity
- transform3D.m34 = -0.002
- var translate: CGFloat = 0
- switch direction {
- case .leading:
- let length = scrollDirection == .horizontal ? attributes.size.width : attributes.size.height
- translate = (1 - cos(resolvedAngle * 1.2 * .pi)) * length
- case .trailing:
- let length = scrollDirection == .horizontal ? attributes.size.width : attributes.size.height
- translate = -(1 - cos(resolvedAngle * 1.2 * .pi)) * length
- resolvedAngle = -resolvedAngle
- case .center:
- resolvedAngle = 0
- resolvedAlpha = 1
- }
- if scrollDirection == .horizontal {
- transform3D = CATransform3DRotate(transform3D, .pi * resolvedAngle, 0, 1, 0)
- } else {
- transform3D = CATransform3DRotate(transform3D, .pi * (-resolvedAngle), 1, 0, 0)
- }
- if layoutConfig.adjustSpacingWhenScrolling {
- if scrollDirection == .horizontal {
- transform3D = CATransform3DTranslate(transform3D, translate, 0, 0)
- } else {
- transform3D = CATransform3DTranslate(transform3D, 0, translate, 0)
- }
- }
- attributes.transform3D = transform3D
- attributes.alpha = resolvedAlpha
- }
- }
|