Тапбар — dual-layer маскировка, плавные анимации и badge
This commit is contained in:
46
Rosetta/DesignSystem/Components/RosettaTab.swift
Normal file
46
Rosetta/DesignSystem/Components/RosettaTab.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
|
||||
/// Tab model for the main tab bar — shared by MainTabView and RosettaTabBar.
|
||||
enum RosettaTab: String, CaseIterable, Sendable {
|
||||
case chats, calls, settings
|
||||
|
||||
static let interactionOrder: [RosettaTab] = [.calls, .chats, .settings]
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .chats: return "Chats"
|
||||
case .calls: return "Calls"
|
||||
case .settings: return "Settings"
|
||||
}
|
||||
}
|
||||
|
||||
var animationName: String {
|
||||
switch self {
|
||||
case .chats: return "TabChats"
|
||||
case .calls: return "TabCalls"
|
||||
case .settings: return "TabSettings"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .chats: return "bubble.left.and.bubble.right"
|
||||
case .calls: return "phone"
|
||||
case .settings: return "gearshape"
|
||||
}
|
||||
}
|
||||
|
||||
var selectedIcon: String {
|
||||
switch self {
|
||||
case .chats: return "bubble.left.and.bubble.right.fill"
|
||||
case .calls: return "phone.fill"
|
||||
case .settings: return "gearshape.fill"
|
||||
}
|
||||
}
|
||||
|
||||
var interactionIndex: Int {
|
||||
Self.interactionOrder.firstIndex(of: self) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
struct TabBadge { let tab: RosettaTab; let text: String }
|
||||
@@ -2,55 +2,6 @@ import SwiftUI
|
||||
import UIKit
|
||||
import Lottie
|
||||
|
||||
// MARK: - Tab
|
||||
|
||||
enum RosettaTab: String, CaseIterable, Sendable {
|
||||
case chats, calls, settings
|
||||
|
||||
static let interactionOrder: [RosettaTab] = [.calls, .chats, .settings]
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .chats: return "Chats"
|
||||
case .calls: return "Calls"
|
||||
case .settings: return "Settings"
|
||||
}
|
||||
}
|
||||
|
||||
var animationName: String {
|
||||
switch self {
|
||||
case .chats: return "TabChats"
|
||||
case .calls: return "TabCalls"
|
||||
case .settings: return "TabSettings"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .chats: return "bubble.left.and.bubble.right"
|
||||
case .calls: return "phone"
|
||||
case .settings: return "gearshape"
|
||||
}
|
||||
}
|
||||
|
||||
var selectedIcon: String {
|
||||
switch self {
|
||||
case .chats: return "bubble.left.and.bubble.right.fill"
|
||||
case .calls: return "phone.fill"
|
||||
case .settings: return "gearshape.fill"
|
||||
}
|
||||
}
|
||||
|
||||
var interactionIndex: Int {
|
||||
Self.interactionOrder.firstIndex(of: self) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
struct TabBadge { let tab: RosettaTab; let text: String }
|
||||
struct TabBarSwipeState {
|
||||
let fromTab: RosettaTab; let hoveredTab: RosettaTab; let fractionalIndex: CGFloat
|
||||
}
|
||||
|
||||
// MARK: - Colors
|
||||
|
||||
private enum TabBarUIColors {
|
||||
@@ -58,7 +9,6 @@ private enum TabBarUIColors {
|
||||
static let selectedIcon = UIColor(RosettaColors.primaryBlue)
|
||||
static let text = UIColor { $0.userInterfaceStyle == .dark ? .white : UIColor(red: 0, green: 0, blue: 0, alpha: 0.8) }
|
||||
static let selectedText = UIColor(RosettaColors.primaryBlue)
|
||||
// Badge always red/white — matches Telegram screenshots in both themes
|
||||
static let badgeBg = UIColor(red: 1, green: 0.23, blue: 0.19, alpha: 1)
|
||||
static let badgeText = UIColor.white
|
||||
static let selectionFill = UIColor.white.withAlphaComponent(0.07)
|
||||
@@ -69,6 +19,12 @@ private enum TabBarUIColors {
|
||||
private final class TabSelectionGesture: UIGestureRecognizer {
|
||||
private(set) var initialLocation: CGPoint = .zero
|
||||
|
||||
override init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
delaysTouchesBegan = false
|
||||
delaysTouchesEnded = false
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
initialLocation = touches.first?.location(in: view) ?? .zero
|
||||
@@ -96,9 +52,7 @@ final class RosettaTabBarUIView: UIView {
|
||||
private let innerInset: CGFloat = 4
|
||||
private let itemHeight: CGFloat = 56
|
||||
private let perItemWidth: CGFloat = 90
|
||||
/// Telegram LiquidLens inset — visible gap between lens and capsule edge.
|
||||
private let lensInset: CGFloat = 4
|
||||
/// Telegram: 1.15 scale when gesture active. Toned down for subtlety.
|
||||
private let liftedScale: CGFloat = 1.08
|
||||
|
||||
private var barWidth: CGFloat { perItemWidth * CGFloat(tabs.count) + innerInset * 2 }
|
||||
@@ -108,17 +62,23 @@ final class RosettaTabBarUIView: UIView {
|
||||
// MARK: State
|
||||
|
||||
var selectedIndex: Int = 1 {
|
||||
didSet { if oldValue != selectedIndex { updateSelection(animated: true) } }
|
||||
didSet {
|
||||
if oldValue != selectedIndex {
|
||||
updateLensClip(animated: true)
|
||||
updateLabelColors()
|
||||
}
|
||||
}
|
||||
}
|
||||
var onTabSelected: ((RosettaTab) -> Void)?
|
||||
var badgeText: String? { didSet { layoutBadge() } }
|
||||
|
||||
private var isDragging = false
|
||||
private var didDrag = false // true if .changed fired (not just a tap)
|
||||
private var dragLensX: CGFloat = 0
|
||||
private var dragStartLensX: CGFloat = 0
|
||||
private var hoveredIndex: Int = 1
|
||||
|
||||
// MARK: Subviews
|
||||
// MARK: Subviews — base (unselected, always visible)
|
||||
|
||||
private let glassBackground = TelegramGlassUIView(frame: .zero)
|
||||
private let selectionView: UIView = {
|
||||
@@ -129,9 +89,28 @@ final class RosettaTabBarUIView: UIView {
|
||||
}()
|
||||
private var iconViews: [LottieAnimationView] = []
|
||||
private var labelViews: [UILabel] = []
|
||||
|
||||
// MARK: Subviews — selected overlay (clipped to lens, Telegram LiquidLens parity)
|
||||
|
||||
private let selectedClipView: UIView = {
|
||||
let v = UIView()
|
||||
v.clipsToBounds = true
|
||||
v.isUserInteractionEnabled = false
|
||||
return v
|
||||
}()
|
||||
private let selectedContentView: UIView = {
|
||||
let v = UIView()
|
||||
v.isUserInteractionEnabled = false
|
||||
return v
|
||||
}()
|
||||
private var selectedIconViews: [LottieAnimationView] = []
|
||||
|
||||
private var badgeBgView: UIView?
|
||||
private var badgeLabel: UILabel?
|
||||
private var appliedColorHex: [Int: UInt64] = [:]
|
||||
|
||||
// Cached RGBA for label interpolation — avoids dynamic UIColor resolution per frame.
|
||||
private var textRGBA: (CGFloat, CGFloat, CGFloat, CGFloat) = (1, 1, 1, 1)
|
||||
private var selectedTextRGBA: (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 1)
|
||||
|
||||
// MARK: Init
|
||||
|
||||
@@ -141,98 +120,168 @@ final class RosettaTabBarUIView: UIView {
|
||||
addSubview(glassBackground)
|
||||
addSubview(selectionView)
|
||||
|
||||
for (i, tab) in tabs.enumerated() {
|
||||
let lottie = LottieAnimationView()
|
||||
lottie.contentMode = .scaleAspectFit
|
||||
lottie.isUserInteractionEnabled = false
|
||||
lottie.backgroundBehavior = .pauseAndRestore
|
||||
if let anim = LottieAnimationCache.shared.animation(named: tab.animationName) {
|
||||
lottie.animation = anim
|
||||
} else {
|
||||
lottie.animation = LottieAnimation.named(tab.animationName)
|
||||
}
|
||||
lottie.loopMode = .playOnce
|
||||
lottie.currentProgress = 1.0
|
||||
addSubview(lottie)
|
||||
iconViews.append(lottie)
|
||||
|
||||
let label = UILabel()
|
||||
label.text = tab.label
|
||||
label.font = .systemFont(ofSize: 10, weight: .semibold)
|
||||
label.textAlignment = .center
|
||||
addSubview(label)
|
||||
labelViews.append(label)
|
||||
applyColor(to: i, selected: i == selectedIndex, animated: false)
|
||||
for tab in tabs {
|
||||
let icon = makeLottie(for: tab); addSubview(icon); iconViews.append(icon)
|
||||
let label = makeLabel(for: tab); addSubview(label); labelViews.append(label)
|
||||
}
|
||||
|
||||
addSubview(selectedClipView)
|
||||
selectedClipView.addSubview(selectedContentView)
|
||||
|
||||
for tab in tabs {
|
||||
let icon = makeLottie(for: tab); selectedContentView.addSubview(icon); selectedIconViews.append(icon)
|
||||
}
|
||||
|
||||
applyAllColors()
|
||||
let g = TabSelectionGesture(target: self, action: #selector(handleGesture(_:)))
|
||||
g.delegate = self
|
||||
addGestureRecognizer(g)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override var intrinsicContentSize: CGSize { CGSize(width: barWidth, height: barHeight) }
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
private func makeLottie(for tab: RosettaTab) -> LottieAnimationView {
|
||||
let v = LottieAnimationView()
|
||||
v.contentMode = .scaleAspectFit
|
||||
v.isUserInteractionEnabled = false
|
||||
v.backgroundBehavior = .pauseAndRestore
|
||||
v.animation = LottieAnimationCache.shared.animation(named: tab.animationName)
|
||||
?? LottieAnimation.named(tab.animationName)
|
||||
v.loopMode = .playOnce; v.currentProgress = 1.0
|
||||
return v
|
||||
}
|
||||
|
||||
private func makeLabel(for tab: RosettaTab) -> UILabel {
|
||||
let l = UILabel(); l.text = tab.label
|
||||
l.font = .systemFont(ofSize: 10, weight: .semibold); l.textAlignment = .center
|
||||
return l
|
||||
}
|
||||
|
||||
private func setLottieColor(_ view: LottieAnimationView, _ color: UIColor) {
|
||||
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||
color.getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
view.setValueProvider(
|
||||
ColorValueProvider(LottieColor(r: Double(r), g: Double(g), b: Double(b), a: Double(a))),
|
||||
keypath: AnimationKeypath(keypath: "**.Color")
|
||||
)
|
||||
}
|
||||
|
||||
private func applyAllColors() {
|
||||
cacheColorComponents()
|
||||
for i in 0..<tabs.count {
|
||||
setLottieColor(iconViews[i], TabBarUIColors.icon)
|
||||
setLottieColor(selectedIconViews[i], TabBarUIColors.selectedIcon)
|
||||
}
|
||||
updateLabelColors()
|
||||
}
|
||||
|
||||
private func cacheColorComponents() {
|
||||
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||
TabBarUIColors.text.getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
textRGBA = (r, g, b, a)
|
||||
TabBarUIColors.selectedText.getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
selectedTextRGBA = (r, g, b, a)
|
||||
}
|
||||
|
||||
private func updateLabelColors() {
|
||||
let active = selectedIndex
|
||||
for i in 0..<tabs.count {
|
||||
labelViews[i].textColor = (i == active) ? TabBarUIColors.selectedText : TabBarUIColors.text
|
||||
}
|
||||
}
|
||||
|
||||
/// Smooth label color interpolation during drag — uses cached RGBA to avoid per-frame UIColor resolution.
|
||||
private func updateLabelsForFraction(_ fraction: CGFloat) {
|
||||
let clamped = max(0, min(fraction, CGFloat(tabs.count - 1)))
|
||||
let lower = Int(clamped)
|
||||
let upper = min(lower + 1, tabs.count - 1)
|
||||
let t = clamped - CGFloat(lower)
|
||||
let (r1, g1, b1, a1) = textRGBA
|
||||
let (r2, g2, b2, a2) = selectedTextRGBA
|
||||
for i in 0..<tabs.count {
|
||||
let s: CGFloat
|
||||
if lower == upper { s = (i == lower) ? 1.0 : 0.0 }
|
||||
else if i == lower { s = 1.0 - t }
|
||||
else if i == upper { s = t }
|
||||
else { s = 0.0 }
|
||||
labelViews[i].textColor = UIColor(
|
||||
red: r1 + (r2 - r1) * s, green: g1 + (g2 - g1) * s,
|
||||
blue: b1 + (b2 - b1) * s, alpha: a1 + (a2 - a1) * s
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Layout
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let w = bounds.width; let h = bounds.height
|
||||
let h = bounds.height
|
||||
glassBackground.frame = bounds
|
||||
glassBackground.updateGlass()
|
||||
updateSelectionFrame(animated: false)
|
||||
|
||||
let iw = itemW
|
||||
for i in 0..<tabs.count {
|
||||
let x = innerInset + CGFloat(i) * iw
|
||||
let iconSize: CGFloat = 44
|
||||
let iconX = x + (iw - iconSize) / 2
|
||||
let iconY = innerInset + 1
|
||||
iconViews[i].frame = CGRect(x: iconX, y: iconY, width: iconSize, height: iconSize)
|
||||
let iconFrame = CGRect(x: x + (iw - iconSize) / 2, y: innerInset + 1, width: iconSize, height: iconSize)
|
||||
iconViews[i].frame = iconFrame
|
||||
selectedIconViews[i].frame = iconFrame
|
||||
|
||||
let labelH: CGFloat = 14
|
||||
let labelY = h - innerInset - labelH - 1
|
||||
labelViews[i].frame = CGRect(x: x, y: labelY, width: iw, height: labelH)
|
||||
let labelFrame = CGRect(x: x, y: h - innerInset - 17, width: iw, height: 14)
|
||||
labelViews[i].frame = labelFrame
|
||||
}
|
||||
// cornerRadius only changes on bounds change, not during drag.
|
||||
let lensH = bounds.height - lensInset * 2
|
||||
selectionView.layer.cornerRadius = lensH / 2
|
||||
selectedClipView.layer.cornerRadius = lensH / 2
|
||||
|
||||
updateLensClip(animated: false)
|
||||
layoutBadge()
|
||||
}
|
||||
|
||||
// MARK: Selection Indicator
|
||||
// MARK: Lens Clip (Telegram LiquidLens masking)
|
||||
|
||||
private func lensFrame(for index: Int) -> CGRect {
|
||||
let iw = itemW
|
||||
let lensH = bounds.height - lensInset * 2
|
||||
let lensW = iw
|
||||
let lensX = innerInset + CGFloat(index) * iw
|
||||
return CGRect(x: lensX, y: lensInset, width: lensW, height: lensH)
|
||||
return CGRect(x: innerInset + CGFloat(index) * itemW, y: lensInset, width: itemW, height: lensH)
|
||||
}
|
||||
|
||||
private func updateSelectionFrame(animated: Bool) {
|
||||
private func updateLensClip(animated: Bool) {
|
||||
guard bounds.width > 0 else { return }
|
||||
let lensH = bounds.height - lensInset * 2
|
||||
selectionView.layer.cornerRadius = lensH / 2
|
||||
|
||||
let target: CGRect
|
||||
if isDragging {
|
||||
// Lens follows finger — clamped to capsule bounds
|
||||
let lensW = itemW
|
||||
let minX = lensInset
|
||||
let maxX = bounds.width - lensW - lensInset
|
||||
let clampedX = max(minX, min(dragLensX, maxX))
|
||||
selectionView.frame = CGRect(x: clampedX, y: lensInset, width: lensW, height: lensH)
|
||||
let clampedX = max(lensInset, min(dragLensX, bounds.width - itemW - lensInset))
|
||||
target = CGRect(x: clampedX, y: lensInset, width: itemW, height: lensH)
|
||||
} else {
|
||||
let target = lensFrame(for: selectedIndex)
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.78,
|
||||
initialSpringVelocity: 0, options: .beginFromCurrentState) {
|
||||
self.selectionView.frame = target
|
||||
self.selectionView.transform = .identity
|
||||
}
|
||||
} else {
|
||||
selectionView.frame = target
|
||||
selectionView.transform = .identity
|
||||
target = lensFrame(for: selectedIndex)
|
||||
}
|
||||
|
||||
let apply = {
|
||||
self.selectionView.frame = target
|
||||
self.selectedClipView.frame = target
|
||||
self.selectedContentView.frame = CGRect(
|
||||
x: -target.origin.x, y: -target.origin.y,
|
||||
width: self.bounds.width, height: self.bounds.height
|
||||
)
|
||||
if !self.isDragging {
|
||||
self.selectionView.transform = .identity
|
||||
}
|
||||
}
|
||||
if animated && !isDragging {
|
||||
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.82,
|
||||
initialSpringVelocity: 0, options: .beginFromCurrentState, animations: apply)
|
||||
} else {
|
||||
// Disable implicit CoreAnimation during drag — direct property set like Telegram's .immediate
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
apply()
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Gesture
|
||||
@@ -242,90 +291,52 @@ final class RosettaTabBarUIView: UIView {
|
||||
switch g.state {
|
||||
case .began:
|
||||
isDragging = true
|
||||
hoveredIndex = selectedIndex
|
||||
// Start lens at selected tab position
|
||||
didDrag = false
|
||||
hoveredIndex = max(0, min(Int((g.initialLocation.x - innerInset) / iw), tabs.count - 1))
|
||||
dragStartLensX = innerInset + CGFloat(selectedIndex) * iw
|
||||
dragLensX = dragStartLensX
|
||||
|
||||
// Lifted state: scale up selection indicator (Telegram isLifted)
|
||||
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7,
|
||||
UIView.animate(withDuration: 0.2, delay: 0, usingSpringWithDamping: 0.8,
|
||||
initialSpringVelocity: 0, options: .beginFromCurrentState) {
|
||||
self.selectionView.transform = CGAffineTransform(scaleX: self.liftedScale, y: self.liftedScale)
|
||||
}
|
||||
updateSelectionFrame(animated: false)
|
||||
updateLensClip(animated: false)
|
||||
|
||||
case .changed:
|
||||
didDrag = true
|
||||
let loc = g.location(in: self)
|
||||
let translation = loc.x - g.initialLocation.x
|
||||
dragLensX = dragStartLensX + translation
|
||||
|
||||
// Update hovered tab
|
||||
let newHover = max(0, min(Int((loc.x - innerInset) / iw), tabs.count - 1))
|
||||
if newHover != hoveredIndex {
|
||||
hoveredIndex = newHover
|
||||
// Smooth color transition (Telegram: colors blend when hovering)
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
for i in 0..<self.tabs.count {
|
||||
self.applyColor(to: i, selected: i == self.hoveredIndex, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
updateSelectionFrame(animated: false)
|
||||
dragLensX = dragStartLensX + (loc.x - g.initialLocation.x)
|
||||
hoveredIndex = max(0, min(Int((loc.x - innerInset) / iw), tabs.count - 1))
|
||||
let fractionalIdx = max(0, min((dragLensX - innerInset) / iw, CGFloat(tabs.count - 1)))
|
||||
updateLabelsForFraction(fractionalIdx)
|
||||
updateLensClip(animated: false)
|
||||
|
||||
case .ended:
|
||||
isDragging = false
|
||||
let target = tabs[hoveredIndex]
|
||||
if hoveredIndex != selectedIndex {
|
||||
let targetChanged = hoveredIndex != selectedIndex
|
||||
if targetChanged {
|
||||
iconViews[hoveredIndex].play(fromProgress: 0, toProgress: 1, loopMode: .playOnce)
|
||||
selectedIndex = hoveredIndex
|
||||
onTabSelected?(target)
|
||||
selectedIconViews[hoveredIndex].play(fromProgress: 0, toProgress: 1, loopMode: .playOnce)
|
||||
selectedIndex = hoveredIndex // → didSet → updateLensClip(animated: true) + updateLabelColors()
|
||||
// Defer SwiftUI state update to next frame — prevents layout pass
|
||||
// from blocking the current frame and causing a freeze.
|
||||
let tab = tabs[hoveredIndex]
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onTabSelected?(tab)
|
||||
}
|
||||
} else {
|
||||
// Spring back with un-lift
|
||||
updateSelection(animated: true)
|
||||
updateLensClip(animated: true)
|
||||
updateLabelColors()
|
||||
}
|
||||
|
||||
case .cancelled:
|
||||
isDragging = false
|
||||
updateSelection(animated: true)
|
||||
updateLensClip(animated: true)
|
||||
updateLabelColors()
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Selection Update
|
||||
|
||||
private func updateSelection(animated: Bool) {
|
||||
for i in 0..<tabs.count {
|
||||
applyColor(to: i, selected: i == selectedIndex, animated: animated)
|
||||
}
|
||||
updateSelectionFrame(animated: animated)
|
||||
}
|
||||
|
||||
// MARK: Colors
|
||||
|
||||
private func applyColor(to index: Int, selected: Bool, animated: Bool) {
|
||||
let iconColor = selected ? TabBarUIColors.selectedIcon : TabBarUIColors.icon
|
||||
let txtColor = selected ? TabBarUIColors.selectedText : TabBarUIColors.text
|
||||
|
||||
let apply = {
|
||||
self.labelViews[index].textColor = txtColor
|
||||
}
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.25, animations: apply)
|
||||
} else {
|
||||
apply()
|
||||
}
|
||||
|
||||
// Lottie color — cache hex to skip redundant setValueProvider
|
||||
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||
iconColor.getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
let hex = (UInt64(r * 255) << 24) | (UInt64(g * 255) << 16) | (UInt64(b * 255) << 8) | UInt64(a * 255)
|
||||
guard appliedColorHex[index] != hex else { return }
|
||||
appliedColorHex[index] = hex
|
||||
let lc = LottieColor(r: Double(r), g: Double(g), b: Double(b), a: Double(a))
|
||||
iconViews[index].setValueProvider(ColorValueProvider(lc), keypath: AnimationKeypath(keypath: "**.Color"))
|
||||
}
|
||||
|
||||
// MARK: Badge
|
||||
|
||||
private func layoutBadge() {
|
||||
@@ -333,12 +344,12 @@ final class RosettaTabBarUIView: UIView {
|
||||
badgeBgView?.isHidden = true; return
|
||||
}
|
||||
let chatsIdx = RosettaTab.chats.interactionIndex
|
||||
let iconFrame = iconViews[chatsIdx].frame
|
||||
let iw = itemW
|
||||
|
||||
if badgeBgView == nil {
|
||||
let bg = UIView(); bg.layer.masksToBounds = true; addSubview(bg); badgeBgView = bg
|
||||
let lbl = UILabel(); lbl.font = .systemFont(ofSize: 13); lbl.textAlignment = .center
|
||||
bg.addSubview(lbl); badgeLabel = lbl
|
||||
let lbl = UILabel(); lbl.font = .systemFont(ofSize: 13, weight: .regular)
|
||||
lbl.textAlignment = .center; bg.addSubview(lbl); badgeLabel = lbl
|
||||
}
|
||||
guard let bg = badgeBgView, let lbl = badgeLabel else { return }
|
||||
bg.isHidden = false
|
||||
@@ -347,19 +358,19 @@ final class RosettaTabBarUIView: UIView {
|
||||
lbl.text = text; lbl.sizeToFit()
|
||||
|
||||
let textW = lbl.frame.width
|
||||
let bgW = text.count == 1 ? 18.0 : max(18.0, textW + 10)
|
||||
let bgW = text.count == 1 ? 18.0 : max(18.0, textW + 11)
|
||||
let bgH: CGFloat = 18
|
||||
bg.frame = CGRect(x: iconFrame.maxX - 6, y: iconFrame.minY - 1, width: bgW, height: bgH)
|
||||
let itemX = innerInset + CGFloat(chatsIdx) * iw
|
||||
bg.frame = CGRect(x: itemX + floor(iw / 2) + 25 - bgW - 1, y: 7, width: bgW, height: bgH)
|
||||
bg.layer.cornerRadius = bgH / 2
|
||||
lbl.frame = CGRect(x: (bgW - textW) / 2, y: 0.5, width: textW, height: bgH - 1)
|
||||
bringSubviewToFront(bg)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ prev: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(prev)
|
||||
if traitCollection.hasDifferentColorAppearance(comparedTo: prev) {
|
||||
appliedColorHex.removeAll()
|
||||
updateSelection(animated: false)
|
||||
layoutBadge()
|
||||
applyAllColors()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user