199 lines
6.6 KiB
Swift
199 lines
6.6 KiB
Swift
//
|
|
// QuickLocationTabBar.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import UIKit
|
|
|
|
protocol QuickLocationTabBarDelegate: AnyObject {
|
|
func tabBar(_ tabBar: QuickLocationTabBar, didSelectTabAt index: Int)
|
|
}
|
|
|
|
final class QuickLocationTabBar: UIView {
|
|
|
|
struct TabItem {
|
|
let title: String
|
|
let image: UIImage?
|
|
let selectedImage: UIImage?
|
|
|
|
init(title: String, image: UIImage?, selectedImage: UIImage? = nil) {
|
|
self.title = title
|
|
self.image = image
|
|
self.selectedImage = selectedImage ?? image
|
|
}
|
|
}
|
|
|
|
// MARK: - Design Constants (in iOS points, from Lanhu @2x 容器 2)
|
|
private let barContentHeight: CGFloat = 56
|
|
private let containerCornerRadius: CGFloat = 28
|
|
private let horizontalInset: CGFloat = 18
|
|
private let iconSize: CGFloat = 32
|
|
private let iconLabelSpacing: CGFloat = 4
|
|
private let labelHeight: CGFloat = 11
|
|
|
|
private let selectedTextColor = UIColor(hexStr: "#1A1A1A")
|
|
private let unselectedTextColor = UIColor(hexStr: "#AAAAAA")
|
|
|
|
private let selectedFont = UIFont(name: "PingFangSC-Semibold", size: 11)
|
|
?? UIFont.systemFont(ofSize: 11, weight: .semibold)
|
|
private let unselectedFont = UIFont.systemFont(ofSize: 11, weight: .regular)
|
|
|
|
// MARK: - Properties
|
|
weak var delegate: QuickLocationTabBarDelegate?
|
|
|
|
private let items: [TabItem]
|
|
private var tabViews: [UIView] = []
|
|
private let containerView = UIView()
|
|
|
|
private(set) var selectedIndex: Int = 0
|
|
|
|
// MARK: - Init
|
|
init(items: [TabItem]) {
|
|
self.items = items
|
|
super.init(frame: .zero)
|
|
setupUI()
|
|
updateSelection(animated: false)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: - Layout
|
|
override var intrinsicContentSize: CGSize {
|
|
CGSize(width: UIView.noIntrinsicMetric, height: barContentHeight + kSafeBottomMargin)
|
|
}
|
|
|
|
// MARK: - Setup
|
|
private func setupUI() {
|
|
backgroundColor = .clear
|
|
|
|
containerView.backgroundColor = .white
|
|
containerView.layer.cornerRadius = containerCornerRadius
|
|
containerView.layer.shadowColor = UIColor(red: 0.059, green: 0.157, blue: 0.275, alpha: 0.1).cgColor
|
|
containerView.layer.shadowOffset = .zero
|
|
containerView.layer.shadowRadius = 8
|
|
containerView.layer.shadowOpacity = 1
|
|
addSubview(containerView)
|
|
|
|
for (index, item) in items.enumerated() {
|
|
let tabView = makeTabView(item: item, index: index)
|
|
containerView.addSubview(tabView)
|
|
tabViews.append(tabView)
|
|
}
|
|
|
|
containerView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
NSLayoutConstraint.activate([
|
|
containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
|
containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
|
|
containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -kSafeBottomMargin),
|
|
containerView.heightAnchor.constraint(equalToConstant: barContentHeight)
|
|
])
|
|
}
|
|
|
|
private func makeTabView(item: TabItem, index: Int) -> UIView {
|
|
let view = UIView()
|
|
view.tag = index
|
|
view.backgroundColor = .clear
|
|
|
|
let imageView = UIImageView(image: item.image?.withRenderingMode(.alwaysOriginal))
|
|
imageView.contentMode = .scaleAspectFit
|
|
imageView.tag = 100
|
|
view.addSubview(imageView)
|
|
|
|
let label = UILabel()
|
|
label.text = item.title
|
|
label.font = unselectedFont
|
|
label.textColor = unselectedTextColor
|
|
label.textAlignment = .center
|
|
label.tag = 200
|
|
view.addSubview(label)
|
|
|
|
let tap = UITapGestureRecognizer(target: self, action: #selector(tabTapped(_:)))
|
|
view.addGestureRecognizer(tap)
|
|
|
|
return view
|
|
}
|
|
|
|
// MARK: - Actions
|
|
@objc private func tabTapped(_ gesture: UITapGestureRecognizer) {
|
|
guard let view = gesture.view else { return }
|
|
let index = view.tag
|
|
guard index != selectedIndex else { return }
|
|
selectedIndex = index
|
|
updateSelection(animated: true)
|
|
delegate?.tabBar(self, didSelectTabAt: index)
|
|
}
|
|
|
|
// MARK: - Selection
|
|
func setSelectedIndex(_ index: Int, animated: Bool = true) {
|
|
guard index != selectedIndex, index >= 0, index < items.count else { return }
|
|
selectedIndex = index
|
|
updateSelection(animated: animated)
|
|
}
|
|
|
|
private func updateSelection(animated: Bool) {
|
|
for (i, view) in tabViews.enumerated() {
|
|
let isSelected = i == selectedIndex
|
|
if let imageView = view.viewWithTag(100) as? UIImageView {
|
|
let img = isSelected ? items[i].selectedImage : items[i].image
|
|
imageView.image = img?.withRenderingMode(.alwaysOriginal)
|
|
}
|
|
if let label = view.viewWithTag(200) as? UILabel {
|
|
label.font = isSelected ? selectedFont : unselectedFont
|
|
label.textColor = isSelected ? selectedTextColor : unselectedTextColor
|
|
}
|
|
}
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
let containerWidth = containerView.bounds.width
|
|
guard containerWidth > 0, !tabViews.isEmpty else { return }
|
|
|
|
let tabWidth = containerWidth / CGFloat(tabViews.count)
|
|
let tabHeight = containerView.bounds.height
|
|
|
|
let totalHeight = iconSize + iconLabelSpacing + labelHeight
|
|
let startY = (tabHeight - totalHeight) / 2
|
|
|
|
for (i, view) in tabViews.enumerated() {
|
|
view.frame = CGRect(
|
|
x: CGFloat(i) * tabWidth,
|
|
y: 0,
|
|
width: tabWidth,
|
|
height: tabHeight
|
|
)
|
|
|
|
if let imageView = view.viewWithTag(100) as? UIImageView {
|
|
imageView.frame = CGRect(
|
|
x: (tabWidth - iconSize) / 2,
|
|
y: startY,
|
|
width: iconSize,
|
|
height: iconSize
|
|
)
|
|
}
|
|
|
|
if let label = view.viewWithTag(200) as? UILabel {
|
|
label.frame = CGRect(
|
|
x: 0,
|
|
y: startY + iconSize + iconLabelSpacing,
|
|
width: tabWidth,
|
|
height: labelHeight
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Hit Testing
|
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
if containerView.frame.contains(point) {
|
|
return true
|
|
}
|
|
let extendedBounds = bounds.insetBy(dx: 0, dy: -kSafeBottomMargin)
|
|
return extendedBounds.contains(point)
|
|
}
|
|
}
|