221 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			221 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Dart
		
	
	
| 
 | |
| import 'package:flutter/cupertino.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| 
 | |
| /// 调用示例:
 | |
| /// DateTime? picked = await BottomDateTimePicker.show(context);
 | |
| /// if (picked != null) {
 | |
| ///   print('用户选择的时间:$picked');
 | |
| /// }
 | |
| class BottomDateTimePicker {
 | |
|   static Future<DateTime?> showDate(BuildContext context) {
 | |
|     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]));
 | |
|         },
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |