231 lines
7.3 KiB
Swift
231 lines
7.3 KiB
Swift
//
|
|
// Copyright 2022-2024 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// Please see LICENSE in the repository root for full details.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
protocol TabListViewDelegate: AnyObject {
|
|
func tabListView(_ tabListView: TabListView, didSelectTabAt index: Int)
|
|
}
|
|
|
|
class TabListView: UIView {
|
|
|
|
// MARK: - Constants
|
|
|
|
enum Constants {
|
|
static let cursorHeight: Double = 3
|
|
static let itemSpacing: Double = 30
|
|
static let cursorPadding: Double = 6
|
|
}
|
|
|
|
// MARK: - Item definition
|
|
|
|
class Item {
|
|
let id: Any
|
|
let text: String?
|
|
let icon: UIImage?
|
|
|
|
init(id: Any = UUID().uuidString,
|
|
text: String? = nil,
|
|
icon: UIImage? = nil) {
|
|
self.id = id
|
|
self.text = text
|
|
self.icon = icon
|
|
}
|
|
}
|
|
|
|
// MARK: - Properties
|
|
|
|
weak var delegate: TabListViewDelegate?
|
|
var items: [Item] = [] {
|
|
didSet {
|
|
populateItemViews()
|
|
}
|
|
}
|
|
var pageIndex: Double = 0 {
|
|
didSet {
|
|
updateCursor()
|
|
}
|
|
}
|
|
var unselectedItemColor: UIColor = .lightGray {
|
|
didSet {
|
|
updateCursor()
|
|
}
|
|
}
|
|
var itemFont: UIFont = .preferredFont(forTextStyle: .body) {
|
|
didSet {
|
|
for button in itemViews {
|
|
button.titleLabel?.font = itemFont
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private var itemViews: [UIButton] = []
|
|
private let scrollView = UIScrollView(frame: .zero)
|
|
private let cursorView = UIView(frame: .zero)
|
|
private let itemsContentView = UIStackView(frame: .zero)
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
setupView()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
|
|
setupView()
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
itemsContentView.layoutIfNeeded()
|
|
updateCursor()
|
|
}
|
|
|
|
override func tintColorDidChange() {
|
|
super.tintColorDidChange()
|
|
|
|
self.cursorView.backgroundColor = tintColor
|
|
updateCursor()
|
|
}
|
|
|
|
// MARK: - Public
|
|
|
|
func setPageIndex(_ pageIndex: Double, animated: Bool) {
|
|
if !animated {
|
|
self.pageIndex = pageIndex
|
|
} else {
|
|
UIView.animate(withDuration: 0.3) {
|
|
self.pageIndex = pageIndex
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
@objc private func tabAction(sender: UIButton) {
|
|
delegate?.tabListView(self, didSelectTabAt: sender.tag)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func setupView() {
|
|
scrollView.backgroundColor = .clear
|
|
scrollView.showsHorizontalScrollIndicator = false
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
|
|
addSubview(scrollView)
|
|
|
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
scrollView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
|
|
scrollView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
|
|
scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
|
|
scrollView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
|
|
|
|
itemsContentView.backgroundColor = .clear
|
|
itemsContentView.axis = .horizontal
|
|
itemsContentView.distribution = .fillProportionally
|
|
itemsContentView.alignment = .center
|
|
itemsContentView.spacing = 0
|
|
|
|
scrollView.addSubview(itemsContentView)
|
|
|
|
itemsContentView.translatesAutoresizingMaskIntoConstraints = false
|
|
itemsContentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor).isActive = true
|
|
itemsContentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor).isActive = true
|
|
itemsContentView.centerYAnchor.constraint(equalTo: scrollView.frameLayoutGuide.centerYAnchor).isActive = true
|
|
itemsContentView.widthAnchor.constraint(greaterThanOrEqualTo: scrollView.frameLayoutGuide.widthAnchor).isActive = true
|
|
|
|
cursorView.backgroundColor = tintColor
|
|
cursorView.isUserInteractionEnabled = false
|
|
cursorView.layer.masksToBounds = true
|
|
|
|
scrollView.addSubview(cursorView)
|
|
}
|
|
|
|
private func populateItemViews() {
|
|
for view in itemViews {
|
|
itemsContentView.removeArrangedSubview(view)
|
|
view.removeFromSuperview()
|
|
}
|
|
|
|
var itemViews: [UIButton] = []
|
|
for (index, item) in items.enumerated() {
|
|
let button = UIButton(type: .system)
|
|
button.titleLabel?.font = itemFont
|
|
button.setTitle(item.text, for: .normal)
|
|
button.setImage(item.icon?.withRenderingMode(.alwaysTemplate), for: .normal)
|
|
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: Constants.itemSpacing / 2, bottom: 0, right: Constants.itemSpacing / 2)
|
|
button.tag = index
|
|
button.tintColor = unselectedItemColor
|
|
button.addTarget(self, action: #selector(tabAction(sender:)), for: .touchUpInside)
|
|
|
|
itemViews.append(button)
|
|
itemsContentView.addArrangedSubview(button)
|
|
}
|
|
|
|
self.itemViews = itemViews
|
|
itemsContentView.layoutIfNeeded()
|
|
setNeedsLayout()
|
|
}
|
|
|
|
private func updateCursor() {
|
|
var integral: Double = 0
|
|
let fractional: Double = modf(pageIndex, &integral)
|
|
|
|
guard Int(integral) < itemViews.count else {
|
|
return
|
|
}
|
|
|
|
let focusedButton = itemViews[Int(integral)]
|
|
let nextButtonIndex = Int(integral) + 1
|
|
|
|
let x: CGFloat
|
|
let width: CGFloat
|
|
let focusedButtonFrame: CGRect = titleLabelFrame(with: focusedButton).insetBy(dx: -Constants.cursorPadding, dy: 0)
|
|
if nextButtonIndex < itemViews.count {
|
|
let nextButtonFrame = titleLabelFrame(with: itemViews[nextButtonIndex]).insetBy(dx: -Constants.cursorPadding, dy: 0)
|
|
x = focusedButtonFrame.minX + (nextButtonFrame.minX - focusedButtonFrame.minX) * fractional
|
|
width = focusedButtonFrame.width + (nextButtonFrame.width - focusedButtonFrame.width) * fractional
|
|
} else {
|
|
x = focusedButtonFrame.minX
|
|
width = focusedButtonFrame.width
|
|
}
|
|
|
|
cursorView.frame = CGRect(x: x,
|
|
y: bounds.height - Constants.cursorHeight,
|
|
width: width,
|
|
height: Constants.cursorHeight)
|
|
cursorView.layer.cornerRadius = cursorView.bounds.height / 2
|
|
|
|
for button in self.itemViews {
|
|
if button == focusedButton {
|
|
button.tintColor = self.tintColor
|
|
} else {
|
|
button.tintColor = self.unselectedItemColor
|
|
}
|
|
}
|
|
}
|
|
|
|
private func titleLabelFrame(with button: UIButton) -> CGRect {
|
|
guard let titleLabel = button.titleLabel else {
|
|
return button.frame
|
|
}
|
|
|
|
return CGRect(x: button.frame.minX + titleLabel.frame.minX,
|
|
y: button.frame.minY + titleLabel.frame.minY,
|
|
width: titleLabel.frame.width,
|
|
height: titleLabel.frame.height)
|
|
}
|
|
|
|
}
|