| 
									
										
										
										
											2025-07-28 14:22:07 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | import 'package:flutter/cupertino.dart'; | 
					
						
							|  |  |  | import 'package:flutter/material.dart'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /// 调用示例:
 | 
					
						
							|  |  |  | /// DateTime? picked = await BottomDateTimePicker.show(context);
 | 
					
						
							|  |  |  | /// if (picked != null) {
 | 
					
						
							|  |  |  | ///   print('用户选择的时间:$picked');
 | 
					
						
							|  |  |  | /// }
 | 
					
						
							|  |  |  | class BottomDateTimePicker { | 
					
						
							| 
									
										
										
										
											2025-07-28 16:50:40 +08:00
										 |  |  |   static Future<DateTime?> showDate(BuildContext context) { | 
					
						
							| 
									
										
										
										
											2025-07-28 14:22:07 +08:00
										 |  |  |     return showModalBottomSheet<DateTime>( | 
					
						
							|  |  |  |       context: context, | 
					
						
							|  |  |  |       backgroundColor: Colors.white, | 
					
						
							|  |  |  |       isScrollControlled: true, | 
					
						
							|  |  |  |       shape: const RoundedRectangleBorder( | 
					
						
							|  |  |  |         borderRadius: BorderRadius.vertical(top: Radius.circular(12)), | 
					
						
							|  |  |  |       ), | 
					
						
							|  |  |  |       builder: (_) => _InlineDateTimePickerContent(), | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class _InlineDateTimePickerContent extends StatefulWidget { | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   State<_InlineDateTimePickerContent> createState() => _InlineDateTimePickerContentState(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class _InlineDateTimePickerContentState extends State<_InlineDateTimePickerContent> { | 
					
						
							|  |  |  |   // 数据源
 | 
					
						
							|  |  |  |   final List<int> years = List.generate(101, (i) => 1970 + i); | 
					
						
							|  |  |  |   final List<int> months = List.generate(12, (i) => i + 1); | 
					
						
							|  |  |  |   final List<int> days = List.generate(31, (i) => i + 1); | 
					
						
							|  |  |  |   final List<int> hours = List.generate(24, (i) => i); | 
					
						
							|  |  |  |   final List<int> minutes = List.generate(60, (i) => i); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Controllers
 | 
					
						
							|  |  |  |   late FixedExtentScrollController yearCtrl; | 
					
						
							|  |  |  |   late FixedExtentScrollController monthCtrl; | 
					
						
							|  |  |  |   late FixedExtentScrollController dayCtrl; | 
					
						
							|  |  |  |   late FixedExtentScrollController hourCtrl; | 
					
						
							|  |  |  |   late FixedExtentScrollController minuteCtrl; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // 当前选中值
 | 
					
						
							|  |  |  |   late int selectedYear; | 
					
						
							|  |  |  |   late int selectedMonth; | 
					
						
							|  |  |  |   late int selectedDay; | 
					
						
							|  |  |  |   late int selectedHour; | 
					
						
							|  |  |  |   late int selectedMinute; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   void initState() { | 
					
						
							|  |  |  |     super.initState(); | 
					
						
							|  |  |  |     final now = DateTime.now(); | 
					
						
							|  |  |  |     selectedYear = now.year; | 
					
						
							|  |  |  |     selectedMonth = now.month; | 
					
						
							|  |  |  |     selectedDay = now.day; | 
					
						
							|  |  |  |     selectedHour = now.hour; | 
					
						
							|  |  |  |     selectedMinute = now.minute; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     yearCtrl = FixedExtentScrollController(initialItem: years.indexOf(selectedYear)); | 
					
						
							|  |  |  |     monthCtrl = FixedExtentScrollController(initialItem: selectedMonth - 1); | 
					
						
							|  |  |  |     dayCtrl = FixedExtentScrollController(initialItem: selectedDay - 1); | 
					
						
							|  |  |  |     hourCtrl = FixedExtentScrollController(initialItem: selectedHour); | 
					
						
							|  |  |  |     minuteCtrl = FixedExtentScrollController(initialItem: selectedMinute); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   void dispose() { | 
					
						
							|  |  |  |     yearCtrl.dispose(); | 
					
						
							|  |  |  |     monthCtrl.dispose(); | 
					
						
							|  |  |  |     dayCtrl.dispose(); | 
					
						
							|  |  |  |     hourCtrl.dispose(); | 
					
						
							|  |  |  |     minuteCtrl.dispose(); | 
					
						
							|  |  |  |     super.dispose(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   void _checkAndClampToNow() { | 
					
						
							|  |  |  |     final picked = DateTime(selectedYear, selectedMonth, selectedDay, selectedHour, selectedMinute); | 
					
						
							|  |  |  |     final now = DateTime.now(); | 
					
						
							|  |  |  |     if (picked.isAfter(now)) { | 
					
						
							|  |  |  |       // 回滚到当前时间
 | 
					
						
							|  |  |  |       selectedYear = now.year; | 
					
						
							|  |  |  |       selectedMonth = now.month; | 
					
						
							|  |  |  |       selectedDay = now.day; | 
					
						
							|  |  |  |       selectedHour = now.hour; | 
					
						
							|  |  |  |       selectedMinute = now.minute; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // 更新各滚轮位置
 | 
					
						
							|  |  |  |       yearCtrl.jumpToItem(years.indexOf(selectedYear)); | 
					
						
							|  |  |  |       monthCtrl.jumpToItem(selectedMonth - 1); | 
					
						
							|  |  |  |       dayCtrl.jumpToItem(selectedDay - 1); | 
					
						
							|  |  |  |       hourCtrl.jumpToItem(selectedHour); | 
					
						
							|  |  |  |       minuteCtrl.jumpToItem(selectedMinute); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   Widget build(BuildContext context) { | 
					
						
							|  |  |  |     return SizedBox( | 
					
						
							|  |  |  |       height: 300, | 
					
						
							|  |  |  |       child: Column( | 
					
						
							|  |  |  |         children: [ | 
					
						
							|  |  |  |           // 顶部按钮
 | 
					
						
							|  |  |  |           Padding( | 
					
						
							|  |  |  |             padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), | 
					
						
							|  |  |  |             child: Row( | 
					
						
							|  |  |  |               mainAxisAlignment: MainAxisAlignment.spaceBetween, | 
					
						
							|  |  |  |               children: [ | 
					
						
							|  |  |  |                 TextButton( | 
					
						
							|  |  |  |                   onPressed: () => Navigator.of(context).pop(), | 
					
						
							|  |  |  |                   child: const Text("取消", style: TextStyle(color: Colors.grey)), | 
					
						
							|  |  |  |                 ), | 
					
						
							|  |  |  |                 TextButton( | 
					
						
							|  |  |  |                   onPressed: () { | 
					
						
							|  |  |  |                     final result = DateTime( | 
					
						
							|  |  |  |                       selectedYear, | 
					
						
							|  |  |  |                       selectedMonth, | 
					
						
							|  |  |  |                       selectedDay, | 
					
						
							|  |  |  |                       selectedHour, | 
					
						
							|  |  |  |                       selectedMinute, | 
					
						
							|  |  |  |                     ); | 
					
						
							|  |  |  |                     Navigator.of(context).pop(result); | 
					
						
							|  |  |  |                   }, | 
					
						
							|  |  |  |                   child: const Text("确定", style: TextStyle(color: Colors.blue)), | 
					
						
							|  |  |  |                 ), | 
					
						
							|  |  |  |               ], | 
					
						
							|  |  |  |             ), | 
					
						
							|  |  |  |           ), | 
					
						
							|  |  |  |           const Divider(height: 1), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // 五列数字滚轮
 | 
					
						
							|  |  |  |           Expanded( | 
					
						
							|  |  |  |             child: Row( | 
					
						
							|  |  |  |               children: [ | 
					
						
							|  |  |  |                 // 年
 | 
					
						
							|  |  |  |                 _buildPicker( | 
					
						
							|  |  |  |                   controller: yearCtrl, | 
					
						
							|  |  |  |                   items: years.map((e) => e.toString()).toList(), | 
					
						
							|  |  |  |                   onSelected: (idx) { | 
					
						
							|  |  |  |                     setState(() { | 
					
						
							|  |  |  |                       selectedYear = years[idx]; | 
					
						
							|  |  |  |                       _checkAndClampToNow(); | 
					
						
							|  |  |  |                     }); | 
					
						
							|  |  |  |                   }, | 
					
						
							|  |  |  |                 ), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // 月
 | 
					
						
							|  |  |  |                 _buildPicker( | 
					
						
							|  |  |  |                   controller: monthCtrl, | 
					
						
							|  |  |  |                   items: months.map((e) => e.toString().padLeft(2, '0')).toList(), | 
					
						
							|  |  |  |                   onSelected: (idx) { | 
					
						
							|  |  |  |                     setState(() { | 
					
						
							|  |  |  |                       selectedMonth = months[idx]; | 
					
						
							|  |  |  |                       _checkAndClampToNow(); | 
					
						
							|  |  |  |                     }); | 
					
						
							|  |  |  |                   }, | 
					
						
							|  |  |  |                 ), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // 日
 | 
					
						
							|  |  |  |                 _buildPicker( | 
					
						
							|  |  |  |                   controller: dayCtrl, | 
					
						
							|  |  |  |                   items: days.map((e) => e.toString().padLeft(2, '0')).toList(), | 
					
						
							|  |  |  |                   onSelected: (idx) { | 
					
						
							|  |  |  |                     setState(() { | 
					
						
							|  |  |  |                       selectedDay = days[idx]; | 
					
						
							|  |  |  |                       _checkAndClampToNow(); | 
					
						
							|  |  |  |                     }); | 
					
						
							|  |  |  |                   }, | 
					
						
							|  |  |  |                 ), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // 时
 | 
					
						
							|  |  |  |                 _buildPicker( | 
					
						
							|  |  |  |                   controller: hourCtrl, | 
					
						
							|  |  |  |                   items: hours.map((e) => e.toString().padLeft(2, '0')).toList(), | 
					
						
							|  |  |  |                   onSelected: (idx) { | 
					
						
							|  |  |  |                     setState(() { | 
					
						
							|  |  |  |                       selectedHour = hours[idx]; | 
					
						
							|  |  |  |                       _checkAndClampToNow(); | 
					
						
							|  |  |  |                     }); | 
					
						
							|  |  |  |                   }, | 
					
						
							|  |  |  |                 ), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // 分
 | 
					
						
							|  |  |  |                 _buildPicker( | 
					
						
							|  |  |  |                   controller: minuteCtrl, | 
					
						
							|  |  |  |                   items: minutes.map((e) => e.toString().padLeft(2, '0')).toList(), | 
					
						
							|  |  |  |                   onSelected: (idx) { | 
					
						
							|  |  |  |                     setState(() { | 
					
						
							|  |  |  |                       selectedMinute = minutes[idx]; | 
					
						
							|  |  |  |                       _checkAndClampToNow(); | 
					
						
							|  |  |  |                     }); | 
					
						
							|  |  |  |                   }, | 
					
						
							|  |  |  |                 ), | 
					
						
							|  |  |  |               ], | 
					
						
							|  |  |  |             ), | 
					
						
							|  |  |  |           ), | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |       ), | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   Widget _buildPicker({ | 
					
						
							|  |  |  |     required FixedExtentScrollController controller, | 
					
						
							|  |  |  |     required List<String> items, | 
					
						
							|  |  |  |     required ValueChanged<int> onSelected, | 
					
						
							|  |  |  |   }) { | 
					
						
							|  |  |  |     return Expanded( | 
					
						
							|  |  |  |       child: CupertinoPicker.builder( | 
					
						
							|  |  |  |         scrollController: controller, | 
					
						
							|  |  |  |         itemExtent: 32, | 
					
						
							|  |  |  |         childCount: items.length, | 
					
						
							|  |  |  |         onSelectedItemChanged: onSelected, | 
					
						
							|  |  |  |         itemBuilder: (context, index) { | 
					
						
							|  |  |  |           return Center(child: Text(items[index])); | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       ), | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |