flutter_integrated_whb/lib/customWidget/picker/CupertinoDatePicker.dart

341 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
/// 调用示例:
/// DateTime? picked = await BottomDateTimePicker.showDate(
/// context,
/// allowFuture: true, // 添加此参数允许选择未来时间
/// minTimeStr: '2025-08-20 08:30', // 可选:不允许选择早于此时间
/// );
/// if (picked != null) {
/// print('用户选择的时间:$picked');
/// }
class BottomDateTimePicker {
static Future<DateTime?> showDate(
BuildContext context, {
bool allowFuture = false,
String? minTimeStr, // 新增:可选起始时间格式 'yyyy-MM-dd HH:mm'
}) {
return showModalBottomSheet<DateTime>(
context: context,
backgroundColor: Colors.white,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (_) => _InlineDateTimePickerContent(
allowFuture: allowFuture,
minTimeStr: minTimeStr,
),
);
}
}
class _InlineDateTimePickerContent extends StatefulWidget {
final bool allowFuture; // 允许未来
final String? minTimeStr; // 新增:最小允许时间字符串 'yyyy-MM-dd HH:mm'
const _InlineDateTimePickerContent({
Key? key,
this.allowFuture = false,
this.minTimeStr,
}) : super(key: key);
@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> hours = List.generate(24, (i) => i);
final List<int> minutes = List.generate(60, (i) => i);
// 动态天数列表(根据年月变化)
late List<int> days;
// 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;
DateTime? _minTime; // 解析后的最小允许时间(如果有)
@override
void initState() {
super.initState();
// 解析 minTimeStr若提供
_minTime = _parseMinTime(widget.minTimeStr);
// 选择初始时间:取 DateTime.now() 与 _minTime 的较大者(确保初始选中合法)
final now = DateTime.now();
DateTime initial = now;
if (_minTime != null && _minTime!.isAfter(initial)) {
initial = _minTime!;
}
selectedYear = initial.year;
selectedMonth = initial.month;
selectedDay = initial.day;
selectedHour = initial.hour;
selectedMinute = initial.minute;
// 初始化天数列表
days = _getDaysInMonth(selectedYear, selectedMonth);
// controllers 初始项索引需在范围内
yearCtrl = FixedExtentScrollController(
initialItem: years.indexOf(selectedYear).clamp(0, years.length - 1));
monthCtrl = FixedExtentScrollController(initialItem: (selectedMonth - 1).clamp(0, months.length - 1));
dayCtrl = FixedExtentScrollController(initialItem: (selectedDay - 1).clamp(0, days.length - 1));
hourCtrl = FixedExtentScrollController(initialItem: selectedHour.clamp(0, hours.length - 1));
minuteCtrl = FixedExtentScrollController(initialItem: selectedMinute.clamp(0, minutes.length - 1));
// 如果初始时间小于 minTime理论上不会因为我们取了较大者再修正一次
WidgetsBinding.instance.addPostFrameCallback((_) {
_enforceConstraintsAndUpdateControllers();
});
}
// 根据年月获取当月天数
List<int> _getDaysInMonth(int year, int month) {
final lastDay = DateUtils.getDaysInMonth(year, month);
return List.generate(lastDay, (i) => i + 1);
}
// 解析 'yyyy-MM-dd HH:mm' 返回 DateTime 或 null
DateTime? _parseMinTime(String? s) {
if (s == null || s.trim().isEmpty) return null;
try {
final trimmed = s.trim();
final parts = trimmed.split(' ');
final dateParts = parts[0].split('-').map((e) => int.parse(e)).toList();
final timeParts = (parts.length > 1)
? parts[1].split(':').map((e) => int.parse(e)).toList()
: [0, 0];
final year = dateParts[0];
final month = dateParts[1];
final day = dateParts[2];
final hour = (timeParts.isNotEmpty) ? timeParts[0] : 0;
final minute = (timeParts.length > 1) ? timeParts[1] : 0;
return DateTime(year, month, day, hour, minute);
} catch (e) {
// 解析失败则忽略
debugPrint('parseMinTime failed for "$s": $e');
return null;
}
}
// 更新天数列表并调整选中日期
void _updateDays({bool jumpDay = true}) {
final newDays = _getDaysInMonth(selectedYear, selectedMonth);
final isDayValid = selectedDay <= newDays.length;
setState(() {
days = newDays;
if (!isDayValid) {
selectedDay = newDays.last;
if (jumpDay) dayCtrl.jumpToItem(selectedDay - 1);
}
});
}
// 检查并限制时间:
// 1) 优先检查 _minTime如果存在不允许早于 _minTime
// 2) 如果没有 _minTime则根据 allowFuture 决定是否限制到 now
// 注意_minTime 优先级高于 allowFuture如果 _minTime 在未来,会选择 _minTime
void _enforceConstraintsAndUpdateControllers() {
final picked = DateTime(selectedYear, selectedMonth, selectedDay, selectedHour, selectedMinute);
// 1) 最小时间约束(优先)
if (_minTime != null && picked.isBefore(_minTime!)) {
final m = _minTime!;
selectedYear = m.year;
selectedMonth = m.month;
selectedDay = m.day;
selectedHour = m.hour;
selectedMinute = m.minute;
// 更新天数列表与控制器索引
_updateDays(jumpDay: false);
yearCtrl.jumpToItem(years.indexOf(selectedYear));
monthCtrl.jumpToItem(selectedMonth - 1);
dayCtrl.jumpToItem(selectedDay - 1);
hourCtrl.jumpToItem(selectedHour);
minuteCtrl.jumpToItem(selectedMinute);
return;
}
// 2) 禁止选择未来(当 allowFuture == false 且没有 minTime 或 minTime <= now
if (!widget.allowFuture) {
final now = DateTime.now();
// 如果 minTime 存在并大于 now我们已在上面处理minTime 优先),所以这里处理的是普通情况
if (picked.isAfter(now)) {
selectedYear = now.year;
selectedMonth = now.month;
selectedDay = now.day;
selectedHour = now.hour;
selectedMinute = now.minute;
_updateDays(jumpDay: false);
yearCtrl.jumpToItem(years.indexOf(selectedYear));
monthCtrl.jumpToItem(selectedMonth - 1);
dayCtrl.jumpToItem(selectedDay - 1);
hourCtrl.jumpToItem(selectedHour);
minuteCtrl.jumpToItem(selectedMinute);
return;
}
}
}
@override
void dispose() {
yearCtrl.dispose();
monthCtrl.dispose();
dayCtrl.dispose();
hourCtrl.dispose();
minuteCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 330,
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];
_updateDays();
_enforceConstraintsAndUpdateControllers();
});
},
),
// 月
_buildPicker(
controller: monthCtrl,
items: months.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
selectedMonth = months[idx];
_updateDays();
_enforceConstraintsAndUpdateControllers();
});
},
),
// 日
_buildPicker(
controller: dayCtrl,
items: days.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
// 防护idx 可能超出当前 days 长度(极小概率)
final safeIdx = idx.clamp(0, days.length - 1);
selectedDay = days[safeIdx];
_enforceConstraintsAndUpdateControllers();
});
},
),
// 时
_buildPicker(
controller: hourCtrl,
items: hours.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
selectedHour = hours[idx];
_enforceConstraintsAndUpdateControllers();
});
},
),
// 分
_buildPicker(
controller: minuteCtrl,
items: minutes.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
selectedMinute = minutes[idx];
_enforceConstraintsAndUpdateControllers();
});
},
),
],
),
),
],
),
);
}
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]));
},
),
);
}
}