feat(mobile-app): 使用 Lottie 动画替换火柴人 CustomPaint 实现

- 用 Lottie.asset 加载 stickman_runner.json 替换手动绘制的火柴人
- 使用 ColorFiltered 保持颜色自定义功能(当前用户金色,其他用户棕色)
- 修复火柴人到达100%时无法到达红旗位置的问题
- 代码从约200行精简到约30行,动画效果更流畅

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-23 04:34:15 -08:00
parent bf63852a0f
commit b052afa065
2 changed files with 23 additions and 186 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
///
class StickmanRankingData {
@ -230,10 +231,13 @@ class _StickmanRaceWidgetState extends State<StickmanRaceWidget>
//
final bounce = _bounceController.value * 3;
// - padding(32) - padding(32) - (60) - (60)
// - padding(32) - padding(32) - (40) - (30)
// 100%
final screenWidth = MediaQuery.of(context).size.width;
final containerWidth = screenWidth - 32 - 32; // padding + padding
final availableWidth = containerWidth - 60 - 60; // -
final stickmanHalfWidth = 30.0; //
final flagAreaWidth = 40.0; // right:824+
final availableWidth = containerWidth - flagAreaWidth - stickmanHalfWidth;
final leftPosition = availableWidth * horizontalProgress;
return Positioned(
@ -587,8 +591,8 @@ class _TrackPainter extends CustomPainter {
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
///
class RunningStickman extends StatefulWidget {
/// - 使 Lottie
class RunningStickman extends StatelessWidget {
final double size;
final Color color;
@ -598,190 +602,22 @@ class RunningStickman extends StatefulWidget {
this.color = Colors.black,
});
@override
State<RunningStickman> createState() => _RunningStickmanState();
}
class _RunningStickmanState extends State<RunningStickman>
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,
),
);
},
return SizedBox(
width: size,
height: size * 1.2,
child: ColorFiltered(
colorFilter: ColorFilter.mode(
color,
BlendMode.srcIn,
),
child: Lottie.asset(
'assets/lottie/stickman_runner.json',
fit: BoxFit.contain,
repeat: true,
),
),
);
}
}
///
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;
}
}