timer_page.dart 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_clock/model/timer_data.dart';
  4. import 'package:flutter_clock/model/timer_settings.dart';
  5. import 'package:flutter_clock/pages/timer/timer_settings_page.dart';
  6. import 'package:flutter_clock/utils/audio_manager.dart';
  7. import 'package:flutter_clock/utils/screen_manager.dart';
  8. import 'package:flutter_clock/utils/background_timer_service.dart';
  9. /// Description: 倒计时页面
  10. /// Time : 04/06/2025 Sunday
  11. /// Author : liuyuqi.gov@msn.cn
  12. class TimerPage extends StatefulWidget {
  13. @override
  14. _TimerPageState createState() => _TimerPageState();
  15. }
  16. enum TimerState { prepare, running, pause, finish, waiting }
  17. class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
  18. // Timer duration values
  19. int _hours = 0;
  20. int _minutes = 10;
  21. int _seconds = 0;
  22. // For timer controller
  23. TimerState timerState = TimerState.prepare;
  24. int _remainingSeconds = 0;
  25. // Waiting for alignment timer
  26. Timer? _alignmentTimer;
  27. DateTime? _alignmentTargetTime;
  28. DateTime? _alignmentStartTime; // 记录开始等待的时间点
  29. Timer? _alignmentUIUpdateTimer; // For refreshing UI during waiting
  30. // Total timer duration in seconds (for progress calculation)
  31. int _totalDurationSeconds = 0;
  32. // Background services
  33. final BackgroundTimerService _backgroundTimerService =
  34. BackgroundTimerService();
  35. // Settings
  36. late TimerSettings _settings;
  37. bool _settingsLoaded = false;
  38. final AudioManager _audioManager = AudioManager();
  39. // Wheel controllers
  40. final FixedExtentScrollController _hoursController =
  41. FixedExtentScrollController(initialItem: 0);
  42. final FixedExtentScrollController _minutesController =
  43. FixedExtentScrollController(initialItem: 10);
  44. final FixedExtentScrollController _secondsController =
  45. FixedExtentScrollController(initialItem: 0);
  46. @override
  47. void initState() {
  48. super.initState();
  49. WidgetsBinding.instance.addObserver(this);
  50. _initializeBackgroundTimer();
  51. _loadSettings();
  52. _restoreTimerState();
  53. }
  54. @override
  55. void dispose() {
  56. _audioManager.dispose();
  57. _backgroundTimerService.dispose();
  58. _alignmentTimer?.cancel();
  59. _alignmentUIUpdateTimer?.cancel(); // Cancel UI update timer
  60. if (ScreenManager.isWakeLockEnabled) {
  61. ScreenManager.disableWakeLock();
  62. }
  63. WidgetsBinding.instance.removeObserver(this);
  64. _hoursController.dispose();
  65. _minutesController.dispose();
  66. _secondsController.dispose();
  67. super.dispose();
  68. }
  69. @override
  70. void didChangeAppLifecycleState(AppLifecycleState state) {
  71. // No additional handling required as background service handles the state
  72. }
  73. /// Restore timer state from persistent storage
  74. Future<void> _restoreTimerState() async {
  75. final timerData = await TimerData.load();
  76. if (timerData.isRunning) {
  77. setState(() {
  78. timerState = TimerState.running;
  79. _remainingSeconds = timerData.calculateRemainingSeconds();
  80. _totalDurationSeconds = _backgroundTimerService.totalDurationSeconds;
  81. });
  82. } else if (timerData.isPaused && timerData.pausedRemaining != null) {
  83. setState(() {
  84. timerState = TimerState.pause;
  85. _remainingSeconds = timerData.pausedRemaining!;
  86. _totalDurationSeconds = _backgroundTimerService.totalDurationSeconds;
  87. });
  88. }
  89. }
  90. /// Initialize the background timer service
  91. Future<void> _initializeBackgroundTimer() async {
  92. await _backgroundTimerService.initialize(
  93. onTimerComplete: _timerCompleted,
  94. onTimerTick: (remainingSeconds) {
  95. setState(() {
  96. _remainingSeconds = remainingSeconds;
  97. if (remainingSeconds <= 0 && timerState != TimerState.finish) {
  98. timerState = TimerState.finish;
  99. }
  100. });
  101. },
  102. );
  103. }
  104. Future<void> _loadSettings() async {
  105. _settings = await TimerSettings.loadSettings();
  106. setState(() {
  107. _settingsLoaded = true;
  108. });
  109. }
  110. /// Calculate the next 10-minute alignment time
  111. DateTime _calculateNextAlignmentTime() {
  112. final now = DateTime.now();
  113. // 计算下一个整10分钟的时间点
  114. int nextMinutes = ((now.minute ~/ 10) + 1) * 10;
  115. int nextHour = now.hour;
  116. // 处理进位
  117. if (nextMinutes >= 60) {
  118. nextMinutes = 0;
  119. nextHour = (nextHour + 1) % 24;
  120. }
  121. return DateTime(
  122. now.year,
  123. now.month,
  124. now.day,
  125. nextHour,
  126. nextMinutes,
  127. 0,
  128. 0,
  129. 0
  130. );
  131. }
  132. // 计算等待进度
  133. double _calculateWaitingProgress() {
  134. if (_alignmentTargetTime == null) return 0.0;
  135. final now = DateTime.now();
  136. final targetTime = _alignmentTargetTime!;
  137. final startTime = _alignmentStartTime ?? now;
  138. // 如果还没开始等待或已经过了目标时间
  139. if (now.isAfter(targetTime)) return 1.0;
  140. if (now.isBefore(startTime)) return 0.0;
  141. // 计算总等待时间和已等待时间
  142. final totalWaitDuration = targetTime.difference(startTime).inMilliseconds;
  143. final elapsedWaitDuration = now.difference(startTime).inMilliseconds;
  144. // 计算进度比例
  145. if (totalWaitDuration <= 0) return 1.0;
  146. final progress = elapsedWaitDuration / totalWaitDuration;
  147. // 确保进度在0.0到1.0之间
  148. return progress.clamp(0.0, 1.0);
  149. }
  150. /// Wait until the next 10-minute mark and then start the timer
  151. void _startTimerWithAlignment(int totalSeconds) {
  152. // 取消任何现有的对齐定时器
  153. _alignmentTimer?.cancel();
  154. _alignmentUIUpdateTimer?.cancel();
  155. // 计算下一个整10分钟的时间点
  156. final alignmentTime = _calculateNextAlignmentTime();
  157. _alignmentTargetTime = alignmentTime;
  158. _alignmentStartTime = DateTime.now(); // 记录开始等待的时间
  159. // 设置状态为等待
  160. setState(() {
  161. timerState = TimerState.waiting;
  162. });
  163. // 计算等待时间(毫秒)
  164. final waitDuration = alignmentTime.difference(DateTime.now());
  165. // 创建定时器等待到指定时间
  166. _alignmentTimer = Timer(waitDuration, () {
  167. // 时间到了,启动实际的倒计时
  168. setState(() {
  169. timerState = TimerState.running;
  170. _remainingSeconds = totalSeconds;
  171. _totalDurationSeconds = totalSeconds;
  172. _alignmentTargetTime = null;
  173. _alignmentStartTime = null;
  174. });
  175. _backgroundTimerService.startTimer(totalSeconds);
  176. _alignmentUIUpdateTimer?.cancel(); // Cancel UI update timer when alignment is done
  177. });
  178. // 创建UI刷新定时器,每秒更新一次
  179. _alignmentUIUpdateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
  180. if (mounted) {
  181. setState(() {
  182. // 仅用于刷新UI
  183. });
  184. }
  185. });
  186. }
  187. /// Start a new timer or resume an existing paused timer
  188. void _startTimer(String status) {
  189. if (status == "resume") {
  190. setState(() {
  191. timerState = TimerState.running;
  192. });
  193. _backgroundTimerService.resumeTimer(
  194. _remainingSeconds, _totalDurationSeconds);
  195. } else if (status == "start") {
  196. // 获取界面输入的时间
  197. int _hourstmp = _hoursController.selectedItem;
  198. int _minutestmp = _minutesController.selectedItem;
  199. int _secondstmp = _secondsController.selectedItem;
  200. // 计算总秒数
  201. final totalSeconds = _hourstmp * 3600 + _minutestmp * 60 + _secondstmp;
  202. if (totalSeconds <= 0) return;
  203. // 保存选择的时间值
  204. _hours = _hourstmp;
  205. _minutes = _minutestmp;
  206. _seconds = _secondstmp;
  207. // 检查是否需要整点对齐
  208. if (_settings.alignToHour && _settings.loop && totalSeconds >= 600) { // 只有在循环模式、时间至少10分钟时才启用整点对齐
  209. _startTimerWithAlignment(totalSeconds);
  210. } else {
  211. setState(() {
  212. timerState = TimerState.running;
  213. _remainingSeconds = totalSeconds;
  214. _totalDurationSeconds = totalSeconds;
  215. });
  216. _backgroundTimerService.startTimer(totalSeconds);
  217. }
  218. } else if (status == "loop") {
  219. // 计算总秒数
  220. final totalSeconds = _hours * 3600 + _minutes * 60 + _seconds;
  221. if (totalSeconds <= 0) return;
  222. // 检查是否需要整点对齐
  223. if (_settings.alignToHour && _settings.loop && totalSeconds >= 600) { // 只有在循环模式、时间至少10分钟时才启用整点对齐
  224. _startTimerWithAlignment(totalSeconds);
  225. } else {
  226. setState(() {
  227. timerState = TimerState.running;
  228. _remainingSeconds = totalSeconds;
  229. _totalDurationSeconds = totalSeconds;
  230. });
  231. _backgroundTimerService.startTimer(totalSeconds);
  232. }
  233. }
  234. }
  235. /// Pause the running timer
  236. void _pauseTimer() {
  237. if (timerState == TimerState.waiting) {
  238. // 如果在等待对齐状态,取消等待定时器
  239. _alignmentTimer?.cancel();
  240. _alignmentUIUpdateTimer?.cancel(); // Cancel UI update timer
  241. _alignmentTargetTime = null;
  242. _alignmentStartTime = null;
  243. } else {
  244. _backgroundTimerService.pauseTimer();
  245. }
  246. setState(() {
  247. timerState = TimerState.pause;
  248. });
  249. }
  250. /// Reset the timer to initial state
  251. void _resetTimer() {
  252. _backgroundTimerService.cancelTimer();
  253. _audioManager.stopSound();
  254. _audioManager.stopVibration();
  255. _alignmentTimer?.cancel();
  256. _alignmentUIUpdateTimer?.cancel(); // Cancel UI update timer
  257. _alignmentTargetTime = null;
  258. _alignmentStartTime = null;
  259. setState(() {
  260. timerState = TimerState.prepare;
  261. });
  262. }
  263. /// Handle timer completion
  264. Future<void> _timerCompleted() async {
  265. setState(() {
  266. timerState = TimerState.finish;
  267. });
  268. // Play sound and vibrate
  269. if (_settings.vibrate) {
  270. _audioManager.triggerVibration();
  271. }
  272. _audioManager.playSound(_settings.sound, _settings.volume, _settings.loop);
  273. // If loop is enabled, restart the timer
  274. if (_settings.loop) {
  275. Future.delayed(Duration(seconds: 5), () {
  276. _audioManager.stopSound();
  277. _audioManager.stopVibration();
  278. });
  279. _startTimer("loop");
  280. }
  281. }
  282. String _formatTime(int seconds) {
  283. final hours = seconds ~/ 3600;
  284. final minutes = (seconds % 3600) ~/ 60;
  285. final secs = seconds % 60;
  286. return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
  287. }
  288. // 格式化剩余等待时间
  289. String _formatWaitingTime() {
  290. if (_alignmentTargetTime == null) return "00:00";
  291. final now = DateTime.now();
  292. final difference = _alignmentTargetTime!.difference(now);
  293. // 如果差异为负,说明已经过了目标时间
  294. if (difference.isNegative) return "00:00";
  295. // 计算分钟和秒数
  296. final minutes = difference.inMinutes;
  297. final seconds = difference.inSeconds % 60;
  298. return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  299. }
  300. @override
  301. Widget build(BuildContext context) {
  302. if (!_settingsLoaded) {
  303. return Center(child: CircularProgressIndicator());
  304. }
  305. if (timerState != TimerState.prepare) {
  306. return _buildCountdownView();
  307. } else {
  308. return _buildTimerSetupView();
  309. }
  310. }
  311. Widget _buildTimerSetupView() {
  312. return Scaffold(
  313. backgroundColor: Colors.white,
  314. body: Column(
  315. mainAxisAlignment: MainAxisAlignment.center,
  316. children: [
  317. Expanded(
  318. child: Row(
  319. children: [
  320. Expanded(
  321. child: _buildTimerWheel(
  322. _hoursController,
  323. List.generate(24, (index) => index),
  324. (value) {
  325. setState(() {
  326. _hours = value;
  327. });
  328. },
  329. 'H',
  330. ),
  331. ),
  332. Expanded(
  333. child: _buildTimerWheel(
  334. _minutesController,
  335. List.generate(60, (index) => index),
  336. (value) {
  337. setState(() {
  338. _minutes = value;
  339. });
  340. },
  341. 'M',
  342. ),
  343. ),
  344. Expanded(
  345. child: _buildTimerWheel(
  346. _secondsController,
  347. List.generate(60, (index) => index),
  348. (value) {
  349. setState(() {
  350. _seconds = value;
  351. });
  352. },
  353. 'S',
  354. ),
  355. ),
  356. ],
  357. ),
  358. ),
  359. SizedBox(height: 20),
  360. Padding(
  361. padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
  362. child: Row(
  363. mainAxisAlignment: MainAxisAlignment.spaceAround,
  364. children: [
  365. _buildCircleButton(
  366. Icons.sunny,
  367. Colors.grey[600]!,
  368. () {
  369. ScreenManager.toggleWakeLock();
  370. setState(() {});
  371. },
  372. isActive: ScreenManager.isWakeLockEnabled,
  373. ),
  374. _buildCircleButton(
  375. Icons.play_arrow,
  376. Colors.blue,
  377. () => _startTimer("start"),
  378. ),
  379. _buildCircleButton(
  380. Icons.settings,
  381. Colors.grey[600]!,
  382. () async {
  383. final result = await Navigator.push(
  384. context,
  385. MaterialPageRoute(
  386. builder: (context) =>
  387. TimerSettingsPage(settings: _settings)),
  388. );
  389. if (result != null) {
  390. setState(() {
  391. _settings = result;
  392. });
  393. }
  394. },
  395. ),
  396. ],
  397. ),
  398. ),
  399. ],
  400. ),
  401. );
  402. }
  403. Widget _buildCountdownView() {
  404. final totalMinutes = _remainingSeconds ~/ 60;
  405. // 如果处于等待整点对齐状态,显示不同的UI
  406. if (timerState == TimerState.waiting) {
  407. return _buildWaitingForAlignmentView();
  408. }
  409. return Scaffold(
  410. backgroundColor: Colors.white,
  411. body: Column(
  412. mainAxisAlignment: MainAxisAlignment.center,
  413. children: [
  414. Expanded(
  415. child: Center(
  416. child: Column(
  417. mainAxisAlignment: MainAxisAlignment.center,
  418. children: [
  419. Container(
  420. width: 300,
  421. height: 300,
  422. decoration: BoxDecoration(
  423. shape: BoxShape.circle,
  424. border: Border.all(
  425. color: Colors.blue.withOpacity(0.3),
  426. width: 3,
  427. ),
  428. ),
  429. child: Stack(
  430. alignment: Alignment.center,
  431. children: [
  432. // Timer progress
  433. SizedBox(
  434. width: 300,
  435. height: 300,
  436. child: CircularProgressIndicator(
  437. value: timerState == TimerState.finish
  438. ? 1
  439. : _totalDurationSeconds > 0
  440. ? _remainingSeconds / _totalDurationSeconds
  441. : 0,
  442. strokeWidth: 5,
  443. backgroundColor: Colors.grey.withOpacity(0.1),
  444. color: Colors.blue,
  445. ),
  446. ),
  447. // Time display
  448. Column(
  449. mainAxisAlignment: MainAxisAlignment.center,
  450. children: [
  451. Text(
  452. _formatTime(_remainingSeconds),
  453. style: TextStyle(
  454. fontSize: 40, fontWeight: FontWeight.bold),
  455. ),
  456. Text(
  457. 'Total ${totalMinutes} minutes',
  458. style:
  459. TextStyle(fontSize: 16, color: Colors.grey),
  460. ),
  461. ],
  462. ),
  463. // Indicator dot
  464. Positioned(
  465. bottom: 0,
  466. child: Container(
  467. width: 20,
  468. height: 20,
  469. decoration: BoxDecoration(
  470. color: Colors.blue,
  471. shape: BoxShape.circle,
  472. ),
  473. ),
  474. ),
  475. ],
  476. ),
  477. ),
  478. ],
  479. ),
  480. ),
  481. ),
  482. Padding(
  483. padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
  484. child: Row(
  485. mainAxisAlignment: MainAxisAlignment.spaceAround,
  486. children: [
  487. // 唤醒屏幕
  488. _buildCircleButton(
  489. Icons.sunny,
  490. Colors.grey[600]!,
  491. () {
  492. ScreenManager.toggleWakeLock();
  493. setState(() {});
  494. },
  495. isActive: ScreenManager.isWakeLockEnabled,
  496. ),
  497. // 暂停/开始
  498. timerState == TimerState.running
  499. ? _buildCircleButton(
  500. Icons.pause,
  501. Colors.blue,
  502. () => _pauseTimer(),
  503. )
  504. : _buildCircleButton(
  505. Icons.play_arrow,
  506. Colors.blue,
  507. () => _startTimer("resume"),
  508. ),
  509. // 重置
  510. _buildCircleButton(
  511. Icons.stop,
  512. Colors.red,
  513. () => _resetTimer(),
  514. ),
  515. ],
  516. ),
  517. ),
  518. ],
  519. ),
  520. );
  521. }
  522. // 构建等待整点对齐的视图
  523. Widget _buildWaitingForAlignmentView() {
  524. final nextAlignmentTime = _alignmentTargetTime;
  525. final waitingProgress = _calculateWaitingProgress();
  526. return Scaffold(
  527. backgroundColor: Colors.white,
  528. body: Column(
  529. mainAxisAlignment: MainAxisAlignment.center,
  530. children: [
  531. Expanded(
  532. child: Center(
  533. child: Column(
  534. mainAxisAlignment: MainAxisAlignment.center,
  535. children: [
  536. Container(
  537. width: 300,
  538. height: 300,
  539. decoration: BoxDecoration(
  540. shape: BoxShape.circle,
  541. border: Border.all(
  542. color: Colors.orange.withOpacity(0.3),
  543. width: 3,
  544. ),
  545. ),
  546. child: Stack(
  547. alignment: Alignment.center,
  548. children: [
  549. // 等待进度指示器
  550. SizedBox(
  551. width: 300,
  552. height: 300,
  553. child: CircularProgressIndicator(
  554. value: waitingProgress, // 显示确定的等待进度
  555. strokeWidth: 5,
  556. backgroundColor: Colors.grey.withOpacity(0.1),
  557. color: Colors.orange,
  558. ),
  559. ),
  560. // 等待时间显示
  561. Column(
  562. mainAxisAlignment: MainAxisAlignment.center,
  563. children: [
  564. Text(
  565. _formatWaitingTime(),
  566. style: TextStyle(
  567. fontSize: 40, fontWeight: FontWeight.bold),
  568. ),
  569. Text(
  570. '等待对齐整10分钟',
  571. style: TextStyle(fontSize: 16, color: Colors.grey),
  572. ),
  573. SizedBox(height: 10),
  574. if (nextAlignmentTime != null)
  575. Text(
  576. '将在 ${nextAlignmentTime.hour.toString().padLeft(2, '0')}:${nextAlignmentTime.minute.toString().padLeft(2, '0')} 开始',
  577. style: TextStyle(fontSize: 14, color: Colors.orange),
  578. ),
  579. ],
  580. ),
  581. ],
  582. ),
  583. ),
  584. ],
  585. ),
  586. ),
  587. ),
  588. Padding(
  589. padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
  590. child: Row(
  591. mainAxisAlignment: MainAxisAlignment.spaceAround,
  592. children: [
  593. // 唤醒屏幕
  594. _buildCircleButton(
  595. Icons.sunny,
  596. Colors.grey[600]!,
  597. () {
  598. ScreenManager.toggleWakeLock();
  599. setState(() {});
  600. },
  601. isActive: ScreenManager.isWakeLockEnabled,
  602. ),
  603. // 在等待状态只显示取消按钮
  604. _buildCircleButton(
  605. Icons.stop,
  606. Colors.red,
  607. () => _resetTimer(),
  608. ),
  609. ],
  610. ),
  611. ),
  612. ],
  613. ),
  614. );
  615. }
  616. Widget _buildCircleButton(IconData icon, Color color, VoidCallback onPressed,
  617. {bool isActive = false}) {
  618. return Container(
  619. width: 70,
  620. height: 70,
  621. decoration: BoxDecoration(
  622. shape: BoxShape.circle,
  623. color: isActive ? color : Colors.white,
  624. boxShadow: [
  625. BoxShadow(
  626. color: Colors.black.withOpacity(0.1),
  627. blurRadius: 8,
  628. offset: Offset(0, 2),
  629. ),
  630. ],
  631. ),
  632. child: IconButton(
  633. icon: Icon(icon, size: 30),
  634. color: isActive ? Colors.white : color,
  635. onPressed: onPressed,
  636. ),
  637. );
  638. }
  639. Widget _buildTimerWheel(
  640. FixedExtentScrollController controller,
  641. List<int> items,
  642. ValueChanged<int> onChanged,
  643. String unit,
  644. ) {
  645. return Container(
  646. height: 400,
  647. decoration: BoxDecoration(
  648. border: Border(
  649. top: BorderSide(color: Colors.grey.withOpacity(0.3), width: 1),
  650. bottom: BorderSide(color: Colors.grey.withOpacity(0.3), width: 1),
  651. ),
  652. ),
  653. child: Stack(
  654. children: [
  655. // Center highlight
  656. Positioned.fill(
  657. child: Center(
  658. child: Container(
  659. height: 50,
  660. decoration: BoxDecoration(
  661. color: Colors.blue.withOpacity(0.1),
  662. borderRadius: BorderRadius.circular(8),
  663. ),
  664. ),
  665. ),
  666. ),
  667. Container(
  668. alignment: Alignment.centerRight,
  669. padding: EdgeInsets.only(right: 15),
  670. child: Text(
  671. unit,
  672. style: TextStyle(
  673. fontSize: 18,
  674. fontWeight: FontWeight.bold,
  675. ),
  676. ),
  677. ),
  678. ListWheelScrollView(
  679. controller: controller,
  680. physics: FixedExtentScrollPhysics(),
  681. diameterRatio: 1.5,
  682. itemExtent: 50,
  683. children: items.map((value) {
  684. return Center(
  685. child: Text(
  686. value.toString().padLeft(2, '0'),
  687. style: TextStyle(
  688. fontSize: 30,
  689. color: Colors.black,
  690. fontWeight: FontWeight.w500,
  691. ),
  692. ),
  693. );
  694. }).toList(),
  695. onSelectedItemChanged: onChanged,
  696. ),
  697. ],
  698. ),
  699. );
  700. }
  701. }