From 3e59d56f92fc682d275e7ffb6a1b5c1c1fe1a23d Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 24 Dec 2025 01:48:08 -0800 Subject: [PATCH] =?UTF-8?q?fix(mobile):=20=E7=AE=80=E5=8C=96=E7=81=AB?= =?UTF-8?q?=E6=9F=B4=E4=BA=BA=E4=BD=8D=E7=BD=AE=E8=AE=A1=E7=AE=97=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用简单直接的比例计算: - 起点 = 昵称区域右边 (70px) - 终点 = 红旗左边 (containerWidth - 62px) - 火柴人位置 = 起点 + (终点 - 起点) * 进度 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../widgets/stickman_race_widget.dart | 245 +++++++++--------- 1 file changed, 118 insertions(+), 127 deletions(-) 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 7a64e7ce..82babe79 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 @@ -183,159 +183,150 @@ class _StickmanRaceWidgetState extends State 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), + ), + ], + ), + ), + ], ); }, );