From 9a4b4afaf407f9d867d637144ed2fa1954d96f06 Mon Sep 17 00:00:00 2001 From: WangDL Date: Mon, 18 May 2026 15:59:31 +0800 Subject: [PATCH] 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 --- .../Features/Analysis/AnalysisHomeView.swift | 26 ++++++++++++++++++- .../Features/Study/LearningSessionView.swift | 25 ++++++++++++++++-- .../Features/Study/ReviewCardView.swift | 1 + .../Features/Study/StudyHomeView.swift | 8 +++--- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift index bc37fcf..ea816fc 100644 --- a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift @@ -60,17 +60,41 @@ struct ZXStatBadge: View { let icon: String; let label: String; let value: Strin struct ZXChartView: View { 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 { VStack(spacing: 0) { GeometryReader { g in ZStack(alignment: .topLeading) { + // Gradient fill under the 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)) } } - }.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) 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 } } } diff --git a/AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift b/AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift index 8f83811..cfbf0db 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift @@ -7,6 +7,7 @@ struct LearningSessionView: View { let taskColor: Color @State private var elapsed: TimeInterval = 0 + @State private var animatedProgress: CGFloat = 0 @State private var isRunning = true @State private var isPaused = false @State private var showEndConfirm = false @@ -55,21 +56,41 @@ struct LearningSessionView: View { private var timerCard: some View { VStack(spacing: 16) { ZStack { + // Background track Circle() - .trim(from: 0, to: min(elapsed / 1800, 1)) - .stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 8, lineCap: .round)) + .trim(from: 0, to: 1) + .stroke(Color.zxFill008, style: StrokeStyle(lineWidth: 8, lineCap: .round)) .rotationEffect(.degrees(-90)) .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) { Text(formatTime(elapsed)) .font(.system(size: 36, weight: .black)) .foregroundColor(Color.zxF0) .tracking(-1) + .contentTransition(.numericText()) Text("已学习") .font(.system(size: 13, weight: .medium)) .foregroundColor(Color.zxF04) } } + .onChange(of: elapsed) { newElapsed in + withAnimation(.easeInOut(duration: 0.5)) { + animatedProgress = min(CGFloat(newElapsed) / 1800, 1) + } + } HStack(spacing: 12) { Button { if isRunning { isPaused = true; isRunning = false } diff --git a/AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift b/AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift index 7d3ca89..6e3d8ba 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift @@ -67,6 +67,7 @@ struct ReviewCardView: View { RoundedRectangle(cornerRadius: 2) .fill(ZXGradient.progressBar) .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) diff --git a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift index 2aa6fdd..52ab951 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift @@ -29,7 +29,7 @@ struct StudyHomeView: View { } } 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) } } .padding(.bottom, 120) } .padding(.horizontal, 20) } @@ -39,9 +39,9 @@ struct StudyHomeView: View { .task { await studyVM.loadSessions() } } 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() - 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(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) } + 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).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).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) } } } .padding(16).background(ZXGradient.progressCard).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.15), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20)) } }