Scroll Segmented Control(Swift)

前端之家收集整理的这篇文章主要介绍了Scroll Segmented Control(Swift)前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

今天用了一个github上一个比较好用的Segmented Control但是发现不是我要效果,我需要支持scrollView。当栏目数量超过一屏幕,需要能够滑动。

由于联系作者没有回复,我就自己在其基础上增加了下scrollView的支持

代码比较简单,直接在UIControl下写的。

其中有一个比较有意思的地方,IndicatorView下面放了一个titleMaskView作为mask。用来遮罩选用的titles标签。已达到过渡效果

代码

  1. //
  2. // SwiftySegmentedControl.swift
  3. // SwiftySegmentedControl
  4. //
  5. // Created by LiuYanghui on 2017/1/10.
  6. // Copyright © 2017年 Yanghui.Liu. All rights reserved.
  7. //
  8.  
  9. import UIKit
  10.  
  11. // MARK: - SwiftySegmentedControl
  12. @IBDesignable open class SwiftySegmentedControl: UIControl {
  13. // MARK: IndicatorView
  14. fileprivate class IndicatorView: UIView {
  15. // MARK: Properties
  16. fileprivate let titleMaskView = UIView()
  17. fileprivate let line = UIView()
  18. fileprivate let lineHeight: CGFloat = 2.0
  19. fileprivate var cornerRadius: CGFloat = 0 {
  20. didSet {
  21. layer.cornerRadius = cornerRadius
  22. titleMaskView.layer.cornerRadius = cornerRadius
  23. }
  24. }
  25. override open var frame: CGRect {
  26. didSet {
  27. titleMaskView.frame = frame
  28. let lineFrame = CGRect(x: 0,y: frame.size.height - lineHeight,width: frame.size.width,height: lineHeight)
  29. line.frame = lineFrame
  30. }
  31. }
  32.  
  33. open var lineColor = UIColor.clear {
  34. didSet {
  35. line.backgroundColor = lineColor
  36. }
  37. }
  38.  
  39. // MARK: Lifecycle
  40. init() {
  41. super.init(frame: CGRect.zero)
  42. finishInit()
  43. }
  44. required init?(coder aDecoder: NSCoder) {
  45. super.init(coder: aDecoder)
  46. finishInit()
  47. }
  48. fileprivate func finishInit() {
  49. layer.masksToBounds = true
  50. titleMaskView.backgroundColor = UIColor.black
  51. addSubview(line)
  52. }
  53.  
  54. override open func layoutSubviews() {
  55. super.layoutSubviews()
  56.  
  57. }
  58. }
  59.  
  60. // MARK: Constants
  61. fileprivate struct Animation {
  62. fileprivate static let withBounceDuration: TimeInterval = 0.3
  63. fileprivate static let springDamping: CGFloat = 0.75
  64. fileprivate static let withoutBounceDuration: TimeInterval = 0.2
  65. }
  66. fileprivate struct Color {
  67. fileprivate static let background: UIColor = UIColor.white
  68. fileprivate static let title: UIColor = UIColor.black
  69. fileprivate static let indicatorViewBackground: UIColor = UIColor.black
  70. fileprivate static let selectedTitle: UIColor = UIColor.white
  71. }
  72.  
  73. // MARK: Error handling
  74. public enum IndexError: Error {
  75. case indexBeyondBounds(UInt)
  76. }
  77.  
  78. // MARK: Properties
  79. /// The selected index
  80. public fileprivate(set) var index: UInt
  81. /// The titles / options available for selection
  82. public var titles: [String] {
  83. get {
  84. let titleLabels = titleLabelsView.subviews as! [UILabel]
  85. return titleLabels.map { $0.text! }
  86. }
  87. set {
  88. guard newValue.count > 1 else {
  89. return
  90. }
  91. let labels: [(UILabel,UILabel)] = newValue.map {
  92. (string) -> (UILabel,UILabel) in
  93.  
  94. let titleLabel = UILabel()
  95. titleLabel.textColor = titleColor
  96. titleLabel.text = string
  97. titleLabel.lineBreakMode = .byTruncatingTail
  98. titleLabel.textAlignment = .center
  99. titleLabel.font = titleFont
  100. titleLabel.layer.borderWidth = titleBorderWidth
  101. titleLabel.layer.borderColor = titleBorderColor
  102. titleLabel.layer.cornerRadius = indicatorView.cornerRadius
  103.  
  104. let selectedTitleLabel = UILabel()
  105. selectedTitleLabel.textColor = selectedTitleColor
  106. selectedTitleLabel.text = string
  107. selectedTitleLabel.lineBreakMode = .byTruncatingTail
  108. selectedTitleLabel.textAlignment = .center
  109. selectedTitleLabel.font = selectedTitleFont
  110.  
  111. return (titleLabel,selectedTitleLabel)
  112. }
  113.  
  114. titleLabelsView.subviews.forEach({ $0.removeFromSuperview() })
  115. selectedTitleLabelsView.subviews.forEach({ $0.removeFromSuperview() })
  116.  
  117. for (inactiveLabel,activeLabel) in labels {
  118. titleLabelsView.addSubview(inactiveLabel)
  119. selectedTitleLabelsView.addSubview(activeLabel)
  120. }
  121.  
  122. setNeedsLayout()
  123. }
  124. }
  125. /// Whether the indicator should bounce when selecting a new index. Defaults to true
  126. public var bouncesOnChange = true
  127. /// Whether the the control should always send the .ValueChanged event,regardless of the index remaining unchanged after interaction. Defaults to false
  128. public var alwaysAnnouncesValue = false
  129. /// Whether to send the .ValueChanged event immediately or wait for animations to complete. Defaults to true
  130. public var announcesValueImmediately = true
  131. /// Whether the the control should ignore pan gestures. Defaults to false
  132. public var panningDisabled = false
  133. /// The control's and indicator's corner radii
  134. @IBInspectable public var cornerRadius: CGFloat {
  135. get {
  136. return layer.cornerRadius
  137. }
  138. set {
  139. layer.cornerRadius = newValue
  140. indicatorView.cornerRadius = newValue - indicatorViewInset
  141. titleLabels.forEach { $0.layer.cornerRadius = indicatorView.cornerRadius }
  142. }
  143. }
  144. /// The indicator view's background color
  145. @IBInspectable public var indicatorViewBackgroundColor: UIColor? {
  146. get {
  147. return indicatorView.backgroundColor
  148. }
  149. set {
  150. indicatorView.backgroundColor = newValue
  151. }
  152. }
  153. /// Margin spacing between titles. Default to 33.
  154. @IBInspectable public var marginSpace: CGFloat = 33 {
  155. didSet { setNeedsLayout() }
  156. }
  157. /// The indicator view's inset. Defaults to 2.0
  158. @IBInspectable public var indicatorViewInset: CGFloat = 2.0 {
  159. didSet { setNeedsLayout() }
  160. }
  161. /// The indicator view's border width
  162. public var indicatorViewBorderWidth: CGFloat {
  163. get {
  164. return indicatorView.layer.borderWidth
  165. }
  166. set {
  167. indicatorView.layer.borderWidth = newValue
  168. }
  169. }
  170. /// The indicator view's border width
  171. public var indicatorViewBorderColor: CGColor? {
  172. get {
  173. return indicatorView.layer.borderColor
  174. }
  175. set {
  176. indicatorView.layer.borderColor = newValue
  177. }
  178. }
  179. /// The indicator view's line color
  180. public var indicatorViewLineColor: UIColor {
  181. get {
  182. return indicatorView.lineColor
  183. }
  184. set {
  185. indicatorView.lineColor = newValue
  186. }
  187. }
  188. /// The text color of the non-selected titles / options
  189. @IBInspectable public var titleColor: UIColor {
  190. didSet {
  191. titleLabels.forEach { $0.textColor = titleColor }
  192. }
  193. }
  194. /// The text color of the selected title / option
  195. @IBInspectable public var selectedTitleColor: UIColor {
  196. didSet {
  197. selectedTitleLabels.forEach { $0.textColor = selectedTitleColor }
  198. }
  199. }
  200. /// The titles' font
  201. public var titleFont: UIFont = UILabel().font {
  202. didSet {
  203. titleLabels.forEach { $0.font = titleFont }
  204. }
  205. }
  206. /// The selected title's font
  207. public var selectedTitleFont: UIFont = UILabel().font {
  208. didSet {
  209. selectedTitleLabels.forEach { $0.font = selectedTitleFont }
  210. }
  211. }
  212. /// The titles' border width
  213. public var titleBorderWidth: CGFloat = 0.0 {
  214. didSet {
  215. titleLabels.forEach { $0.layer.borderWidth = titleBorderWidth }
  216. }
  217. }
  218. /// The titles' border color
  219. public var titleBorderColor: CGColor = UIColor.clear.cgColor {
  220. didSet {
  221. titleLabels.forEach { $0.layer.borderColor = titleBorderColor }
  222. }
  223. }
  224.  
  225. // MARK: - Private properties
  226. fileprivate let contentScrollView: UIScrollView = {
  227. let scrollView = UIScrollView()
  228. scrollView.showsVerticalScrollIndicator = false
  229. scrollView.showsHorizontalScrollIndicator = false
  230. return scrollView
  231. }()
  232. fileprivate let titleLabelsView = UIView()
  233. fileprivate let selectedTitleLabelsView = UIView()
  234. fileprivate let indicatorView = IndicatorView()
  235. fileprivate var initialIndicatorViewFrame: CGRect?
  236.  
  237. fileprivate var tapGestureRecognizer: UITapGestureRecognizer!
  238. fileprivate var panGestureRecognizer: UIPanGestureRecognizer!
  239.  
  240. fileprivate var width: CGFloat { return bounds.width }
  241. fileprivate var height: CGFloat { return bounds.height }
  242. fileprivate var titleLabelsCount: Int { return titleLabelsView.subviews.count }
  243. fileprivate var titleLabels: [UILabel] { return titleLabelsView.subviews as! [UILabel] }
  244. fileprivate var selectedTitleLabels: [UILabel] { return selectedTitleLabelsView.subviews as! [UILabel] }
  245. fileprivate var totalInsetSize: CGFloat { return indicatorViewInset * 2.0 }
  246. fileprivate lazy var defaultTitles: [String] = { return ["First","Second"] }()
  247. fileprivate var titlesWidth: [CGFloat] {
  248. return titles.map {
  249. let statusLabelText: NSString = $0 as NSString
  250. let size = CGSize(width: width,height: height - totalInsetSize)
  251. let dic = NSDictionary(object: titleFont,forKey: NSFontAttributeName as NSCopying)
  252. let strSize = statusLabelText.boundingRect(with: size,options: .usesLineFragmentOrigin,attributes: dic as? [String : AnyObject],context: nil).size
  253. return strSize.width
  254. }
  255. }
  256.  
  257. // MARK: Lifecycle
  258. required public init?(coder aDecoder: NSCoder) {
  259. index = 0
  260. titleColor = Color.title
  261. selectedTitleColor = Color.selectedTitle
  262. super.init(coder: aDecoder)
  263. titles = defaultTitles
  264. finishInit()
  265. }
  266. public init(frame: CGRect,titles: [String],index: UInt,backgroundColor: UIColor,titleColor: UIColor,indicatorViewBackgroundColor: UIColor,selectedTitleColor: UIColor) {
  267. self.index = index
  268. self.titleColor = titleColor
  269. self.selectedTitleColor = selectedTitleColor
  270. super.init(frame: frame)
  271. self.titles = titles
  272. self.backgroundColor = backgroundColor
  273. self.indicatorViewBackgroundColor = indicatorViewBackgroundColor
  274. finishInit()
  275. }
  276.  
  277. @available(*,deprecated,message: "Use init(frame:titles:index:backgroundColor:titleColor:indicatorViewBackgroundColor:selectedTitleColor:) instead.")
  278. convenience override public init(frame: CGRect) {
  279. self.init(frame: frame,titles: ["First","Second"],index: 0,backgroundColor: Color.background,titleColor: Color.title,indicatorViewBackgroundColor: Color.indicatorViewBackground,selectedTitleColor: Color.selectedTitle)
  280. }
  281.  
  282. @available(*,unavailable,message: "Use init(frame:titles:index:backgroundColor:titleColor:indicatorViewBackgroundColor:selectedTitleColor:) instead.")
  283. convenience init() {
  284. self.init(frame: CGRect.zero,selectedTitleColor: Color.selectedTitle)
  285. }
  286.  
  287.  
  288. fileprivate func finishInit() {
  289. layer.masksToBounds = true
  290.  
  291. addSubview(contentScrollView)
  292. contentScrollView.addSubview(titleLabelsView)
  293. contentScrollView.addSubview(indicatorView)
  294. contentScrollView.addSubview(selectedTitleLabelsView)
  295. selectedTitleLabelsView.layer.mask = indicatorView.titleMaskView.layer
  296.  
  297. tapGestureRecognizer = UITapGestureRecognizer(target: self,action: #selector(SwiftySegmentedControl.tapped(_:)))
  298. addGestureRecognizer(tapGestureRecognizer)
  299.  
  300. panGestureRecognizer = UIPanGestureRecognizer(target: self,action: #selector(SwiftySegmentedControl.panned(_:)))
  301. panGestureRecognizer.delegate = self
  302. addGestureRecognizer(panGestureRecognizer)
  303. }
  304. override open func layoutSubviews() {
  305. super.layoutSubviews()
  306. guard titleLabelsCount > 1 else {
  307. return
  308. }
  309.  
  310. contentScrollView.frame = bounds
  311. let allElementsWidth = titlesWidth.reduce(0,{$0 + $1}) + CGFloat(titleLabelsCount) * marginSpace
  312. contentScrollView.contentSize = CGSize(width: max(allElementsWidth,width),height: 0)
  313.  
  314. titleLabelsView.frame = bounds
  315. selectedTitleLabelsView.frame = bounds
  316.  
  317. indicatorView.frame = elementFrame(forIndex: index)
  318.  
  319. for index in 0...titleLabelsCount-1 {
  320. let frame = elementFrame(forIndex: UInt(index))
  321. titleLabelsView.subviews[index].frame = frame
  322. selectedTitleLabelsView.subviews[index].frame = frame
  323. }
  324. }
  325.  
  326. // MARK: Index Setting
  327. /*! Sets the control's index. - parameter index: The new index - parameter animated: (Optional) Whether the change should be animated or not. Defaults to true. - throws: An error of type IndexBeyondBounds(UInt) is thrown if an index beyond the available indices is passed. */
  328. public func setIndex(_ index: UInt,animated: Bool = true) throws {
  329. guard titleLabels.indices.contains(Int(index)) else {
  330. throw IndexError.indexBeyondBounds(index)
  331. }
  332. let oldIndex = self.index
  333. self.index = index
  334. moveIndicatorViewToIndex(animated,shouldSendEvent: (self.index != oldIndex || alwaysAnnouncesValue))
  335. fixedScrollViewOffset(Int(self.index))
  336. }
  337.  
  338. // MARK: Fixed ScrollView offset
  339. fileprivate func fixedScrollViewOffset(_ focusIndex: Int) {
  340. guard contentScrollView.contentSize.width > width else {
  341. return
  342. }
  343.  
  344. let targetMidX = self.titleLabels[Int(self.index)].frame.midX
  345. let offsetX = contentScrollView.contentOffset.x
  346. let addOffsetX = targetMidX - offsetX - width / 2
  347. let newOffSetX = min(max(0,offsetX + addOffsetX),contentScrollView.contentSize.width - width)
  348. let point = CGPoint(x: newOffSetX,y: contentScrollView.contentOffset.y)
  349. contentScrollView.setContentOffset(point,animated: true)
  350. }
  351.  
  352. // MARK: Animations
  353. fileprivate func moveIndicatorViewToIndex(_ animated: Bool,shouldSendEvent: Bool) {
  354. if animated {
  355. if shouldSendEvent && announcesValueImmediately {
  356. sendActions(for: .valueChanged)
  357. }
  358. UIView.animate(withDuration: bouncesOnChange ? Animation.withBounceDuration : Animation.withoutBounceDuration,delay: 0.0,usingSpringWithDamping: bouncesOnChange ? Animation.springDamping : 1.0,initialSpringVelocity: 0.0,options: [UIViewAnimationOptions.beginFromCurrentState,UIViewAnimationOptions.curveEaSEOut],animations: {
  359. () -> Void in
  360. self.moveIndicatorView()
  361. },completion: { (finished) -> Void in
  362. if finished && shouldSendEvent && !self.announcesValueImmediately {
  363. self.sendActions(for: .valueChanged)
  364. }
  365. })
  366. } else {
  367. moveIndicatorView()
  368. sendActions(for: .valueChanged)
  369. }
  370. }
  371.  
  372. // MARK: Helpers
  373. fileprivate func elementFrame(forIndex index: UInt) -> CGRect {
  374. // 计算出label的宽度,label宽度 = (text宽度) + marginSpace
  375. // | <= 0.5 * marginSpace => text1 <= 0.5 * marginSpace => | <= 0.5 * marginSpace => text2 <= 0.5 * marginSpace => |
  376. // 如果总宽度小于bunds.width,则均分宽度 label宽度 = bunds.width / count
  377. let allElementsWidth = titlesWidth.reduce(0,{$0 + $1}) + CGFloat(titleLabelsCount) * marginSpace
  378. if allElementsWidth < width {
  379. let elementWidth = (width - totalInsetSize) / CGFloat(titleLabelsCount)
  380. return CGRect(x: CGFloat(index) * elementWidth + indicatorViewInset,y: indicatorViewInset,width: elementWidth,height: height - totalInsetSize)
  381. } else {
  382. let titlesWidth = self.titlesWidth
  383. let frontTitlesWidth = titlesWidth.enumerated().reduce(CGFloat(0)) { (total,current) in
  384. return current.0 < Int(index) ? total + current.1 : total
  385. }
  386. let x = frontTitlesWidth + CGFloat(index) * marginSpace
  387. return CGRect(x: x,width: titlesWidth[Int(index)] + marginSpace,height: height - totalInsetSize)
  388. }
  389. }
  390. fileprivate func nearestIndex(toPoint point: CGPoint) -> UInt {
  391. let distances = titleLabels.map { abs(point.x - $0.center.x) }
  392. return UInt(distances.index(of: distances.min()!)!)
  393. }
  394. fileprivate func moveIndicatorView() {
  395. indicatorView.frame = titleLabels[Int(self.index)].frame
  396. layoutIfNeeded()
  397. }
  398.  
  399. // MARK: Action handlers
  400. @objc fileprivate func tapped(_ gestureRecognizer: UITapGestureRecognizer!) {
  401. let location = gestureRecognizer.location(in: contentScrollView)
  402. try! setIndex(nearestIndex(toPoint: location))
  403. }
  404. @objc fileprivate func panned(_ gestureRecognizer: UIPanGestureRecognizer!) {
  405. guard !panningDisabled else {
  406. return
  407. }
  408.  
  409. switch gestureRecognizer.state {
  410. case .began:
  411. initialIndicatorViewFrame = indicatorView.frame
  412. case .changed:
  413. var frame = initialIndicatorViewFrame!
  414. frame.origin.x += gestureRecognizer.translation(in: self).x
  415. frame.origin.x = max(min(frame.origin.x,bounds.width - indicatorViewInset - frame.width),indicatorViewInset)
  416. indicatorView.frame = frame
  417. case .ended,.Failed,.cancelled:
  418. try! setIndex(nearestIndex(toPoint: indicatorView.center))
  419. default: break
  420. }
  421. }
  422. }
  423.  
  424. // MARK: - UIGestureRecognizerDelegate
  425. extension SwiftySegmentedControl: UIGestureRecognizerDelegate {
  426. override open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
  427. if gestureRecognizer == panGestureRecognizer {
  428. return indicatorView.frame.contains(gestureRecognizer.location(in: contentScrollView))
  429. }
  430. return super.gestureRecognizerShouldBegin(gestureRecognizer)
  431. }
  432. }

使用方式

  1. fileprivate func setupControl() {
  2. let viewSegmentedControl = SwiftySegmentedControl(
  3. frame: CGRect(x: 0.0,y: 430.0,width: view.bounds.width,height: 50.0),titles: ["All","New","Pictures","One","Two","Three","Four","Five","Six","Artists","Albums","Recent"],index: 1,backgroundColor: UIColor(red:0.11,green:0.12,blue:0.13,alpha:1.00),titleColor: .white,indicatorViewBackgroundColor: UIColor(red:0.11,selectedTitleColor: UIColor(red:0.97,green:0.00,blue:0.24,alpha:1.00))
  4. viewSegmentedControl.autoresizingMask = [.flexibleWidth]
  5. viewSegmentedControl.indicatorViewInset = 0
  6. viewSegmentedControl.cornerRadius = 0.0
  7. viewSegmentedControl.titleFont = UIFont(name: "HelveticaNeue",size: 16.0)!
  8. viewSegmentedControl.selectedTitleFont = UIFont(name: "HelveticaNeue",size: 16.0)!
  9. viewSegmentedControl.bouncesOnChange = false
  10. // 是否禁止拖动选择,注意,这里有个问题我还没改,如果titles长度超过1屏幕,或者说,你想使用下划线,建议禁止拖动选择。
  11. viewSegmentedControl.panningDisabled = true
  12. // 下划线颜色。默认透明
  13. viewSegmentedControl.indicatorViewLineColor = UIColor.red
  14. view.addSubview(viewSegmentedControl)
  15. }

Github: SwiftySegmentedControl

猜你在找的Swift相关文章