Flutter中如何将StartPomodoro的启停按钮集成到GNav组件
问题描述
我正在开发一款Flutter计时器应用,UI重构时需要把StartPomodoro组件里的开始/暂停按钮迁移到HomePageTimerUI的GNav组件中,同时保留计时器的功能和动画控制。
当前布局:
目标布局:
难点
计时器的控制逻辑和动画代码都在StartPomodoro组件里,直接剪切按钮到HomePageTimerUI后无法正常集成。
解决方案
核心思路
将计时器的核心状态与控制逻辑上移至HomePageTimerUI(父组件),通过参数传递给StartPomodoro(子组件)负责UI渲染,同时在GNav中绑定控制方法实现按钮功能。
修改后的代码
HomePageTimerUI.dart
class HomePageTimerUI extends StatefulWidget { const HomePageTimerUI({Key? key}) : super(key: key); @override State<HomePageTimerUI> createState() => _HomePageTimerUIState(); } class _HomePageTimerUIState extends State<HomePageTimerUI> with TickerProviderStateMixin { late TabController _tabController; late AnimationController controller; bool isTimerAnimating = false; bool isLongBreak = true; String get countText { Duration count = controller.duration! * controller.value; return controller.isDismissed ? '${controller.duration!.inHours.toString().padLeft(2, '0')}:${(controller.duration!.inMinutes % 60).toString().padLeft(2, '0')}:${(controller.duration!.inSeconds % 60).toString().padLeft(2, '0')}' : '${count.inHours.toString().padLeft(2, '0')}:${(count.inMinutes % 60).toString().padLeft(2, '0')}:${(count.inSeconds % 60).toString().padLeft(2, '0')}'; } @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); // 初始化计时器控制器,默认25分钟 controller = AnimationController( vsync: this, duration: const Duration(seconds: 1500), ); controller.addListener(() { notify(); setState(() { isTimerAnimating = controller.isAnimating; if (!controller.isAnimating) { isLongBreak = true; } }); }); } @override void dispose() { _tabController.dispose(); controller.dispose(); super.dispose(); } void notify() { if (countText == '00:00:00') { _tabController.animateTo(1, duration: const Duration(milliseconds: 300)); } } // 切换开始/暂停状态 void toggleTimer() { if (controller.isAnimating) { controller.stop(); setState(() => isLongBreak = false); } else { controller.reverse( from: controller.value == 0 ? 1.0 : controller.value); setState(() => isLongBreak = false); } } // 打开时长选择弹窗 void showDurationPicker() { if (controller.isDismissed) { showModalBottomSheet( context: context, builder: (context) => Container( height: 300, child: CupertinoTimerPicker( initialTimerDuration: controller.duration!, onTimerDurationChanged: (time) { setState(() { controller.duration = time; }); }, ), ), ); } } @override Widget build(BuildContext context) { return Container( height: 600, width: double.infinity, child: DefaultTabController( length: 3, child: Scaffold( appBar: AppBar( elevation: 0, backgroundColor: Colors.transparent, bottom: PreferredSize( preferredSize: const Size.fromHeight(55), child: Container( color: Colors.transparent, child: SafeArea( child: Column( children: <Widget>[ TabBar( controller: _tabController, indicator: const UnderlineTabIndicator( borderSide: BorderSide( color: Color(0xff3B3B3B), width: 4.0), insets: EdgeInsets.fromLTRB( 12.0, 12.0, 12.0, 11.0)), indicatorWeight: 15, indicatorSize: TabBarIndicatorSize.label, labelColor: const Color(0xff3B3B3B), labelStyle: const TextStyle( fontSize: 12, letterSpacing: 1.3, fontWeight: FontWeight.w500), unselectedLabelColor: const Color(0xffD7D7D7), tabs: const [ Tab( text: "POMODORO", icon: Icon(Icons.work_history, size: 40), ), Tab( text: "SHORT BREAK", icon: Icon(Icons.ramen_dining, size: 40), ), Tab( text: "LONG BREAK", icon: Icon( Icons.battery_charging_full_rounded, size: 40), ), ]) ], ), ), ), ), ), body: TabBarView( controller: _tabController, children: <Widget>[ StartPomodoro( controller: controller, countText: countText, isLongBreak: isLongBreak, onTap: showDurationPicker, ), // Center(child: ShortBreak()), // Center(child: LongBreak()), ], ), bottomNavigationBar: Container( height: 110, color: const Color(0xffFAFAFA), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 15.0, vertical: 20), child: GNav( iconSize: 40, mainAxisAlignment: MainAxisAlignment.spaceEvenly, backgroundColor: const Color(0xffFAFAFA), color: const Color(0xffD7D7D7), activeColor: const Color(0xff3B3B3B), tabBackgroundColor: const Color(0xffF0F0F0), gap: 8, padding: const EdgeInsets.all(15), tabs: [ const GButton( icon: Icons.settings, text: 'Settings', ), GButton( icon: isTimerAnimating ? Icons.pause : Icons.play_arrow, text: isTimerAnimating ? "Pause" : "Start", onPressed: toggleTimer, iconColor: const Color(0xff3B3B3B), ), const GButton( icon: Icons.task, text: "Tasks", ), const GButton( icon: Icons.show_chart, text: "Performance", ), ], ), ), ), ))); } }
startpomodoro.dart
class StartPomodoro extends StatelessWidget { final AnimationController controller; final String countText; final bool isLongBreak; final VoidCallback onTap; const StartPomodoro({ Key? key, required this.controller, required this.countText, required this.isLongBreak, required this.onTap, }) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: isLongBreak ? const Color(0xffD94530) : const Color(0xff6351c5), body: GestureDetector( onTap: onTap, child: AnimatedBuilder( animation: controller, builder: (context, child) { return Stack( children: <Widget>[ Align( alignment: Alignment.bottomCenter, child: Container( color: const Color(0xffD94530), height: controller.value * MediaQuery.of(context).size.height * 0.640, ), ), Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Expanded( child: Align( alignment: Alignment.bottomCenter, child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Text( countText, style: const TextStyle( fontSize: 90.0, color: Color(0xffFAFAFA), ), ), ], ), ), ), ), const SizedBox(height: 100), ], ), ), ], ); }, ), ), ); } }
关键修改说明
- 状态上移:将
AnimationController、计时器状态(isTimerAnimating、isLongBreak)和控制方法(toggleTimer、showDurationPicker)迁移到HomePageTimerUI,让父组件持有核心逻辑,便于底部GNav调用。 - 组件解耦:StartPomodoro改为无状态组件,通过构造函数接收所需参数,仅负责UI渲染,不再处理业务逻辑。
- GNav集成:替换GNav的第二个tab为开始/暂停按钮,根据
isTimerAnimating动态切换图标和文字,点击时调用toggleTimer方法。 - 初始化优化:给
AnimationController设置默认时长(25分钟),避免初始值为0导致的异常。
内容的提问来源于stack exchange,提问作者pomoworko.com
相关产品推荐
相关产品推荐

