fix: add missing animations to progress rings, bars, and charts in learning views
- StudyHomeView: ring trim animation + bar width animation + week bar spring animations with staggered delay + numeric text transitions - LearningSessionView: animatedProgress state with onChange driving smooth ring animation + background track ring - ReviewCardView: progress bar width animation - AnalysisHomeView: chart line trim animation + gradient fill fade-in Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8c486c73ae
commit
9a4b4afaf4
@ -60,17 +60,41 @@ struct ZXStatBadge: View { let icon: String; let label: String; let value: Strin
|
|||||||
|
|
||||||
struct ZXChartView: View {
|
struct ZXChartView: View {
|
||||||
let data: [(String, CGFloat)] = [("一", 0.62), ("二", 0.65), ("三", 0.71), ("四", 0.68), ("五", 0.75), ("六", 0.79), ("今", 0.78)]
|
let data: [(String, CGFloat)] = [("一", 0.62), ("二", 0.65), ("三", 0.71), ("四", 0.68), ("五", 0.75), ("六", 0.79), ("今", 0.78)]
|
||||||
|
@State private var showChart = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
GeometryReader { g in
|
GeometryReader { g in
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
|
// Gradient fill under the line
|
||||||
Path { path in let w = g.size.width / 7
|
Path { path in let w = g.size.width / 7
|
||||||
for (i, d) in data.enumerated() { let x = w * CGFloat(i) + w / 2; let y = (1 - d.1) * g.size.height
|
for (i, d) in data.enumerated() { let x = w * CGFloat(i) + w / 2; let y = (1 - d.1) * g.size.height
|
||||||
if i == 0 { path.move(to: CGPoint(x: x, y: y)) } else { path.addLine(to: CGPoint(x: x, y: y)) } }
|
if i == 0 { path.move(to: CGPoint(x: x, y: y)) } else { path.addLine(to: CGPoint(x: x, y: y)) } }
|
||||||
}.stroke(Color.zxPurple, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
|
path.addLine(to: CGPoint(x: g.size.width, y: g.size.height))
|
||||||
|
path.addLine(to: CGPoint(x: w / 2, y: g.size.height))
|
||||||
|
path.closeSubpath()
|
||||||
|
}
|
||||||
|
.fill(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [Color.zxPurple.opacity(0.2), Color.zxPurple.opacity(0.0)],
|
||||||
|
startPoint: .top, endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.opacity(showChart ? 1 : 0)
|
||||||
|
.animation(.easeOut(duration: 0.8).delay(0.3), value: showChart)
|
||||||
|
|
||||||
|
// Animated line
|
||||||
|
Path { path in let w = g.size.width / 7
|
||||||
|
for (i, d) in data.enumerated() { let x = w * CGFloat(i) + w / 2; let y = (1 - d.1) * g.size.height
|
||||||
|
if i == 0 { path.move(to: CGPoint(x: x, y: y)) } else { path.addLine(to: CGPoint(x: x, y: y)) } }
|
||||||
|
}
|
||||||
|
.trim(from: 0, to: showChart ? 1 : 0)
|
||||||
|
.stroke(Color.zxPurple, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
|
||||||
|
.animation(.easeOut(duration: 1.0), value: showChart)
|
||||||
}
|
}
|
||||||
}.frame(height: 100)
|
}.frame(height: 100)
|
||||||
HStack(spacing: 0) { ForEach(data, id: \.0) { d in Text(d.0).font(.system(size: 9)).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.35)).frame(maxWidth: .infinity) } }
|
HStack(spacing: 0) { ForEach(data, id: \.0) { d in Text(d.0).font(.system(size: 9)).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.35)).frame(maxWidth: .infinity) } }
|
||||||
}
|
}
|
||||||
|
.onAppear { showChart = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ struct LearningSessionView: View {
|
|||||||
let taskColor: Color
|
let taskColor: Color
|
||||||
|
|
||||||
@State private var elapsed: TimeInterval = 0
|
@State private var elapsed: TimeInterval = 0
|
||||||
|
@State private var animatedProgress: CGFloat = 0
|
||||||
@State private var isRunning = true
|
@State private var isRunning = true
|
||||||
@State private var isPaused = false
|
@State private var isPaused = false
|
||||||
@State private var showEndConfirm = false
|
@State private var showEndConfirm = false
|
||||||
@ -55,21 +56,41 @@ struct LearningSessionView: View {
|
|||||||
private var timerCard: some View {
|
private var timerCard: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
// Background track
|
||||||
Circle()
|
Circle()
|
||||||
.trim(from: 0, to: min(elapsed / 1800, 1))
|
.trim(from: 0, to: 1)
|
||||||
.stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 8, lineCap: .round))
|
.stroke(Color.zxFill008, style: StrokeStyle(lineWidth: 8, lineCap: .round))
|
||||||
.rotationEffect(.degrees(-90))
|
.rotationEffect(.degrees(-90))
|
||||||
.frame(width: 180, height: 180)
|
.frame(width: 180, height: 180)
|
||||||
|
// Animated progress
|
||||||
|
Circle()
|
||||||
|
.trim(from: 0, to: animatedProgress)
|
||||||
|
.stroke(
|
||||||
|
AngularGradient(
|
||||||
|
colors: [Color.zxPurple, Color.zxAccent, Color.zxPurple],
|
||||||
|
center: .center
|
||||||
|
),
|
||||||
|
style: StrokeStyle(lineWidth: 8, lineCap: .round)
|
||||||
|
)
|
||||||
|
.rotationEffect(.degrees(-90))
|
||||||
|
.frame(width: 180, height: 180)
|
||||||
|
.animation(.easeInOut(duration: 0.5), value: animatedProgress)
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Text(formatTime(elapsed))
|
Text(formatTime(elapsed))
|
||||||
.font(.system(size: 36, weight: .black))
|
.font(.system(size: 36, weight: .black))
|
||||||
.foregroundColor(Color.zxF0)
|
.foregroundColor(Color.zxF0)
|
||||||
.tracking(-1)
|
.tracking(-1)
|
||||||
|
.contentTransition(.numericText())
|
||||||
Text("已学习")
|
Text("已学习")
|
||||||
.font(.system(size: 13, weight: .medium))
|
.font(.system(size: 13, weight: .medium))
|
||||||
.foregroundColor(Color.zxF04)
|
.foregroundColor(Color.zxF04)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: elapsed) { newElapsed in
|
||||||
|
withAnimation(.easeInOut(duration: 0.5)) {
|
||||||
|
animatedProgress = min(CGFloat(newElapsed) / 1800, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Button {
|
Button {
|
||||||
if isRunning { isPaused = true; isRunning = false }
|
if isRunning { isPaused = true; isRunning = false }
|
||||||
|
|||||||
@ -67,6 +67,7 @@ struct ReviewCardView: View {
|
|||||||
RoundedRectangle(cornerRadius: 2)
|
RoundedRectangle(cornerRadius: 2)
|
||||||
.fill(ZXGradient.progressBar)
|
.fill(ZXGradient.progressBar)
|
||||||
.frame(width: max(3, CGFloat(current.count) / CGFloat(current.total) * (UIScreen.main.bounds.width - 40)), height: 3)
|
.frame(width: max(3, CGFloat(current.count) / CGFloat(current.total) * (UIScreen.main.bounds.width - 40)), height: 3)
|
||||||
|
.animation(.easeInOut(duration: 0.5), value: current.count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
|
|||||||
@ -29,7 +29,7 @@ struct StudyHomeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 14) { Text("本周学习活跃").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0)
|
VStack(alignment: .leading, spacing: 14) { Text("本周学习活跃").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0)
|
||||||
HStack(alignment: .bottom, spacing: 8) { ForEach(0..<7, id: \.self) { i in VStack(spacing: 8) { RoundedRectangle(cornerRadius: 6).fill(i == 6 ? Color.zxFill01 : Color(hex: "#7C6EFA", opacity: studyHomeVM.weekActivity[i] * 0.9 + 0.1)).frame(height: studyHomeVM.weekActivity[i] * 60); Text(studyHomeVM.dayLabels[i]).font(.system(size: 10, weight: i == 2 ? .bold : .regular)).foregroundColor(i == 2 ? Color.zxPurple : Color.zxF03) }.frame(maxWidth: .infinity) } }
|
HStack(alignment: .bottom, spacing: 8) { ForEach(0..<7, id: \.self) { i in VStack(spacing: 8) { RoundedRectangle(cornerRadius: 6).fill(i == 6 ? Color.zxFill01 : Color(hex: "#7C6EFA", opacity: studyHomeVM.weekActivity[i] * 0.9 + 0.1)).frame(height: studyHomeVM.weekActivity[i] * 60).animation(.spring(response: 0.6, dampingFraction: 0.7).delay(Double(i) * 0.05), value: studyHomeVM.weekActivity[i]); Text(studyHomeVM.dayLabels[i]).font(.system(size: 10, weight: i == 2 ? .bold : .regular)).foregroundColor(i == 2 ? Color.zxPurple : Color.zxF03) }.frame(maxWidth: .infinity) } }
|
||||||
HStack { Text("总计 3.5 小时").font(.system(size: 11)).foregroundColor(Color.zxF03); Spacer(); Text("日均 30 分钟").font(.system(size: 11)).foregroundColor(Color.zxF03) } }
|
HStack { Text("总计 3.5 小时").font(.system(size: 11)).foregroundColor(Color.zxF03); Spacer(); Text("日均 30 分钟").font(.system(size: 11)).foregroundColor(Color.zxF03) } }
|
||||||
.padding(.bottom, 120) }
|
.padding(.bottom, 120) }
|
||||||
.padding(.horizontal, 20) }
|
.padding(.horizontal, 20) }
|
||||||
@ -39,9 +39,9 @@ struct StudyHomeView: View {
|
|||||||
.task { await studyVM.loadSessions() }
|
.task { await studyVM.loadSessions() }
|
||||||
}
|
}
|
||||||
private var pc: some View { let dn = studyHomeVM.doneCount; let pct = CGFloat(dn) / 5
|
private var pc: some View { let dn = studyHomeVM.doneCount; let pct = CGFloat(dn) / 5
|
||||||
return VStack(spacing: 12) { HStack { VStack(alignment: .leading, spacing: 2) { Text("今日进度").font(.system(size: 13, weight: .medium)).foregroundColor(Color.zxF05); HStack(alignment: .lastTextBaseline, spacing: 6) { Text("\(dn)").font(.system(size: 26, weight: .black)).foregroundColor(Color.zxF0); Text("/ 5"); Text("个任务").font(.system(size: 14, weight: .medium)).foregroundColor(Color.zxF04) } }; Spacer()
|
return VStack(spacing: 12) { HStack { VStack(alignment: .leading, spacing: 2) { Text("今日进度").font(.system(size: 13, weight: .medium)).foregroundColor(Color.zxF05); HStack(alignment: .lastTextBaseline, spacing: 6) { Text("\(dn)").font(.system(size: 26, weight: .black)).foregroundColor(Color.zxF0).contentTransition(.numericText()); Text("/ 5"); Text("个任务").font(.system(size: 14, weight: .medium)).foregroundColor(Color.zxF04) } }; Spacer()
|
||||||
ZStack { Circle().trim(from: 0, to: pct).stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 8, lineCap: .round)).rotationEffect(.degrees(-90)).frame(width: 64, height: 64); Text("\(Int(pct * 100))%").font(.system(size: 14, weight: .heavy)).foregroundColor(Color.zxPurple) } }
|
ZStack { Circle().trim(from: 0, to: pct).stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 8, lineCap: .round)).rotationEffect(.degrees(-90)).frame(width: 64, height: 64).animation(.easeInOut(duration: 0.8), value: pct); Text("\(Int(pct * 100))%").font(.system(size: 14, weight: .heavy)).foregroundColor(Color.zxPurple).contentTransition(.numericText()) } }
|
||||||
ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 3).fill(Color.zxFill008).frame(height: 6); RoundedRectangle(cornerRadius: 3).fill(LinearGradient(colors: [Color.zxPurple, Color.zxAccent], startPoint: .leading, endPoint: .trailing)).frame(width: max(6, pct * (UIScreen.main.bounds.width - 72)), height: 6) }
|
ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 3).fill(Color.zxFill008).frame(height: 6); RoundedRectangle(cornerRadius: 3).fill(LinearGradient(colors: [Color.zxPurple, Color.zxAccent], startPoint: .leading, endPoint: .trailing)).frame(width: max(6, pct * (UIScreen.main.bounds.width - 72)), height: 6).animation(.easeInOut(duration: 0.6), value: pct) }
|
||||||
HStack { VStack(alignment: .leading, spacing: 2) { Text("\(dn * 12) 分钟").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("已学").font(.system(size: 10)).foregroundColor(Color.zxF04) }; Spacer(); VStack(spacing: 2) { Text("\((5 - dn) * 11) 分钟").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("剩余").font(.system(size: 10)).foregroundColor(Color.zxF04) }; Spacer(); VStack(alignment: .trailing, spacing: 2) { Text("+5 点").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("掌握").font(.system(size: 10)).foregroundColor(Color.zxF04) } } }
|
HStack { VStack(alignment: .leading, spacing: 2) { Text("\(dn * 12) 分钟").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("已学").font(.system(size: 10)).foregroundColor(Color.zxF04) }; Spacer(); VStack(spacing: 2) { Text("\((5 - dn) * 11) 分钟").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("剩余").font(.system(size: 10)).foregroundColor(Color.zxF04) }; Spacer(); VStack(alignment: .trailing, spacing: 2) { Text("+5 点").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("掌握").font(.system(size: 10)).foregroundColor(Color.zxF04) } } }
|
||||||
.padding(16).background(ZXGradient.progressCard).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.15), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20)) }
|
.padding(16).background(ZXGradient.progressCard).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.15), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20)) }
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user