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 ...
随机推荐
- 卷积神经网络的一些经典网络2(Inception)
在架构内容设计方面,其中一个比较有帮助的想法是使用1x1卷积.1x1卷积能做什么? 对于6x6x1的通道的图片来说,1x1卷积效果不佳,如果是一张6x6x32的图片,那么使用1x1卷积核进行卷积效果更 ...
- ORACLE 行转列 案例解析
-- 创建 国家城市信息 临时表 WITH T_NATION_CITY_INFO AS( SELECT '北京' AS CITY,'中国' AS N ...
- JavaScript树(一) 简介
树的相关术语 一个树结构包含一系列存在父子关系的节点. 每个节点都有一个父节点 (除了顶部的第一个节点)以及零个或多个子节点: 位于树顶部的节点叫作根节点(11) .它没有父节点.树中的每个元素都叫作 ...
- angularjs中的几种工具方法
1.比较两个字符串是否相等 2.对象形式转化成json和json转化成字符串形式 3.便利对象遍历数组 4.绑定数据 5.多个app功能模块的实现 <!doctype html><h ...
- [LeetCode] Shortest Completing Word 最短完整的单词
Find the minimum length word from a given dictionary words, which has all the letters from the strin ...
- 机器学习技法:05 Kernel Logistic Regression
Roadmap Soft-Margin SVM as Regularized Model SVM versus Logistic Regression SVM for Soft Binary Clas ...
- [HNOI2016]序列
题目描述 给定长度为n的序列:a1,a2,...,an,记为a[1:n].类似地,a[l:r](1<=l<=r<=N)是指序 列:al,al+1,...,ar-1,ar.若1< ...
- [BZOJ]4650: [Noi2016]优秀的拆分
Time Limit: 30 Sec Memory Limit: 512 MB Description 如果一个字符串可以被拆分为 AABBAABB 的形式,其中 AA 和 BB 是任意非空字符串, ...
- ●BZOJ 2149 拆迁队
题链: http://www.lydsy.com/JudgeOnline/problem.php?id=2149 题解: 斜率优化DP,栈维护凸包,LIS,分治(我也不晓得是不是CDQ分治...) 一 ...
- hdu 5137(2014广州—最短路)
题意:给你一个图,求删除一个点后使1->n的距离最大 思路: 枚举删除点,然后求最短路,取这些最短路的最大值 #include <iostream> #include <cst ...