FSPageControl.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. //
  2. // FSPageControl.swift
  3. // FSPagerView
  4. //
  5. // Created by Wenchao Ding on 17/12/2016.
  6. // Copyright © 2016 Wenchao Ding. All rights reserved.
  7. //
  8. import UIKit
  9. @IBDesignable
  10. open class FSPageControl: UIControl {
  11. /// The number of page indicators of the page control. Default is 0.
  12. @IBInspectable
  13. open var numberOfPages: Int = 0 {
  14. didSet {
  15. self.setNeedsCreateIndicators()
  16. }
  17. }
  18. /// The current page, highlighted by the page control. Default is 0.
  19. @IBInspectable
  20. open var currentPage: Int = 0 {
  21. didSet {
  22. self.setNeedsUpdateIndicators()
  23. }
  24. }
  25. /// The spacing to use of page indicators in the page control.
  26. @IBInspectable
  27. open var itemSpacing: CGFloat = 6 {
  28. didSet {
  29. self.setNeedsUpdateIndicators()
  30. }
  31. }
  32. /// The spacing to use between page indicators in the page control.
  33. @IBInspectable
  34. open var interitemSpacing: CGFloat = 6 {
  35. didSet {
  36. self.setNeedsLayout()
  37. }
  38. }
  39. /// The distance that the page indicators is inset from the enclosing page control.
  40. @IBInspectable
  41. open var contentInsets: UIEdgeInsets = .zero {
  42. didSet {
  43. self.setNeedsLayout()
  44. }
  45. }
  46. /// The horizontal alignment of content within the control’s bounds. Default is center.
  47. open override var contentHorizontalAlignment: UIControl.ContentHorizontalAlignment {
  48. didSet {
  49. self.setNeedsLayout()
  50. }
  51. }
  52. /// Hide the indicator if there is only one page. default is NO
  53. @IBInspectable
  54. open var hidesForSinglePage: Bool = false {
  55. didSet {
  56. self.setNeedsUpdateIndicators()
  57. }
  58. }
  59. internal var strokeColors: [UIControl.State: UIColor] = [:]
  60. internal var fillColors: [UIControl.State: UIColor] = [:]
  61. internal var paths: [UIControl.State: UIBezierPath] = [:]
  62. internal var images: [UIControl.State: UIImage] = [:]
  63. internal var alphas: [UIControl.State: CGFloat] = [:]
  64. internal var transforms: [UIControl.State: CGAffineTransform] = [:]
  65. fileprivate weak var contentView: UIView!
  66. fileprivate var needsUpdateIndicators = false
  67. fileprivate var needsCreateIndicators = false
  68. fileprivate var indicatorLayers = [CAShapeLayer]()
  69. public override init(frame: CGRect) {
  70. super.init(frame: frame)
  71. commonInit()
  72. }
  73. public required init?(coder aDecoder: NSCoder) {
  74. super.init(coder: aDecoder)
  75. commonInit()
  76. }
  77. open override func layoutSubviews() {
  78. super.layoutSubviews()
  79. self.contentView.frame = {
  80. let x = self.contentInsets.left
  81. let y = self.contentInsets.top
  82. let width = self.frame.width - self.contentInsets.left - self.contentInsets.right
  83. let height = self.frame.height - self.contentInsets.top - self.contentInsets.bottom
  84. let frame = CGRect(x: x, y: y, width: width, height: height)
  85. return frame
  86. }()
  87. }
  88. open override func layoutSublayers(of layer: CALayer) {
  89. super.layoutSublayers(of: layer)
  90. let diameter = self.itemSpacing
  91. let spacing = self.interitemSpacing
  92. var x: CGFloat = {
  93. switch self.contentHorizontalAlignment {
  94. case .left, .leading:
  95. return 0
  96. case .center, .fill:
  97. let midX = self.contentView.bounds.midX
  98. let amplitude = CGFloat(self.numberOfPages/2) * diameter + spacing*CGFloat((self.numberOfPages-1)/2)
  99. return midX - amplitude
  100. case .right, .trailing:
  101. let contentWidth = diameter*CGFloat(self.numberOfPages) + CGFloat(self.numberOfPages-1)*spacing
  102. return contentView.frame.width - contentWidth
  103. default:
  104. return 0
  105. }
  106. }()
  107. for (index,value) in self.indicatorLayers.enumerated() {
  108. let state: UIControl.State = (index == self.currentPage) ? .selected : .normal
  109. let image = self.images[state]
  110. let size = image?.size ?? CGSize(width: diameter, height: diameter)
  111. let origin = CGPoint(x: x - (size.width-diameter)*0.5, y: self.contentView.bounds.midY-size.height*0.5)
  112. value.frame = CGRect(origin: origin, size: size)
  113. x = x + spacing + diameter
  114. }
  115. }
  116. /// Sets the stroke color for page indicators to use for the specified state. (selected/normal).
  117. ///
  118. /// - Parameters:
  119. /// - strokeColor: The stroke color to use for the specified state.
  120. /// - state: The state that uses the specified stroke color.
  121. @objc(setStrokeColor:forState:)
  122. open func setStrokeColor(_ strokeColor: UIColor?, for state: UIControl.State) {
  123. guard self.strokeColors[state] != strokeColor else {
  124. return
  125. }
  126. self.strokeColors[state] = strokeColor
  127. self.setNeedsUpdateIndicators()
  128. }
  129. /// Sets the fill color for page indicators to use for the specified state. (selected/normal).
  130. ///
  131. /// - Parameters:
  132. /// - fillColor: The fill color to use for the specified state.
  133. /// - state: The state that uses the specified fill color.
  134. @objc(setFillColor:forState:)
  135. open func setFillColor(_ fillColor: UIColor?, for state: UIControl.State) {
  136. guard self.fillColors[state] != fillColor else {
  137. return
  138. }
  139. self.fillColors[state] = fillColor
  140. self.setNeedsUpdateIndicators()
  141. }
  142. /// Sets the image for page indicators to use for the specified state. (selected/normal).
  143. ///
  144. /// - Parameters:
  145. /// - image: The image to use for the specified state.
  146. /// - state: The state that uses the specified image.
  147. @objc(setImage:forState:)
  148. open func setImage(_ image: UIImage?, for state: UIControl.State) {
  149. guard self.images[state] != image else {
  150. return
  151. }
  152. self.images[state] = image
  153. self.setNeedsUpdateIndicators()
  154. }
  155. @objc(setAlpha:forState:)
  156. /// Sets the alpha value for page indicators to use for the specified state. (selected/normal).
  157. ///
  158. /// - Parameters:
  159. /// - alpha: The alpha value to use for the specified state.
  160. /// - state: The state that uses the specified alpha.
  161. open func setAlpha(_ alpha: CGFloat, for state: UIControl.State) {
  162. guard self.alphas[state] != alpha else {
  163. return
  164. }
  165. self.alphas[state] = alpha
  166. self.setNeedsUpdateIndicators()
  167. }
  168. /// Sets the path for page indicators to use for the specified state. (selected/normal).
  169. ///
  170. /// - Parameters:
  171. /// - path: The path to use for the specified state.
  172. /// - state: The state that uses the specified path.
  173. @objc(setPath:forState:)
  174. open func setPath(_ path: UIBezierPath?, for state: UIControl.State) {
  175. guard self.paths[state] != path else {
  176. return
  177. }
  178. self.paths[state] = path
  179. self.setNeedsUpdateIndicators()
  180. }
  181. // MARK: - Private functions
  182. fileprivate func commonInit() {
  183. // Content View
  184. let view = UIView(frame: .zero)
  185. view.backgroundColor = UIColor.clear
  186. self.addSubview(view)
  187. self.contentView = view
  188. self.isUserInteractionEnabled = false
  189. }
  190. fileprivate func setNeedsUpdateIndicators() {
  191. self.needsUpdateIndicators = true
  192. self.setNeedsLayout()
  193. DispatchQueue.main.async {
  194. self.updateIndicatorsIfNecessary()
  195. }
  196. }
  197. fileprivate func updateIndicatorsIfNecessary() {
  198. guard self.needsUpdateIndicators else {
  199. return
  200. }
  201. guard self.indicatorLayers.count > 0 else {
  202. return
  203. }
  204. self.needsUpdateIndicators = false
  205. self.contentView.isHidden = self.hidesForSinglePage && self.numberOfPages <= 1
  206. if !self.contentView.isHidden {
  207. self.indicatorLayers.forEach { (layer) in
  208. layer.isHidden = false
  209. self.updateIndicatorAttributes(for: layer)
  210. }
  211. }
  212. }
  213. fileprivate func updateIndicatorAttributes(for layer: CAShapeLayer) {
  214. let index = self.indicatorLayers.firstIndex(of: layer)
  215. let state: UIControl.State = index == self.currentPage ? .selected : .normal
  216. if let image = self.images[state] {
  217. layer.strokeColor = nil
  218. layer.fillColor = nil
  219. layer.path = nil
  220. layer.contents = image.cgImage
  221. } else {
  222. layer.contents = nil
  223. let strokeColor = self.strokeColors[state]
  224. let fillColor = self.fillColors[state]
  225. if strokeColor == nil && fillColor == nil {
  226. layer.fillColor = (state == .selected ? UIColor.white : UIColor.gray).cgColor
  227. layer.strokeColor = nil
  228. } else {
  229. layer.strokeColor = strokeColor?.cgColor
  230. layer.fillColor = fillColor?.cgColor
  231. }
  232. layer.path = self.paths[state]?.cgPath ?? UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: self.itemSpacing, height: self.itemSpacing)).cgPath
  233. }
  234. if let transform = self.transforms[state] {
  235. layer.transform = CATransform3DMakeAffineTransform(transform)
  236. }
  237. layer.opacity = Float(self.alphas[state] ?? 1.0)
  238. }
  239. fileprivate func setNeedsCreateIndicators() {
  240. self.needsCreateIndicators = true
  241. DispatchQueue.main.async {
  242. self.createIndicatorsIfNecessary()
  243. }
  244. }
  245. fileprivate func createIndicatorsIfNecessary() {
  246. guard self.needsCreateIndicators else {
  247. return
  248. }
  249. self.needsCreateIndicators = false
  250. CATransaction.begin()
  251. CATransaction.setDisableActions(true)
  252. if self.currentPage >= self.numberOfPages {
  253. self.currentPage = self.numberOfPages - 1
  254. }
  255. self.indicatorLayers.forEach { (layer) in
  256. layer.removeFromSuperlayer()
  257. }
  258. self.indicatorLayers.removeAll()
  259. for _ in 0..<self.numberOfPages {
  260. let layer = CAShapeLayer()
  261. layer.actions = ["bounds": NSNull()]
  262. self.contentView.layer.addSublayer(layer)
  263. self.indicatorLayers.append(layer)
  264. }
  265. self.setNeedsUpdateIndicators()
  266. self.updateIndicatorsIfNecessary()
  267. CATransaction.commit()
  268. }
  269. }
  270. extension UIControl.State: Hashable {
  271. public var hashValue: Int {
  272. return Int((6777*self.rawValue+3777)%UInt(UInt16.max))
  273. }
  274. }