|
|
@@ -0,0 +1,537 @@
|
|
|
+import UIKit
|
|
|
+
|
|
|
+public struct LNIndexSection: Equatable {
|
|
|
+ public var index: Int
|
|
|
+ public var section: Int
|
|
|
+
|
|
|
+ public init(index: Int, section: Int) {
|
|
|
+ self.index = index
|
|
|
+ self.section = section
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+public enum LNPagerScrollDirection {
|
|
|
+ case left
|
|
|
+ case right
|
|
|
+}
|
|
|
+
|
|
|
+public final class LNCyclePager: UIView {
|
|
|
+ private let maxSectionCount = 200
|
|
|
+ private let minSectionCount = 18
|
|
|
+
|
|
|
+ private let transformLayout = LNCyclePagerTransformLayout()
|
|
|
+ private(set) lazy var collectionView: UICollectionView = {
|
|
|
+ let view = UICollectionView(frame: .zero, collectionViewLayout: transformLayout)
|
|
|
+ view.backgroundColor = .clear
|
|
|
+ view.showsHorizontalScrollIndicator = false
|
|
|
+ view.showsVerticalScrollIndicator = false
|
|
|
+ view.dataSource = self
|
|
|
+ view.delegate = self
|
|
|
+ view.decelerationRate = UIScrollView.DecelerationRate(rawValue: 1 - 0.0076)
|
|
|
+ if #available(iOS 10.0, *) {
|
|
|
+ view.isPrefetchingEnabled = false
|
|
|
+ }
|
|
|
+ return view
|
|
|
+ }()
|
|
|
+
|
|
|
+ public var numberOfItemsProvider: (() -> Int)?
|
|
|
+ public var cellProvider: ((_ pagerView: LNCyclePager, _ index: Int) -> UICollectionViewCell)?
|
|
|
+ public var layoutProvider: (() -> LNCyclePagerViewLayout)?
|
|
|
+
|
|
|
+ public var didScrollFromIndexToIndex: ((_ from: Int, _ to: Int) -> Void)?
|
|
|
+ public var didSelectItem: ((_ cell: UICollectionViewCell, _ index: Int) -> Void)?
|
|
|
+ public var didSelectItemAtIndexSection: ((_ cell: UICollectionViewCell, _ indexSection: LNIndexSection) -> Void)?
|
|
|
+ public var didScroll: (() -> Void)?
|
|
|
+
|
|
|
+ public var initializeTransformAttributes: ((UICollectionViewLayoutAttributes) -> Void)? {
|
|
|
+ didSet { updateTransformLayoutDelegateBinding() }
|
|
|
+ }
|
|
|
+ public var applyTransformAttributes: ((UICollectionViewLayoutAttributes) -> Void)? {
|
|
|
+ didSet { updateTransformLayoutDelegateBinding() }
|
|
|
+ }
|
|
|
+
|
|
|
+ public var backgroundView: UIView? {
|
|
|
+ get { collectionView.backgroundView }
|
|
|
+ set { collectionView.backgroundView = newValue }
|
|
|
+ }
|
|
|
+
|
|
|
+ public var layout: LNCyclePagerViewLayout? {
|
|
|
+ didSet {
|
|
|
+ layout?.isInfiniteLoop = isInfiniteLoop
|
|
|
+ updateTransformLayoutDelegateBinding()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public var isInfiniteLoop = true {
|
|
|
+ didSet {
|
|
|
+ layout?.isInfiniteLoop = isInfiniteLoop
|
|
|
+ reloadData()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public var autoScrollInterval: TimeInterval = 0 {
|
|
|
+ didSet {
|
|
|
+ stopTimer()
|
|
|
+ if autoScrollInterval > 0, superview != nil {
|
|
|
+ startTimerIfNeeded()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public var reloadDataNeedResetIndex = false
|
|
|
+ public var currentIndex: Int { indexSection.index }
|
|
|
+ public private(set) var indexSection = LNIndexSection(index: -1, section: -1)
|
|
|
+
|
|
|
+ public var contentOffset: CGPoint { collectionView.contentOffset }
|
|
|
+ public var tracking: Bool { collectionView.isTracking }
|
|
|
+ public var dragging: Bool { collectionView.isDragging }
|
|
|
+ public var decelerating: Bool { collectionView.isDecelerating }
|
|
|
+
|
|
|
+ private var timer: Timer?
|
|
|
+ private var numberOfItems = 0
|
|
|
+ private var dequeueSection = 0
|
|
|
+ private var beginDragIndexSection = LNIndexSection(index: 0, section: 0)
|
|
|
+ private var firstScrollIndex = -1
|
|
|
+
|
|
|
+ private var needClearLayout = false
|
|
|
+ private var didReloadData = false
|
|
|
+ private var didLayout = false
|
|
|
+ private var needResetIndex = false
|
|
|
+ private weak var boundPageControl: LNCyclePageControl?
|
|
|
+
|
|
|
+ public override init(frame: CGRect) {
|
|
|
+ super.init(frame: frame)
|
|
|
+ addSubview(collectionView)
|
|
|
+ updateTransformLayoutDelegateBinding()
|
|
|
+ }
|
|
|
+
|
|
|
+ public required init?(coder: NSCoder) {
|
|
|
+ super.init(coder: coder)
|
|
|
+ addSubview(collectionView)
|
|
|
+ updateTransformLayoutDelegateBinding()
|
|
|
+ }
|
|
|
+
|
|
|
+ deinit {
|
|
|
+ stopTimer()
|
|
|
+ }
|
|
|
+
|
|
|
+ public override func didMoveToSuperview() {
|
|
|
+ super.didMoveToSuperview()
|
|
|
+ stopTimer()
|
|
|
+ if superview != nil, autoScrollInterval > 0 {
|
|
|
+ startTimerIfNeeded()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public override func layoutSubviews() {
|
|
|
+ super.layoutSubviews()
|
|
|
+ let needUpdateLayout = collectionView.frame != bounds
|
|
|
+ collectionView.frame = bounds
|
|
|
+ if (indexSection.section < 0 || needUpdateLayout), (numberOfItems > 0 || didReloadData) {
|
|
|
+ didLayout = true
|
|
|
+ setNeedUpdateLayout()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func bindPageControl(_ pageControl: LNCyclePageControl?) {
|
|
|
+ boundPageControl = pageControl
|
|
|
+ guard let pageControl else { return }
|
|
|
+ pageControl.numberOfPages = numberOfItems
|
|
|
+ pageControl.currentPage = max(indexSection.index, 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func register(_ cellClass: AnyClass, forCellWithReuseIdentifier identifier: String) {
|
|
|
+ collectionView.register(cellClass, forCellWithReuseIdentifier: identifier)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func register(_ nib: UINib, forCellWithReuseIdentifier identifier: String) {
|
|
|
+ collectionView.register(nib, forCellWithReuseIdentifier: identifier)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func dequeueReusableCell(withReuseIdentifier identifier: String, for index: Int) -> UICollectionViewCell {
|
|
|
+ collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: IndexPath(item: index, section: dequeueSection))
|
|
|
+ }
|
|
|
+
|
|
|
+ public func reloadData() {
|
|
|
+ didReloadData = true
|
|
|
+ needResetIndex = true
|
|
|
+ setNeedClearLayout()
|
|
|
+ clearLayoutIfNeeded()
|
|
|
+ updateData()
|
|
|
+ }
|
|
|
+
|
|
|
+ public func updateData() {
|
|
|
+ updateLayout()
|
|
|
+ numberOfItems = max(0, numberOfItemsProvider?() ?? 0)
|
|
|
+ collectionView.reloadData()
|
|
|
+
|
|
|
+ if !didLayout, !collectionView.frame.isEmpty, indexSection.index < 0 {
|
|
|
+ didLayout = true
|
|
|
+ }
|
|
|
+
|
|
|
+ let shouldReset = needResetIndex && reloadDataNeedResetIndex
|
|
|
+ needResetIndex = false
|
|
|
+
|
|
|
+ if shouldReset {
|
|
|
+ stopTimer()
|
|
|
+ }
|
|
|
+
|
|
|
+ let fallback = (indexSection.index < 0 && !collectionView.frame.isEmpty) || shouldReset ? 0 : indexSection.index
|
|
|
+ resetPagerView(at: fallback)
|
|
|
+
|
|
|
+ boundPageControl?.numberOfPages = numberOfItems
|
|
|
+ boundPageControl?.setCurrentPage(max(indexSection.index, 0), animate: false)
|
|
|
+
|
|
|
+ if shouldReset {
|
|
|
+ startTimerIfNeeded()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func setNeedUpdateLayout() {
|
|
|
+ guard effectiveLayout() != nil else { return }
|
|
|
+ clearLayoutIfNeeded()
|
|
|
+ updateLayout()
|
|
|
+ collectionView.collectionViewLayout.invalidateLayout()
|
|
|
+ resetPagerView(at: indexSection.index < 0 ? 0 : indexSection.index)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func setNeedClearLayout() {
|
|
|
+ needClearLayout = true
|
|
|
+ }
|
|
|
+
|
|
|
+ public func currentIndexCell() -> UICollectionViewCell? {
|
|
|
+ guard indexSection.index >= 0, indexSection.section >= 0 else { return nil }
|
|
|
+ return collectionView.cellForItem(at: IndexPath(item: indexSection.index, section: indexSection.section))
|
|
|
+ }
|
|
|
+
|
|
|
+ public func visibleCells() -> [UICollectionViewCell] {
|
|
|
+ collectionView.visibleCells
|
|
|
+ }
|
|
|
+
|
|
|
+ public func visibleIndexes() -> [Int] {
|
|
|
+ collectionView.indexPathsForVisibleItems.map(\.item)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollToItem(at index: Int, animated: Bool) {
|
|
|
+ if !didLayout, didReloadData {
|
|
|
+ firstScrollIndex = index
|
|
|
+ } else {
|
|
|
+ firstScrollIndex = -1
|
|
|
+ }
|
|
|
+
|
|
|
+ if !isInfiniteLoop {
|
|
|
+ scrollToItem(at: LNIndexSection(index: index, section: 0), animated: animated)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ let targetSection = index >= currentIndex ? indexSection.section : indexSection.section + 1
|
|
|
+ scrollToItem(at: LNIndexSection(index: index, section: targetSection), animated: animated)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollToItem(at target: LNIndexSection, animated: Bool) {
|
|
|
+ guard numberOfItems > 0, isValid(target) else { return }
|
|
|
+ let offset = calculateOffset(at: target)
|
|
|
+ if resolvedScrollDirection() == .horizontal {
|
|
|
+ collectionView.setContentOffset(CGPoint(x: offset, y: collectionView.contentOffset.y), animated: animated)
|
|
|
+ } else {
|
|
|
+ collectionView.setContentOffset(CGPoint(x: collectionView.contentOffset.x, y: offset), animated: animated)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollToNearlyIndex(at direction: LNPagerScrollDirection, animated: Bool) {
|
|
|
+ let target = nearlyIndexSection(from: indexSection, direction: direction)
|
|
|
+ scrollToItem(at: target, animated: animated)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func startTimerIfNeeded() {
|
|
|
+ guard timer == nil, autoScrollInterval > 0 else { return }
|
|
|
+ let timer = Timer(timeInterval: autoScrollInterval, repeats: true) { [weak self] _ in
|
|
|
+ self?.timerFired()
|
|
|
+ }
|
|
|
+ RunLoop.main.add(timer, forMode: .common)
|
|
|
+ self.timer = timer
|
|
|
+ }
|
|
|
+
|
|
|
+ private func stopTimer() {
|
|
|
+ timer?.invalidate()
|
|
|
+ timer = nil
|
|
|
+ }
|
|
|
+
|
|
|
+ private func timerFired() {
|
|
|
+ guard superview != nil, window != nil, numberOfItems > 0, !tracking else { return }
|
|
|
+ scrollToNearlyIndex(at: .right, animated: true)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateTransformLayoutDelegateBinding() {
|
|
|
+ transformLayout.transformDelegate = (applyTransformAttributes != nil || initializeTransformAttributes != nil) ? self : nil
|
|
|
+ }
|
|
|
+
|
|
|
+ private func effectiveLayout() -> LNCyclePagerViewLayout? {
|
|
|
+ if layout == nil, let provider = layoutProvider {
|
|
|
+ layout = provider()
|
|
|
+ layout?.isInfiniteLoop = isInfiniteLoop
|
|
|
+ }
|
|
|
+ if let layout, layout.itemSize.width > 0, layout.itemSize.height > 0 {
|
|
|
+ return layout
|
|
|
+ }
|
|
|
+ layout = nil
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateLayout() {
|
|
|
+ guard let layout = effectiveLayout() else { return }
|
|
|
+ layout.isInfiniteLoop = isInfiniteLoop
|
|
|
+ transformLayout.layoutConfig = layout
|
|
|
+ }
|
|
|
+
|
|
|
+ private func clearLayoutIfNeeded() {
|
|
|
+ if needClearLayout {
|
|
|
+ layout = nil
|
|
|
+ transformLayout.layoutConfig = nil
|
|
|
+ needClearLayout = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func isValid(_ target: LNIndexSection) -> Bool {
|
|
|
+ target.index >= 0 &&
|
|
|
+ target.index < numberOfItems &&
|
|
|
+ target.section >= 0 &&
|
|
|
+ target.section < maxSectionCount
|
|
|
+ }
|
|
|
+
|
|
|
+ private func nearlyIndexSection(from current: LNIndexSection, direction: LNPagerScrollDirection) -> LNIndexSection {
|
|
|
+ guard current.index >= 0, current.index < numberOfItems else { return current }
|
|
|
+
|
|
|
+ if !isInfiniteLoop {
|
|
|
+ if direction == .right, current.index == numberOfItems - 1 {
|
|
|
+ return autoScrollInterval > 0 ? LNIndexSection(index: 0, section: 0) : current
|
|
|
+ } else if direction == .right {
|
|
|
+ return LNIndexSection(index: current.index + 1, section: 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ if current.index == 0 {
|
|
|
+ return autoScrollInterval > 0 ? LNIndexSection(index: numberOfItems - 1, section: 0) : current
|
|
|
+ }
|
|
|
+ return LNIndexSection(index: current.index - 1, section: 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ if direction == .right {
|
|
|
+ if current.index < numberOfItems - 1 {
|
|
|
+ return LNIndexSection(index: current.index + 1, section: current.section)
|
|
|
+ }
|
|
|
+ if current.section >= maxSectionCount - 1 {
|
|
|
+ return LNIndexSection(index: current.index, section: maxSectionCount - 1)
|
|
|
+ }
|
|
|
+ return LNIndexSection(index: 0, section: current.section + 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ if current.index > 0 {
|
|
|
+ return LNIndexSection(index: current.index - 1, section: current.section)
|
|
|
+ }
|
|
|
+ if current.section <= 0 {
|
|
|
+ return LNIndexSection(index: current.index, section: 0)
|
|
|
+ }
|
|
|
+ return LNIndexSection(index: numberOfItems - 1, section: current.section - 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func calculateIndexSection(with offset: CGPoint) -> LNIndexSection {
|
|
|
+ guard numberOfItems > 0 else { return LNIndexSection(index: 0, section: 0) }
|
|
|
+ guard let layout = effectiveLayout() else { return LNIndexSection(index: 0, section: 0) }
|
|
|
+
|
|
|
+ let itemLength = (resolvedScrollDirection() == .horizontal ? transformLayout.itemSize.width : transformLayout.itemSize.height) + transformLayout.minimumInteritemSpacing
|
|
|
+ guard itemLength > 0 else { return LNIndexSection(index: 0, section: 0) }
|
|
|
+
|
|
|
+ let edge = isInfiniteLoop
|
|
|
+ ? (resolvedScrollDirection() == .horizontal ? layout.sectionInset.left : layout.sectionInset.top)
|
|
|
+ : (resolvedScrollDirection() == .horizontal ? layout.onlyOneSectionInset.left : layout.onlyOneSectionInset.top)
|
|
|
+
|
|
|
+ let viewportLength = resolvedScrollDirection() == .horizontal ? collectionView.bounds.width : collectionView.bounds.height
|
|
|
+ let middleOffset = (resolvedScrollDirection() == .horizontal ? offset.x : offset.y) + viewportLength * 0.5
|
|
|
+
|
|
|
+ guard middleOffset - edge >= 0 else { return LNIndexSection(index: 0, section: 0) }
|
|
|
+ let raw = Int((middleOffset - edge + transformLayout.minimumInteritemSpacing * 0.5) / itemLength)
|
|
|
+ let maxRaw = max(numberOfItems * maxSectionCount - 1, 0)
|
|
|
+ let itemIndex = max(0, min(raw, maxRaw))
|
|
|
+ return LNIndexSection(index: itemIndex % numberOfItems, section: itemIndex / numberOfItems)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func calculateOffset(at target: LNIndexSection) -> CGFloat {
|
|
|
+ guard numberOfItems > 0, let layout = effectiveLayout() else { return 0 }
|
|
|
+
|
|
|
+ let edge = isInfiniteLoop ? layout.sectionInset : layout.onlyOneSectionInset
|
|
|
+ let itemLength = (resolvedScrollDirection() == .horizontal ? transformLayout.itemSize.width : transformLayout.itemSize.height) + transformLayout.minimumInteritemSpacing
|
|
|
+ let viewportLength = resolvedScrollDirection() == .horizontal ? collectionView.bounds.width : collectionView.bounds.height
|
|
|
+
|
|
|
+ let leftOrTop = resolvedScrollDirection() == .horizontal ? edge.left : edge.top
|
|
|
+ let rightOrBottom = resolvedScrollDirection() == .horizontal ? edge.right : edge.bottom
|
|
|
+
|
|
|
+ let logical = CGFloat(target.index + target.section * numberOfItems)
|
|
|
+ let normal = leftOrTop + itemLength * logical - transformLayout.minimumInteritemSpacing * 0.5 - (viewportLength - itemLength) * 0.5
|
|
|
+
|
|
|
+ if !isInfiniteLoop, target.index == numberOfItems - 1 {
|
|
|
+ if resolvedScrollDirection() == .horizontal, !layout.itemHorizontalCenter {
|
|
|
+ let alignedEnd = leftOrTop + itemLength * logical - (viewportLength - itemLength) - transformLayout.minimumInteritemSpacing + rightOrBottom
|
|
|
+ return max(alignedEnd, 0)
|
|
|
+ }
|
|
|
+ if resolvedScrollDirection() == .vertical {
|
|
|
+ let alignedEnd = leftOrTop + itemLength * logical - (viewportLength - itemLength) - transformLayout.minimumInteritemSpacing + rightOrBottom
|
|
|
+ return max(alignedEnd, 0)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return max(normal, 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func resetPagerView(at index: Int) {
|
|
|
+ var targetIndex = index
|
|
|
+ if didLayout, firstScrollIndex >= 0 {
|
|
|
+ targetIndex = firstScrollIndex
|
|
|
+ firstScrollIndex = -1
|
|
|
+ }
|
|
|
+ if targetIndex < 0 { return }
|
|
|
+ if targetIndex >= numberOfItems { targetIndex = 0 }
|
|
|
+
|
|
|
+ let section = isInfiniteLoop ? (maxSectionCount / 3) : 0
|
|
|
+ scrollToItem(at: LNIndexSection(index: targetIndex, section: section), animated: false)
|
|
|
+ if !isInfiniteLoop, indexSection.index < 0 {
|
|
|
+ scrollViewDidScroll(collectionView)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func recyclePagerViewIfNeeded() {
|
|
|
+ guard isInfiniteLoop else { return }
|
|
|
+ if indexSection.section > maxSectionCount - minSectionCount || indexSection.section < minSectionCount {
|
|
|
+ resetPagerView(at: indexSection.index)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func resolvedScrollDirection() -> LNCyclePagerScrollDirection {
|
|
|
+ effectiveLayout()?.scrollDirection ?? .horizontal
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+extension LNCyclePager: UICollectionViewDataSource {
|
|
|
+ public func numberOfSections(in collectionView: UICollectionView) -> Int {
|
|
|
+ isInfiniteLoop ? maxSectionCount : 1
|
|
|
+ }
|
|
|
+
|
|
|
+ public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
|
+ numberOfItems = max(0, numberOfItemsProvider?() ?? 0)
|
|
|
+ return numberOfItems
|
|
|
+ }
|
|
|
+
|
|
|
+ public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
|
+ dequeueSection = indexPath.section
|
|
|
+ guard let cellProvider else {
|
|
|
+ assertionFailure("LNCyclePager cellProvider is nil")
|
|
|
+ return UICollectionViewCell()
|
|
|
+ }
|
|
|
+ return cellProvider(self, indexPath.item)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+extension LNCyclePager: UICollectionViewDelegateFlowLayout {
|
|
|
+ public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
|
|
+ guard let layout = effectiveLayout() else { return .zero }
|
|
|
+ if !isInfiniteLoop {
|
|
|
+ return layout.onlyOneSectionInset
|
|
|
+ }
|
|
|
+ if section == 0 {
|
|
|
+ return layout.firstSectionInset
|
|
|
+ }
|
|
|
+ if section == maxSectionCount - 1 {
|
|
|
+ return layout.lastSectionInset
|
|
|
+ }
|
|
|
+ return layout.middleSectionInset
|
|
|
+ }
|
|
|
+
|
|
|
+ public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
|
+ guard let cell = collectionView.cellForItem(at: indexPath) else { return }
|
|
|
+ didSelectItem?(cell, indexPath.item)
|
|
|
+ didSelectItemAtIndexSection?(cell, LNIndexSection(index: indexPath.item, section: indexPath.section))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+extension LNCyclePager: UIScrollViewDelegate {
|
|
|
+ public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
+ guard didLayout else { return }
|
|
|
+ let newIndexSection = calculateIndexSection(with: scrollView.contentOffset)
|
|
|
+ guard numberOfItems > 0, isValid(newIndexSection) else { return }
|
|
|
+ let old = indexSection
|
|
|
+ indexSection = newIndexSection
|
|
|
+
|
|
|
+ didScroll?()
|
|
|
+
|
|
|
+ if old != indexSection {
|
|
|
+ didScrollFromIndexToIndex?(max(old.index, 0), indexSection.index)
|
|
|
+ boundPageControl?.setCurrentPage(indexSection.index, animate: true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
|
+ if autoScrollInterval > 0 {
|
|
|
+ stopTimer()
|
|
|
+ }
|
|
|
+ beginDragIndexSection = calculateIndexSection(with: scrollView.contentOffset)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
|
|
+ let horizontal = resolvedScrollDirection() == .horizontal
|
|
|
+ let velocityValue = horizontal ? abs(velocity.x) : abs(velocity.y)
|
|
|
+ if velocityValue < 0.35 || beginDragIndexSection != indexSection {
|
|
|
+ let target = calculateOffset(at: indexSection)
|
|
|
+ if horizontal {
|
|
|
+ targetContentOffset.pointee.x = target
|
|
|
+ } else {
|
|
|
+ targetContentOffset.pointee.y = target
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ let direction: LNPagerScrollDirection
|
|
|
+ if horizontal {
|
|
|
+ let isLeft = (scrollView.contentOffset.x < 0 && targetContentOffset.pointee.x <= 0) ||
|
|
|
+ (targetContentOffset.pointee.x < scrollView.contentOffset.x &&
|
|
|
+ scrollView.contentOffset.x < scrollView.contentSize.width - scrollView.frame.width)
|
|
|
+ direction = isLeft ? .left : .right
|
|
|
+ } else {
|
|
|
+ let isUp = (scrollView.contentOffset.y < 0 && targetContentOffset.pointee.y <= 0) ||
|
|
|
+ (targetContentOffset.pointee.y < scrollView.contentOffset.y &&
|
|
|
+ scrollView.contentOffset.y < scrollView.contentSize.height - scrollView.frame.height)
|
|
|
+ direction = isUp ? .left : .right
|
|
|
+ }
|
|
|
+
|
|
|
+ let targetIndexSection = nearlyIndexSection(from: indexSection, direction: direction)
|
|
|
+ let target = calculateOffset(at: targetIndexSection)
|
|
|
+ if horizontal {
|
|
|
+ targetContentOffset.pointee.x = target
|
|
|
+ } else {
|
|
|
+ targetContentOffset.pointee.y = target
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
|
+ if autoScrollInterval > 0 {
|
|
|
+ startTimerIfNeeded()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
|
+ recyclePagerViewIfNeeded()
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
|
|
+ recyclePagerViewIfNeeded()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+extension LNCyclePager: LNCyclePagerTransformLayoutDelegate {
|
|
|
+ public func pagerTransformLayout(_ layout: LNCyclePagerTransformLayout, initialize attributes: UICollectionViewLayoutAttributes) {
|
|
|
+ initializeTransformAttributes?(attributes)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func pagerTransformLayout(_ layout: LNCyclePagerTransformLayout, apply attributes: UICollectionViewLayoutAttributes) {
|
|
|
+ applyTransformAttributes?(attributes)
|
|
|
+ }
|
|
|
+}
|