暫無描述

SeMobSlideTabView.swift 18KB

    // // SeMobSlideTabView.swift // PaiAi // // Created by mac on 16/7/21. // Copyright © 2016年 FFIB. All rights reserved. // import UIKit private let firstTabTag = 1231115 private let firstLayoutViewTag = 131115 protocol SeMobSlideTabViewDelegate { func slideTabViewDidSelect(_ index: Int) } enum SeMobSlideTabAlignType { case alignCenter case alignFixMargin(left : CGFloat?, mid : CGFloat?, right : CGFloat?) } @IBDesignable class SeMobSlideTabView: UIView { // MARK: - -delegate-- var delegate: SeMobSlideTabViewDelegate? var alignType: SeMobSlideTabAlignType = .alignCenter // MARK: - -class members-- //绑定的scrollview,随着scrollview滑动改变slider滑块的位置 var scrollview: UIScrollView? { didSet { if scrollview != oldValue { oldValue?.removeObserver(self, forKeyPath: "contentOffset") scrollview?.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil) if scrollview != nil { setPortionWithScrollViewWithOffset(scrollview!.contentOffset) } } } } //标题 @IBInspectable var titles: [String] = [] { didSet { while badgeTypes.count < titles.count { badgeTypes.append(.number) } badgeNumbers = [Int](repeating: 0, count: titles.count) needsRelayout = true } } //标题字体 @IBInspectable var titleFont = UIFont.systemFont(ofSize: 13) { didSet { self.subviews.forEach { if let label = $0 as? UILabel { label.font = titleFont } } } } //竖条分隔符,预留,暂不支持 var seperaterLineView: UIView? { didSet { needsRelayout = true } } //滑块自定义view var slideLineView: UIView? { didSet { needsRelayout = true } } //单色滑块颜色,优先使用slideLineView,没有则使用该颜色创建一个纯色滑块 @IBInspectable var slideLineColor: UIColor? { didSet { needsRelayout = true } } //滑块大小(宽、高) @IBInspectable var slideLineSize = CGSize.zero { didSet { needsRelayout = true } } //滑块距离底部的间距 @IBInspectable var slideLineBottom: CGFloat = 0 { didSet { needsRelayout = true } } //选中的title颜色 @IBInspectable var selectedTitleTextColor: UIColor! { didSet { needsRelayout = true } } //未选中的title颜色 @IBInspectable var titleTextColor: UIColor = UIColor.black { didSet { needsRelayout = true } } @IBInspectable var selectedIndex: Int = 0 { didSet { let value = min(titles.count, max(0, selectedIndex)) selectedIndex = value if scrollview == nil { sliderPortion = Double(value) } for i in 0 ..< self.titles.count { if let label = self.viewWithTag(firstTabTag + i) as? UILabel { if i != selectedIndex { label.textColor = self.titleTextColor } else { label.textColor = self.selectedTitleTextColor } } } if selectedIndex != oldValue { self.delegate?.slideTabViewDidSelect(selectedIndex) } } } fileprivate var sliderPortion = 0.0 { didSet { if sliderPortion != oldValue { moveSlider(sliderPortion < oldValue ? true : false) if (sliderPortion - Double(Int(sliderPortion))) < 0.00001 { selectedIndex = Int(sliderPortion) } } } } //-- badge -- enum BadgeType { case none case dot case number //使用自定义的block添加badge,UIView参数为当前tab文字view,使用该参数作为layout依据 case customView((_ targetView:UIView, _ badgeNumber:Int, _ viewTag:Int)->Void) } var badgeTypes: [BadgeType] = [.none, .dot, .number] var badgeNumbers: [Int] = [] //-- end badge -- fileprivate var needsRelayout = true { didSet { if needsRelayout { self.sliderView = nil self.setNeedsLayout() } } } fileprivate var sliderView: UIView? fileprivate var sliderPosConstraint: NSLayoutConstraint? // MARK: - - init methods -- override init(frame: CGRect) { sliderPortion = 0 super.init(frame:frame) let gr = UITapGestureRecognizer(target: self, action: #selector(SeMobSlideTabView.didTap(_:))) self.addGestureRecognizer(gr) } required init?(coder aDecoder: NSCoder) { sliderPortion = 0 super.init(coder:aDecoder) let gr = UITapGestureRecognizer(target: self, action: #selector(SeMobSlideTabView.didTap(_:))) self.addGestureRecognizer(gr) } deinit { scrollview?.removeObserver(self, forKeyPath: "contentOffset") } // MARK: - - class methods -- @objc func didTap(_ gr: UITapGestureRecognizer) { let pt = gr.location(in: self) let index = pt.x / (self.bounds.width/CGFloat(self.titles.count)) self.selectedIndex = Int(index) } override func layoutSubviews() { if needsRelayout { self.relayout() } super.layoutSubviews() } func relayout() { func relayoutTitleForAlignCenter() { for i in 0 ..< self.titles.count { let label = UILabel() label.tag = firstTabTag + i label.text = self.titles[i] label.font = self.titleFont label.textAlignment = .center label.textColor = self.titleTextColor if (self.selectedTitleTextColor != nil && i == selectedIndex) { label.textColor = self.selectedTitleTextColor } self.addSubview(label) label.useAutoLayout() let multiplierBase = CGFloat(1.0) / CGFloat(self.titles.count) let multiplierCenterX = (multiplierBase * CGFloat(i) + multiplierBase / CGFloat(2)) * CGFloat(2) NSLayoutConstraint(item: label, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: multiplierCenterX, constant: 0).active() NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[label]-0-|", options: [], metrics: nil, views: ["label": label]).autolayoutInstall() buildBadgeNumber(atIndex: i) } } func relayoutTitleForAlignFixMargin(_ left: CGFloat?, _ mid: CGFloat?, _ right: CGFloat?) { for i in 0 ..< self.titles.count { // --label let label = UILabel() label.tag = firstTabTag + i label.text = self.titles[i] label.font = self.titleFont label.textAlignment = .center label.textColor = self.titleTextColor if (self.selectedTitleTextColor != nil && i == selectedIndex) { label.textColor = self.selectedTitleTextColor } self.addSubview(label) label.useAutoLayout() NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[label]-0-|", options: [], metrics: nil, views: ["label": label]).autolayoutInstall() //--- margin let leftMarginView = UIView().useAutoLayout() leftMarginView.tag = firstLayoutViewTag + i self.addSubview(leftMarginView) leftMarginView.backgroundColor = UIColor.clear NSLayoutConstraint.horizontalSpace(leftMarginView, secondView: label) NSLayoutConstraint.centerY(leftMarginView, secondView: label) NSLayoutConstraint.equalHeight(firstView: leftMarginView, secondView: label) if i == 0 { NSLayoutConstraint.pinLeading(self, secondView: leftMarginView) if let left = left { leftMarginView.setLayoutWidth(left) } } else if i == 1 { if let mid = mid { leftMarginView.setLayoutWidth(mid) } if let lastLabel = self.viewWithTag(firstTabTag + i - 1) { NSLayoutConstraint.horizontalSpace(lastLabel, secondView: leftMarginView) } } else { if let lastMarginView = self.viewWithTag(firstLayoutViewTag + i - 1) { NSLayoutConstraint.equalWidth(firstView: lastMarginView, secondView: leftMarginView) } if let lastLabel = self.viewWithTag(firstTabTag + i - 1) { NSLayoutConstraint.horizontalSpace(lastLabel, secondView: leftMarginView) } } if i == self.titles.count - 1 { let rightEdgeView = UIView().useAutoLayout() rightEdgeView.tag = firstLayoutViewTag + i + 1 self.addSubview(rightEdgeView) rightEdgeView.backgroundColor = UIColor.clear NSLayoutConstraint.horizontalSpace(label, secondView: rightEdgeView) NSLayoutConstraint.centerY(rightEdgeView, secondView: label) NSLayoutConstraint.equalHeight(firstView: rightEdgeView, secondView: label) NSLayoutConstraint.pinTrailing(rightEdgeView, secondView: self) let leftEdgeView = self.viewWithTag(firstLayoutViewTag)! if let right = right { //规定了右侧的距离 rightEdgeView.setLayoutWidth(right) if let _ = left { //规定了左侧距离,已经在i=0时处理 } else { //没有规定左侧距离 NSLayoutConstraint.equalWidth(firstView: leftEdgeView, secondView: leftMarginView) } } else if let _ = left { NSLayoutConstraint.equalWidth(firstView: leftMarginView, secondView: rightEdgeView) } else { NSLayoutConstraint.equalWidth(firstView: leftEdgeView, secondView: rightEdgeView) NSLayoutConstraint.equalWidth(firstView: leftEdgeView, secondView: leftMarginView) } } buildBadgeNumber(atIndex: i) } } self.subviews.forEach {$0.removeFromSuperview()} //titles switch alignType { case .alignCenter: relayoutTitleForAlignCenter() case .alignFixMargin(let left, let mid, let right) : relayoutTitleForAlignFixMargin(left, mid, right) } // slider self.createSlider() //text color for i in 0 ..< self.titles.count { if let label = self.viewWithTag(firstTabTag + i) as? UILabel { if i != selectedIndex { label.textColor = self.titleTextColor } else { label.textColor = self.selectedTitleTextColor } } } self.needsRelayout = false } func createSlider() { let targetLabel = self.viewWithTag(firstTabTag+selectedIndex) if slideLineView != nil { sliderView = slideLineView } else if slideLineColor != nil { sliderView = UIImageView(image: UIImage.imageWithColor(slideLineColor!)) } if sliderView != nil { self.addSubview(sliderView!) sliderView!.useAutoLayout() NSLayoutConstraint.constraints(withVisualFormat: "H:[slider(width)]", options: [], metrics: ["width": slideLineSize.width], views: ["slider": sliderView!]).autolayoutInstall() NSLayoutConstraint.constraints(withVisualFormat: "V:[slider(height)]-bottom-|", options: [], metrics: ["bottom": slideLineBottom, "height": slideLineSize.height], views: ["slider": sliderView!]).autolayoutInstall() self.sliderPosConstraint = NSLayoutConstraint(item: sliderView!, attribute: .centerX, relatedBy: .equal, toItem: targetLabel!, attribute: .centerX, multiplier: 1, constant: 0) self.sliderPosConstraint!.active() } } func moveSlider(_ left: Bool = false) { if sliderView != nil { self.sliderPosConstraint?.deActive() switch alignType { case .alignCenter: let multiplierBase = CGFloat(1.0) / CGFloat(self.titles.count) let multiplierCenterX = (multiplierBase * CGFloat(self.sliderPortion) + multiplierBase / CGFloat(2)) * CGFloat(2) sliderPosConstraint = NSLayoutConstraint(item: sliderView!, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: multiplierCenterX, constant: 0) case .alignFixMargin: let portion = sliderPortion - Double(Int(sliderPortion)) if abs(portion) < 0.00001 { if let targetLable = self.viewWithTag(Int(sliderPortion) + firstTabTag) { sliderPosConstraint = NSLayoutConstraint.centerX(sliderView!, secondView: targetLable) } } else { if let leftLable = self.viewWithTag(Int(sliderPortion) + firstTabTag), let rightLabel = self.viewWithTag(Int(sliderPortion) + firstTabTag + 1) { let totalLength = abs(leftLable.center.x - rightLabel.center.x) sliderPosConstraint = NSLayoutConstraint.centerX(sliderView!, secondView: leftLable, constant: CGFloat(portion) * totalLength ) } } } sliderPosConstraint!.active() UIView.animate(withDuration: 0.2, animations: { () -> Void in self.sliderView!.superview?.layoutIfNeeded() }) } } static let badgeTagStart = 8090 func updateBadgeNumber(index: Int, number: Int) { if index < badgeNumbers.count { badgeNumbers[index] = number buildBadgeNumber(atIndex: index) } } fileprivate func buildBadgeNumber(atIndex index: Int) { if index >= titles.count { return } let badgeType = badgeTypes[index] let number = badgeNumbers[index] if number == 0 { let badge = self.viewWithTag(SeMobSlideTabView.badgeTagStart+index) badge?.removeFromSuperview() } else { if let titleLabel = self.viewWithTag(firstTabTag+index) as? UILabel { let badge = self.viewWithTag(SeMobSlideTabView.badgeTagStart+index) badge?.removeFromSuperview() switch badgeType { case .none : break case .dot : let badgeDot = UIView() badgeDot.backgroundColor = UIColor.red badgeDot.cornerRadius = 2 badgeDot.tag = SeMobSlideTabView.badgeTagStart+index self.addSubview(badgeDot) badgeDot.useAutoLayout() NSLayoutConstraint.constraints(withVisualFormat: "H:[title]-1-[badge(==4)]", options: [], metrics: nil, views: ["title": titleLabel, "badge": badgeDot]).autolayoutInstall() NSLayoutConstraint(item: badgeDot, attribute: .top, relatedBy: .equal, toItem: titleLabel, attribute: .top, multiplier: 1, constant: 8).active() NSLayoutConstraint(item: badgeDot, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 4).active() case .number: let badgeLabel = UILabel() badgeLabel.backgroundColor = UIColor.red badgeLabel.cornerRadius = 6 badgeLabel.font = UIFont.systemFont(ofSize: 10) badgeLabel.textColor = UIColor.white badgeLabel.tag = SeMobSlideTabView.badgeTagStart+index self.addSubview(badgeLabel) badgeLabel.useAutoLayout() badgeLabel.textAlignment = .center badgeLabel.text = "\(number)" let metrics = ["width": 13] NSLayoutConstraint.constraints(withVisualFormat: "H:[title]-1-[badge(>=width)]", options: [], metrics: metrics, views: ["title": titleLabel, "badge": badgeLabel]).autolayoutInstall() NSLayoutConstraint(item: badgeLabel, attribute: .top, relatedBy: .equal, toItem: titleLabel, attribute: .top, multiplier: 1, constant: 7).active() NSLayoutConstraint(item: badgeLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 12).active() case .customView(let block): block(titleLabel, number, SeMobSlideTabView.badgeTagStart+index) } } } } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if let _ = object as? UIScrollView { if let point: NSValue = change?[NSKeyValueChangeKey.newKey] as! NSValue? { var pt: CGPoint = CGPoint.zero point.getValue(&pt) self.setPortionWithScrollViewWithOffset(pt) } } } fileprivate func setPortionWithScrollViewWithOffset(_ point: CGPoint) { if let sv = scrollview { if sv.contentSize.width > 0 { let portion = (point.x / sv.contentSize.width) * CGFloat(titles.count) sliderPortion = Double(portion) } } } }