今天用了一个github上一个比较好用的Segmented Control但是发现不是我要效果,我需要支持scrollVIEw。当栏目数量超过一屏幕,需要能够滑动。
由于联系作者没有回复,我就自己在其基础上增加了下scrollVIEw的支持。
代码比较简单,直接在UIControl下写的。
其中有一个比较有意思的地方,IndicatorVIEw下面放了一个TitleMaskVIEw作为mask。用来遮罩选用的Titles标签。已达到过渡效果。
源代码:
//// SwiftySegmentedControl.swift// SwiftySegmentedControl//// Created by liuYanghui on 2017/1/10.// copyright © 2017年 Yanghui.liu. All rights reserved.//import UIKit// MARK: - SwiftySegmentedControl@IBDesignable open class SwiftySegmentedControl: UIControl { // MARK: IndicatorVIEw fileprivate class IndicatorVIEw: UIVIEw { // MARK: PropertIEs fileprivate let TitleMaskVIEw = UIVIEw() fileprivate let line = UIVIEw() fileprivate let lineHeight: CGfloat = 2.0 fileprivate var cornerRadius: CGfloat = 0 { dIDSet { layer.cornerRadius = cornerRadius TitleMaskVIEw.layer.cornerRadius = cornerRadius } } overrIDe open var frame: CGRect { dIDSet { TitleMaskVIEw.frame = frame let lineFrame = CGRect(x: 0,y: frame.size.height - lineHeight,wIDth: frame.size.wIDth,height: lineHeight) line.frame = lineFrame } } open var linecolor = UIcolor.clear { dIDSet { line.backgroundcolor = linecolor } } // MARK: lifecycle init() { super.init(frame: CGRect.zero) finishInit() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) finishInit() } fileprivate func finishInit() { layer.masksToBounds = true TitleMaskVIEw.backgroundcolor = UIcolor.black addSubvIEw(line) } overrIDe open func layoutSubvIEws() { super.layoutSubvIEws() } } // MARK: Constants fileprivate struct Animation { fileprivate static let withBounceDuration: TimeInterval = 0.3 fileprivate static let springdamPing: CGfloat = 0.75 fileprivate static let withoutBounceDuration: TimeInterval = 0.2 } fileprivate struct color { fileprivate static let background: UIcolor = UIcolor.white fileprivate static let Title: UIcolor = UIcolor.black fileprivate static let indicatorVIEwBackground: UIcolor = UIcolor.black fileprivate static let selectedTitle: UIcolor = UIcolor.white } // MARK: Error handling public enum IndexError: Error { case indexBeyondBounds(UInt) } // MARK: PropertIEs /// The selected index public fileprivate(set) var index: UInt /// The Titles / options available for selection public var Titles: [String] { get { let TitleLabels = TitleLabelsVIEw.subvIEws as! [UILabel] return TitleLabels.map { $0.text! } } set { guard newValue.count > 1 else { return } let labels: [(UILabel,UILabel)] = newValue.map { (string) -> (UILabel,UILabel) in let TitleLabel = UILabel() TitleLabel.textcolor = Titlecolor TitleLabel.text = string TitleLabel.lineBreakMode = .byTruncatingTail TitleLabel.textAlignment = .center TitleLabel.Font = TitleFont TitleLabel.layer.borderWIDth = TitleborderWIDth TitleLabel.layer.bordercolor = Titlebordercolor TitleLabel.layer.cornerRadius = indicatorVIEw.cornerRadius let selectedTitleLabel = UILabel() selectedTitleLabel.textcolor = selectedTitlecolor selectedTitleLabel.text = string selectedTitleLabel.lineBreakMode = .byTruncatingTail selectedTitleLabel.textAlignment = .center selectedTitleLabel.Font = selectedTitleFont return (TitleLabel,selectedTitleLabel) } TitleLabelsVIEw.subvIEws.forEach({ $0.removeFromSupervIEw() }) selectedTitleLabelsVIEw.subvIEws.forEach({ $0.removeFromSupervIEw() }) for (inactiveLabel,activeLabel) in labels { TitleLabelsVIEw.addSubvIEw(inactiveLabel) selectedTitleLabelsVIEw.addSubvIEw(activeLabel) } setNeedsLayout() } } /// Whether the indicator should bounce when selecting a new index. Defaults to true public var bouncesOnChange = true /// Whether the the control should always send the .ValueChanged event,regardless of the index remaining unchanged after interaction. Defaults to false public var alwaysAnnouncesValue = false /// Whether to send the .ValueChanged event immediately or wait for animations to complete. Defaults to true public var announcesValueImmediately = true /// Whether the the control should ignore pan gestures. Defaults to false public var panningDisabled = false /// The control's and indicator's corner radii @IBInspectable public var cornerRadius: CGfloat { get { return layer.cornerRadius } set { layer.cornerRadius = newValue indicatorVIEw.cornerRadius = newValue - indicatorVIEwInset TitleLabels.forEach { $0.layer.cornerRadius = indicatorVIEw.cornerRadius } } } /// The indicator vIEw's background color @IBInspectable public var indicatorVIEwBackgroundcolor: UIcolor? { get { return indicatorVIEw.backgroundcolor } set { indicatorVIEw.backgroundcolor = newValue } } /// margin spacing between Titles. Default to 33. @IBInspectable public var marginSpace: CGfloat = 33 { dIDSet { setNeedsLayout() } } /// The indicator vIEw's inset. Defaults to 2.0 @IBInspectable public var indicatorVIEwInset: CGfloat = 2.0 { dIDSet { setNeedsLayout() } } /// The indicator vIEw's border wIDth public var indicatorVIEwborderWIDth: CGfloat { get { return indicatorVIEw.layer.borderWIDth } set { indicatorVIEw.layer.borderWIDth = newValue } } /// The indicator vIEw's border wIDth public var indicatorVIEwbordercolor: CGcolor? { get { return indicatorVIEw.layer.bordercolor } set { indicatorVIEw.layer.bordercolor = newValue } } /// The indicator vIEw's line color public var indicatorVIEwlinecolor: UIcolor { get { return indicatorVIEw.linecolor } set { indicatorVIEw.linecolor = newValue } } /// The text color of the non-selected Titles / options @IBInspectable public var Titlecolor: UIcolor { dIDSet { TitleLabels.forEach { $0.textcolor = Titlecolor } } } /// The text color of the selected Title / option @IBInspectable public var selectedTitlecolor: UIcolor { dIDSet { selectedTitleLabels.forEach { $0.textcolor = selectedTitlecolor } } } /// The Titles' Font public var TitleFont: UIFont = UILabel().Font { dIDSet { TitleLabels.forEach { $0.Font = TitleFont } } } /// The selected Title's Font public var selectedTitleFont: UIFont = UILabel().Font { dIDSet { selectedTitleLabels.forEach { $0.Font = selectedTitleFont } } } /// The Titles' border wIDth public var TitleborderWIDth: CGfloat = 0.0 { dIDSet { TitleLabels.forEach { $0.layer.borderWIDth = TitleborderWIDth } } } /// The Titles' border color public var Titlebordercolor: CGcolor = UIcolor.clear.cgcolor { dIDSet { TitleLabels.forEach { $0.layer.bordercolor = Titlebordercolor } } } // MARK: - Private propertIEs fileprivate let contentScrollVIEw: UIScrollVIEw = { let scrollVIEw = UIScrollVIEw() scrollVIEw.showsverticalScrollindicator = false scrollVIEw.showsHorizontalScrollindicator = false return scrollVIEw }() fileprivate let TitleLabelsVIEw = UIVIEw() fileprivate let selectedTitleLabelsVIEw = UIVIEw() fileprivate let indicatorVIEw = IndicatorVIEw() fileprivate var initialindicatorVIEwFrame: CGRect? fileprivate var tapGestureRecognizer: UITapGestureRecognizer! fileprivate var panGestureRecognizer: UIPanGestureRecognizer! fileprivate var wIDth: CGfloat { return bounds.wIDth } fileprivate var height: CGfloat { return bounds.height } fileprivate var TitleLabelsCount: Int { return TitleLabelsVIEw.subvIEws.count } fileprivate var TitleLabels: [UILabel] { return TitleLabelsVIEw.subvIEws as! [UILabel] } fileprivate var selectedTitleLabels: [UILabel] { return selectedTitleLabelsVIEw.subvIEws as! [UILabel] } fileprivate var totalinsetSize: CGfloat { return indicatorVIEwInset * 2.0 } fileprivate lazy var defaultTitles: [String] = { return ["First","Second"] }() fileprivate var TitlesWIDth: [CGfloat] { return Titles.map { let statusLabelText: Nsstring = $0 as Nsstring let size = CGSize(wIDth: wIDth,height: height - totalinsetSize) let dic = NSDictionary(object: TitleFont,forKey: NSFontAttributename as NScopying) let strSize = statusLabelText.boundingRect(with: size,options: .useslineFragmentOrigin,attributes: dic as? [String : AnyObject],context: nil).size return strSize.wIDth } } // MARK: lifecycle required public init?(coder aDecoder: NSCoder) { index = 0 Titlecolor = color.Title selectedTitlecolor = color.selectedTitle super.init(coder: aDecoder) Titles = defaultTitles finishInit() } public init(frame: CGRect,Titles: [String],index: UInt,backgroundcolor: UIcolor,Titlecolor: UIcolor,indicatorVIEwBackgroundcolor: UIcolor,selectedTitlecolor: UIcolor) { self.index = index self.Titlecolor = Titlecolor self.selectedTitlecolor = selectedTitlecolor super.init(frame: frame) self.Titles = Titles self.backgroundcolor = backgroundcolor self.indicatorVIEwBackgroundcolor = indicatorVIEwBackgroundcolor finishInit() } @available(*,deprecated,message: "Use init(frame:Titles:index:backgroundcolor:Titlecolor:indicatorVIEwBackgroundcolor:selectedTitlecolor:) instead.") convenIEnce overrIDe public init(frame: CGRect) { self.init(frame: frame,Titles: ["First","Second"],index: 0,backgroundcolor: color.background,Titlecolor: color.Title,indicatorVIEwBackgroundcolor: color.indicatorVIEwBackground,selectedTitlecolor: color.selectedTitle) } @available(*,unavailable,message: "Use init(frame:Titles:index:backgroundcolor:Titlecolor:indicatorVIEwBackgroundcolor:selectedTitlecolor:) instead.") convenIEnce init() { self.init(frame: CGRect.zero,selectedTitlecolor: color.selectedTitle) } fileprivate func finishInit() { layer.masksToBounds = true addSubvIEw(contentScrollVIEw) contentScrollVIEw.addSubvIEw(TitleLabelsVIEw) contentScrollVIEw.addSubvIEw(indicatorVIEw) contentScrollVIEw.addSubvIEw(selectedTitleLabelsVIEw) selectedTitleLabelsVIEw.layer.mask = indicatorVIEw.TitleMaskVIEw.layer tapGestureRecognizer = UITapGestureRecognizer(target: self,action: #selector(SwiftySegmentedControl.tapped(_:))) addGestureRecognizer(tapGestureRecognizer) panGestureRecognizer = UIPanGestureRecognizer(target: self,action: #selector(SwiftySegmentedControl.panned(_:))) panGestureRecognizer.delegate = self addGestureRecognizer(panGestureRecognizer) } overrIDe open func layoutSubvIEws() { super.layoutSubvIEws() guard TitleLabelsCount > 1 else { return } contentScrollVIEw.frame = bounds let allElementsWIDth = TitlesWIDth.reduce(0,{$0 + $1}) + CGfloat(TitleLabelsCount) * marginSpace contentScrollVIEw.contentSize = CGSize(wIDth: max(allElementsWIDth,wIDth),height: 0) TitleLabelsVIEw.frame = bounds selectedTitleLabelsVIEw.frame = bounds indicatorVIEw.frame = elementFrame(forIndex: index) for index in 0...TitleLabelsCount-1 { let frame = elementFrame(forIndex: UInt(index)) TitleLabelsVIEw.subvIEws[index].frame = frame selectedTitleLabelsVIEw.subvIEws[index].frame = frame } } // MARK: Index Setting /*! 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. */ public func setIndex(_ index: UInt,animated: Bool = true) throws { guard TitleLabels.indices.contains(Int(index)) else { throw IndexError.indexBeyondBounds(index) } let oldindex = self.index self.index = index moveIndicatorVIEwToIndex(animated,shouldSendEvent: (self.index != oldindex || alwaysAnnouncesValue)) fixedScrollVIEwOffset(Int(self.index)) } // MARK: Fixed ScrollVIEw offset fileprivate func fixedScrollVIEwOffset(_ focusIndex: Int) { guard contentScrollVIEw.contentSize.wIDth > wIDth else { return } let targetMIDX = self.TitleLabels[Int(self.index)].frame.mIDX let offsetX = contentScrollVIEw.contentOffset.x let addOffsetX = targetMIDX - offsetX - wIDth / 2 let newOffSetX = min(max(0,offsetX + addOffsetX),contentScrollVIEw.contentSize.wIDth - wIDth) let point = CGPoint(x: newOffSetX,y: contentScrollVIEw.contentOffset.y) contentScrollVIEw.setContentOffset(point,animated: true) } // MARK: Animations fileprivate func moveIndicatorVIEwToIndex(_ animated: Bool,shouldSendEvent: Bool) { if animated { if shouldSendEvent && announcesValueImmediately { sendActions(for: .valueChanged) } 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: { () -> VoID in self.moveIndicatorVIEw() },completion: { (finished) -> VoID in if finished && shouldSendEvent && !self.announcesValueImmediately { self.sendActions(for: .valueChanged) } }) } else { moveIndicatorVIEw() sendActions(for: .valueChanged) } } // MARK: Helpers fileprivate func elementFrame(forIndex index: UInt) -> CGRect { // 计算出label的宽度,label宽度 = (text宽度) + marginSpace // | <= 0.5 * marginSpace => text1 <= 0.5 * marginSpace => | <= 0.5 * marginSpace => text2 <= 0.5 * marginSpace => | // 如果总宽度小于bunds.wIDth,则均分宽度 label宽度 = bunds.wIDth / count let allElementsWIDth = TitlesWIDth.reduce(0,{$0 + $1}) + CGfloat(TitleLabelsCount) * marginSpace if allElementsWIDth < wIDth { let elementWIDth = (wIDth - totalinsetSize) / CGfloat(TitleLabelsCount) return CGRect(x: CGfloat(index) * elementWIDth + indicatorVIEwInset,y: indicatorVIEwInset,wIDth: elementWIDth,height: height - totalinsetSize) } else { let TitlesWIDth = self.TitlesWIDth let frontTitlesWIDth = TitlesWIDth.enumerated().reduce(CGfloat(0)) { (total,current) in return current.0 < Int(index) ? total + current.1 : total } let x = frontTitlesWIDth + CGfloat(index) * marginSpace return CGRect(x: x,wIDth: TitlesWIDth[Int(index)] + marginSpace,height: height - totalinsetSize) } } fileprivate func nearestIndex(topoint point: CGPoint) -> UInt { let distances = TitleLabels.map { abs(point.x - $0.center.x) } return UInt(distances.index(of: distances.min()!)!) } fileprivate func moveIndicatorVIEw() { indicatorVIEw.frame = TitleLabels[Int(self.index)].frame layoutIfNeeded() } // MARK: Action handlers @objc fileprivate func tapped(_ gestureRecognizer: UITapGestureRecognizer!) { let location = gestureRecognizer.location(in: contentScrollVIEw) try! setIndex(nearestIndex(topoint: location)) } @objc fileprivate func panned(_ gestureRecognizer: UIPanGestureRecognizer!) { guard !panningDisabled else { return } switch gestureRecognizer.state { case .began: initialindicatorVIEwFrame = indicatorVIEw.frame case .changed: var frame = initialindicatorVIEwFrame! frame.origin.x += gestureRecognizer.translation(in: self).x frame.origin.x = max(min(frame.origin.x,bounds.wIDth - indicatorVIEwInset - frame.wIDth),indicatorVIEwInset) indicatorVIEw.frame = frame case .ended,.Failed,.cancelled: try! setIndex(nearestIndex(topoint: indicatorVIEw.center)) default: break } }}// MARK: - UIGestureRecognizerDelegateextension SwiftySegmentedControl: UIGestureRecognizerDelegate { overrIDe open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer == panGestureRecognizer { return indicatorVIEw.frame.contains(gestureRecognizer.location(in: contentScrollVIEw)) } return super.gestureRecognizerShouldBegin(gestureRecognizer) }}
使用方式
fileprivate func setupControl() { let vIEwSegmentedControl = SwiftySegmentedControl( 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)) vIEwSegmentedControl.autoresizingMask = [.flexibleWIDth] vIEwSegmentedControl.indicatorVIEwInset = 0 vIEwSegmentedControl.cornerRadius = 0.0 vIEwSegmentedControl.TitleFont = UIFont(name: "HelveticaNeue",size: 16.0)! vIEwSegmentedControl.selectedTitleFont = UIFont(name: "HelveticaNeue",size: 16.0)! vIEwSegmentedControl.bouncesOnChange = false // 是否禁止拖动选择,注意,这里有个问题我还没改,如果Titles长度超过1屏幕,或者说,你想使用下划线,建议禁止拖动选择。 vIEwSegmentedControl.panningDisabled = true // 下划线颜色。默认透明 vIEwSegmentedControl.indicatorVIEwlinecolor = UIcolor.red vIEw.addSubvIEw(vIEwSegmentedControl) }
Github: SwiftySegmentedControl
总结以上是内存溢出为你收集整理的Scroll Segmented Control(Swift)全部内容,希望文章能够帮你解决Scroll Segmented Control(Swift)所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)