先看效果
直接上代码
calendar_popup_view.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'custom_calendar.dart';
import 'hotel_app_theme.dart';
class CalendarPopupView extends StatefulWidget {
const CalendarPopupView({
required this.initialStartDate,
required this.initialEndDate,
required this.onApplyClick,
required this.onCancelClick,
required this.state,
this.minimumDate,
this.maximumDate,
this.barrierDismissible = true,
super.key,
});
final DateTime? minimumDate;
final DateTime? maximumDate;
final bool barrierDismissible;
final DateTime initialStartDate;
final DateTime initialEndDate;
final Function(DateTime, DateTime) onApplyClick;
final Function onCancelClick;
final Function state;
@override
State<CalendarPopupView> createState() => _CalendarPopupViewState();
}
class _CalendarPopupViewState extends State<CalendarPopupView>
with TickerProviderStateMixin {
late DateTime startDate;
late DateTime endDate;
late final AnimationController animationController;
late DateTime curSelectStartData;
late DateTime curSelectEndData;
@override
void initState() {
super.initState();
animationController = AnimationController(
duration: const Duration(milliseconds: 400), vsync: this);
startDate = widget.initialStartDate;
endDate = widget.initialEndDate;
curSelectStartData = startDate;
curSelectEndData = endDate;
animationController.forward();
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
builder: (BuildContext context, _) {
return AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: animationController.value,
child: InkWell(
splashColor: Colors.transparent,
focusColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
onTap: () {
if (widget.barrierDismissible) {
Navigator.pop(context);
}
},
child: Container(
decoration: BoxDecoration(
color: HotelAppTheme.buildLightTheme().colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(24.0)),
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.grey.withOpacity(0.2),
offset: const Offset(4, 4),
blurRadius: 8.0),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'From',
textAlign: TextAlign.left,
style: grayTitle(),
),
const SizedBox(
height: 4,
),
Text(
DateFormat('EEE, dd MMM')
.format(curSelectStartData),
style:
curSelectStartData == widget.initialStartDate
? grayTime()
: primaryTime(),
),
],
),
),
Container(
height: 74,
width: 1,
color: HotelAppTheme.buildLightTheme().dividerColor,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'To',
style: grayTitle(),
),
const SizedBox(height: 4),
Text(
DateFormat('EEE, dd MMM')
.format(curSelectEndData),
style: curSelectEndData == widget.initialEndDate
? grayTime()
: primaryTime(),
),
],
),
)
],
),
const Divider(height: 1),
CustomCalendarView(
minimumDate: widget.minimumDate,
maximumDate: widget.maximumDate,
initialEndDate: widget.initialEndDate,
initialStartDate: widget.initialStartDate,
startEndDateChange:
(DateTime startDateData, DateTime endDateData) {
if (mounted) {
setState(() {
startDate = startDateData;
endDate = endDateData;
curSelectStartData = startDateData;
curSelectEndData = endDateData;
});
toUpdateState();
}
},
endDateChange: (DateTime endData) {
print("endDateChange");
setState(() {
endDate = endData;
curSelectEndData = endData;
});
toUpdateState();
},
startDateChange: (DateTime startData) {
print("startDateChange");
setState(() {
startDate = startData;
curSelectStartData = startData;
});
toUpdateState();
}),
Padding(
padding: const EdgeInsets.only(
left: 16, right: 16, bottom: 16, top: 8),
child: Container(
height: 48,
decoration: BoxDecoration(
color: HotelAppTheme.buildLightTheme().primaryColor,
borderRadius:
const BorderRadius.all(Radius.circular(24.0)),
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.grey.withOpacity(0.6),
blurRadius: 8,
offset: const Offset(4, 4),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(24.0)),
highlightColor: Colors.transparent,
onTap: () {
try {
// animationController?.reverse().then((f) {
// });
widget.onApplyClick(startDate, endDate);
Navigator.pop(context);
} catch (_) {}
},
child: const Center(
child: Text(
'Apply',
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 18,
color: Colors.white),
),
),
),
),
),
)
],
),
),
),
);
},
);
}
toUpdateState() {
widget.state(() {});
}
TextStyle grayTitle() {
return const TextStyle(
fontWeight: FontWeight.w100,
fontSize: 16,
color: Color(0xff676970),
);
}
TextStyle grayTime() {
return const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Color(0xff45474D),
);
}
TextStyle primaryTime() {
return TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: HotelAppTheme.buildLightTheme().primaryColorDark,
);
}
}
custom_calendar.dart
import 'package:app/common/util/k_date_util.dart';
import 'package:app/common/util/k_log_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import 'hotel_app_theme.dart';
class CustomCalendarView extends StatefulWidget {
const CustomCalendarView({
required this.initialStartDate,
required this.initialEndDate,
required this.startEndDateChange,
required this.startDateChange,
required this.endDateChange,
this.minimumDate,
this.maximumDate,
super.key,
});
final DateTime? minimumDate;
final DateTime? maximumDate;
final DateTime initialStartDate;
final DateTime initialEndDate;
final Function(DateTime, DateTime) startEndDateChange;
final Function(DateTime) startDateChange;
final Function(DateTime) endDateChange;
@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() {
super.initState();
setListOfDate(currentMonthDate);
startDate = widget.initialStartDate;
endDate = widget.initialEndDate;
}
@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 = newDate.weekday;
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: HotelAppTheme.buildLightTheme().dividerColor,
),
),
child: InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(24.0)),
onTap: () {
if (mounted) {
setState(() {
if (getCurMonthIsInMinMaxRange(DateTime(
currentMonthDate.year,
currentMonthDate.month,
0))) {
currentMonthDate = DateTime(currentMonthDate.year,
currentMonthDate.month, 0);
setListOfDate(currentMonthDate);
}
});
}
},
child: const Icon(
Icons.keyboard_arrow_left,
color: Colors.grey,
),
),
),
),
Expanded(
child: Center(
child: Text(
DateFormat('MMMM, yyyy').format(currentMonthDate),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 15.sp,
color: Colors.white,
),
),
),
),
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: HotelAppTheme.buildLightTheme().dividerColor,
),
),
child: InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(24.0)),
onTap: () {
if (mounted) {
setState(() {
if (getCurMonthIsInMinMaxRange(DateTime(
currentMonthDate.year,
currentMonthDate.month + 2,
0))) {
currentMonthDate = DateTime(currentMonthDate.year,
currentMonthDate.month + 2, 0);
setListOfDate(currentMonthDate);
}
});
}
},
child: const 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>[];
final weekendList = [5, 6];
for (int i = 0; i < 7; i++) {
listUI.add(
Expanded(
child: Center(
child: Text(
DateFormat('EEE').format(dateList[i]),
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: weekendList.contains(i)
? HotelAppTheme.buildLightTheme().primaryColorDark
: Colors.white,
),
),
),
),
);
}
return listUI;
}
List<Widget> getDaysNoUI() {
final List<Widget> noList = <Widget>[];
int count = 0;
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: Stack(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 3, bottom: 3),
child: Padding(
padding: EdgeInsets.only(
top: 0,
bottom: 0,
left: isStartDateRadius(date) ? 4 : 0,
right: isEndDateRadius(date) ? 4 : 0),
child: Container(
decoration: BoxDecoration(
color: startDate != null && endDate != null
? getIsItStartAndEndDate(date) ||
getIsInRange(date)
? HotelAppTheme.buildLightTheme()
.primaryColorDark
.withOpacity(0.1)
: Colors.transparent
: Colors.transparent,
borderRadius: BorderRadius.only(
bottomLeft: isStartDateRadius(date)
? Radius.circular(15.r)
: const Radius.circular(0.0),
topLeft: isStartDateRadius(date)
? Radius.circular(15.r)
: const Radius.circular(0.0),
topRight: isEndDateRadius(date)
? Radius.circular(15.r)
: const Radius.circular(0.0),
bottomRight: isEndDateRadius(date)
? Radius.circular(15.r)
: const Radius.circular(0.0),
),
),
),
),
),
InkWell(
borderRadius: const BorderRadius.all(Radius.circular(32.0)),
onTap: () {
if (currentMonthDate.month == date.month) {
final DateTime? minimumDate = widget.minimumDate;
final DateTime? maximumDate = widget.maximumDate;
if (minimumDate != null && maximumDate != null) {
final DateTime newminimumDate = DateTime(
minimumDate.year,
minimumDate.month,
minimumDate.day - 1);
final DateTime newmaximumDate = DateTime(
maximumDate.year,
maximumDate.month,
maximumDate.day + 1);
if (date.isAfter(newminimumDate) &&
date.isBefore(newmaximumDate)) {
onDateClick(date);
}
} else if (minimumDate != null) {
final DateTime newminimumDate = DateTime(
minimumDate.year,
minimumDate.month,
minimumDate.day - 1);
if (date.isAfter(newminimumDate)) {
onDateClick(date);
}
} else if (maximumDate != null) {
final DateTime newmaximumDate = DateTime(
maximumDate.year,
maximumDate.month,
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)
? HotelAppTheme.buildLightTheme().primaryColorDark
: Colors.transparent,
borderRadius: getStartOrEndPoint(date),
// 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),
// ]
// : null,
),
child: Center(
child: Text(
'${date.day}',
style: ceilStyle(date),
),
),
),
),
),
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
: HotelAppTheme.buildLightTheme()
.primaryColorDark
: Colors.transparent,
shape: BoxShape.circle),
),
),
],
),
),
),
);
count += 1;
}
noList.add(Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: listUI,
));
}
return noList;
}
BorderRadius getStartOrEndPoint(DateTime date) {
KLogUtil.log([startDate, endDate, KDateUtils.isSome(startDate, endDate)]);
if (startDate.toString() == endDate.toString()) {
return BorderRadius.all(
Radius.circular(15.r),
);
} else if (getIsItStart(date)) {
return BorderRadius.only(
topLeft: Radius.circular(15.r),
bottomLeft: Radius.circular(15.r),
);
} else {
return BorderRadius.only(
topRight: Radius.circular(15.r),
bottomRight: Radius.circular(15.r),
);
}
}
// 日期每一格的样式
TextStyle ceilStyle(DateTime date) {
// 不能选择日期的样式
if (!getIsInMinMaxRange(date)) {
return TextStyle(
color: Colors.grey.withOpacity(0.6),
fontSize: MediaQuery.of(context).size.width > 360 ? 14.sp : 16.sp,
fontWeight:
getIsItStartAndEndDate(date) ? FontWeight.bold : FontWeight.normal,
);
}
return TextStyle(
color: getIsItStartAndEndDate(date) || getIsInRange(date)
? Colors.white
: currentMonthDate.month == date.month
? Colors.white
: Colors.grey.withOpacity(0.6),
fontSize: MediaQuery.of(context).size.width > 360 ? 14.sp : 16.sp,
fontWeight:
getIsItStartAndEndDate(date) ? FontWeight.bold : FontWeight.normal);
}
bool getIsInMinMaxRange(DateTime date) {
if (widget.minimumDate != null && widget.maximumDate != null) {
if (date.isAfter(widget.minimumDate!) &&
date.isBefore(widget.maximumDate!)) {
return true;
}
}
return false;
}
// 当前月份是否在
bool getCurMonthIsInMinMaxRange(DateTime tgtMonth) {
if (widget.minimumDate != null && widget.maximumDate != null) {
if (tgtMonth.isAfter(DateTime(
widget.minimumDate!.year, widget.minimumDate!.month, 0)) &&
tgtMonth.isBefore(DateTime(
widget.maximumDate!.year, widget.maximumDate!.month + 2, 0))) {
return true;
}
}
return false;
}
bool getIsInRange(DateTime date) {
if (startDate != null && endDate != null) {
if (date.isAfter(startDate!) && date.isBefore(endDate!)) {
return true;
}
}
return false;
}
bool getIsItStartAndEndDate(DateTime date) {
if ((startDate != null &&
startDate!.day == date.day &&
startDate!.month == date.month &&
startDate!.year == date.year) ||
(endDate != null &&
endDate!.day == date.day &&
endDate!.month == date.month &&
endDate!.year == date.year)) return true;
return false;
}
bool getIsItStart(DateTime date) {
if (startDate != null &&
startDate!.day == date.day &&
startDate!.month == date.month &&
startDate!.year == date.year) return true;
return false;
}
bool isStartDateRadius(DateTime date) {
if (startDate != null &&
startDate!.day == date.day &&
startDate!.month == date.month) {
return true;
} else if (date.weekday == 1) {
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 == 7) {
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;
endDate = startDate;
} else if (endDate != null &&
endDate!.day == date.day &&
endDate!.month == date.month) {
if (endDate != null) {
startDate = endDate;
} else {
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;
} else if (date.isAfter(endDate!)) {
endDate = date;
} else {
final int daysToStartDate = startDate!.difference(date).inDays.abs();
final int daysToEndDate = endDate!.difference(date).inDays.abs();
daysToStartDate > daysToEndDate ? endDate = date : startDate = date;
}
}
if (mounted) {
setState(
() {
if (startDate != null && endDate != null) {
try {
widget.startEndDateChange(startDate!, endDate!);
} catch (_) {}
}
if (startDate != null) {
try {
widget.startDateChange(startDate!);
} catch (_) {}
}
if (endDate != null) {
try {
widget.endDateChange(endDate!);
} catch (_) {}
}
},
);
}
}
}
hotel_app_theme.dart
import 'package:flutter/material.dart';
class HotelAppTheme {
static TextTheme _buildTextTheme(TextTheme base) {
const String fontName = 'WorkSans';
return base.copyWith(
displayLarge: base.displayLarge?.copyWith(fontFamily: fontName),
displayMedium: base.displayMedium?.copyWith(fontFamily: fontName),
displaySmall: base.displaySmall?.copyWith(fontFamily: fontName),
headlineMedium: base.headlineMedium?.copyWith(fontFamily: fontName),
headlineSmall: base.headlineSmall?.copyWith(fontFamily: fontName),
titleLarge: base.titleLarge?.copyWith(fontFamily: fontName),
labelLarge: base.labelLarge?.copyWith(fontFamily: fontName),
bodySmall: base.bodySmall?.copyWith(fontFamily: fontName),
bodyLarge: base.bodyLarge?.copyWith(fontFamily: fontName),
bodyMedium: base.bodyMedium?.copyWith(fontFamily: fontName),
titleMedium: base.titleMedium?.copyWith(fontFamily: fontName),
titleSmall: base.titleSmall?.copyWith(fontFamily: fontName),
labelSmall: base.labelSmall?.copyWith(fontFamily: fontName),
);
}
static ThemeData buildLightTheme() {
// #54D3C2
// #54D3C2
// #4677FF
const Color primaryColor = Color(0xff1C1D1F);
const Color secondaryColor = Color(0xff1C1D1F);
const Color primaryColorDark = Color(0xff4677FF);
final ColorScheme colorScheme = const ColorScheme.light().copyWith(
primary: primaryColor,
secondary: secondaryColor,
);
final ThemeData base = ThemeData.light();
return base.copyWith(
primaryColor: primaryColor,
primaryColorDark: primaryColorDark,
indicatorColor: Colors.white,
splashColor: Colors.white24,
splashFactory: InkRipple.splashFactory,
canvasColor: Colors.white,
// #F6F6F6
scaffoldBackgroundColor: const Color(0xFFF6F6F6),
buttonTheme: ButtonThemeData(
colorScheme: colorScheme,
textTheme: ButtonTextTheme.primary,
),
textTheme: _buildTextTheme(base.textTheme),
primaryTextTheme: _buildTextTheme(base.primaryTextTheme),
platform: TargetPlatform.iOS,
colorScheme: colorScheme
.copyWith(background: const Color(0xff1C1D1F))
// #B00020
.copyWith(error: const Color(0xFFB00020)),
);
}
}
RangePicker.dart 这个文件是使用的地方 这里还使用到了 getx 的组件 从底部弹出
// ignore_for_file: file_names
import 'package:app/gen/assets.gen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'calendar_popup_view.dart';
class RangePicker extends StatefulWidget {
RangePicker({super.key, required this.apply});
final Function(DateTime, DateTime) apply;
@override
State<RangePicker> createState() => _RangePickerState();
}
class _RangePickerState extends State<RangePicker> {
DateTime startDate = DateTime.now().subtract(const Duration(days: 90));
DateTime endDate = DateTime.now();
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
openCalendarPopupView(context);
},
child: Row(
children: [
Text(
"${DateFormat('MM/dd').format(startDate)} - ${DateFormat('MM/dd').format(endDate)}",
style: TextStyle(
color: const Color(0xff676970),
fontWeight: FontWeight.w500,
fontSize: 12.sp,
),
),
SizedBox(
width: 4.w,
),
Assets.icon.botomArrowhead.image(width: 17.w),
],
),
);
}
void openCalendarPopupView(BuildContext context) {
Get.bottomSheet(
isScrollControlled: true,
StatefulBuilder(builder: (context, state) {
return CalendarPopupView(
state: state,
minimumDate: DateTime.now().subtract(const Duration(days: 365)),
maximumDate: DateTime.now(),
initialEndDate: endDate,
initialStartDate: startDate,
onApplyClick: (DateTime startData, DateTime endData) {
if (mounted) {
setState(() {
startDate = startData;
endDate = endData;
});
state(() {
startDate = startData;
endDate = endData;
});
widget.apply(startDate, endDate);
}
},
onCancelClick: () {},
);
}),
);
}
}