full_calendar.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import 'package:cs_resources/generated/assets.dart';
  2. import 'package:cs_resources/theme/app_colors_theme.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:intl/intl.dart';
  5. import 'package:shared/utils/log_utils.dart';
  6. import 'package:widgets/ext/ex_widget.dart';
  7. import 'package:widgets/my_load_image.dart';
  8. import '../../utils/dark_theme_util.dart';
  9. //日历的具体展示
  10. class FullCalendar extends StatefulWidget {
  11. final DateTime startDate;
  12. final DateTime? endDate;
  13. final DateTime? selectedDate;
  14. final Color? dateColor;
  15. final Color? dateSelectedColor;
  16. final Color? dateSelectedBg;
  17. final double? padding;
  18. final String? locale;
  19. final Widget? calendarBackground;
  20. final List<String>? events;
  21. final Function onDateChange;
  22. const FullCalendar({
  23. Key? key,
  24. this.endDate,
  25. required this.startDate,
  26. required this.padding,
  27. required this.onDateChange,
  28. this.calendarBackground,
  29. this.events,
  30. this.dateColor,
  31. this.dateSelectedColor,
  32. this.dateSelectedBg,
  33. this.locale,
  34. this.selectedDate,
  35. }) : super(key: key);
  36. @override
  37. State<FullCalendar> createState() => _FullCalendarState();
  38. }
  39. class _FullCalendarState extends State<FullCalendar> {
  40. late DateTime endDate;
  41. late DateTime startDate;
  42. late int _initialPage;
  43. List<String>? _events = [];
  44. late PageController _horizontalScroll;
  45. @override
  46. void initState() {
  47. setState(() {
  48. startDate = DateTime.parse("${widget.startDate.toString().split(" ").first} 00:00:00.000");
  49. endDate = DateTime.parse("${widget.endDate.toString().split(" ").first} 23:00:00.000");
  50. _events = widget.events;
  51. });
  52. super.initState();
  53. }
  54. @override
  55. Widget build(BuildContext context) {
  56. List<String> partsStart = startDate.toString().split(" ").first.split("-");
  57. DateTime firstDate = DateTime.parse("${partsStart.first}-${partsStart[1].padLeft(2, '0')}-01 00:00:00.000");
  58. List<String> partsEnd = endDate.toString().split(" ").first.split("-");
  59. DateTime lastDate =
  60. DateTime.parse("${partsEnd.first}-${(int.parse(partsEnd[1]) + 1).toString().padLeft(2, '0')}-01 23:00:00.000").subtract(const Duration(days: 1));
  61. double width = MediaQuery.of(context).size.width - (2 * widget.padding!);
  62. List<DateTime?> dates = [];
  63. DateTime referenceDate = firstDate;
  64. while (referenceDate.isBefore(lastDate)) {
  65. List<String> referenceParts = referenceDate.toString().split(" ");
  66. DateTime newDate = DateTime.parse("${referenceParts.first} 12:00:00.000");
  67. dates.add(newDate);
  68. referenceDate = newDate.add(const Duration(days: 1));
  69. }
  70. if (firstDate.year == lastDate.year && firstDate.month == lastDate.month) {
  71. return Padding(
  72. padding: EdgeInsets.fromLTRB(widget.padding!, 40.0, widget.padding!, 0.0),
  73. child: month(dates, width, widget.locale),
  74. );
  75. } else {
  76. List<DateTime?> months = [];
  77. for (int i = 0; i < dates.length; i++) {
  78. if (i == 0 || (dates[i]!.month != dates[i - 1]!.month)) {
  79. months.add(dates[i]);
  80. }
  81. }
  82. months.sort((b, a) => a!.compareTo(b!));
  83. final index = months.indexWhere((element) => element!.month == widget.selectedDate!.month && element.year == widget.selectedDate!.year);
  84. _initialPage = index;
  85. _horizontalScroll = PageController(initialPage: _initialPage);
  86. double monthHeight = 6 * (width / 7) + 16 + 10 + 10 + 80; // 固定高度,6行的高度加上80额外空间
  87. return Container(
  88. height: monthHeight, // 使用固定的高度
  89. padding: const EdgeInsets.fromLTRB(25, 10.0, 25, 20.0),
  90. //只是PageView
  91. child: Stack(
  92. children: [
  93. //主题
  94. PageView.builder(
  95. physics: const BouncingScrollPhysics(),
  96. controller: _horizontalScroll,
  97. reverse: true,
  98. scrollDirection: Axis.horizontal,
  99. itemCount: months.length,
  100. itemBuilder: (context, index) {
  101. DateTime? date = months[index];
  102. List<DateTime?> daysOfMonth = [];
  103. for (var item in dates) {
  104. if (date!.month == item!.month && date.year == item.year) {
  105. daysOfMonth.add(item);
  106. }
  107. }
  108. bool isLast = index == 0;
  109. return Container(
  110. padding: EdgeInsets.only(bottom: isLast ? 0.0 : 10.0),
  111. child: month(daysOfMonth, width, widget.locale),
  112. );
  113. },
  114. ),
  115. //返回按钮
  116. Positioned(
  117. top: -11,
  118. width: MediaQuery.of(context).size.width * 0.88,
  119. child: Row(
  120. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  121. children: [
  122. MyAssetImage(
  123. Assets.baseLibCalendarLeftIcon,
  124. width: 44,
  125. height: 44,
  126. color: DarkThemeUtil.multiColors(context, AppColorsTheme.colorPrimary, darkColor: Colors.white),
  127. ).onTap(() {
  128. _horizontalScroll.nextPage(
  129. duration: const Duration(milliseconds: 300),
  130. curve: Curves.ease,
  131. );
  132. }),
  133. MyAssetImage(
  134. Assets.baseLibCalendarRightIcon,
  135. width: 44,
  136. height: 44,
  137. color: DarkThemeUtil.multiColors(context, AppColorsTheme.colorPrimary, darkColor: Colors.white),
  138. ).onTap(() {
  139. _horizontalScroll.previousPage(
  140. duration: const Duration(milliseconds: 300),
  141. curve: Curves.ease,
  142. );
  143. }),
  144. ],
  145. ),
  146. )
  147. ],
  148. ),
  149. );
  150. }
  151. }
  152. //顶部星期的文本数据
  153. Widget daysOfWeek(double width, String? locale) {
  154. List daysNames = [];
  155. for (var day = 12; day <= 18; day++) {
  156. daysNames.add(DateFormat.E(locale.toString()).format(DateTime.parse('1970-01-$day')));
  157. }
  158. return Row(
  159. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  160. children: [
  161. dayName(width / 7, daysNames[0]),
  162. dayName(width / 7, daysNames[1]),
  163. dayName(width / 7, daysNames[2]),
  164. dayName(width / 7, daysNames[3]),
  165. dayName(width / 7, daysNames[4]),
  166. dayName(width / 7, daysNames[5]),
  167. dayName(width / 7, daysNames[6]),
  168. ],
  169. );
  170. }
  171. //顶部星期的文本控件展示
  172. Widget dayName(double width, String text) {
  173. return Container(
  174. width: width,
  175. alignment: Alignment.center,
  176. child: Text(
  177. text,
  178. style: const TextStyle(
  179. fontSize: 13.0,
  180. fontWeight: FontWeight.w500,
  181. ),
  182. overflow: TextOverflow.ellipsis,
  183. ),
  184. );
  185. }
  186. //当前月份,每一天的布局
  187. Widget dateInCalendar(DateTime date, bool outOfRange, double width, bool event) {
  188. bool isSelectedDate = date.toString().split(" ").first == widget.selectedDate.toString().split(" ").first;
  189. return GestureDetector(
  190. onTap: () => outOfRange ? null : widget.onDateChange(date),
  191. child: Container(
  192. width: width / 7,
  193. height: width / 7,
  194. decoration: BoxDecoration(
  195. shape: BoxShape.circle,
  196. color: isSelectedDate ? widget.dateSelectedBg : Colors.transparent,
  197. ),
  198. alignment: Alignment.center,
  199. child: Column(
  200. mainAxisAlignment: MainAxisAlignment.center,
  201. children: [
  202. const SizedBox(
  203. height: 5.0,
  204. ),
  205. Padding(
  206. padding: const EdgeInsets.symmetric(vertical: 4.0),
  207. child: Text(
  208. DateFormat("dd").format(date),
  209. style: TextStyle(
  210. color: outOfRange
  211. ? isSelectedDate
  212. ? widget.dateSelectedColor!.withOpacity(0.9)
  213. : widget.dateColor!.withOpacity(0.4)
  214. : isSelectedDate
  215. ? widget.dateSelectedColor
  216. : widget.dateColor,
  217. fontWeight: FontWeight.w500,
  218. fontSize: 13),
  219. ),
  220. ),
  221. event
  222. ? Icon(
  223. Icons.bookmark,
  224. size: 8,
  225. color: isSelectedDate ? widget.dateSelectedColor : widget.dateSelectedBg,
  226. )
  227. : const SizedBox(height: 5.0),
  228. ],
  229. ),
  230. ),
  231. );
  232. }
  233. //单独一个月的Page布局
  234. Widget month(List dates, double width, String? locale) {
  235. DateTime first = dates.first;
  236. // 获取这个月的第一天
  237. DateTime firstDayOfMonth = DateTime(first.year, first.month, 1);
  238. // 找到这个月的第一天是星期几
  239. int firstWeekday = firstDayOfMonth.weekday;
  240. // 计算需要的前导空格数量
  241. int leadingDaysCount = (firstWeekday - DateTime.monday + 7) % 7;
  242. // 只保留当前月份的日期
  243. List<DateTime?> fullDates = List.from(dates);
  244. // 在视图中添加用于填充的空日期(如果需要,这里就不填充上个月尾的日期了)
  245. for (int i = 0; i < leadingDaysCount; i++) {
  246. fullDates.insert(0, null); // 用null填充前导位置
  247. }
  248. return Column(
  249. mainAxisAlignment: MainAxisAlignment.start,
  250. children: [
  251. Text(
  252. DateFormat.yMMMM(Locale(locale!).toString()).format(first),
  253. style: TextStyle(fontSize: 18.0, color: widget.dateColor, fontWeight: FontWeight.w500),
  254. ),
  255. // 周一到周天的星期文本
  256. Padding(
  257. padding: const EdgeInsets.only(top: 30.0),
  258. child: daysOfWeek(width, widget.locale),
  259. ),
  260. // 底部的每月的每一天
  261. Container(
  262. padding: const EdgeInsets.only(top: 10.0),
  263. height: (fullDates.length > 28)
  264. ? (fullDates.length > 35 ? 6.2 * width / 7 : 5.2 * width / 7)
  265. : 4 * width / 7,
  266. width: MediaQuery.of(context).size.width - 2 * widget.padding!,
  267. child: GridView.builder(
  268. itemCount: fullDates.length,
  269. physics: const NeverScrollableScrollPhysics(),
  270. gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 7),
  271. itemBuilder: (context, index) {
  272. DateTime? date = fullDates[index]; // 使用 DateTime? 类型以支持 null
  273. // 如果 date 为 null,表示该位置为空,返回一个透明的容器
  274. if (date == null) {
  275. return Container(
  276. width: width / 7,
  277. height: width / 7,
  278. color: Colors.transparent, // 透明或其他样式
  279. );
  280. }
  281. bool outOfRange = date.isBefore(startDate) || date.isAfter(endDate);
  282. return dateInCalendar(
  283. date,
  284. outOfRange,
  285. width,
  286. _events!.contains(date.toString().split(" ").first) && !outOfRange,
  287. );
  288. },
  289. ),
  290. )
  291. ],
  292. );
  293. }
  294. }