diff --git a/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart b/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart index a2cf4d49..99797925 100644 --- a/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart +++ b/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:lottie/lottie.dart'; /// 火柴人排名数据模型 class StickmanRankingData { @@ -267,14 +266,11 @@ class _StickmanRaceWidgetState extends State const SizedBox(height: 2), // 火柴人动画 - SizedBox( - width: 40, - height: 50, - child: Lottie.asset( - 'assets/lottie/stickman_running.json', - fit: BoxFit.contain, - repeat: true, - ), + RunningStickman( + size: 36, + color: data.isCurrentUser + ? const Color(0xFFD4AF37) + : const Color(0xFF5D4037), ), // 昵称标签 @@ -592,3 +588,202 @@ class _TrackPainter extends CustomPainter { @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } + +/// 跑步火柴人动画组件 +class RunningStickman extends StatefulWidget { + final double size; + final Color color; + + const RunningStickman({ + super.key, + this.size = 40, + this.color = Colors.black, + }); + + @override + State createState() => _RunningStickmanState(); +} + +class _RunningStickmanState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: this, + )..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return CustomPaint( + size: Size(widget.size, widget.size * 1.2), + painter: _RunningStickmanPainter( + progress: _controller.value, + color: widget.color, + ), + ); + }, + ); + } +} + +/// 跑步火柴人绘制器 +class _RunningStickmanPainter extends CustomPainter { + final double progress; + final Color color; + + _RunningStickmanPainter({ + required this.progress, + required this.color, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = size.width * 0.08 + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + + final fillPaint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + // 尺寸计算 + final centerX = size.width * 0.5; + final headRadius = size.width * 0.18; + final headY = size.height * 0.15; + + // 身体各部位位置 + final neckY = headY + headRadius; + final shoulderY = size.height * 0.35; + final hipY = size.height * 0.55; + final bodyX = centerX; + + // 动画相位 (0-1 循环) + final phase = progress * 2 * 3.14159; + + // 腿部摆动幅度 + final legSwing = size.width * 0.35 * (phase < 3.14159 ? + (phase / 3.14159) * 2 - 1 : + 1 - ((phase - 3.14159) / 3.14159) * 2); + + // 手臂摆动(与腿相反) + final armSwing = -legSwing * 0.8; + + // 绘制头部 + canvas.drawCircle( + Offset(centerX, headY), + headRadius, + fillPaint, + ); + + // 绘制身体 + canvas.drawLine( + Offset(bodyX, neckY), + Offset(bodyX, hipY), + paint, + ); + + // 绘制速度线(在身体左侧) + final speedLinePaint = Paint() + ..color = color + ..strokeWidth = size.width * 0.05 + ..strokeCap = StrokeCap.round; + + for (int i = 0; i < 3; i++) { + final lineY = shoulderY + i * size.height * 0.08; + final lineStartX = centerX - size.width * 0.5; + final lineEndX = lineStartX + size.width * 0.2; + canvas.drawLine( + Offset(lineStartX, lineY), + Offset(lineEndX, lineY), + speedLinePaint, + ); + } + + // 绘制手臂 + // 后手臂 + final backArmEndX = bodyX - size.width * 0.25 + armSwing * 0.5; + final backArmEndY = shoulderY + size.height * 0.15; + canvas.drawLine( + Offset(bodyX, shoulderY), + Offset(backArmEndX, backArmEndY), + paint, + ); + // 后手臂前臂 + canvas.drawLine( + Offset(backArmEndX, backArmEndY), + Offset(backArmEndX + size.width * 0.15, backArmEndY - size.height * 0.1), + paint, + ); + + // 前手臂 + final frontArmEndX = bodyX + size.width * 0.2 - armSwing * 0.5; + final frontArmEndY = shoulderY + size.height * 0.12; + canvas.drawLine( + Offset(bodyX, shoulderY), + Offset(frontArmEndX, frontArmEndY), + paint, + ); + // 前手臂前臂 + canvas.drawLine( + Offset(frontArmEndX, frontArmEndY), + Offset(frontArmEndX + size.width * 0.1, frontArmEndY - size.height * 0.08), + paint, + ); + + // 绘制腿部 + final footY = size.height * 0.95; + + // 后腿 + final backKneeX = bodyX - size.width * 0.1 - legSwing * 0.3; + final backKneeY = hipY + size.height * 0.2; + final backFootX = bodyX - size.width * 0.3 + legSwing * 0.5; + + canvas.drawLine( + Offset(bodyX, hipY), + Offset(backKneeX, backKneeY), + paint, + ); + canvas.drawLine( + Offset(backKneeX, backKneeY), + Offset(backFootX, footY), + paint, + ); + + // 前腿 + final frontKneeX = bodyX + size.width * 0.15 + legSwing * 0.3; + final frontKneeY = hipY + size.height * 0.18; + final frontFootX = bodyX + size.width * 0.35 - legSwing * 0.5; + + canvas.drawLine( + Offset(bodyX, hipY), + Offset(frontKneeX, frontKneeY), + paint, + ); + canvas.drawLine( + Offset(frontKneeX, frontKneeY), + Offset(frontFootX, footY), + paint, + ); + } + + @override + bool shouldRepaint(covariant _RunningStickmanPainter oldDelegate) { + return oldDelegate.progress != progress; + } +}