No Description

SeMobSlideTabView.swift 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. //
  2. // SeMobSlideTabView.swift
  3. // PaiAi
  4. //
  5. // Created by mac on 16/7/21.
  6. // Copyright © 2016年 FFIB. All rights reserved.
  7. //
  8. import UIKit
  9. private let firstTabTag = 1231115
  10. private let firstLayoutViewTag = 131115
  11. protocol SeMobSlideTabViewDelegate {
  12. func slideTabViewDidSelect(_ index: Int)
  13. }
  14. enum SeMobSlideTabAlignType {
  15. case alignCenter
  16. case alignFixMargin(left : CGFloat?, mid : CGFloat?, right : CGFloat?)
  17. }
  18. @IBDesignable
  19. class SeMobSlideTabView: UIView {
  20. // MARK: - -delegate--
  21. var delegate: SeMobSlideTabViewDelegate?
  22. var alignType: SeMobSlideTabAlignType = .alignCenter
  23. // MARK: - -class members--
  24. //绑定的scrollview,随着scrollview滑动改变slider滑块的位置
  25. var scrollview: UIScrollView? {
  26. didSet {
  27. if scrollview != oldValue {
  28. oldValue?.removeObserver(self, forKeyPath: "contentOffset")
  29. scrollview?.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
  30. if scrollview != nil {
  31. setPortionWithScrollViewWithOffset(scrollview!.contentOffset)
  32. }
  33. }
  34. }
  35. }
  36. //标题
  37. @IBInspectable var titles: [String] = [] {
  38. didSet {
  39. while badgeTypes.count < titles.count {
  40. badgeTypes.append(.number)
  41. }
  42. badgeNumbers = [Int](repeating: 0, count: titles.count)
  43. needsRelayout = true
  44. }
  45. }
  46. //标题字体
  47. @IBInspectable var titleFont = UIFont.systemFont(ofSize: 13) {
  48. didSet {
  49. self.subviews.forEach {
  50. if let label = $0 as? UILabel {
  51. label.font = titleFont
  52. }
  53. }
  54. }
  55. }
  56. //竖条分隔符,预留,暂不支持
  57. var seperaterLineView: UIView? {
  58. didSet {
  59. needsRelayout = true
  60. }
  61. }
  62. //滑块自定义view
  63. var slideLineView: UIView? {
  64. didSet {
  65. needsRelayout = true
  66. }
  67. }
  68. //单色滑块颜色,优先使用slideLineView,没有则使用该颜色创建一个纯色滑块
  69. @IBInspectable var slideLineColor: UIColor? {
  70. didSet {
  71. needsRelayout = true
  72. }
  73. }
  74. //滑块大小(宽、高)
  75. @IBInspectable var slideLineSize = CGSize.zero {
  76. didSet {
  77. needsRelayout = true
  78. }
  79. }
  80. //滑块距离底部的间距
  81. @IBInspectable var slideLineBottom: CGFloat = 0 {
  82. didSet {
  83. needsRelayout = true
  84. }
  85. }
  86. //选中的title颜色
  87. @IBInspectable var selectedTitleTextColor: UIColor! {
  88. didSet {
  89. needsRelayout = true
  90. }
  91. }
  92. //未选中的title颜色
  93. @IBInspectable var titleTextColor: UIColor = UIColor.black {
  94. didSet {
  95. needsRelayout = true
  96. }
  97. }
  98. @IBInspectable var selectedIndex: Int = 0 {
  99. didSet {
  100. let value = min(titles.count, max(0, selectedIndex))
  101. selectedIndex = value
  102. if scrollview == nil {
  103. sliderPortion = Double(value)
  104. }
  105. for i in 0 ..< self.titles.count {
  106. if let label = self.viewWithTag(firstTabTag + i) as? UILabel {
  107. if i != selectedIndex {
  108. label.textColor = self.titleTextColor
  109. } else {
  110. label.textColor = self.selectedTitleTextColor
  111. }
  112. }
  113. }
  114. if selectedIndex != oldValue {
  115. self.delegate?.slideTabViewDidSelect(selectedIndex)
  116. }
  117. }
  118. }
  119. fileprivate var sliderPortion = 0.0 {
  120. didSet {
  121. if sliderPortion != oldValue {
  122. moveSlider(sliderPortion < oldValue ? true : false)
  123. if (sliderPortion - Double(Int(sliderPortion))) < 0.00001 {
  124. selectedIndex = Int(sliderPortion)
  125. }
  126. }
  127. }
  128. }
  129. //-- badge --
  130. enum BadgeType {
  131. case none
  132. case dot
  133. case number
  134. //使用自定义的block添加badge,UIView参数为当前tab文字view,使用该参数作为layout依据
  135. case customView((_ targetView:UIView, _ badgeNumber:Int, _ viewTag:Int)->Void)
  136. }
  137. var badgeTypes: [BadgeType] = [.none, .dot, .number]
  138. var badgeNumbers: [Int] = []
  139. //-- end badge --
  140. fileprivate var needsRelayout = true {
  141. didSet {
  142. if needsRelayout {
  143. self.sliderView = nil
  144. self.setNeedsLayout()
  145. }
  146. }
  147. }
  148. fileprivate var sliderView: UIView?
  149. fileprivate var sliderPosConstraint: NSLayoutConstraint?
  150. // MARK: - - init methods --
  151. override init(frame: CGRect) {
  152. sliderPortion = 0
  153. super.init(frame:frame)
  154. let gr = UITapGestureRecognizer(target: self, action: #selector(SeMobSlideTabView.didTap(_:)))
  155. self.addGestureRecognizer(gr)
  156. }
  157. required init?(coder aDecoder: NSCoder) {
  158. sliderPortion = 0
  159. super.init(coder:aDecoder)
  160. let gr = UITapGestureRecognizer(target: self, action: #selector(SeMobSlideTabView.didTap(_:)))
  161. self.addGestureRecognizer(gr)
  162. }
  163. deinit {
  164. scrollview?.removeObserver(self, forKeyPath: "contentOffset")
  165. }
  166. // MARK: - - class methods --
  167. @objc func didTap(_ gr: UITapGestureRecognizer) {
  168. let pt = gr.location(in: self)
  169. let index = pt.x / (self.bounds.width/CGFloat(self.titles.count))
  170. self.selectedIndex = Int(index)
  171. }
  172. override func layoutSubviews() {
  173. if needsRelayout {
  174. self.relayout()
  175. }
  176. super.layoutSubviews()
  177. }
  178. func relayout() {
  179. func relayoutTitleForAlignCenter() {
  180. for i in 0 ..< self.titles.count {
  181. let label = UILabel()
  182. label.tag = firstTabTag + i
  183. label.text = self.titles[i]
  184. label.font = self.titleFont
  185. label.textAlignment = .center
  186. label.textColor = self.titleTextColor
  187. if (self.selectedTitleTextColor != nil && i == selectedIndex) {
  188. label.textColor = self.selectedTitleTextColor
  189. }
  190. self.addSubview(label)
  191. label.useAutoLayout()
  192. let multiplierBase = CGFloat(1.0) / CGFloat(self.titles.count)
  193. let multiplierCenterX = (multiplierBase * CGFloat(i) + multiplierBase / CGFloat(2)) * CGFloat(2)
  194. NSLayoutConstraint(item: label, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: multiplierCenterX, constant: 0).active()
  195. NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[label]-0-|", options: [], metrics: nil, views: ["label": label]).autolayoutInstall()
  196. buildBadgeNumber(atIndex: i)
  197. }
  198. }
  199. func relayoutTitleForAlignFixMargin(_ left: CGFloat?, _ mid: CGFloat?, _ right: CGFloat?) {
  200. for i in 0 ..< self.titles.count {
  201. // --label
  202. let label = UILabel()
  203. label.tag = firstTabTag + i
  204. label.text = self.titles[i]
  205. label.font = self.titleFont
  206. label.textAlignment = .center
  207. label.textColor = self.titleTextColor
  208. if (self.selectedTitleTextColor != nil && i == selectedIndex) {
  209. label.textColor = self.selectedTitleTextColor
  210. }
  211. self.addSubview(label)
  212. label.useAutoLayout()
  213. NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[label]-0-|", options: [], metrics: nil, views: ["label": label]).autolayoutInstall()
  214. //--- margin
  215. let leftMarginView = UIView().useAutoLayout()
  216. leftMarginView.tag = firstLayoutViewTag + i
  217. self.addSubview(leftMarginView)
  218. leftMarginView.backgroundColor = UIColor.clear
  219. NSLayoutConstraint.horizontalSpace(leftMarginView, secondView: label)
  220. NSLayoutConstraint.centerY(leftMarginView, secondView: label)
  221. NSLayoutConstraint.equalHeight(firstView: leftMarginView, secondView: label)
  222. if i == 0 {
  223. NSLayoutConstraint.pinLeading(self, secondView: leftMarginView)
  224. if let left = left {
  225. leftMarginView.setLayoutWidth(left)
  226. }
  227. } else if i == 1 {
  228. if let mid = mid {
  229. leftMarginView.setLayoutWidth(mid)
  230. }
  231. if let lastLabel = self.viewWithTag(firstTabTag + i - 1) {
  232. NSLayoutConstraint.horizontalSpace(lastLabel, secondView: leftMarginView)
  233. }
  234. } else {
  235. if let lastMarginView = self.viewWithTag(firstLayoutViewTag + i - 1) {
  236. NSLayoutConstraint.equalWidth(firstView: lastMarginView, secondView: leftMarginView)
  237. }
  238. if let lastLabel = self.viewWithTag(firstTabTag + i - 1) {
  239. NSLayoutConstraint.horizontalSpace(lastLabel, secondView: leftMarginView)
  240. }
  241. }
  242. if i == self.titles.count - 1 {
  243. let rightEdgeView = UIView().useAutoLayout()
  244. rightEdgeView.tag = firstLayoutViewTag + i + 1
  245. self.addSubview(rightEdgeView)
  246. rightEdgeView.backgroundColor = UIColor.clear
  247. NSLayoutConstraint.horizontalSpace(label, secondView: rightEdgeView)
  248. NSLayoutConstraint.centerY(rightEdgeView, secondView: label)
  249. NSLayoutConstraint.equalHeight(firstView: rightEdgeView, secondView: label)
  250. NSLayoutConstraint.pinTrailing(rightEdgeView, secondView: self)
  251. let leftEdgeView = self.viewWithTag(firstLayoutViewTag)!
  252. if let right = right {
  253. //规定了右侧的距离
  254. rightEdgeView.setLayoutWidth(right)
  255. if let _ = left {
  256. //规定了左侧距离,已经在i=0时处理
  257. } else {
  258. //没有规定左侧距离
  259. NSLayoutConstraint.equalWidth(firstView: leftEdgeView, secondView: leftMarginView)
  260. }
  261. } else if let _ = left {
  262. NSLayoutConstraint.equalWidth(firstView: leftMarginView, secondView: rightEdgeView)
  263. } else {
  264. NSLayoutConstraint.equalWidth(firstView: leftEdgeView, secondView: rightEdgeView)
  265. NSLayoutConstraint.equalWidth(firstView: leftEdgeView, secondView: leftMarginView)
  266. }
  267. }
  268. buildBadgeNumber(atIndex: i)
  269. }
  270. }
  271. self.subviews.forEach {$0.removeFromSuperview()}
  272. //titles
  273. switch alignType {
  274. case .alignCenter:
  275. relayoutTitleForAlignCenter()
  276. case .alignFixMargin(let left, let mid, let right) :
  277. relayoutTitleForAlignFixMargin(left, mid, right)
  278. }
  279. // slider
  280. self.createSlider()
  281. //text color
  282. for i in 0 ..< self.titles.count {
  283. if let label = self.viewWithTag(firstTabTag + i) as? UILabel {
  284. if i != selectedIndex {
  285. label.textColor = self.titleTextColor
  286. } else {
  287. label.textColor = self.selectedTitleTextColor
  288. }
  289. }
  290. }
  291. self.needsRelayout = false
  292. }
  293. func createSlider() {
  294. let targetLabel = self.viewWithTag(firstTabTag+selectedIndex)
  295. if slideLineView != nil {
  296. sliderView = slideLineView
  297. } else if slideLineColor != nil {
  298. sliderView = UIImageView(image: UIImage.imageWithColor(slideLineColor!))
  299. }
  300. if sliderView != nil {
  301. self.addSubview(sliderView!)
  302. sliderView!.useAutoLayout()
  303. NSLayoutConstraint.constraints(withVisualFormat: "H:[slider(width)]", options: [], metrics: ["width": slideLineSize.width], views: ["slider": sliderView!]).autolayoutInstall()
  304. NSLayoutConstraint.constraints(withVisualFormat: "V:[slider(height)]-bottom-|", options: [], metrics: ["bottom": slideLineBottom, "height": slideLineSize.height], views: ["slider": sliderView!]).autolayoutInstall()
  305. self.sliderPosConstraint = NSLayoutConstraint(item: sliderView!, attribute: .centerX, relatedBy: .equal, toItem: targetLabel!, attribute: .centerX, multiplier: 1, constant: 0)
  306. self.sliderPosConstraint!.active()
  307. }
  308. }
  309. func moveSlider(_ left: Bool = false) {
  310. if sliderView != nil {
  311. self.sliderPosConstraint?.deActive()
  312. switch alignType {
  313. case .alignCenter:
  314. let multiplierBase = CGFloat(1.0) / CGFloat(self.titles.count)
  315. let multiplierCenterX = (multiplierBase * CGFloat(self.sliderPortion) + multiplierBase / CGFloat(2)) * CGFloat(2)
  316. sliderPosConstraint = NSLayoutConstraint(item: sliderView!, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: multiplierCenterX, constant: 0)
  317. case .alignFixMargin:
  318. let portion = sliderPortion - Double(Int(sliderPortion))
  319. if abs(portion) < 0.00001 {
  320. if let targetLable = self.viewWithTag(Int(sliderPortion) + firstTabTag) {
  321. sliderPosConstraint = NSLayoutConstraint.centerX(sliderView!, secondView: targetLable)
  322. }
  323. } else {
  324. if let leftLable = self.viewWithTag(Int(sliderPortion) + firstTabTag), let rightLabel = self.viewWithTag(Int(sliderPortion) + firstTabTag + 1) {
  325. let totalLength = abs(leftLable.center.x - rightLabel.center.x)
  326. sliderPosConstraint = NSLayoutConstraint.centerX(sliderView!, secondView: leftLable, constant: CGFloat(portion) * totalLength )
  327. }
  328. }
  329. }
  330. sliderPosConstraint!.active()
  331. UIView.animate(withDuration: 0.2, animations: { () -> Void in
  332. self.sliderView!.superview?.layoutIfNeeded()
  333. })
  334. }
  335. }
  336. static let badgeTagStart = 8090
  337. func updateBadgeNumber(index: Int, number: Int) {
  338. if index < badgeNumbers.count {
  339. badgeNumbers[index] = number
  340. buildBadgeNumber(atIndex: index)
  341. }
  342. }
  343. fileprivate func buildBadgeNumber(atIndex index: Int) {
  344. if index >= titles.count {
  345. return
  346. }
  347. let badgeType = badgeTypes[index]
  348. let number = badgeNumbers[index]
  349. if number == 0 {
  350. let badge = self.viewWithTag(SeMobSlideTabView.badgeTagStart+index)
  351. badge?.removeFromSuperview()
  352. } else {
  353. if let titleLabel = self.viewWithTag(firstTabTag+index) as? UILabel {
  354. let badge = self.viewWithTag(SeMobSlideTabView.badgeTagStart+index)
  355. badge?.removeFromSuperview()
  356. switch badgeType {
  357. case .none :
  358. break
  359. case .dot :
  360. let badgeDot = UIView()
  361. badgeDot.backgroundColor = UIColor.red
  362. badgeDot.cornerRadius = 2
  363. badgeDot.tag = SeMobSlideTabView.badgeTagStart+index
  364. self.addSubview(badgeDot)
  365. badgeDot.useAutoLayout()
  366. NSLayoutConstraint.constraints(withVisualFormat: "H:[title]-1-[badge(==4)]", options: [], metrics: nil, views: ["title": titleLabel, "badge": badgeDot]).autolayoutInstall()
  367. NSLayoutConstraint(item: badgeDot, attribute: .top, relatedBy: .equal, toItem: titleLabel, attribute: .top, multiplier: 1, constant: 8).active()
  368. NSLayoutConstraint(item: badgeDot, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 4).active()
  369. case .number:
  370. let badgeLabel = UILabel()
  371. badgeLabel.backgroundColor = UIColor.red
  372. badgeLabel.cornerRadius = 6
  373. badgeLabel.font = UIFont.systemFont(ofSize: 10)
  374. badgeLabel.textColor = UIColor.white
  375. badgeLabel.tag = SeMobSlideTabView.badgeTagStart+index
  376. self.addSubview(badgeLabel)
  377. badgeLabel.useAutoLayout()
  378. badgeLabel.textAlignment = .center
  379. badgeLabel.text = "\(number)"
  380. let metrics = ["width": 13]
  381. NSLayoutConstraint.constraints(withVisualFormat: "H:[title]-1-[badge(>=width)]", options: [], metrics: metrics, views: ["title": titleLabel, "badge": badgeLabel]).autolayoutInstall()
  382. NSLayoutConstraint(item: badgeLabel, attribute: .top, relatedBy: .equal, toItem: titleLabel, attribute: .top, multiplier: 1, constant: 7).active()
  383. NSLayoutConstraint(item: badgeLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 12).active()
  384. case .customView(let block):
  385. block(titleLabel, number, SeMobSlideTabView.badgeTagStart+index)
  386. }
  387. }
  388. }
  389. }
  390. override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  391. if let _ = object as? UIScrollView {
  392. if let point: NSValue = change?[NSKeyValueChangeKey.newKey] as! NSValue? {
  393. var pt: CGPoint = CGPoint.zero
  394. point.getValue(&pt)
  395. self.setPortionWithScrollViewWithOffset(pt)
  396. }
  397. }
  398. }
  399. fileprivate func setPortionWithScrollViewWithOffset(_ point: CGPoint) {
  400. if let sv = scrollview {
  401. if sv.contentSize.width > 0 {
  402. let portion = (point.x / sv.contentSize.width) * CGFloat(titles.count)
  403. sliderPortion = Double(portion)
  404. }
  405. }
  406. }
  407. }