Scroll Segmented Control(Swift)
今天用了一个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)的更多相关文章
- OpenStack Object Storage(Swift)概述
概述 OpenStack Object Storage(Swift)是OpenStack开源云计算项目的子项目之一,被称为对象存储,提供了强大的扩展性.冗余和持久性. Swift并不是文件系统或者实时 ...
- 用UILocalNotification实现一个闹钟(Swift)
之前项目需求要实现一个闹钟,github上找了半天发现都是很旧的代码了,所以就准备自己写一个,刚好最近在学习Swift,就用Swift写了一个demo放在这里:https://github.com/P ...
- 开发基于Handoff的App(Swift)
iOS8推出一个新特性,叫做Handoff.Handoff中文含义为换手(把接力棒传给下一个人),可以在一台Mac和iOS设备上开始工作,中途将工作交换到另一个Mac或iOS设备中进行 ...
- 函数(swift)
输入输出参数(In-Out Parameters) 如果你想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Paramet ...
- RNA Spike-in Control(转)
Spike-in Control:添加/加入(某种物质)的对照(组)在某些情况下,待检验样本中不含待测物质或者含有但是浓度很低,为了证明自己建立的方法能对样本中待测物质进行有效的检测,可在待检样本中加 ...
- about Version Control(版本控制)
what: 版本控制系统是一种软件,它可以帮助您跟踪代码随时间的变化. 在编辑代码时,您告诉版本控制系统对文件进行快照. 版本控制系统将永久保存该快照,以便在以后需要时可以收回它. 如果没有版本控制, ...
- JSON数据的解析和生成(Swift)
Codable public typealias Codable = Decodable & Encodable public protocol Decodable {} public pro ...
- 正则表达式(Swift)
课题 使用正则表达式匹配字符串 使用正则表达式 "\d{3}-(\d{4})-\d{2}" 匹配字符串 "123-4567-89" 返回匹配结果:'" ...
- iScroll框架解析——Android 设备页面内 div(容器,非页面)overflow:scroll; 失效解决(转)
移动平台的活,兼容问题超多,今儿又遇到一个.客户要求在弹出层容器内显示内容,但内容条数过多,容器显示滚动条.按说是So easy,容器设死宽.高,CSS加属性 overflow:scroll; -we ...
随机推荐
- Java-NIO(一):简介
Java NIO简介: Java New IO Non Blocking IO,从java1.4版本就开始引入了新的IO API,可以替代标准的Java IO API.NIO与原来的IO有同样的作用和 ...
- [论文阅读] A Discriminative Feature Learning Approach for Deep Face Recognition (Center Loss)
原文: A Discriminative Feature Learning Approach for Deep Face Recognition 用于人脸识别的center loss. 1)同时学习每 ...
- angularJs-route路由详解
本篇基于ng-route来讲下angular中的路由,路由功能主要是 $routeProvider服务 与 ng-view 实现. ng-view的实现原理,是根据路由的切换,动态编译html模板-- ...
- java集合详解
1.java集合框架的层次结构 Collection接口: Set接口: HashSet具体类 LinkedHashSet具体类 TreeSet具体类 List接口: ArrayList具体类 L ...
- C++程序设计语言(特别版) -- 一个桌面计算器
前言 这里要介绍各种语句和表达式,将通过一个桌面计算器的程序做些事情,该计算器提供四种座位浮点数的中缀运算符的标准算术运算. 这个计算器由四个部分组成:一个分析器,一个输入函数,一个符号表和一个驱动程 ...
- 用DotTrace 来分析.NET-Core程序
1. 前言 看园子里面讲dotTrace 的文章不多,最近也有这方面的需要,于是去搜索了一下,.NET 性能分析方面的工具.目的呢,主要是想发现我的代码中,哪些代码占用了最多时间,来进行优化.主要 ...
- [Codeforces]856D - Masha and Cactus
题目大意:给出一棵树和若干条可以加入的边,要求加入若干条边使图是仙人掌并且加入的边权和最大,仙人掌定义为没有一个点属于超过1个环.(n,m<=200,000) 做法:这题的仙人掌跟平时见到的不太 ...
- UVA1658:Admiral
题意:给定一个有向带权图,求两条不相交(无公共点)的路径且路径权值之和最小,路径由1到v 题解:这题的关键就在于每个点只能走一遍,于是我们想到以边换点的思想,用边来代替点,怎么代替呢? 把i拆成i和i ...
- HDU2303(数论)大整数求余+素数筛选
Sample Input 143 10 143 20 667 20 667 30 2573 30 2573 40 0 0 Sample Output GOOD BAD 11 GOOD BAD 23 ...
- 伸展树Splay【非指针版】
·伸展树有以下基本操作(基于一道强大模板题:codevs维护队列): a[]读入的数组;id[]表示当前数组中的元素在树中节点的临时标号;fa[]当前节点的父节点的编号;c[][]类似于Trie,就是 ...