full_calendar.dart 11 KB

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