Created
July 4, 2020 05:43
-
-
Save trilliwon/4331183e82f5644a2399f73b6aa89f6b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
final class ScrollingPageControl: UIView { | |
// The number of dots | |
var pages: Int = 0 { | |
didSet { | |
guard pages != oldValue else { return } | |
pages = max(0, pages) | |
invalidateIntrinsicContentSize() | |
makeDotViews() | |
} | |
} | |
private func makeDotViews() { | |
dotViews = (0..<pages).map { index in | |
Circle(frame: CGRect(origin: .zero, size: CGSize(width: dotSize, height: dotSize))) | |
} | |
} | |
// The index of the currently selected page | |
var currentPage: Int = 0 { | |
didSet { | |
guard currentPage != oldValue else { return } | |
currentPage = max(0, min (currentPage, pages - 1)) | |
updateColors() | |
if (0..<centerDots).contains(currentPage - pageOffset) { | |
centerOffset = currentPage - pageOffset | |
} else { | |
pageOffset = currentPage - centerOffset | |
} | |
} | |
} | |
// The maximum number of dots that will show in the control | |
var maxDots = 7 { | |
didSet { | |
maxDots = max(3, maxDots) | |
if maxDots % 2 == 0 { | |
maxDots += 1 | |
print("maxPages has to be an odd number") | |
} | |
invalidateIntrinsicContentSize() | |
updatePositions() | |
} | |
} | |
// The number of dots that will be centered and full-sized | |
var centerDots = 3 { | |
didSet { | |
centerDots = max(1, centerDots) | |
if centerDots % 2 == 0 { | |
centerDots += 1 | |
print("centerDots has to be an odd number") | |
} | |
updatePositions() | |
} | |
} | |
// The duration, in seconds, of the dot slide animation | |
var slideDuration: TimeInterval = 0.15 | |
private var centerOffset = 0 | |
private var pageOffset = 0 { | |
didSet { | |
UIView.animate( | |
withDuration: slideDuration, | |
delay: 0.15, | |
options: [.curveEaseInOut], | |
animations: { [weak self] in | |
self?.updatePositions() | |
}) | |
} | |
} | |
private var dotViews: [UIView] = [] { | |
didSet { | |
oldValue.forEach { $0.removeFromSuperview() } | |
dotViews.forEach(addSubview) | |
updateColors() | |
updatePositions() | |
} | |
} | |
// The color of all the unselected dots | |
var dotColor = UIColor(white: 224 / 255, alpha: 1) | |
// The color of the currently selected dot | |
var selectedColor = UIColor(red: 4 / 255, green: 166 / 255, blue: 225 / 255, alpha: 1) | |
// The size of the dots | |
var dotSize: CGFloat = 6 | |
// The space between dots | |
var spacing: CGFloat = 6 | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
} | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
isOpaque = false | |
} | |
private var lastSize = CGSize.zero | |
override func layoutSubviews() { | |
super.layoutSubviews() | |
guard bounds.size != lastSize else { return } | |
lastSize = bounds.size | |
updatePositions() | |
} | |
private func updateColors() { | |
dotViews.enumerated().forEach { page, dot in | |
dot.tintColor = page == currentPage ? selectedColor : dotColor | |
} | |
} | |
private func updatePositions() { | |
if pages > 5 { | |
let sidePages = (maxDots - centerDots) / 2 | |
let horizontalOffset = CGFloat(-pageOffset + sidePages) * (dotSize + spacing) + (bounds.width - intrinsicContentSize.width) / 2 | |
let centerPage = centerDots / 2 + pageOffset | |
dotViews.enumerated().forEach { page, dot in | |
let center = CGPoint(x: horizontalOffset + bounds.minX + dotSize / 2 + (dotSize + spacing) * CGFloat(page), y: bounds.midY) | |
let scale: CGFloat = { | |
let distance = abs(page - centerPage) | |
if distance > (maxDots / 2) { return 0 } | |
return [1, 0.66, 0.33, 0.16][max(0, min(3, distance - centerDots / 2))] | |
}() | |
dot.frame = CGRect(origin: .zero, size: CGSize(width: dotSize * scale, height: dotSize * scale)) | |
dot.center = center | |
} | |
} else { | |
dotViews.enumerated().forEach { page, dot in | |
dot.frame = CGRect(origin: .zero, size: CGSize(width: dotSize, height: dotSize)) | |
let center = CGPoint(x: bounds.minX + dotSize / 2 + (dotSize + spacing) * CGFloat(page), y: bounds.midY) | |
dot.center = center | |
} | |
} | |
} | |
override var intrinsicContentSize: CGSize { | |
let pages = min(maxDots, self.pages) | |
let width = CGFloat(pages) * dotSize + CGFloat(pages - 1) * spacing | |
let height = dotSize | |
return CGSize(width: width, height: height) | |
} | |
class Circle: UIView { | |
override func tintColorDidChange() { | |
self.backgroundColor = tintColor | |
} | |
override func layoutSubviews() { | |
super.layoutSubviews() | |
layer.cornerRadius = min(bounds.width, bounds.height) / 2 | |
} | |
override var frame: CGRect { | |
didSet { | |
layer.cornerRadius = min(bounds.width, bounds.height) / 2 | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment