加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

Scroll Segmented Control(Swift)

发布时间:2020-12-14 06:29:14 所属栏目:百科 来源:网络整理
导读:今天用了一个github上一个比较好用的Segmented Control但是发现不是我要效果,我需要支持scrollView。当栏目数量超过一屏幕,需要能够滑动。 由于联系作者没有回复,我就自己在其基础上增加了下scrollView的支持。 代码比较简单,直接在UIControl下写的。 其

今天用了一个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: - 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,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

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读