paged_vertical_calendar.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import 'package:flutter/material.dart' hide DateUtils;
  2. import 'package:flutter/rendering.dart';
  3. import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
  4. import 'package:intl/intl.dart';
  5. import 'package:paged_vertical_calendar/utils/date_models.dart';
  6. import 'package:paged_vertical_calendar/utils/date_utils.dart';
  7. /// a minimalistic paginated calendar widget providing infinite customisation
  8. /// options and usefull paginated callbacks. all paremeters are optional.
  9. ///
  10. /// ```
  11. /// PagedVerticalCalendar(
  12. /// startDate: DateTime(2021, 1, 1),
  13. /// endDate: DateTime(2021, 12, 31),
  14. /// onDayPressed: (day) {
  15. /// print('Date selected: $day');
  16. /// },
  17. /// onMonthLoaded: (year, month) {
  18. /// print('month loaded: $month-$year');
  19. /// },
  20. /// onPaginationCompleted: () {
  21. /// print('end reached');
  22. /// },
  23. /// ),
  24. /// ```
  25. class PagedVerticalCalendar extends StatefulWidget {
  26. PagedVerticalCalendar({
  27. this.startDate,
  28. this.endDate,
  29. this.monthBuilder,
  30. this.dayBuilder,
  31. this.addAutomaticKeepAlives = false,
  32. this.onDayPressed,
  33. this.onMonthLoaded,
  34. this.onPaginationCompleted,
  35. this.invisibleMonthsThreshold = 1,
  36. this.physics,
  37. this.scrollController,
  38. this.listPadding = EdgeInsets.zero,
  39. this.initialDate,
  40. this.previousYear = false,
  41. });
  42. /// the [DateTime] to start the calendar from, if no [startDate] is provided
  43. /// `DateTime.now()` will be used
  44. final DateTime? startDate;
  45. /// optional [DateTime] to end the calendar pagination, of no [endDate] is
  46. /// provided the calendar can paginate indefinitely
  47. final DateTime? endDate;
  48. /// a Builder used for month header generation. a default [MonthBuilder] is
  49. /// used when no custom [MonthBuilder] is provided.
  50. /// * [context]
  51. /// * [int] year: 2021
  52. /// * [int] month: 1-12
  53. final MonthBuilder? monthBuilder;
  54. /// a Builder used for day generation. a default [DayBuilder] is
  55. /// used when no custom [DayBuilder] is provided.
  56. /// * [context]
  57. /// * [DateTime] date
  58. final DayBuilder? dayBuilder;
  59. /// if the calendar should stay cached when the widget is no longer loaded.
  60. /// this can be used for maintaining the last state. defaults to `false`
  61. final bool addAutomaticKeepAlives;
  62. /// callback that provides the [DateTime] of the day that's been interacted
  63. /// with
  64. final ValueChanged<DateTime>? onDayPressed;
  65. /// callback when a new paginated month is loaded.
  66. final OnMonthLoaded? onMonthLoaded;
  67. /// called when the calendar pagination is completed. if no [endDate] is
  68. /// provided this method is never called
  69. final Function? onPaginationCompleted;
  70. /// how many months should be loaded outside of the view. defaults to `1`
  71. final int invisibleMonthsThreshold;
  72. /// list padding, defaults to `EdgeInsets.zero`
  73. final EdgeInsetsGeometry listPadding;
  74. /// scroll physics, defaults to matching platform conventions
  75. final ScrollPhysics? physics;
  76. /// scroll controller for making programmable scroll interactions
  77. final ScrollController? scrollController;
  78. /// the initial date displayed by the calendar.
  79. /// if inititial date is nulll, the start date will be used
  80. final DateTime? initialDate;
  81. final bool previousYear;
  82. @override
  83. _PagedVerticalCalendarState createState() => _PagedVerticalCalendarState();
  84. }
  85. class _PagedVerticalCalendarState extends State<PagedVerticalCalendar> {
  86. late PagingController<int, Month> _pagingReplyUpController;
  87. late PagingController<int, Month> _pagingReplyDownController;
  88. final Key downListKey = UniqueKey();
  89. late DateTime initDate;
  90. late bool hideUp;
  91. @override
  92. void initState() {
  93. super.initState();
  94. if (widget.initialDate != null) {
  95. if (widget.endDate != null) {
  96. int diffDaysEndDate =
  97. widget.endDate!.difference(widget.initialDate!).inDays;
  98. if (diffDaysEndDate.isNegative) {
  99. initDate = widget.endDate!;
  100. } else {
  101. initDate = widget.initialDate!;
  102. }
  103. } else {
  104. initDate = widget.initialDate!;
  105. }
  106. } else {
  107. initDate = DateTime.now().removeTime();
  108. }
  109. if (widget.startDate != null) {
  110. int diffDaysStartDate = widget.startDate!.difference(initDate).inDays;
  111. if (diffDaysStartDate.isNegative) {
  112. if(widget.previousYear == false && initDate.month == 1) {
  113. hideUp = false;
  114. }else{
  115. hideUp = true;
  116. }
  117. } else {
  118. hideUp = false;
  119. }
  120. } else {
  121. hideUp = true;
  122. }
  123. _pagingReplyUpController = PagingController<int, Month>(
  124. firstPageKey: 0,
  125. invisibleItemsThreshold: widget.invisibleMonthsThreshold,
  126. );
  127. _pagingReplyUpController.addPageRequestListener(_fetchUpPage);
  128. _pagingReplyUpController.addStatusListener(paginationStatusUp);
  129. _pagingReplyDownController = PagingController<int, Month>(
  130. firstPageKey: 0,
  131. invisibleItemsThreshold: widget.invisibleMonthsThreshold,
  132. );
  133. _pagingReplyDownController.addPageRequestListener(_fetchDownPage);
  134. _pagingReplyDownController.addStatusListener(paginationStatusDown);
  135. }
  136. void paginationStatusUp(PagingStatus state) {
  137. if (state == PagingStatus.completed)
  138. return widget.onPaginationCompleted?.call();
  139. }
  140. void paginationStatusDown(PagingStatus state) {
  141. if (state == PagingStatus.completed)
  142. return widget.onPaginationCompleted?.call();
  143. }
  144. /// fetch a new [Month] object based on the [pageKey] which is the Nth month
  145. /// from the start date
  146. void _fetchUpPage(int pageKey) async {
  147. // DateTime startDateUp = widget.startDate != null
  148. // ? DateTime(widget.startDate!.year,
  149. // widget.startDate!.month + initialIndex, widget.startDate!.day)
  150. // : DateTime.now();
  151. // DateTime initDateUp =
  152. // Jiffy(DateTime(initialDate.year, initialDate.month, 1))
  153. // .subtract(months: 1)
  154. // .dateTime;
  155. try {
  156. final month = DateUtils.getMonth(
  157. DateTime(initDate.year, initDate.month - 1, 1),
  158. widget.startDate,
  159. pageKey,
  160. true);
  161. WidgetsBinding.instance?.addPostFrameCallback(
  162. (_) => widget.onMonthLoaded?.call(month.year, month.month),
  163. );
  164. final newItems = [month];
  165. final isLastPage = widget.startDate != null &&
  166. widget.startDate!.isSameDayOrAfter(month.weeks.first.firstDay);
  167. if (isLastPage) {
  168. return _pagingReplyUpController.appendLastPage(newItems);
  169. }
  170. final nextPageKey = pageKey + newItems.length;
  171. _pagingReplyUpController.appendPage(newItems, nextPageKey);
  172. } catch (_) {
  173. _pagingReplyUpController.error;
  174. }
  175. }
  176. void _fetchDownPage(int pageKey) async {
  177. try {
  178. final month = DateUtils.getMonth(
  179. DateTime(initDate.year, initDate.month, 1),
  180. widget.endDate,
  181. pageKey,
  182. false,
  183. );
  184. WidgetsBinding.instance?.addPostFrameCallback(
  185. (_) => widget.onMonthLoaded?.call(month.year, month.month),
  186. );
  187. final newItems = [month];
  188. final isLastPage = widget.endDate != null &&
  189. widget.endDate!.isSameDayOrBefore(month.weeks.last.lastDay);
  190. if (isLastPage) {
  191. return _pagingReplyDownController.appendLastPage(newItems);
  192. }
  193. final nextPageKey = pageKey + newItems.length;
  194. _pagingReplyDownController.appendPage(newItems, nextPageKey);
  195. } catch (_) {
  196. _pagingReplyDownController.error;
  197. }
  198. }
  199. @override
  200. Widget build(BuildContext context) {
  201. return Scrollable(
  202. viewportBuilder: (BuildContext context, ViewportOffset position) {
  203. return Viewport(
  204. offset: position,
  205. center: downListKey,
  206. slivers: [
  207. if (hideUp)
  208. PagedSliverList(
  209. pagingController: _pagingReplyUpController,
  210. builderDelegate: PagedChildBuilderDelegate<Month>(
  211. itemBuilder: (BuildContext context, Month month, int index) {
  212. return _MonthView(
  213. month: month,
  214. monthBuilder: widget.monthBuilder,
  215. dayBuilder: widget.dayBuilder,
  216. onDayPressed: widget.onDayPressed,
  217. );
  218. },
  219. ),
  220. ),
  221. PagedSliverList(
  222. key: downListKey,
  223. pagingController: _pagingReplyDownController,
  224. builderDelegate: PagedChildBuilderDelegate<Month>(
  225. itemBuilder: (BuildContext context, Month month, int index) {
  226. return _MonthView(
  227. month: month,
  228. monthBuilder: widget.monthBuilder,
  229. dayBuilder: widget.dayBuilder,
  230. onDayPressed: widget.onDayPressed,
  231. );
  232. },
  233. ),
  234. ),
  235. ],
  236. );
  237. },
  238. );
  239. }
  240. @override
  241. void dispose() {
  242. _pagingReplyUpController.dispose();
  243. _pagingReplyDownController.dispose();
  244. super.dispose();
  245. }
  246. }
  247. class _MonthView extends StatelessWidget {
  248. _MonthView({
  249. required this.month,
  250. this.monthBuilder,
  251. this.dayBuilder,
  252. this.onDayPressed,
  253. });
  254. final Month month;
  255. final MonthBuilder? monthBuilder;
  256. final DayBuilder? dayBuilder;
  257. final ValueChanged<DateTime>? onDayPressed;
  258. @override
  259. Widget build(BuildContext context) {
  260. return Column(
  261. children: <Widget>[
  262. /// display the default month header if none is provided
  263. monthBuilder?.call(context, month.month, month.year) ??
  264. _DefaultMonthView(
  265. month: month.month,
  266. year: month.year,
  267. ),
  268. Table(
  269. children: month.weeks.map((Week week) {
  270. return _generateWeekRow(context, week);
  271. }).toList(growable: false),
  272. ),
  273. SizedBox(
  274. height: 20,
  275. ),
  276. ],
  277. );
  278. }
  279. TableRow _generateWeekRow(BuildContext context, Week week) {
  280. DateTime firstDay = week.firstDay;
  281. return TableRow(
  282. children: List<Widget>.generate(
  283. DateTime.daysPerWeek,
  284. (int position) {
  285. DateTime day = DateTime(
  286. week.firstDay.year,
  287. week.firstDay.month,
  288. firstDay.day + (position - (firstDay.weekday - 1)),
  289. );
  290. if ((position + 1) < week.firstDay.weekday ||
  291. (position + 1) > week.lastDay.weekday) {
  292. return const SizedBox();
  293. } else {
  294. return AspectRatio(
  295. aspectRatio: 1.0,
  296. child: InkWell(
  297. onTap: onDayPressed == null ? null : () => onDayPressed!(day),
  298. child: dayBuilder?.call(context, day) ??
  299. _DefaultDayView(date: day),
  300. ),
  301. );
  302. }
  303. },
  304. growable: false,
  305. ),
  306. );
  307. }
  308. }
  309. class _DefaultMonthView extends StatelessWidget {
  310. final int month;
  311. final int year;
  312. _DefaultMonthView({required this.month, required this.year});
  313. @override
  314. Widget build(BuildContext context) {
  315. return Padding(
  316. padding: const EdgeInsets.all(8.0),
  317. child: Text(
  318. DateFormat('MMMM yyyy').format(DateTime(year, month)),
  319. style: Theme.of(context).textTheme.headline6,
  320. ),
  321. );
  322. }
  323. }
  324. class _DefaultDayView extends StatelessWidget {
  325. final DateTime date;
  326. _DefaultDayView({required this.date});
  327. @override
  328. Widget build(BuildContext context) {
  329. return Center(
  330. child: Text(
  331. DateFormat('d').format(date),
  332. ),
  333. );
  334. }
  335. }
  336. typedef MonthBuilder = Widget Function(
  337. BuildContext context, int month, int year);
  338. typedef DayBuilder = Widget Function(BuildContext context, DateTime date);
  339. typedef OnMonthLoaded = void Function(int year, int month);