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循环结构示例
本文章主要是帮助大家学习循环结构.学习循环时,最重要的是理清思路,那些最经典算法实际中我们并不会单拿出来用,而是会用到当时做这个算法时的思想.如果把这个思路想明白了,那么实际中用到他的时候自然而然就想 ...
- 洛谷 P3038 [USACO11DEC]牧草种植Grass Planting(树链剖分)
题解:仍然是无脑树剖,要注意一下边权,然而这种没有初始边权的题目其实和点权也没什么区别了 代码如下: #include<cstdio> #include<vector> #in ...
- Java并发编程之Lock
重入锁ReentrantLock 可以代替synchronized, 但synchronized更灵活. 但是, 必须必须必须要手动释放锁. try { lock.lock(); } finally ...
- IOS开发-UIDynamic(物理仿真)简单使用
UIDynamic是从IOS7开始引入的一种新技术,隶属于UIKit框架,我们可以认为是一种物理引擎能模拟和仿真现实生活中的物理现象,比如重力,弹性碰撞等. 可以让开发人员远离物理公式的情况下,实现一 ...
- [Sdoi2016]征途
Description Pine开始了从S地到T地的征途. 从S地到T地的路可以划分成n段,相邻两段路的分界点设有休息站. Pine计划用m天到达T地.除第m天外,每一天晚上Pine都必须在休息站过夜 ...
- 洛谷P1446 [HNOI2008]Cards
置换群+dp #include<cstdio> #include<cstdlib> #include<algorithm> #include<cstring& ...
- NOIP2014-5-10模拟赛
Problem 1 机器人(robot.cpp/c/pas) [题目描述] 早苗入手了最新的Gundam模型.最新款自然有着与以往不同的功能,那就是它能够自动行走,厉害吧. 早苗的新模型可以按照输入的 ...
- 【Codeforces Round #431 (Div. 1) D.Shake It!】
·最小割和组合数放在了一起,产生了这道题目. 英文题,述大意: 一张初始化为仅有一个起点0,一个终点1和一条边的图.输入n,m表示n次操作(1<=n,m<=50),每次操作是任选一 ...
- c++中成员函数的参数名与成员变量名重合的问题
有一天写类的时候突然想到了这个问题,下面就来介绍如何解决这个问题. 定义一个类: class test{ public: void setnum(); void getnum(); private: ...
- Linux之软链接与硬链接
什么是链接? 链接简单说实际上是一种文件共享的方式,是 POSIX 中的概念,主流文件系统都支持链接文件. 它是用来干什么的? 你可以将链接简单地理解为 Windows 中常见的快捷方式(或是 OS ...