fix(mobile): 简化火柴人位置计算逻辑
使用简单直接的比例计算: - 起点 = 昵称区域右边 (70px) - 终点 = 红旗左边 (containerWidth - 62px) - 火柴人位置 = 起点 + (终点 - 起点) * 进度 🤖 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
71304dbb69
commit
3e59d56f92
|
|
@ -183,159 +183,150 @@ class _StickmanRaceWidgetState extends State<StickmanRaceWidget>
|
|||
|
||||
return SizedBox(
|
||||
height: raceTrackHeight,
|
||||
child: Stack(
|
||||
children: [
|
||||
// 背景赛道线
|
||||
Positioned.fill(
|
||||
child: CustomPaint(
|
||||
painter: _TrackPainter(
|
||||
trackCount: sortedRankings.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final containerWidth = constraints.maxWidth;
|
||||
|
||||
// 每个跑道的终点红旗
|
||||
...List.generate(trackCount, (index) {
|
||||
final verticalPosition = index * trackHeight + 10 + trackHeight / 2 - 16;
|
||||
return Positioned(
|
||||
right: 8,
|
||||
top: verticalPosition,
|
||||
child: const Icon(
|
||||
Icons.flag,
|
||||
color: Colors.red,
|
||||
size: 24,
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
// 背景赛道线
|
||||
Positioned.fill(
|
||||
child: CustomPaint(
|
||||
painter: _TrackPainter(
|
||||
trackCount: sortedRankings.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// 火柴人们
|
||||
...sortedRankings.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final data = entry.value;
|
||||
return _buildStickman(data, index, sortedRankings.length, raceTrackHeight);
|
||||
}),
|
||||
],
|
||||
// 每个跑道的终点红旗
|
||||
...List.generate(trackCount, (index) {
|
||||
final verticalPosition = index * trackHeight + 10 + trackHeight / 2 - 16;
|
||||
return Positioned(
|
||||
right: 8,
|
||||
top: verticalPosition,
|
||||
child: const Icon(
|
||||
Icons.flag,
|
||||
color: Colors.red,
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// 火柴人们
|
||||
...sortedRankings.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final data = entry.value;
|
||||
return _buildStickman(data, index, sortedRankings.length, raceTrackHeight, containerWidth);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建单个火柴人
|
||||
Widget _buildStickman(StickmanRankingData data, int rank, int total, double raceTrackHeight) {
|
||||
// 计算水平位置 (根据进度)
|
||||
final horizontalProgress = data.progress;
|
||||
Widget _buildStickman(StickmanRankingData data, int rank, int total, double raceTrackHeight, double containerWidth) {
|
||||
// 进度 (0.0 - 1.0)
|
||||
final progress = data.progress;
|
||||
|
||||
// 计算垂直位置 (不同排名在不同跑道)
|
||||
// 预留顶部和底部空间
|
||||
final usableHeight = raceTrackHeight - 40;
|
||||
final trackHeight = usableHeight / total;
|
||||
final verticalPosition = rank * trackHeight + 10;
|
||||
|
||||
// 简单直接的计算:
|
||||
// 起点:昵称区域右边 (70)
|
||||
// 终点:红旗左边 (containerWidth - 8(红旗right) - 24(红旗宽) - 30(火柴人半宽))
|
||||
// 火柴人中心位置 = 起点 + (终点 - 起点) * 进度
|
||||
const double startX = 70.0;
|
||||
final double endX = containerWidth - 62.0;
|
||||
final double stickmanCenterX = startX + (endX - startX) * progress;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _bounceController,
|
||||
builder: (context, child) {
|
||||
// 添加上下弹跳效果
|
||||
final bounce = _bounceController.value * 3;
|
||||
|
||||
// 计算可用宽度,让火柴人在100%时正好到达红旗位置
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final containerWidth = screenWidth - 32 - 32; // 页面padding + 容器padding
|
||||
final stickmanColumnWidth = 60.0; // 火柴人+数量标签列的宽度
|
||||
final flagRightMargin = 8.0; // 红旗的right值
|
||||
final flagWidth = 24.0; // 红旗图标宽度
|
||||
final nicknameAreaWidth = 65.0; // 昵称区域宽度
|
||||
final gap = 4.0; // 昵称和火柴人之间的最小间距
|
||||
|
||||
// 100%时火柴人列的右边缘应该对齐红旗左边缘
|
||||
// 红旗左边位置 = containerWidth - flagRightMargin - flagWidth
|
||||
// 火柴人列右边 = stickmanLeftPosition + stickmanColumnWidth
|
||||
// 所以 100% 时: stickmanLeftPosition = containerWidth - flagRightMargin - flagWidth - stickmanColumnWidth
|
||||
final endPosition = containerWidth - flagRightMargin - flagWidth - stickmanColumnWidth;
|
||||
final startPosition = nicknameAreaWidth + gap;
|
||||
final availableWidth = endPosition - startPosition;
|
||||
final stickmanLeftPosition = startPosition + availableWidth * horizontalProgress;
|
||||
|
||||
return Positioned(
|
||||
left: 0,
|
||||
top: verticalPosition - bounce,
|
||||
child: SizedBox(
|
||||
width: containerWidth,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 昵称标签 - 固定在左边,与数量标签同一水平高度
|
||||
SizedBox(
|
||||
width: nicknameAreaWidth,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: data.isCurrentUser
|
||||
? const Color(0xFFD4AF37).withValues(alpha: 0.2)
|
||||
: Colors.white.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: data.isCurrentUser
|
||||
? Border.all(color: const Color(0xFFD4AF37), width: 1)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
data.nickname,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight:
|
||||
data.isCurrentUser ? FontWeight.w600 : FontWeight.w400,
|
||||
color: data.isCurrentUser
|
||||
? const Color(0xFFD4AF37)
|
||||
: const Color(0xFF5D4037),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
// 昵称标签 - 固定在左边
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: verticalPosition + 15 - bounce,
|
||||
child: SizedBox(
|
||||
width: 65,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: data.isCurrentUser
|
||||
? const Color(0xFFD4AF37).withValues(alpha: 0.2)
|
||||
: Colors.white.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: data.isCurrentUser
|
||||
? Border.all(color: const Color(0xFFD4AF37), width: 1)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
// 间隔 + 火柴人位置
|
||||
SizedBox(
|
||||
width: stickmanLeftPosition - nicknameAreaWidth,
|
||||
),
|
||||
// 火柴人和数量标签
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 完成数量标签
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 60),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: data.isCurrentUser
|
||||
? const Color(0xFFD4AF37)
|
||||
: const Color(0xFF8B5A2B),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'${_formatNumber(data.completedCount)}棵',
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// 火柴人动画
|
||||
RunningStickman(
|
||||
size: 36,
|
||||
child: Text(
|
||||
data.nickname,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: data.isCurrentUser ? FontWeight.w600 : FontWeight.w400,
|
||||
color: data.isCurrentUser
|
||||
? const Color(0xFFD4AF37)
|
||||
: const Color(0xFF5D4037),
|
||||
),
|
||||
],
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// 火柴人和数量标签
|
||||
Positioned(
|
||||
left: stickmanCenterX - 30, // 火柴人宽度60的一半
|
||||
top: verticalPosition - bounce,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 完成数量标签
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 60),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: data.isCurrentUser
|
||||
? const Color(0xFFD4AF37)
|
||||
: const Color(0xFF8B5A2B),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'${_formatNumber(data.completedCount)}棵',
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// 火柴人动画
|
||||
RunningStickman(
|
||||
size: 36,
|
||||
color: data.isCurrentUser
|
||||
? const Color(0xFFD4AF37)
|
||||
: const Color(0xFF5D4037),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue