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:
parent
bf63852a0f
commit
b052afa065
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lottie/lottie.dart';
|
||||||
|
|
||||||
/// 火柴人排名数据模型
|
/// 火柴人排名数据模型
|
||||||
class StickmanRankingData {
|
class StickmanRankingData {
|
||||||
|
|
@ -230,10 +231,13 @@ class _StickmanRaceWidgetState extends State<StickmanRaceWidget>
|
||||||
// 添加上下弹跳效果
|
// 添加上下弹跳效果
|
||||||
final bounce = _bounceController.value * 3;
|
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 screenWidth = MediaQuery.of(context).size.width;
|
||||||
final containerWidth = screenWidth - 32 - 32; // 页面padding + 容器padding
|
final containerWidth = screenWidth - 32 - 32; // 页面padding + 容器padding
|
||||||
final availableWidth = containerWidth - 60 - 60; // 终点区域 - 火柴人宽度
|
final stickmanHalfWidth = 30.0; // 火柴人宽度的一半,用于居中对齐
|
||||||
|
final flagAreaWidth = 40.0; // 红旗区域宽度(红旗在right:8位置,图标24+边距)
|
||||||
|
final availableWidth = containerWidth - flagAreaWidth - stickmanHalfWidth;
|
||||||
final leftPosition = availableWidth * horizontalProgress;
|
final leftPosition = availableWidth * horizontalProgress;
|
||||||
|
|
||||||
return Positioned(
|
return Positioned(
|
||||||
|
|
@ -587,8 +591,8 @@ class _TrackPainter extends CustomPainter {
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 跑步火柴人动画组件
|
/// 跑步火柴人动画组件 - 使用 Lottie 动画
|
||||||
class RunningStickman extends StatefulWidget {
|
class RunningStickman extends StatelessWidget {
|
||||||
final double size;
|
final double size;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
|
|
@ -598,190 +602,22 @@ class RunningStickman extends StatefulWidget {
|
||||||
this.color = Colors.black,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedBuilder(
|
return SizedBox(
|
||||||
animation: _controller,
|
width: size,
|
||||||
builder: (context, child) {
|
height: size * 1.2,
|
||||||
return CustomPaint(
|
child: ColorFiltered(
|
||||||
size: Size(widget.size, widget.size * 1.2),
|
colorFilter: ColorFilter.mode(
|
||||||
painter: _RunningStickmanPainter(
|
color,
|
||||||
progress: _controller.value,
|
BlendMode.srcIn,
|
||||||
color: widget.color,
|
),
|
||||||
),
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue