LNCyclePageControl.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import UIKit
  2. public final class LNCyclePageControl: UIControl {
  3. public var numberOfPages: Int = 0 {
  4. didSet {
  5. guard numberOfPages != oldValue else { return }
  6. if currentPage >= numberOfPages {
  7. currentPage = 0
  8. }
  9. updateIndicatorViews()
  10. if !indicatorViews.isEmpty { setNeedsLayout() }
  11. }
  12. }
  13. public var currentPage: Int = 0 {
  14. didSet {
  15. guard oldValue != currentPage, currentPage < indicatorViews.count else { return }
  16. if currentPage < 0 { currentPage = 0; return }
  17. if !currentPageIndicatorSize.equalTo(pageIndicatorSize) {
  18. setNeedsLayout()
  19. }
  20. updateIndicatorViewsBehavior()
  21. if isUserInteractionEnabled {
  22. sendActions(for: .valueChanged)
  23. }
  24. }
  25. }
  26. public var hidesForSinglePage = false
  27. public var pageIndicatorSpacing: CGFloat = 10 {
  28. didSet {
  29. guard oldValue != pageIndicatorSpacing else { return }
  30. if !indicatorViews.isEmpty { setNeedsLayout() }
  31. }
  32. }
  33. public var contentInset: UIEdgeInsets = .zero {
  34. didSet { setNeedsLayout() }
  35. }
  36. public var contentSize: CGSize {
  37. let count = indicatorViews.count
  38. let width = CGFloat(max(count - 1, 0)) * (pageIndicatorSize.width + pageIndicatorSpacing) + pageIndicatorSize.width + contentInset.left + contentInset.right
  39. let height = currentPageIndicatorSize.height + contentInset.top + contentInset.bottom
  40. return CGSize(width: width, height: height)
  41. }
  42. public var pageIndicatorTintColor: UIColor? = UIColor(white: 0.5, alpha: 1) {
  43. didSet { updateIndicatorViewsBehavior() }
  44. }
  45. public var currentPageIndicatorTintColor: UIColor? = .white {
  46. didSet { updateIndicatorViewsBehavior() }
  47. }
  48. public var pageIndicatorImage: UIImage? {
  49. didSet { updateIndicatorViewsBehavior() }
  50. }
  51. public var currentPageIndicatorImage: UIImage? {
  52. didSet { updateIndicatorViewsBehavior() }
  53. }
  54. public var indicatorImageContentMode: UIView.ContentMode = .center {
  55. didSet {
  56. indicatorViews.forEach { $0.contentMode = indicatorImageContentMode }
  57. }
  58. }
  59. public var pageIndicatorSize: CGSize = CGSize(width: 6, height: 6) {
  60. didSet {
  61. guard !pageIndicatorSize.equalTo(oldValue) else { return }
  62. if currentPageIndicatorSize == .zero || (currentPageIndicatorSize.width < pageIndicatorSize.width && currentPageIndicatorSize.height < pageIndicatorSize.height) {
  63. currentPageIndicatorSize = pageIndicatorSize
  64. }
  65. if !indicatorViews.isEmpty { setNeedsLayout() }
  66. }
  67. }
  68. public var currentPageIndicatorSize: CGSize = CGSize(width: 6, height: 6) {
  69. didSet {
  70. guard !currentPageIndicatorSize.equalTo(oldValue) else { return }
  71. if !indicatorViews.isEmpty { setNeedsLayout() }
  72. }
  73. }
  74. public var animateDuration: TimeInterval = 0.3
  75. private var indicatorViews: [UIImageView] = []
  76. private var forceUpdate = false
  77. public override init(frame: CGRect) {
  78. super.init(frame: frame)
  79. configure()
  80. }
  81. public required init?(coder: NSCoder) {
  82. super.init(coder: coder)
  83. configure()
  84. }
  85. private func configure() {
  86. isUserInteractionEnabled = false
  87. currentPageIndicatorSize = pageIndicatorSize
  88. }
  89. public override func willMove(toSuperview newSuperview: UIView?) {
  90. super.willMove(toSuperview: newSuperview)
  91. if newSuperview != nil {
  92. forceUpdate = true
  93. updateIndicatorViews()
  94. forceUpdate = false
  95. }
  96. }
  97. public func setCurrentPage(_ page: Int, animate: Bool) {
  98. if animate {
  99. UIView.animate(withDuration: animateDuration) {
  100. self.currentPage = page
  101. self.layoutIfNeeded()
  102. }
  103. } else {
  104. currentPage = page
  105. }
  106. }
  107. public override func layoutSubviews() {
  108. super.layoutSubviews()
  109. layoutIndicatorViews()
  110. }
  111. private func updateIndicatorViews() {
  112. if superview == nil, !forceUpdate { return }
  113. if indicatorViews.count < numberOfPages {
  114. for _ in indicatorViews.count..<numberOfPages {
  115. let iv = UIImageView()
  116. iv.contentMode = indicatorImageContentMode
  117. addSubview(iv)
  118. indicatorViews.append(iv)
  119. }
  120. } else if indicatorViews.count > numberOfPages {
  121. for idx in stride(from: indicatorViews.count - 1, through: numberOfPages, by: -1) {
  122. indicatorViews[idx].removeFromSuperview()
  123. indicatorViews.remove(at: idx)
  124. }
  125. }
  126. updateIndicatorViewsBehavior()
  127. }
  128. private func updateIndicatorViewsBehavior() {
  129. guard !indicatorViews.isEmpty else { return }
  130. if superview == nil, !forceUpdate { return }
  131. if hidesForSinglePage, indicatorViews.count == 1 {
  132. indicatorViews[0].isHidden = true
  133. return
  134. }
  135. for (idx, view) in indicatorViews.enumerated() {
  136. if let pageIndicatorImage {
  137. view.contentMode = indicatorImageContentMode
  138. view.image = (idx == currentPage) ? currentPageIndicatorImage : pageIndicatorImage
  139. view.backgroundColor = .clear
  140. } else {
  141. view.image = nil
  142. view.backgroundColor = (idx == currentPage) ? currentPageIndicatorTintColor : pageIndicatorTintColor
  143. }
  144. view.isHidden = false
  145. }
  146. }
  147. private func layoutIndicatorViews() {
  148. guard !indicatorViews.isEmpty else { return }
  149. var originX: CGFloat = 0
  150. var centerY: CGFloat = 0
  151. var spacing = pageIndicatorSpacing
  152. switch contentHorizontalAlignment {
  153. case .center:
  154. originX = (bounds.width - CGFloat(indicatorViews.count - 1) * (pageIndicatorSize.width + pageIndicatorSpacing) - currentPageIndicatorSize.width) * 0.5
  155. case .left:
  156. originX = contentInset.left
  157. case .right:
  158. originX = bounds.width - (CGFloat(indicatorViews.count - 1) * (pageIndicatorSize.width + pageIndicatorSpacing) + currentPageIndicatorSize.width) - contentInset.right
  159. case .fill:
  160. originX = contentInset.left
  161. if indicatorViews.count > 1 {
  162. spacing = (bounds.width - contentInset.left - contentInset.right - pageIndicatorSize.width - CGFloat(indicatorViews.count - 1) * pageIndicatorSize.width) / CGFloat(indicatorViews.count - 1)
  163. }
  164. default:
  165. break
  166. }
  167. switch contentVerticalAlignment {
  168. case .center:
  169. centerY = bounds.height * 0.5
  170. case .top:
  171. centerY = contentInset.top + currentPageIndicatorSize.height * 0.5
  172. case .bottom:
  173. centerY = bounds.height - currentPageIndicatorSize.height * 0.5 - contentInset.bottom
  174. case .fill:
  175. centerY = (bounds.height - contentInset.top - contentInset.bottom) * 0.5 + contentInset.top
  176. default:
  177. break
  178. }
  179. for (idx, view) in indicatorViews.enumerated() {
  180. let size = (idx == currentPage) ? currentPageIndicatorSize : pageIndicatorSize
  181. view.layer.cornerRadius = (pageIndicatorImage == nil) ? (size.height * 0.5) : 0
  182. view.clipsToBounds = true
  183. view.frame = CGRect(x: originX, y: centerY - size.height * 0.5, width: size.width, height: size.height)
  184. originX += size.width + spacing
  185. }
  186. }
  187. }