欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net
示例效果




1. 导论:数字健身的生物力学坍缩与重构
在传统的移动端健身应用开发中,大多数软件仅停留在"倒计时器"与"动作视频播放器"的表层逻辑上,忽略了力量训练真正的核心------生物力学参数(Biomechanics)与周期性负荷(Periodization)。真实的肌纤维断裂与超量恢复,是由确切的张力时间(Time Under Tension, TUT)、容量负荷(Volume Load, VL)以及神经疲劳度所共同决定的非线性物理过程。
本架构研究彻底颠覆了扁平的健身记录模式,基于 Flutter 强劲的跨平台 2D 渲染引擎,构建了一套带有动态生理学插值反馈的"臂力生物力学与力量周期训练矩阵"。通过在屏幕空间内建立神经肌肉负荷仪与 Epley 1RM 极限推演仪表盘,系统不仅能够实时计算当前的肌肉绝对张力,更通过波函数的几何形变,精确测绘出肌纤维在不同训练周期(肌肥大、绝对力量、爆发力、耐力)下的超负荷状态。
2. 生理学数据建模:肌肉群域与训练周期的状态机解耦
要在一台终端设备上模拟真实的物理重力抗阻行为,必须首先在内存中建立起严密的人体骨骼肌状态机与多维特征向量模型。在本系统中,我们将一套训练动作(Exercise)定义为包含了生物力学特征参数的封闭域空间。
2.1 训练特征空间与基底映射
通过枚举类型切分人体大臂至前臂的拮抗与协同肌群:biceps(肱二头肌)、triceps(肱三头肌)、forearms(前臂肌群)、shoulders(三角肌)。每个物理动作在代码层级被强制绑定唯一的受力靶向。
在系统的物理实体域中,我们利用以下 UML 类图模型,实现了训练指标集与图形学渲染算子的彻底解耦:
target
limits
execution flow
render telemetry
1
1
1
1
1
*
<<enumeration>>
MuscleGroup
biceps
triceps
forearms
shoulders
<<enumeration>>
TrainingPhase
hypertrophy
strength
power
endurance
Exercise
+String id
+String name
+MuscleGroup targetMuscle
+TrainingPhase phase
+int sets
+int reps
+double weight
+double estimated1RM
+double volumeLoad
BiomechanicsDashboard
+List<Exercise> exercises
+int selectedExerciseIndex
+_updateWeight(double)
BiomechanicsGaugePainter
+double max1RM
+double current1RM
+Color themeColor
+double volumeLoad
+paint(Canvas, Size)
2.2 绝对力量(1RM)与容量负荷积分方程
每个动作不仅包含基础的组数( s s s)、次数( r r r)与负重( w w w),更在实体内部实时演算生物力学的高阶参数。
我们利用了经典的 Epley 公式推导当前动作配置下的 1RM(One-Rep Max,即做且仅能做一次的最大重量):
1 R M = w ⋅ ( 1 + r 30 ) 1RM = w \cdot \left(1 + \frac{r}{30} \right) 1RM=w⋅(1+30r)
同时,训练的容量负荷(Volume Load, VL)作为衡量肌肉微小损伤累计程度的绝对标量,其偏导数模型为:
V L ( t ) = ∫ 0 t ∑ i = 1 n s i ⋅ r i ⋅ w i d t VL(t) = \int_{0}^{t} \sum_{i=1}^{n} s_i \cdot r_i \cdot w_i \, dt VL(t)=∫0ti=1∑nsi⋅ri⋅widt
当用户在界面的轨道滑块上增加工作重量 w w w 时,这套隐式的力学方程将立刻被触发。参数的改变经过底层状态控制机的过滤后,直接推入 CustomPainter 的渲染管线,从而引发中央仪表盘指针的剧烈偏转以及高频容量波形的震荡。
3. 极客仪表盘引擎:生物力学压力图与容量波函数的重叠
要表现出极其硬核的机械张力压迫感,简单的线性进度条是完全无法承担的。系统利用 Flutter底层的 Canvas 图形指令集,在屏幕上生生切开了一个带有霓虹极光效果的物理仪表盘。
3.1 扫略弧梯度与游丝刻度盘的绘制
给定画布中心点 ( C x , C y ) (C_x, C_y) (Cx,Cy),仪表盘外沿最大半径为 R m a x R_{max} Rmax。表盘从极坐标角 3 π 4 \frac{3\pi}{4} 43π 处起步,环扫过 3 π 2 \frac{3\pi}{2} 23π 弧度,覆盖了底部以外的全部区域。为了表现出极其精密的"负荷测量仪"质感,我们利用三角函数算子 cos 与 sin 在圆周上精准切割出 60 个刻度游丝(Tick mark):
对于第 i i i 个刻度,其绝对坐标 ( X i , Y i ) (X_i, Y_i) (Xi,Yi) 的方程为:
X i = C x + R t i c k ⋅ cos ( θ 0 + i ⋅ Δ θ ) X_i = C_x + R_{tick} \cdot \cos(\theta_0 + i \cdot \Delta \theta) Xi=Cx+Rtick⋅cos(θ0+i⋅Δθ)
Y i = C y + R t i c k ⋅ sin ( θ 0 + i ⋅ Δ θ ) Y_i = C_y + R_{tick} \cdot \sin(\theta_0 + i \cdot \Delta \theta) Yi=Cy+Rtick⋅sin(θ0+i⋅Δθ)
代码层面,游丝长短交错,并且根据当前的 1RM 极限值对到达阈值的刻度实行染色与加粗高亮指令。
3.2 容量波函数(Volume Wave Function)的内积干涉
为了让训练容量(VL)具有可被肉眼观测的实体化形态,我们在压力圆弧的内部,注入了一个受常微分方程控制的正弦波函数。
它的核心极坐标闭合路径遵循以下调制公式:
R ( θ ) = R b a s e + A ⋅ sin ( k ⋅ θ ) R(\theta) = R_{base} + A \cdot \sin(k \cdot \theta) R(θ)=Rbase+A⋅sin(k⋅θ)
其中振幅 A A A 与该动作的容量负荷直接挂钩 A ∝ V L A \propto VL A∝VL,频率 k k k 被设定为 8。由于 V L VL VL 会随滑块发生动态变化,这条位于仪表盘内部的波浪线圈会如同检测心电图一样,伴随疲劳程度的提升而发生幅度巨大的震荡干涉,呈现出极具视觉压迫感的工程美学。
4. 神经疲劳抑制机制与 TweenAnimationBuilder 阻尼引擎
如果切换不同训练动作时,仪表盘的数字和进度条发生突变,将严重破坏系统的重量沉浸感。我们必须模拟真实的物理表盘------指针带有惯量和弹簧力矩。
为此,系统在雷达外围包裹了带有回调闭包的 TweenAnimationBuilder<double>,利用了高阶贝塞尔曲线 Curves.easeOutCubic:
触发 setState 脏数据刷新
下推 current1RM 目标值
计算阻尼微分方程
插值 AnimationValue
重绘 SweepGradient
触发波函数限流裁剪
用户滑动重量调优滑块
更新 Exercise 向量
TweenAnimationBuilder 核心
Ticker 发起 60Hz 刷新波段
同步分发给数字面板与 Canvas
表盘平滑滑行到指定阻力位
输出高精度工业级动画帧
这不仅抹平了数据突变带来的毛刺感,更让数字在极速翻滚跳跃时,给使用者带来一种巨大的"对抗引力与重量"的沉浸反馈。
5. 核心代码解构与极客实现法则
在本款训练矩阵的代码建设中,内存指针的防御与底层图形加速被运用到了极限。以下四段核心代码支撑了整个力学仪表盘的撕裂渲染。
5.1 训练域模型与 Epley 生物力学方程
我们不存储冗余计算后的数据,而是采用在数据实体内部利用 getter 定义公式法则,这是极简与高性能架构的核心。
103:107:lib/main.dart
// Epley 1RM 公式计算
double get estimated1RM => weight * (1 + reps / 30.0);
// 容量负荷 (Volume Load)
double get volumeLoad => sets * reps * weight;
每当重量变量 weight 被重置时,该实体所映射出来的 estimated1RM 极限抗阻将同时跟随其内存链条被重新推导。
5.2 响应式瀑布流卡片架构(防溢出机制)
在旧有客户端开发中,复杂的仪表与列表极易引发溢出灾难。而在 Flutter 架构下,我们引入了 LayoutBuilder 与 CustomScrollView 滑动视口检测:
184:204:lib/main.dart
builder: (context, constraints) {
final isCompact = constraints.maxWidth < 900;
if (isCompact) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SizedBox(
height: 400,
child: _buildBiomechanicsAnalyzer(),
),
),
SliverToBoxAdapter(
child: Container(
height: 1,
color: const Color(0xFF27272A),
),
),
SliverToBoxAdapter(
child: _buildWorkoutList(isCompact: true),
),
],
);
}
针对屏幕空间狭小的折叠屏或手机,系统会在 0.1 0.1 0.1 秒内进行渲染树重构,将并排排列的物理仪表盘与计划列表强行降维打击,压缩进 Sliver 滑动槽中,彻底杜绝所有的越界警报(Overflow Error)。
5.3 刻度游丝微积分渲染引擎
这正是那个深邃硬核的物理表盘刻度的绘制管线:
496:518:lib/main.dart
const int totalTicks = 60;
const double startAngle = math.pi * 0.75;
const double sweepAngle = math.pi * 1.5;
for (int i = 0; i <= totalTicks; i++) {
final double progress = i / totalTicks;
final double angle = startAngle + progress * sweepAngle;
final bool isHighlight = (progress * max1RM) <= current1RM;
final double tickLength = i % 5 == 0 ? 15.0 : 8.0;
final p1 = Offset(
center.dx + (radius - tickLength) * math.cos(angle),
center.dy + (radius - tickLength) * math.sin(angle),
);
final p2 = Offset(
center.dx + radius * math.cos(angle),
center.dy + radius * math.sin(angle),
);
canvas.drawLine(p1, p2, isHighlight ? highlightScalePaint : scalePaint);
}
通过求余算子 i % 5 == 0,系统精准打击长短不一的机械表盘网格;通过 isHighlight 阻断阀门,在未跨越绝对力量阈值时将颜色涂刷为死寂的灰色 0xFF27272A。
5.4 容量波函数的裁剪与干涉算法
利用 Path.arcTo 生成了切割面具,并将高亮波形强行锁定在有效能量环内:
563:581:lib/main.dart
// 内部波形高亮截断
canvas.save();
canvas.clipPath(
Path()
..moveTo(center.dx, center.dy)
..arcTo(
Rect.fromCircle(center: center, radius: radius),
startAngle,
currentSweep,
false,
)
..lineTo(center.dx, center.dy)
);
final activeWavePaint = Paint()
..color = themeColor.withValues(alpha: 0.4)
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawPath(wavePath, activeWavePaint);
canvas.restore();
canvas.clipPath 这一神级指令在这里直接把底层的绘制空间锁死在了从中心点发散出去的一个扇形中。这样即使 V L VL VL 波函数覆盖全圆周,能够发亮发光的也只有当前负载所在的刻度区域以内!
6. 结语:计算机图形学重构数字体育边界
通过深度剖析人体骨骼肌做功的偏微分极限物理,我们利用 Flutter 平台极其纯粹的 Dart 画笔 API,打造了一个仿佛从太空舱仪表台直接切割下来的"生物力学与力量周期训练矩阵"。
从 Epley 公式的微观数学计算,到底层渲染循环所引发的游丝、波纹的图形跳动。这套系统剥离了所有粗制滥造的表层控件,直接与男人的肌肉张力发生物理学层面的高能共鸣。这也是工业级代码走向专业垂直领域的必然趋势。所有的机械对抗、所有的容量爆炸都在这深邃暗黑的数据雷达中被精确捕捉。这是科技与狂暴重力抗衡的最强工业赞歌!
源码
bash
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
DeviceOrientation.portraitUp,
]);
runApp(const ArmStrengthTrainingApp());
}
class ArmStrengthTrainingApp extends StatelessWidget {
const ArmStrengthTrainingApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '臂力生物力学与力量周期训练矩阵',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF09090B), // 极致深邃黑
useMaterial3: true,
fontFamily: 'Roboto',
),
home: const BiomechanicsDashboard(),
);
}
}
// ==========================================
// 领域模型 (Domain Models)
// ==========================================
enum MuscleGroup {
biceps,
triceps,
forearms,
shoulders,
}
extension MuscleGroupExt on MuscleGroup {
String get label {
switch (this) {
case MuscleGroup.biceps:
return '肱二头肌 (Biceps Brachii)';
case MuscleGroup.triceps:
return '肱三头肌 (Triceps Brachii)';
case MuscleGroup.forearms:
return '前臂肌群 (Forearm Muscles)';
case MuscleGroup.shoulders:
return '三角肌 (Deltoid)';
}
}
Color get color {
switch (this) {
case MuscleGroup.biceps:
return const Color(0xFFEAB308); // 力量黄
case MuscleGroup.triceps:
return const Color(0xFFEF4444); // 燃脂红
case MuscleGroup.forearms:
return const Color(0xFF10B981); // 耐力绿
case MuscleGroup.shoulders:
return const Color(0xFF3B82F6); // 塑形蓝
}
}
}
enum TrainingPhase {
hypertrophy, // 肌肥大
strength, // 绝对力量
power, // 爆发力
endurance, // 肌肉耐力
}
class Exercise {
final String id;
final String name;
final MuscleGroup targetMuscle;
final TrainingPhase phase;
final int sets;
final int reps;
final double weight; // kg
Exercise({
required this.id,
required this.name,
required this.targetMuscle,
required this.phase,
required this.sets,
required this.reps,
required this.weight,
});
// Epley 1RM 公式计算
double get estimated1RM => weight * (1 + reps / 30.0);
// 容量负荷 (Volume Load)
double get volumeLoad => sets * reps * weight;
}
// ==========================================
// 核心状态控制面板 (Main Dashboard)
// ==========================================
class BiomechanicsDashboard extends StatefulWidget {
const BiomechanicsDashboard({super.key});
@override
State<BiomechanicsDashboard> createState() => _BiomechanicsDashboardState();
}
class _BiomechanicsDashboardState extends State<BiomechanicsDashboard> with TickerProviderStateMixin {
late List<Exercise> exercises;
late AnimationController _engineController;
int selectedExerciseIndex = 0;
@override
void initState() {
super.initState();
_initData();
_engineController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 3000),
)..repeat(reverse: true);
}
@override
void dispose() {
_engineController.dispose();
super.dispose();
}
void _initData() {
exercises = [
Exercise(
id: 'EX01',
name: '杠铃弯举 (Barbell Curl)',
targetMuscle: MuscleGroup.biceps,
phase: TrainingPhase.hypertrophy,
sets: 4,
reps: 10,
weight: 40.0,
),
Exercise(
id: 'EX02',
name: '碎颅者 (Skull Crusher)',
targetMuscle: MuscleGroup.triceps,
phase: TrainingPhase.hypertrophy,
sets: 4,
reps: 12,
weight: 35.0,
),
Exercise(
id: 'EX03',
name: '农夫行走 (Farmer\'s Walk)',
targetMuscle: MuscleGroup.forearms,
phase: TrainingPhase.endurance,
sets: 3,
reps: 20, // 距离换算为等效次数
weight: 60.0, // 每手 30kg
),
Exercise(
id: 'EX04',
name: '重磅推举 (Overhead Press)',
targetMuscle: MuscleGroup.shoulders,
phase: TrainingPhase.strength,
sets: 5,
reps: 5,
weight: 65.0,
),
Exercise(
id: 'EX05',
name: '哑铃集中弯举 (Concentration Curl)',
targetMuscle: MuscleGroup.biceps,
phase: TrainingPhase.hypertrophy,
sets: 3,
reps: 15,
weight: 15.0,
),
Exercise(
id: 'EX06',
name: '双杠臂屈伸 (Dips)',
targetMuscle: MuscleGroup.triceps,
phase: TrainingPhase.strength,
sets: 4,
reps: 8,
weight: 85.0, // 体重+负重
),
];
}
void _updateWeight(double val) {
setState(() {
final current = exercises[selectedExerciseIndex];
exercises[selectedExerciseIndex] = Exercise(
id: current.id,
name: current.name,
targetMuscle: current.targetMuscle,
phase: current.phase,
sets: current.sets,
reps: current.reps,
weight: val,
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
final isCompact = constraints.maxWidth < 900;
if (isCompact) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SizedBox(
height: 400,
child: _buildBiomechanicsAnalyzer(),
),
),
SliverToBoxAdapter(
child: Container(
height: 1,
color: const Color(0xFF27272A),
),
),
SliverToBoxAdapter(
child: _buildWorkoutList(isCompact: true),
),
],
);
}
return Row(
children: [
Expanded(
flex: 4,
child: _buildWorkoutList(isCompact: false),
),
Container(
width: 1,
color: const Color(0xFF27272A),
),
Expanded(
flex: 5,
child: _buildBiomechanicsAnalyzer(),
),
],
);
},
),
);
}
Widget _buildWorkoutList({required bool isCompact}) {
return Container(
color: const Color(0xFF09090B),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(24.0),
child: Row(
children: [
const Icon(Icons.fitness_center, color: Color(0xFFEAB308), size: 28),
const SizedBox(width: 12),
const Text(
'神经肌肉训练控制台',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 2,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFEAB308).withValues(alpha: 0.15),
border: Border.all(color: const Color(0xFFEAB308).withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'HYPERTROPHY',
style: TextStyle(
color: Color(0xFFEAB308),
fontWeight: FontWeight.bold,
fontSize: 12,
letterSpacing: 1,
),
),
),
],
),
),
if (isCompact)
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: exercises.length,
itemBuilder: (context, index) {
return _buildExerciseCard(index);
},
)
else
Expanded(
child: ListView.builder(
itemCount: exercises.length,
itemBuilder: (context, index) {
return _buildExerciseCard(index);
},
),
),
],
),
);
}
Widget _buildExerciseCard(int index) {
final ex = exercises[index];
final isSelected = index == selectedExerciseIndex;
return InkWell(
onTap: () {
setState(() {
selectedExerciseIndex = index;
});
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF18181B) : Colors.transparent,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? ex.targetMuscle.color : const Color(0xFF27272A),
width: isSelected ? 2 : 1,
),
boxShadow: isSelected
? [
BoxShadow(
color: ex.targetMuscle.color.withValues(alpha: 0.15),
blurRadius: 20,
spreadRadius: -5,
)
]
: [],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 4,
height: 16,
decoration: BoxDecoration(
color: ex.targetMuscle.color,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
ex.id,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.grey[500],
fontFamily: 'monospace',
),
),
const Spacer(),
Text(
ex.targetMuscle.label,
style: TextStyle(
fontSize: 12,
color: ex.targetMuscle.color,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
Text(
ex.name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.white : Colors.grey[300],
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildStatItem('SETS', '${ex.sets}', isSelected),
_buildStatItem('REPS', '${ex.reps}', isSelected),
_buildStatItem('WEIGHT', '${ex.weight}kg', isSelected),
_buildStatItem('1RM', '${ex.estimated1RM.toStringAsFixed(1)}kg', isSelected, highlight: true),
],
),
],
),
),
);
}
Widget _buildStatItem(String label, String value, bool isSelected, {bool highlight = false}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w900,
fontFamily: 'monospace',
color: highlight
? (isSelected ? const Color(0xFFEAB308) : Colors.grey[400])
: (isSelected ? Colors.white : Colors.grey[500]),
),
),
],
);
}
Widget _buildBiomechanicsAnalyzer() {
final currentEx = exercises[selectedExerciseIndex];
return Container(
color: const Color(0xFF09090B),
child: Column(
children: [
Expanded(
flex: 6,
child: Stack(
alignment: Alignment.center,
children: [
// 呼吸光环背景
AnimatedBuilder(
animation: _engineController,
builder: (context, child) {
return Container(
width: 300 + (_engineController.value * 40),
height: 300 + (_engineController.value * 40),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
currentEx.targetMuscle.color.withValues(alpha: 0.1),
Colors.transparent,
],
stops: const [0.2, 1.0],
),
),
);
},
),
// 核心生物力学仪表盘
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: currentEx.estimated1RM),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return CustomPaint(
size: const Size(280, 280),
painter: BiomechanicsGaugePainter(
max1RM: 150.0, // 假设仪表盘上限
current1RM: value,
themeColor: currentEx.targetMuscle.color,
volumeLoad: currentEx.volumeLoad,
),
);
},
),
// 仪表盘中心文本
Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'EST. 1RM',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
letterSpacing: 2,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: currentEx.estimated1RM),
duration: const Duration(milliseconds: 600),
builder: (context, value, child) {
return Text(
value.toStringAsFixed(1),
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w900,
fontFamily: 'monospace',
color: currentEx.targetMuscle.color,
),
);
},
),
const Text(
'KG',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
// 底部控制与参数区
Container(
padding: const EdgeInsets.all(32),
decoration: const BoxDecoration(
color: Color(0xFF18181B),
border: Border(top: BorderSide(color: Color(0xFF27272A))),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'工作负重调优 (Load Adjustment)',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'${currentEx.weight.toStringAsFixed(1)} KG',
style: TextStyle(
color: currentEx.targetMuscle.color,
fontSize: 16,
fontWeight: FontWeight.w900,
fontFamily: 'monospace',
),
),
],
),
const SizedBox(height: 16),
SliderTheme(
data: SliderThemeData(
trackHeight: 4,
activeTrackColor: currentEx.targetMuscle.color,
inactiveTrackColor: const Color(0xFF27272A),
thumbColor: Colors.white,
overlayColor: currentEx.targetMuscle.color.withValues(alpha: 0.2),
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
),
child: Slider(
value: currentEx.weight,
min: 5.0,
max: 120.0,
divisions: 230, // 0.5kg increments roughly
onChanged: _updateWeight,
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: _buildMechanicsDetail(
'容量负荷 (VL)',
'${currentEx.volumeLoad.toStringAsFixed(0)}',
'Kg',
currentEx.targetMuscle.color,
),
),
Container(width: 1, height: 40, color: const Color(0xFF27272A)),
Expanded(
child: _buildMechanicsDetail(
'张力时间 (TUT)',
'${currentEx.reps * 3}', // 假定每组完成时间 3s * reps
'Sec',
Colors.white,
),
),
Container(width: 1, height: 40, color: const Color(0xFF27272A)),
Expanded(
child: _buildMechanicsDetail(
'神经疲劳度',
((currentEx.weight / currentEx.estimated1RM) * 100).toStringAsFixed(1),
'%',
const Color(0xFFEF4444),
),
),
],
),
],
),
),
],
),
);
}
Widget _buildMechanicsDetail(String label, String val, String unit, Color color) {
return Column(
children: [
Text(
label,
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
val,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: color,
fontFamily: 'monospace',
),
),
const SizedBox(width: 2),
Padding(
padding: const EdgeInsets.only(bottom: 3.0),
child: Text(
unit,
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
fontWeight: FontWeight.bold,
),
),
),
],
)
],
);
}
}
// ==========================================
// 极客物理渲染:生物力学压力仪表盘
// ==========================================
class BiomechanicsGaugePainter extends CustomPainter {
final double max1RM;
final double current1RM;
final Color themeColor;
final double volumeLoad;
BiomechanicsGaugePainter({
required this.max1RM,
required this.current1RM,
required this.themeColor,
required this.volumeLoad,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = math.min(size.width, size.height) / 2;
// 1. 绘制外部刻度环 (Outer Dial Scale)
final scalePaint = Paint()
..color = const Color(0xFF27272A)
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
final highlightScalePaint = Paint()
..color = themeColor
..style = PaintingStyle.stroke
..strokeWidth = 3.0;
const int totalTicks = 60;
const double startAngle = math.pi * 0.75;
const double sweepAngle = math.pi * 1.5;
for (int i = 0; i <= totalTicks; i++) {
final double progress = i / totalTicks;
final double angle = startAngle + progress * sweepAngle;
final bool isHighlight = (progress * max1RM) <= current1RM;
final double tickLength = i % 5 == 0 ? 15.0 : 8.0;
final p1 = Offset(
center.dx + (radius - tickLength) * math.cos(angle),
center.dy + (radius - tickLength) * math.sin(angle),
);
final p2 = Offset(
center.dx + radius * math.cos(angle),
center.dy + radius * math.sin(angle),
);
canvas.drawLine(p1, p2, isHighlight ? highlightScalePaint : scalePaint);
}
// 2. 绘制内层压力圆弧 (Inner Stress Arc)
final arcRect = Rect.fromCircle(center: center, radius: radius - 30);
final bgArcPaint = Paint()
..color = const Color(0xFF18181B)
..style = PaintingStyle.stroke
..strokeWidth = 12.0
..strokeCap = StrokeCap.round;
canvas.drawArc(arcRect, startAngle, sweepAngle, false, bgArcPaint);
final double currentSweep = (current1RM / max1RM).clamp(0.0, 1.0) * sweepAngle;
final fgArcPaint = Paint()
..shader = ui.Gradient.sweep(
center,
[
themeColor.withValues(alpha: 0.2),
themeColor,
],
[0.0, 1.0],
TileMode.clamp,
startAngle,
startAngle + currentSweep,
)
..style = PaintingStyle.stroke
..strokeWidth = 12.0
..strokeCap = StrokeCap.round;
canvas.drawArc(arcRect, startAngle, currentSweep, false, fgArcPaint);
// 3. 容量波函数生成器 (Volume Wave Function Inner Plot)
// 根据 VolumeLoad 在内部画一圈表示"累积工作量"的波浪图
final wavePath = Path();
final int wavePoints = 120;
final double waveBaseRadius = radius - 60;
final double waveAmplitude = math.min(15.0, volumeLoad / 200.0); // 振幅受容量影响
for (int i = 0; i <= wavePoints; i++) {
final double angle = (i / wavePoints) * math.pi * 2;
// 叠加正弦波
final double r = waveBaseRadius + math.sin(angle * 8) * waveAmplitude;
final double x = center.dx + r * math.cos(angle);
final double y = center.dy + r * math.sin(angle);
if (i == 0) {
wavePath.moveTo(x, y);
} else {
wavePath.lineTo(x, y);
}
}
wavePath.close();
final wavePaint = Paint()
..color = const Color(0xFF27272A).withValues(alpha: 0.5)
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
canvas.drawPath(wavePath, wavePaint);
// 内部波形高亮截断
canvas.save();
canvas.clipPath(
Path()
..moveTo(center.dx, center.dy)
..arcTo(
Rect.fromCircle(center: center, radius: radius),
startAngle,
currentSweep,
false,
)
..lineTo(center.dx, center.dy)
);
final activeWavePaint = Paint()
..color = themeColor.withValues(alpha: 0.4)
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawPath(wavePath, activeWavePaint);
canvas.restore();
}
@override
bool shouldRepaint(covariant BiomechanicsGaugePainter oldDelegate) {
return oldDelegate.current1RM != current1RM ||
oldDelegate.themeColor != themeColor ||
oldDelegate.volumeLoad != volumeLoad;
}
}