element-ios/Riot/Modules/Common/SectionHeaders/TabListView.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)
}
}