mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Working better segmented control
This commit is contained in:
parent
b7e3cff21d
commit
f8bdda0033
1 changed files with 115 additions and 32 deletions
|
|
@ -98,6 +98,12 @@ struct DraggableProgressBar: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
|
Group {
|
||||||
|
if Defaults[.progressBarStyle] == .segmented {
|
||||||
|
// Segmented progress bar - completely different layout
|
||||||
|
SegmentedProgressContent(value: value, geometry: geo)
|
||||||
|
} else {
|
||||||
|
// Traditional capsule-based progress bar
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
Capsule()
|
Capsule()
|
||||||
.fill(.tertiary)
|
.fill(.tertiary)
|
||||||
|
|
@ -114,11 +120,13 @@ struct DraggableProgressBar: View {
|
||||||
.frame(width: max(0, min(geo.size.width * value, geo.size.width)))
|
.frame(width: max(0, min(geo.size.width * value, geo.size.width)))
|
||||||
.shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3)
|
.shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3)
|
||||||
case .segmented:
|
case .segmented:
|
||||||
SegmentedProgressContent(value: value, geometry: geo)
|
EmptyView() // This case won't be reached due to the outer if condition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.opacity(value.isZero ? 0 : 1)
|
.opacity(value.isZero ? 0 : 1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.gesture(
|
.gesture(
|
||||||
DragGesture(minimumDistance: 0)
|
DragGesture(minimumDistance: 0)
|
||||||
.onChanged { gesture in
|
.onChanged { gesture in
|
||||||
|
|
@ -150,25 +158,100 @@ struct SegmentedProgressContent: View {
|
||||||
let value: CGFloat
|
let value: CGFloat
|
||||||
let geometry: GeometryProxy
|
let geometry: GeometryProxy
|
||||||
|
|
||||||
private let segmentCount = 20
|
private let segmentCount = 16
|
||||||
|
@State private var glowIndex: Int? = nil
|
||||||
|
@State private var lastValue: CGFloat = 0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 1) {
|
let spacing: CGFloat = 1.5
|
||||||
ForEach(0..<segmentCount, id: \.self) { index in
|
let computed = (geometry.size.width - CGFloat(segmentCount - 1) * spacing) / CGFloat(segmentCount)
|
||||||
let segmentValue = CGFloat(index + 1) / CGFloat(segmentCount)
|
let barWidth = max(3.0, min(6.0, computed))
|
||||||
let isActive = value >= segmentValue
|
let activeCount = Int(round(value * CGFloat(segmentCount)))
|
||||||
|
|
||||||
RoundedRectangle(cornerRadius: 1)
|
HStack(spacing: spacing) {
|
||||||
.fill(isActive ?
|
ForEach(0..<segmentCount, id: \.self) { index in
|
||||||
(Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor] : .white) :
|
RoundedRectangle(cornerRadius: 2)
|
||||||
.clear)
|
.fill(segmentColor(isActive: index < activeCount))
|
||||||
.frame(width: max(1, (geometry.size.width - CGFloat(segmentCount - 1) * 1) / CGFloat(segmentCount)))
|
.shadow(
|
||||||
.shadow(color: isActive && Defaults[.systemEventIndicatorShadow] ?
|
color: Defaults[.systemEventIndicatorShadow]
|
||||||
(Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white) :
|
? glowShadowColor(for: index < activeCount, index: index)
|
||||||
.clear, radius: 4, x: 1)
|
: .clear,
|
||||||
.opacity(value.isZero ? 0 : (isActive ? 1 : 0.3))
|
radius: Defaults[.systemEventIndicatorShadow]
|
||||||
|
? (glowIndex == index ? 12 : (index < activeCount ? 4 : 0))
|
||||||
|
: 0,
|
||||||
|
x: 0, y: 0
|
||||||
|
)
|
||||||
|
.frame(width: barWidth)
|
||||||
|
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: activeCount)
|
||||||
|
.scaleEffect(glowIndex == index ? 1.15 : 1.0)
|
||||||
|
.animation(.spring(response: 0.2, dampingFraction: 0.6), value: glowIndex == index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: geometry.size.width)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.opacity(value.isZero ? 0 : 1)
|
||||||
|
.onChange(of: value) { old, newVal in
|
||||||
|
handleGlow(oldValue: old, newValue: newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func segmentColor(isActive: Bool) -> Color {
|
||||||
|
if isActive {
|
||||||
|
if Defaults[.systemEventIndicatorUseAccent] {
|
||||||
|
return Defaults[.accentColor]
|
||||||
|
}
|
||||||
|
return .white
|
||||||
|
} else {
|
||||||
|
return .white.opacity(0.15)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func glowShadowColor(for isActive: Bool, index: Int) -> Color {
|
||||||
|
if isActive {
|
||||||
|
if let glowIndex = glowIndex, index == glowIndex {
|
||||||
|
return Defaults[.systemEventIndicatorUseAccent]
|
||||||
|
? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.8)
|
||||||
|
: .white
|
||||||
|
} else {
|
||||||
|
return Defaults[.systemEventIndicatorUseAccent]
|
||||||
|
? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7)
|
||||||
|
: .white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleGlow(oldValue: CGFloat, newValue: CGFloat) {
|
||||||
|
defer { lastValue = newValue }
|
||||||
|
let oldIndex = Int(round(oldValue * CGFloat(segmentCount)))
|
||||||
|
let newIndex = Int(round(newValue * CGFloat(segmentCount)))
|
||||||
|
guard oldIndex != newIndex else { return }
|
||||||
|
|
||||||
|
if newIndex > oldIndex {
|
||||||
|
animateWave(from: oldIndex, to: newIndex, step: 1)
|
||||||
|
} else {
|
||||||
|
animateWave(from: oldIndex - 1, to: newIndex - 1, step: -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func animateWave(from start: Int, to end: Int, step: Int) {
|
||||||
|
let clampedStart = max(0, min(segmentCount - 1, start))
|
||||||
|
let clampedEnd = max(-1, min(segmentCount - 1, end))
|
||||||
|
let range = stride(from: clampedStart, through: clampedEnd, by: step)
|
||||||
|
var delay: Double = 0
|
||||||
|
for i in range {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||||
|
withAnimation(.spring(response: 0.2, dampingFraction: 0.7)) {
|
||||||
|
glowIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay + 0.2) {
|
||||||
|
if glowIndex == i {
|
||||||
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
|
||||||
|
glowIndex = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay += 0.015
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue