今天用了一个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: IndicatorViewfileprivate class IndicatorView: UIView {// MARK: Propertiesfileprivate let titleMaskView = UIView()fileprivate let line = UIView()fileprivate let lineHeight: CGFloat = 2.0fileprivate var cornerRadius: CGFloat = 0 {didSet {layer.cornerRadius = cornerRadiustitleMaskView.layer.cornerRadius = cornerRadius}}override open var frame: CGRect {didSet {titleMaskView.frame = framelet 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: Lifecycleinit() {super.init(frame: CGRect.zero)finishInit()}required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder)finishInit()}fileprivate func finishInit() {layer.masksToBounds = truetitleMaskView.backgroundColor = UIColor.blackaddSubview(line)}override open func layoutSubviews() {super.layoutSubviews()}}// MARK: Constantsfileprivate struct Animation {fileprivate static let withBounceDuration: TimeInterval = 0.3fileprivate static let springDamping: CGFloat = 0.75fileprivate static let withoutBounceDuration: TimeInterval = 0.2}fileprivate struct Color {fileprivate static let background: UIColor = UIColor.whitefileprivate static let title: UIColor = UIColor.blackfileprivate static let indicatorViewBackground: UIColor = UIColor.blackfileprivate static let selectedTitle: UIColor = UIColor.white}// MARK: Error handlingpublic enum IndexError: Error {case indexBeyondBounds(UInt)}// MARK: Properties/// The selected indexpublic fileprivate(set) var index: UInt/// The titles / options available for selectionpublic 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) inlet titleLabel = UILabel()titleLabel.textColor = titleColortitleLabel.text = stringtitleLabel.lineBreakMode = .byTruncatingTailtitleLabel.textAlignment = .centertitleLabel.font = titleFonttitleLabel.layer.borderWidth = titleBorderWidthtitleLabel.layer.borderColor = titleBorderColortitleLabel.layer.cornerRadius = indicatorView.cornerRadiuslet selectedTitleLabel = UILabel()selectedTitleLabel.textColor = selectedTitleColorselectedTitleLabel.text = stringselectedTitleLabel.lineBreakMode = .byTruncatingTailselectedTitleLabel.textAlignment = .centerselectedTitleLabel.font = selectedTitleFontreturn (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 truepublic var bouncesOnChange = true/// Whether the the control should always send the .ValueChanged event, regardless of the index remaining unchanged after interaction. Defaults to falsepublic var alwaysAnnouncesValue = false/// Whether to send the .ValueChanged event immediately or wait for animations to complete. Defaults to truepublic var announcesValueImmediately = true/// Whether the the control should ignore pan gestures. Defaults to falsepublic var panningDisabled = false/// The control's and indicator's corner radii@IBInspectable public var cornerRadius: CGFloat {get {return layer.cornerRadius}set {layer.cornerRadius = newValueindicatorView.cornerRadius = newValue - indicatorViewInsettitleLabels.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 widthpublic var indicatorViewBorderWidth: CGFloat {get {return indicatorView.layer.borderWidth}set {indicatorView.layer.borderWidth = newValue}}/// The indicator view's border widthpublic var indicatorViewBorderColor: CGColor? {get {return indicatorView.layer.borderColor}set {indicatorView.layer.borderColor = newValue}}/// The indicator view's line colorpublic 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' fontpublic var titleFont: UIFont = UILabel().font {didSet {titleLabels.forEach { $0.font = titleFont }}}/// The selected title's fontpublic var selectedTitleFont: UIFont = UILabel().font {didSet {selectedTitleLabels.forEach { $0.font = selectedTitleFont }}}/// The titles' border widthpublic var titleBorderWidth: CGFloat = 0.0 {didSet {titleLabels.forEach { $0.layer.borderWidth = titleBorderWidth }}}/// The titles' border colorpublic var titleBorderColor: CGColor = UIColor.clear.cgColor {didSet {titleLabels.forEach { $0.layer.borderColor = titleBorderColor }}}// MARK: - Private propertiesfileprivate let contentScrollView: UIScrollView = {let scrollView = UIScrollView()scrollView.showsVerticalScrollIndicator = falsescrollView.showsHorizontalScrollIndicator = falsereturn 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 NSStringlet 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).sizereturn strSize.width}}// MARK: Lifecyclerequired public init?(coder aDecoder: NSCoder) {index = 0titleColor = Color.titleselectedTitleColor = Color.selectedTitlesuper.init(coder: aDecoder)titles = defaultTitlesfinishInit()}public init(frame: CGRect,titles: [String],index: UInt,backgroundColor: UIColor,titleColor: UIColor,indicatorViewBackgroundColor: UIColor,selectedTitleColor: UIColor) {self.index = indexself.titleColor = titleColorself.selectedTitleColor = selectedTitleColorsuper.init(frame: frame)self.titles = titlesself.backgroundColor = backgroundColorself.indicatorViewBackgroundColor = indicatorViewBackgroundColorfinishInit()}@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,titles: ["First", "Second"],index: 0,backgroundColor: Color.background,titleColor: Color.title,indicatorViewBackgroundColor: Color.indicatorViewBackground,selectedTitleColor: Color.selectedTitle)}fileprivate func finishInit() {layer.masksToBounds = trueaddSubview(contentScrollView)contentScrollView.addSubview(titleLabelsView)contentScrollView.addSubview(indicatorView)contentScrollView.addSubview(selectedTitleLabelsView)selectedTitleLabelsView.layer.mask = indicatorView.titleMaskView.layertapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(SwiftySegmentedControl.tapped(_:)))addGestureRecognizer(tapGestureRecognizer)panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(SwiftySegmentedControl.panned(_:)))panGestureRecognizer.delegate = selfaddGestureRecognizer(panGestureRecognizer)}override open func layoutSubviews() {super.layoutSubviews()guard titleLabelsCount > 1 else {return}contentScrollView.frame = boundslet allElementsWidth = titlesWidth.reduce(0, {$0 + $1}) + CGFloat(titleLabelsCount) * marginSpacecontentScrollView.contentSize = CGSize(width: max(allElementsWidth, width), height: 0)titleLabelsView.frame = boundsselectedTitleLabelsView.frame = boundsindicatorView.frame = elementFrame(forIndex: index)for index in 0...titleLabelsCount-1 {let frame = elementFrame(forIndex: UInt(index))titleLabelsView.subviews[index].frame = frameselectedTitleLabelsView.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.indexself.index = indexmoveIndicatorViewToIndex(animated, shouldSendEvent: (self.index != oldIndex || alwaysAnnouncesValue))fixedScrollViewOffset(Int(self.index))}// MARK: Fixed ScrollView offsetfileprivate func fixedScrollViewOffset(_ focusIndex: Int) {guard contentScrollView.contentSize.width > width else {return}let targetMidX = self.titleLabels[Int(self.index)].frame.midXlet offsetX = contentScrollView.contentOffset.xlet addOffsetX = targetMidX - offsetX - width / 2let 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: Animationsfileprivate 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 inself.moveIndicatorView()}, completion: { (finished) -> Void inif finished && shouldSendEvent && !self.announcesValueImmediately {self.sendActions(for: .valueChanged)}})} else {moveIndicatorView()sendActions(for: .valueChanged)}}// MARK: Helpersfileprivate 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 / countlet allElementsWidth = titlesWidth.reduce(0, {$0 + $1}) + CGFloat(titleLabelsCount) * marginSpaceif 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.titlesWidthlet frontTitlesWidth = titlesWidth.enumerated().reduce(CGFloat(0)) { (total, current) inreturn current.0 < Int(index) ? total + current.1 : total}let x = frontTitlesWidth + CGFloat(index) * marginSpacereturn CGRect(x: x,y: indicatorViewInset,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)].framelayoutIfNeeded()}// 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.framecase .changed:var frame = initialIndicatorViewFrame!frame.origin.x += gestureRecognizer.translation(in: self).xframe.origin.x = max(min(frame.origin.x, bounds.width - indicatorViewInset - frame.width), indicatorViewInset)indicatorView.frame = framecase .ended, .failed, .cancelled:try! setIndex(nearestIndex(toPoint: indicatorView.center))default: break}}
}// MARK: - UIGestureRecognizerDelegate
extension 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, green:0.12, blue:0.13, alpha:1.00),selectedTitleColor: UIColor(red:0.97, green:0.00, blue:0.24, alpha:1.00))viewSegmentedControl.autoresizingMask = [.flexibleWidth]viewSegmentedControl.indicatorViewInset = 0viewSegmentedControl.cornerRadius = 0.0viewSegmentedControl.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.redview.addSubview(viewSegmentedControl)}

Github: SwiftySegmentedControl

Scroll Segmented Control(Swift)相关推荐

  1. J0ker的CISSP之路:复习-Access Control(4)

    本文同时发表在:[url]http://netsecurity.51cto.com/art/200801/63945.htm[/url] 在<J0ker的CISSP之路>的上一篇文章里,J ...

  2. OpenStack-M版(Mitaka)搭建基于(Centos7.2)+++十、Openstack对象存储服务(swift)上

    十.Openstack对象存储服务(swift)上 配置:我在计算节点添加了两块硬盘(sdb,sdc)用来当存储用,在我这搭建中计算节点也就是存储节点了,原因电脑无法拉动更多虚拟几所以咯... 简单介 ...

  3. OpenStack-M版(Mitaka)搭建基于(Centos7.2)+++十、Openstack对象存储服务(swift)中

    十.Openstack对象存储服务(swift)中 计算节点上(我把计算节点当存储节点用添加了sdb,sdc两块硬盘) 1.安装软件包: yum install xfsprogs rsync  ope ...

  4. MFC绘图工具High-speed Charting Control(VS2019)

    MFC绘图工具High-speed Charting Control(VS2019) 前言 一.High-speed Charting Control 1.1 下载 1.2 添加到项目中 二.绘图测试 ...

  5. 关于plist文件存储方式(swift)

    关于数据持久化操作一直是大家非常关注的问题,我最近看了好多关于数据存储方式的文章,小编在这里写下我的见解,希望能对大家有所帮助! 谈到数据储存,首先要明确区分两个概念,数据结构和储存方式.所谓数据结构 ...

  6. P1344 [USACO4.4] 追查坏牛奶 Pollutant Control (网络流)

    P1344 [USACO4.4] 追查坏牛奶 Pollutant Control (网络流) 题目链接 文章目录 P1344 [USACO4.4] 追查坏牛奶 Pollutant Control (网 ...

  7. 学习使用Bing Maps Silverlight Control(五):离线使用和自定义地图模式

    6 离线使用 在笔记第一部分的时候就提到如果要使用Bing Maps Silverlight Control 进行开发,需要申请一个key,不让会显示一个错误提示出来.但是在实际开发或使用过程中,使用 ...

  8. Linux Kernel TCP/IP Stack — L2 Layer — Traffic Control(流量控制)的实现原理

    目录 文章目录 目录 基本概念 QoS.Bandwidth 和 Traffic Control 队列 FIFO 队列 pfifo_fast 队列 SFQ 队列 令牌桶队列 数据流(Data Flow) ...

  9. Linux Kernel TCP/IP Stack — L2 Layer — Traffic Control(流量控制)

    目录 文章目录 目录 tc CLI - Linux 流量控制工具 TC 的基本原理 TC 的组件 Qdisc 无类别队列规定(Classless Qdiscs) 分类队列规定(Classful Qdi ...

最新文章

  1. Raspberry Pi 3B 安装NoneBot2
  2. keras冻结_【连载】深度学习第22讲:搭建一个基于keras的迁移学习花朵识别系统(附数据)...
  3. 【数据结构与算法】之深入解析“石子游戏VIII”的求解思路与算法示例
  4. 程序员必备工具包(实物)
  5. 数据 3 分钟 | Oracle 首度失去榜首位置、PingCAP 发布 TiDB 荣耀体验官活动、华为召开开发者大会 2021
  6. c 设计计算机报告,C课程设计(计算器)报告
  7. python切片输出_Python语言之详解切片
  8. MES助力中国制药行业 (GMP)
  9. 关于URL编码(转载)
  10. 3.9上午(周彤彤)
  11. matlab imagesc jet,matlab imagesc
  12. Chrome浏览器多账户登录
  13. MHL技术剖析,比HDMI更强的东东
  14. 用go编写区块链系列之7--网络
  15. iDrac6 虚拟控制台 连接失败
  16. 控制台报错For input string: ““、empty String
  17. 大数据架构选型与设计
  18. 全面、详细、通俗易懂的C语言语法和标准库
  19. 看涨期权计算函数实现(Python)
  20. 沧海一粟 之 杏花村

热门文章

  1. mysql团购活动报名_社区团购平台如何策划营销活动
  2. TOP100summit分享实录 | 服务快消品牌的数字营销解决方案
  3. flask学习之日志logging
  4. java知识分享篇一
  5. django ForeignKey参数
  6. charm-crypto安装
  7. 基于滑模观测器估计误差反馈的永磁同步电机转速控制策略
  8. MyBatis下标越界异常解决
  9. 翻译的中国电影名称 -ZZ
  10. window修改处理器类型