Scroll Segmented Control(Swift)
来源:互联网 发布:贪玩蓝月法神升级数据 编辑:程序博客网 时间:2024/06/06 02:53
今天用了一个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: - 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, 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
0 0
- Scroll Segmented Control(Swift)
- swift UI专项训练30 Segmented Control段控件
- 自定义 Segmented Control
- 什么是segmented control
- segmented control的使用
- Segmented Control - 仿 app store的Segmented Control
- iPhone Segmented Control:UISegmentedControl Tutorial
- IOS学习之segmented control
- IOS学习之segmented control
- 三十而立,从零开始学ios开发(六):IOS控件(3),Segmented Control、
- How to use the Segmented Control
- 一个定制SEGMENTED CONTROL的开源代码
- iOS学习——Segmented Control
- TextView,TextField,slider,segmented Control等各类控件的使用
- 《iOS Human Interface Guidelines》——Segmented Control
- segmented control切换segment界面小技巧,小注意
- Android仿IOS的Segmented Control 选项卡。
- Swift 控制流(Control Flow)
- 怎样编辑pdf文件的贝茨编号
- Objective-c - @property的参数strong与weak
- Jaxb 完全手册
- Spring环境配置(安装springsource-tool-suite插件)及第一个Spring HelloWorld
- 欢迎使用CSDN-markdown编辑器
- Scroll Segmented Control(Swift)
- Android 上拉加载下拉刷新
- ffmpeg学习十二:滤镜(实现视频缩放,裁剪,水印等)
- JS缩略轮播
- LeetCode - 414. Third Maximum Number-思路详解- C++
- clone 和 fork 的区别
- ExtJS学习笔记(四) Datetime控件
- jquery.min.js:3 GET http://xx/advert.css net::ERR_BLOCKED_BY_CLIENT
- 文章标题