flutter 效果实现 —— 日历选择器
效果:
代码:(已更正,周日是第一天,周六为最后一天)
class _HomePageState extends State<HomePage> {
DateTime startDate = DateTime.now();
DateTime endDate = DateTime.now().add(const Duration(days: 5));
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Calendar"),
),
body: CustomCalendarView(
minimumDate: DateTime.now(),
initialEndDate: endDate,
initialStartDate: startDate,
startEndDateChange: (DateTime startDateData, DateTime endDateData) {
setState(() {
startDate = startDateData;
endDate = endDateData;
});
},
),
);
}
}
class CustomCalendarView extends StatefulWidget {
const CustomCalendarView(
{Key? key,
this.initialStartDate,
this.initialEndDate,
this.startEndDateChange,
this.minimumDate,
this.maximumDate})
: super(key: key);
final DateTime? minimumDate;
final DateTime? maximumDate;
final DateTime? initialStartDate;
final DateTime? initialEndDate;
final Function(DateTime, DateTime)? startEndDateChange;
@override
State<CustomCalendarView> createState() => _CustomCalendarViewState();
}
class _CustomCalendarViewState extends State<CustomCalendarView> {
List<DateTime> dateList = <DateTime>[];
DateTime currentMonthDate = DateTime.now();
DateTime? startDate;
DateTime? endDate;
@override
void initState() {
setListOfDate(currentMonthDate);
if (widget.initialStartDate != null) {
startDate = widget.initialStartDate;
}
if (widget.initialEndDate != null) {
endDate = widget.initialEndDate;
}
super.initState();
}
@override
void dispose() {
super.dispose();
}
void setListOfDate(DateTime monthDate) {
dateList.clear();
//上个月的最后一天零点
final DateTime newDate = DateTime(monthDate.year, monthDate.month, 0);
int previousMothDay = 0;
//判断上个月末是否是星期六,不是的话当前月需要显示上个月的最后一周中的最后几天
if (newDate.weekday == 7) {
previousMothDay = 1;
dateList.add(newDate);
} else if (newDate.weekday < 6) {
previousMothDay = newDate.weekday + 1;
for (int i = 1; i <= previousMothDay; i++) {
dateList.add(newDate.subtract(Duration(days: previousMothDay - i)));
}
}
for (int i = 0; i < (42 - previousMothDay); i++) {
dateList.add(newDate.add(Duration(days: i + 1)));
}
// if (dateList[dateList.length - 7].month != monthDate.month) {
// dateList.removeRange(dateList.length - 7, dateList.length);
// }
}
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
//显示当前月,切换上、下个月
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 4, bottom: 4),
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 38,
width: 38,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(24.0)),
border: Border.all(
color: ThemeData.light().dividerColor,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(24.0)),
onTap: () {
setState(() {
currentMonthDate = DateTime(currentMonthDate.year, currentMonthDate.month, 0);
setListOfDate(currentMonthDate);
});
},
child: Icon(
Icons.keyboard_arrow_left,
color: Colors.grey,
),
),
),
),
),
Expanded(
child: Center(
child: Text(
DateFormat('MMMM, yyyy').format(currentMonthDate),
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20, color: Colors.black),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 38,
width: 38,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(24.0)),
border: Border.all(
color: ThemeData.light().dividerColor,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(24.0)),
onTap: () {
setState(() {
currentMonthDate = DateTime(currentMonthDate.year, currentMonthDate.month + 2, 0);
setListOfDate(currentMonthDate);
});
},
child: Icon(
Icons.keyboard_arrow_right,
color: Colors.grey,
),
),
),
),
),
],
),
),
//显示当前是星期几
Padding(
padding: const EdgeInsets.only(right: 8, left: 8, bottom: 8),
child: Row(
children: getDaysNameUI(),
),
),
Padding(
padding: const EdgeInsets.only(right: 8, left: 8),
child: Column(
children: getDaysNoUI(),
),
),
],
),
);
}
List<Widget> getDaysNameUI() {
final List<Widget> listUI = <Widget>[];
for (int i = 0; i < 7; i++) {
listUI.add(
Expanded(
child: Center(
child: Text(
DateFormat('EEE').format(dateList[i]),
style:
TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: ThemeData.light().primaryColor),
),
),
),
);
}
return listUI;
}
List<Widget> getDaysNoUI() {
final List<Widget> noList = <Widget>[];
int count = 0;
//dateList.length 为 42,一行是 7 列,总共是 6 行。
for (int i = 0; i < dateList.length / 7; i++) {
//当前行
final List<Widget> listUI = <Widget>[];
for (int i = 0; i < 7; i++) {
final DateTime date = dateList[count];
listUI.add(
Expanded(
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
child: Stack(
children: <Widget>[
//选择区间的背景图层
Padding(
padding: const EdgeInsets.only(top: 3, bottom: 3),
child: Material(
color: Colors.transparent,
child: Padding(
padding: EdgeInsets.only(
top: 2,
bottom: 2,
left: isStartDateRadius(date) ? 4 : 0,
right: isEndDateRadius(date) ? 4 : 0),
child: Container(
decoration: BoxDecoration(
color: startDate != null && endDate != null
? getIsItStartAndEndDate(date) || getIsInRange(date)
? ThemeData.light().primaryColor.withOpacity(0.4)
: Colors.transparent
: Colors.transparent,
borderRadius: BorderRadius.only(
bottomLeft: isStartDateRadius(date)
? const Radius.circular(24.0)
: const Radius.circular(0.0),
topLeft: isStartDateRadius(date)
? const Radius.circular(24.0)
: const Radius.circular(0.0),
topRight: isEndDateRadius(date)
? const Radius.circular(24.0)
: const Radius.circular(0.0),
bottomRight: isEndDateRadius(date)
? const Radius.circular(24.0)
: const Radius.circular(0.0),
),
),
),
),
),
),
// 当前行的所有日期
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(32.0)),
onTap: () {
// 选区
if (currentMonthDate.month == date.month) {
if (widget.minimumDate != null && widget.maximumDate != null) {
final DateTime newminimumDate = DateTime(widget.minimumDate!.year,
widget.minimumDate!.month, widget.minimumDate!.day - 1);
final DateTime newmaximumDate = DateTime(widget.maximumDate!.year,
widget.maximumDate!.month, widget.maximumDate!.day + 1);
if (date.isAfter(newminimumDate) && date.isBefore(newmaximumDate)) {
onDateClick(date);
}
} else if (widget.minimumDate != null) {
final DateTime newminimumDate = DateTime(widget.minimumDate!.year,
widget.minimumDate!.month, widget.minimumDate!.day - 1);
if (date.isAfter(newminimumDate)) {
onDateClick(date);
}
} else if (widget.maximumDate != null) {
final DateTime newmaximumDate = DateTime(widget.maximumDate!.year,
widget.maximumDate!.month, widget.maximumDate!.day + 1);
if (date.isBefore(newmaximumDate)) {
onDateClick(date);
}
} else {
onDateClick(date);
}
}
},
child: Padding(
padding: const EdgeInsets.all(2),
child: Container(
decoration: BoxDecoration(
color: getIsItStartAndEndDate(date)
? ThemeData.light().primaryColor
: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(32.0)),
border: Border.all(
color: getIsItStartAndEndDate(date) ? Colors.white : Colors.transparent,
width: 2,
),
boxShadow: getIsItStartAndEndDate(date)
? <BoxShadow>[
BoxShadow(
color: Colors.grey.withOpacity(0.6),
blurRadius: 4,
offset: const Offset(0, 0)),
]
: null,
),
child: Center(
child: Text(
'${date.day}',
style: TextStyle(
color: getIsItStartAndEndDate(date)
? Colors.white
: currentMonthDate.month == date.month
? Colors.black
//不是当前月的,灰度显示
: Colors.grey.withOpacity(0.6),
fontSize: MediaQuery.of(context).size.width > 360 ? 18 : 16,
fontWeight:
getIsItStartAndEndDate(date) ? FontWeight.bold : FontWeight.normal),
),
),
),
),
),
),
// 当前日期底部的高亮点
Positioned(
bottom: 9,
right: 0,
left: 0,
child: Container(
height: 6,
width: 6,
decoration: BoxDecoration(
color: DateTime.now().day == date.day &&
DateTime.now().month == date.month &&
DateTime.now().year == date.year
? getIsInRange(date)
? Colors.white
: ThemeData.light().primaryColor
: Colors.transparent,
shape: BoxShape.circle),
),
),
],
),
),
),
),
);
count += 1;
}
noList.add(Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: listUI,
));
}
return noList;
}
bool getIsInRange(DateTime date) {
if (startDate != null && endDate != null) {
if (date.isAfter(startDate!) && date.isBefore(endDate!)) {
return true;
} else {
return false;
}
} else {
return false;
}
}
bool getIsItStartAndEndDate(DateTime date) {
if (startDate != null &&
startDate!.day == date.day &&
startDate!.month == date.month &&
startDate!.year == date.year) {
return true;
} else if (endDate != null &&
endDate!.day == date.day &&
endDate!.month == date.month &&
endDate!.year == date.year) {
return true;
} else {
return false;
}
}
bool isStartDateRadius(DateTime date) {
if (startDate != null && startDate!.day == date.day && startDate!.month == date.month) {
return true;
} else if (date.weekday == 7) {
return true;
} else {
return false;
}
}
bool isEndDateRadius(DateTime date) {
if (endDate != null && endDate!.day == date.day && endDate!.month == date.month) {
return true;
} else if (date.weekday == 6) {
return true;
} else {
return false;
}
}
void onDateClick(DateTime date) {
if (startDate == null) {
startDate = date;
} else if (startDate != date && endDate == null) {
endDate = date;
} else if (startDate!.day == date.day && startDate!.month == date.month) {
startDate = null;
} else if (endDate!.day == date.day && endDate!.month == date.month) {
endDate = null;
}
if (startDate == null && endDate != null) {
startDate = endDate;
endDate = null;
}
if (startDate != null && endDate != null) {
if (!endDate!.isAfter(startDate!)) {
final DateTime d = startDate!;
startDate = endDate;
endDate = d;
}
if (date.isBefore(startDate!)) {
startDate = date;
}
if (date.isAfter(endDate!)) {
endDate = date;
}
}
setState(() {
try {
widget.startEndDateChange!(startDate!, endDate!);
} catch (_) {}
});
}
}