今天用了一个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,
                  titles: ["First", "Second"],
                  index: 0,
                  backgroundColor: Color.background,
                  titleColor: Color.title,
                  indicatorViewBackgroundColor: Color.indicatorViewBackground,
                  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,
                          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)].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: - 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 = 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)的更多相关文章

  1. OpenStack Object Storage(Swift)概述

    概述 OpenStack Object Storage(Swift)是OpenStack开源云计算项目的子项目之一,被称为对象存储,提供了强大的扩展性.冗余和持久性. Swift并不是文件系统或者实时 ...

  2. 用UILocalNotification实现一个闹钟(Swift)

    之前项目需求要实现一个闹钟,github上找了半天发现都是很旧的代码了,所以就准备自己写一个,刚好最近在学习Swift,就用Swift写了一个demo放在这里:https://github.com/P ...

  3. 开发基于Handoff的App(Swift)

            iOS8推出一个新特性,叫做Handoff.Handoff中文含义为换手(把接力棒传给下一个人),可以在一台Mac和iOS设备上开始工作,中途将工作交换到另一个Mac或iOS设备中进行 ...

  4. 函数(swift)

    输入输出参数(In-Out Parameters) 如果你想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Paramet ...

  5. RNA Spike-in Control(转)

    Spike-in Control:添加/加入(某种物质)的对照(组)在某些情况下,待检验样本中不含待测物质或者含有但是浓度很低,为了证明自己建立的方法能对样本中待测物质进行有效的检测,可在待检样本中加 ...

  6. about Version Control(版本控制)

    what: 版本控制系统是一种软件,它可以帮助您跟踪代码随时间的变化. 在编辑代码时,您告诉版本控制系统对文件进行快照. 版本控制系统将永久保存该快照,以便在以后需要时可以收回它. 如果没有版本控制, ...

  7. JSON数据的解析和生成(Swift)

    Codable public typealias Codable = Decodable & Encodable public protocol Decodable {} public pro ...

  8. 正则表达式(Swift)

    课题 使用正则表达式匹配字符串 使用正则表达式 "\d{3}-(\d{4})-\d{2}" 匹配字符串 "123-4567-89" 返回匹配结果:'" ...

  9. iScroll框架解析——Android 设备页面内 div(容器,非页面)overflow:scroll; 失效解决(转)

    移动平台的活,兼容问题超多,今儿又遇到一个.客户要求在弹出层容器内显示内容,但内容条数过多,容器显示滚动条.按说是So easy,容器设死宽.高,CSS加属性 overflow:scroll; -we ...

随机推荐

  1. SpringMVC(四):@RequestMapping结合org.springframework.web.filter.HiddenHttpMethodFilter实现REST请求

    1)REST具体表现: --- /account/1  HTTP GET       获取id=1的account --- /account/1  HTTP DELETE 删除id=1的account ...

  2. 1.3WEB API 默认以json格式返回数据,同时定义时间格式,返回格式

    首先我们知道,web api 是可以返回任意类型的,然后在输出的过程中转为(默认的)xml. 但是xml是比较费流量的,而且大多前端都是用json对接,所以我们也只能随大流,把它输出改成json. 不 ...

  3. Canvas-自由绘制

    #自由绘制 from tkinter import * master=Tk() c=Canvas(master,width=400,height=200) c.pack() def paint(eve ...

  4. jacascript 函数声明、函数表达式与声明提升(hoisting机制)

    前言:这是笔者学习之后自己的理解与整理.如果有错误或者疑问的地方,请大家指正,我会持续更新! 声明.定义.初始化 声明的意思是宣称一个变量名的存在,定义则为这个变量分配存储空间,初始化则是给该变量名的 ...

  5. Ubuntu Sublime 配置

    p { margin-bottom: 0.25cm; line-height: 120% } a:link { } 2018.4.14 Ubuntu Sublime 配置 承 Ubuntu Apach ...

  6. Java入门1

    一.eclipse的简单使用 1.新建项目 在package explorer的空白处点击右键,新建一个项目(new->Java Project)或者点击菜单栏的File->JavaPro ...

  7. LAMP+Wordpress搭建博客

    作为一名技术控,看到别人都有专属于自己的博客,小夜也忍不住自己操刀做个自己的博客.有兴趣的童鞋接下来可要认真地练习比对哦.如果出现错误也不要着急上火,咱们可以交流学习.我们的口号是:遇水搭桥,遇山凿山 ...

  8. angualar2——八大组成

    Angular2 模块 理解: Angular 应用是模块化的,并且 Angular 有自己的模块系统,它被称为 Angular 模块或 NgModules. 组件 组件是一个项目主干,一个模块由多个 ...

  9. Linux的基本命令(CentOS)

    1.ll:列出当前文件夹下所有的文件夹的详细信息.2.ls:列出当前文件夹下的所有文件(只有名字)   ls -a查看隐藏文件   ls / 根目录下的文件   pwd 查看当前所在目录   who ...

  10. pytorch 移动端框架 thnets 附c示例代码

    前年年前做一个手机移动端图像识别项目的时候, 先后尝试了mxnet,thnets,caffe,tensorflow. 当时的情况是,mxnet内存管理奇差,内存经常由于模型运算分配不足,app挂掉. ...