简介
仿照《Flutter 仿ios自定义一个DatePicker》实行的日期弹窗选择器(DatePicker)、时间弹窗选择器(TimePicker)
效果
范例
class _TestPageState extends State<TestPage> {
void initState() {
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Picker')),
body: SingleChildScrollView(
child: Column(
children: [
GestureDetector(
child: Container(
alignment: Alignment.center,
width: 160,
height: 60,
child: const Text('日期选择(年月日)'),
),
onTap: () {
DatePicker.show(
context,
startDate: DateTime(2022, 2, 2),
selectedDate: DateTime(2023, 3, 3),
endDate: DateTime(2025, 5, 5),
onSelected: (date) {
MsgUtil.toast(date.toString());
},
);
},
),
GestureDetector(
child: Container(
alignment: Alignment.center,
width: 160,
height: 60,
child: const Text('日期选择(年月)'),
),
onTap: () {
DatePicker.show(
context,
hideDay: true,
startDate: DateTime(2022, 2),
selectedDate: DateTime(2023, 3),
endDate: DateTime(2025, 5),
onSelected: (date) {
MsgUtil.toast(date.toString());
},
);
},
),
GestureDetector(
child: Container(
alignment: Alignment.center,
width: 160,
height: 60,
child: const Text('时间选择(时分秒)'),
),
onTap: () {
TimePicker.show(
context,
startTime: TimeData(11, 11, 11),
selectedTime: TimeData(15, 15, 15),
endTime: TimeData(22, 22, 22),
onSelected: (time) {
MsgUtil.toast(time.toString());
},
);
},
),
GestureDetector(
child: Container(
alignment: Alignment.center,
width: 160,
height: 60,
child: const Text('时间选择(时分)'),
),
onTap: () {
TimePicker.show(
context,
hideSecond: true,
startTime: TimeData(11, 11),
selectedTime: TimeData(15, 15),
endTime: TimeData(22, 22),
onSelected: (time) {
MsgUtil.toast(time.toString());
},
);
},
),
],
),
),
);
}
}
说明(DatePicker)
1、支持选中日期(selectedDate)、开始日期(startDate)、结束日期(endDate)的配置
2、支持“年月日”的选择,也支持“年月”的选择
代码(DatePicker)
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
typedef OnSelected = Function(DateTime date);
class DatePicker extends StatefulWidget {
static void show(
BuildContext context, {
DateTime? startDate,
DateTime? endDate,
DateTime? selectedDate,
bool hideDay = false,
Function()? onCancel,
required OnSelected onSelected,
}) async {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: DatePicker._(
selectedDate: selectedDate,
startDate: startDate,
endDate: endDate,
onSelected: onSelected,
hideDay: hideDay,
),
);
},
).then((value) => onCancel?.call());
}
const DatePicker._({
this.selectedDate,
this.startDate,
this.endDate,
this.hideDay = false,
required this.onSelected,
});
final DateTime? selectedDate;
final DateTime? startDate;
final DateTime? endDate;
final bool hideDay;
final OnSelected onSelected;
State createState() => _DatePickerState();
}
class _DatePickerState extends State<DatePicker> {
late FixedExtentScrollController yearScrollController;
late FixedExtentScrollController monthScrollController;
late FixedExtentScrollController dayScrollController;
List<String> yearList = []; // 年数组
List<String> monthList = []; // 月数组
List<String> dayList = []; // 天数组
int yearIndex = 0; // 年的索引
int monthIndex = 0; // 月的索引
int dayIndex = 0; //天的索引
late DateTime startDate;
late DateTime endDate;
late DateTime selectedDate;
final double itemExtent = 44;
/// 初始化数据
void initData() {
// 初始化年份数
for (int i = startDate.year; i <= endDate.year; i++) {
yearList.add(i.toString());
}
int selectYear = selectedDate.year;
int selectMonth = selectedDate.month;
// 初始化月份数
monthList = getMonthList(selectYear);
// 初始化天数
dayList = getDayList(selectYear, selectMonth);
// 初始化时间索引
final List uniqueYearList = Set.from(yearList).toList();
final List uniqueMonthList = Set.from(monthList).toList();
final List uniqueDayList = Set.from(dayList).toList();
// 获取索引
setState(() {
yearIndex = uniqueYearList.indexOf("${selectedDate.year}");
monthIndex = uniqueMonthList.indexOf("${selectedDate.month}");
dayIndex = uniqueDayList.indexOf("${selectedDate.day}");
});
// 设置Picker初始值
yearScrollController = FixedExtentScrollController(initialItem: yearIndex);
monthScrollController = FixedExtentScrollController(initialItem: monthIndex);
dayScrollController = FixedExtentScrollController(initialItem: dayIndex);
}
List<String> getMonthList(int selectYear) {
List<String> monthList = [];
if (selectYear == startDate.year) {
for (int i = startDate.month; i <= 12; i++) {
monthList.add(i.toString());
}
} else if (selectYear == endDate.year) {
for (int i = 1; i <= endDate.month; i++) {
monthList.add(i.toString());
}
} else {
for (int i = 1; i <= 12; i++) {
monthList.add(i.toString());
}
}
return monthList;
}
List<String> getDayList(int selectYear, int selectMonth) {
List<String> dayList = [];
int days = getDayCount(selectYear, selectMonth);
if (selectYear == startDate.year && selectMonth == startDate.month) {
for (int i = startDate.day; i <= days; i++) {
dayList.add(i.toString());
}
} else if (selectYear == endDate.year && selectMonth == endDate.month) {
for (int i = 1; i <= endDate.day; i++) {
dayList.add(i.toString());
}
} else {
for (int i = 1; i <= days; i++) {
dayList.add(i.toString());
}
}
return dayList;
}
int getDayCount(int year, int month) {
int dayCount = DateTime(year, month + 1, 0).day;
return dayCount;
}
/// 选中年月后更新天
void updateDayList() {
int year = int.parse(yearList[yearIndex]);
int month = int.parse(monthList[monthIndex]);
setState(() {
dayIndex = 0;
dayList = getDayList(year, month);
if (dayScrollController.positions.isNotEmpty) {
dayScrollController.jumpTo(0);
}
});
}
/// 选中年后更新月
void updateMonthList() {
int selectYear = int.parse(yearList[yearIndex]);
setState(() {
monthIndex = 0;
monthList = getMonthList(selectYear);
if (monthScrollController.positions.isNotEmpty) {
monthScrollController.jumpTo(0);
}
});
}
void initState() {
super.initState();
startDate = widget.startDate ?? DateTime(1970, 1, 1);
endDate = widget.endDate ?? DateTime(2099, 1, 1);
selectedDate = widget.selectedDate ?? DateTime.now();
if (endDate.difference(startDate).inSeconds < 0) {
endDate = startDate;
}
if (selectedDate.difference(startDate).inSeconds < 0) {
selectedDate = startDate;
}
if (selectedDate.difference(endDate).inSeconds > 0) {
selectedDate = endDate;
}
initData();
}
void dispose() {
yearScrollController.dispose();
monthScrollController.dispose();
dayScrollController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
color: Colors.white,
width: double.maxFinite,
height: 200,
child: Stack(
alignment: AlignmentDirectional.center,
children: [
Container(
width: MediaQuery.of(context).size.width - 32,
height: itemExtent - 8,
decoration: BoxDecoration(
color: const Color(0xFFF1F1F1),
borderRadius: BorderRadius.circular(12),
),
),
Positioned(
left: 20,
right: 20,
top: 0,
bottom: 0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(child: yearPickerView()),
Expanded(child: monthPickerView()),
widget.hideDay
? const SizedBox()
: Expanded(child: dayPickerView()),
],
),
),
],
),
),
Container(
color: Colors.white,
height: 68,
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: TextButton(
child: const Text('取 消'),
onPressed: () {
Navigator.pop(context, false);
},
),
),
const SizedBox(width: 12),
Expanded(
child: TextButton(
child: const Text('确 定'),
onPressed: () {
Navigator.pop(context, true);
widget.onSelected.call(DateTime(
int.parse(yearList[yearIndex]),
int.parse(monthList[monthIndex]),
int.parse(dayList[dayIndex]),
));
},
),
),
],
),
),
SizedBox(height: MediaQuery.of(context).padding.bottom),
],
),
);
}
/// 年Picker
Widget yearPickerView() {
return buildPickerBorder(
child: CupertinoPicker(
scrollController: yearScrollController,
looping: false,
selectionOverlay: const Center(),
onSelectedItemChanged: (index) {
setState(() {
yearIndex = index;
});
updateMonthList();
updateDayList();
},
itemExtent: itemExtent,
children: buildYearWidget(),
),
);
}
/// 月Picker
Widget monthPickerView() {
return buildPickerBorder(
child: CupertinoPicker(
scrollController: monthScrollController,
looping: false,
selectionOverlay: const Center(),
onSelectedItemChanged: (index) {
setState(() {
monthIndex = index;
});
updateDayList();
},
itemExtent: itemExtent,
children: buildMonthWidget(),
),
);
}
/// 日Picker
Widget dayPickerView() {
return buildPickerBorder(
child: CupertinoPicker(
scrollController: dayScrollController,
looping: false,
selectionOverlay: const Center(),
onSelectedItemChanged: (index) {
setState(() {
dayIndex = index;
});
},
itemExtent: itemExtent,
children: buildDayWidget(),
),
);
}
/// 年Widget
List<Widget> buildYearWidget() {
List<Widget> widgets = [];
for (var i = 0; i < yearList.length; i++) {
widgets.add(
Center(
child: Text(
'${yearList[i]}年',
style: getTextStyle(i == yearIndex),
maxLines: 1,
),
),
);
}
return widgets;
}
/// 月Widget
List<Widget> buildMonthWidget() {
List<Widget> widgets = [];
for (var i = 0; i < monthList.length; i++) {
widgets.add(
Center(
child: Text(
// monthList[i].padLeft(2, '0')
'${monthList[i]}月',
style: getTextStyle(i == monthIndex),
maxLines: 1,
),
),
);
}
return widgets;
}
/// 日Widget
List<Widget> buildDayWidget() {
List<Widget> widgets = [];
for (var i = 0; i < dayList.length; i++) {
widgets.add(
Center(
child: Text(
// dayList[i].padLeft(2, '0')
'${dayList[i]}日',
style: getTextStyle(i == dayIndex),
maxLines: 1,
),
),
);
}
return widgets;
}
TextStyle getTextStyle(bool bold) {
return TextStyle(
color: Colors.black,
fontSize: 20,
height: 1,
fontWeight: bold ? FontWeight.w600 : FontWeight.w400,
);
}
Widget buildPickerBorder({required Widget child}) {
return Stack(
alignment: AlignmentDirectional.center,
children: [
/*Container(
width: 90,
height: itemExtent - 8,
decoration: BoxDecoration(
color: const Color(0xffEFEFF0),
borderRadius: BorderRadius.circular(10),
),
),*/
child,
],
);
}
}
说明(TimePicker)
1、支持选中时间(selectedtTime)、开始时间(starttTime)、结束时间(endtTime)的配置
2、支持“时分秒”的选择,也支持“时分”的选择
3、自定义时间数据类(TimeData)
代码(TimePicker)
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
typedef OnSelected = Function(TimeData time);
typedef PickerBuilder = Widget Function(BuildContext context);
class TimeData {
final int hour;
final int minute;
final int second;
TimeData(this.hour, [this.minute = 0, this.second = 0])
: assert(hour >= 0 && hour <= 23),
assert(minute >= 0 && minute <= 59),
assert(second >= 0 && second <= 59);
factory TimeData.now() {
var now = DateTime.now();
return TimeData(now.hour, now.minute, now.second);
}
bool precede(TimeData other) {
return (hour < other.hour) ||
(hour == other.hour && minute < other.minute) ||
(hour == other.hour && minute == other.minute && second < other.second);
}
String toString() {
return '$hour:$minute:$second';
}
}
class TimePicker extends StatefulWidget {
static void show(
BuildContext context, {
TimeData? startTime,
TimeData? endTime,
TimeData? selectedTime,
bool hideSecond = false,
Function()? onCancel,
required OnSelected onSelected,
}) async {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: TimePicker._(
selectedTime: selectedTime,
startTime: startTime,
endTime: endTime,
hideSecond: hideSecond,
onSelected: onSelected,
),
);
},
).then((value) => onCancel?.call());
}
const TimePicker._({
this.selectedTime,
this.startTime,
this.endTime,
this.hideSecond = false,
required this.onSelected,
});
final TimeData? selectedTime;
final TimeData? startTime;
final TimeData? endTime;
final bool hideSecond;
final OnSelected onSelected;
State createState() => _TimePickerState();
}
class _TimePickerState extends State<TimePicker> {
late FixedExtentScrollController hourScrollController;
late FixedExtentScrollController minuteScrollController;
late FixedExtentScrollController secondScrollController;
List<String> hourList = [];
List<String> minuteList = [];
List<String> secondList = [];
int hourIndex = 0;
int minuteIndex = 0;
int secondIndex = 0;
late TimeData startTime;
late TimeData endTime;
late TimeData selectedTime;
final double itemExtent = 44;
/// 初始化数据
void initData() {
// 初始化时
for (int i = startTime.hour; i <= endTime.hour; i++) {
hourList.add(i.toString());
}
int selectHour = selectedTime.hour;
int selectMinute = selectedTime.minute;
// 初始化分
minuteList = getMinuteList(selectHour);
// 初始化秒
secondList = getSecondList(selectHour, selectMinute);
// 初始化时间索引
final List uniqueHourList = Set.from(hourList).toList();
final List uniqueMinuteList = Set.from(minuteList).toList();
final List uniqueSecondList = Set.from(secondList).toList();
// 获取索引
setState(() {
hourIndex = uniqueHourList.indexOf("${selectedTime.hour}");
minuteIndex = uniqueMinuteList.indexOf("${selectedTime.minute}");
secondIndex = uniqueSecondList.indexOf("${selectedTime.second}");
});
// 设置Picker初始值
hourScrollController = FixedExtentScrollController(initialItem: hourIndex);
minuteScrollController = FixedExtentScrollController(initialItem: minuteIndex);
secondScrollController = FixedExtentScrollController(initialItem: secondIndex);
}
List<String> getMinuteList(int selectHour) {
List<String> list = [];
if (selectHour == startTime.hour) {
for (int i = startTime.minute; i <= 59; i++) {
list.add(i.toString());
}
} else if (selectHour == endTime.hour) {
for (int i = 0; i <= endTime.minute; i++) {
list.add(i.toString());
}
} else {
for (int i = 0; i <= 59; i++) {
list.add(i.toString());
}
}
return list;
}
List<String> getSecondList(int selectHour, int selectMinute) {
List<String> list = [];
if (selectHour == startTime.hour && selectMinute == startTime.minute) {
for (int i = startTime.second; i <= 59; i++) {
list.add(i.toString());
}
} else if (selectHour == endTime.hour && selectMinute == endTime.minute) {
for (int i = 0; i <= endTime.second; i++) {
list.add(i.toString());
}
} else {
for (int i = 0; i <= 59; i++) {
list.add(i.toString());
}
}
return list;
}
/// 选中时分后更新秒
void updateSecondList() {
int hour = int.parse(hourList[hourIndex]);
int minute = int.parse(minuteList[minuteIndex]);
setState(() {
secondIndex = 0;
secondList = getSecondList(hour, minute);
if (secondScrollController.positions.isNotEmpty) {
secondScrollController.jumpTo(0);
}
});
}
/// 选中时后更新分
void updateMinuteList() {
int selectHour = int.parse(hourList[hourIndex]);
setState(() {
minuteIndex = 0;
minuteList = getMinuteList(selectHour);
if (minuteScrollController.positions.isNotEmpty) {
minuteScrollController.jumpTo(0);
}
});
}
void initState() {
super.initState();
DateTime now = DateTime.now();
startTime = widget.startTime ?? TimeData(0, 0, 0);
endTime = widget.endTime ?? TimeData(23, 59, 59);
selectedTime = widget.selectedTime ?? TimeData(now.hour, now.minute, now.second);
if (endTime.precede(startTime)) {
endTime = startTime;
}
if (selectedTime.precede(startTime)) {
selectedTime = startTime;
}
if (endTime.precede(selectedTime)) {
selectedTime = endTime;
}
initData();
}
void dispose() {
hourScrollController.dispose();
minuteScrollController.dispose();
secondScrollController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
color: Colors.white,
width: double.maxFinite,
height: 200,
child: Stack(
alignment: AlignmentDirectional.center,
children: [
Container(
width: MediaQuery.of(context).size.width - 32,
height: itemExtent - 8,
decoration: BoxDecoration(
color: const Color(0xFFF1F1F1),
borderRadius: BorderRadius.circular(12),
),
),
Positioned(
left: 20,
right: 20,
top: 0,
bottom: 0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(child: hourPickerView()),
Expanded(child: minutePickerView()),
widget.hideSecond
? const SizedBox()
: Expanded(child: secondPickerView()),
],
),
),
],
),
),
Container(
color: Colors.white,
height: 68,
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: TextButton(
child: const Text('取 消'),
onPressed: () {
Navigator.pop(context, false);
},
),
),
const SizedBox(width: 12),
Expanded(
child: TextButton(
child: const Text('确 定'),
onPressed: () {
Navigator.pop(context, true);
widget.onSelected.call(TimeData(
int.parse(hourList[hourIndex]),
int.parse(minuteList[minuteIndex]),
int.parse(secondList[secondIndex]),
));
},
),
),
],
),
),
SizedBox(height: MediaQuery.of(context).padding.bottom),
],
),
);
}
/// 时Picker
Widget hourPickerView() {
return buildPickerBorder(
child: CupertinoPicker(
scrollController: hourScrollController,
looping: false,
selectionOverlay: const Center(),
onSelectedItemChanged: (index) {
setState(() {
hourIndex = index;
});
updateMinuteList();
updateSecondList();
},
itemExtent: itemExtent,
children: buildHourWidget(),
),
);
}
/// 分Picker
Widget minutePickerView() {
return buildPickerBorder(
child: CupertinoPicker(
scrollController: minuteScrollController,
looping: false,
selectionOverlay: const Center(),
onSelectedItemChanged: (index) {
setState(() {
minuteIndex = index;
});
updateSecondList();
},
itemExtent: itemExtent,
children: buildMinuteWidget(),
),
);
}
/// 秒Picker
Widget secondPickerView() {
return buildPickerBorder(
child: CupertinoPicker(
scrollController: secondScrollController,
looping: false,
selectionOverlay: const Center(),
onSelectedItemChanged: (index) {
setState(() {
secondIndex = index;
});
},
itemExtent: itemExtent,
children: buildSecondWidget(),
),
);
}
/// 时Widget
List<Widget> buildHourWidget() {
List<Widget> widgets = [];
for (var i = 0; i < hourList.length; i++) {
widgets.add(
Center(
child: Text(
// hourList[i].padLeft(2, '0')
'${hourList[i]}时',
style: getTextStyle(i == hourIndex),
maxLines: 1,
),
),
);
}
return widgets;
}
/// 分Widget
List<Widget> buildMinuteWidget() {
List<Widget> widgets = [];
for (var i = 0; i < minuteList.length; i++) {
widgets.add(
Center(
child: Text(
// minuteList[i].padLeft(2, '0')
'${minuteList[i]}分',
style: getTextStyle(i == minuteIndex),
maxLines: 1,
),
),
);
}
return widgets;
}
/// 秒Widget
List<Widget> buildSecondWidget() {
List<Widget> widgets = [];
for (var i = 0; i < secondList.length; i++) {
widgets.add(
Center(
child: Text(
// secondList[i].padLeft(2, '0')
'${secondList[i]}秒',
style: getTextStyle(i == secondIndex),
maxLines: 1,
),
),
);
}
return widgets;
}
TextStyle getTextStyle(bool bold) {
return TextStyle(
color: Colors.black,
fontSize: 20,
height: 1,
fontWeight: bold ? FontWeight.w600 : FontWeight.w400,
);
}
Widget buildPickerBorder({required Widget child}) {
return Stack(
alignment: AlignmentDirectional.center,
children: [
/*Container(
width: 90,
height: itemExtent - 8,
decoration: BoxDecoration(
color: const Color(0xffEFEFF0),
borderRadius: BorderRadius.circular(10),
),
),*/
child,
],
);
}
}