Skip to content

Commit 1b1ec03

Browse files
committed
🐛 Fix scroll behavior by replacing ScrollController with ZoomScrollController and updating scroll listener parameters across day, multi-day, and week views
1 parent 0cc89e4 commit 1b1ec03

9 files changed

Lines changed: 417 additions & 114 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Updated documentation for `DayView`, `WeekView`, `MonthView` and `MultiDayView` to add more details about the parameters and their usage. [#448](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/pull/448)
1212
- Fixed `onlyShowToday` parameter in `WeekView` to update `liveTimeIndicator` properly. [#518](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/518)
1313
- Added `ZoomScrollController` to `DayView`, `WeekView` and `MultiDayView` for programmatic control of zoom level and scroll position. [#522](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/522)
14+
- Fixed issue with scroll exception when using `ZoomScrollController` in `DayView` and `WeekView`. [#543](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/pull/543)
1415

1516
# [2.0.0 - 17 Mar 2026](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/tree/2.0.0)
1617

example/lib/widgets/day_view_widget.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class DayViewWidget extends StatelessWidget {
2222
timeLineBuilder: (date) => _timeLineBuilder(date, isLtr),
2323
scrollPhysics: const BouncingScrollPhysics(),
2424
eventArranger: SideEventArranger(),
25+
keepScrollOffset: true,
2526
showQuarterHours: false,
2627
showMidnightHour: true,
2728
hourIndicatorSettings: HourIndicatorSettings(

example/lib/widgets/week_view_widget.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class WeekViewWidget extends StatelessWidget {
2424
showWeekends: true,
2525
showMidnightHour: true,
2626
showLiveTimeLineInAllDays: true,
27+
keepScrollOffset: true,
2728
timeSlotColorBuilder: (_, slotStartTime, __, ___) {
2829
final hour = slotStartTime.hour;
2930
final isBusinessHours = hour >= 9 && hour < 17;

lib/src/day_view/_internal_day_view_page.dart

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,6 @@ class InternalDayViewPage<T extends Object?> extends StatefulWidget {
103103
/// Display full day events.
104104
final FullDayEventBuilder<T> fullDayEventBuilder;
105105

106-
final ScrollController dayViewScrollController;
107-
108106
/// Flag to display half hours.
109107
final bool showHalfHours;
110108

@@ -120,8 +118,18 @@ class InternalDayViewPage<T extends Object?> extends StatefulWidget {
120118
/// Settings for half hour indicator lines.
121119
final HourIndicatorSettings quarterHourIndicatorSettings;
122120

123-
/// Scroll listener to set every page's last offset
124-
final void Function(ScrollController) scrollListener;
121+
/// Scroll listener to set every page's last offset.
122+
final void Function(
123+
int pageIndex,
124+
double offset,
125+
ZoomScrollController controller,
126+
) scrollListener;
127+
128+
/// Page index in the parent [PageView].
129+
final int pageIndex;
130+
131+
/// Whether this page is currently visible in parent [PageView].
132+
final bool isCurrentPage;
125133

126134
/// Last scroll offset of day view page.
127135
final double lastScrollOffset;
@@ -176,9 +184,10 @@ class InternalDayViewPage<T extends Object?> extends StatefulWidget {
176184
required this.minuteSlotSize,
177185
required this.scrollNotifier,
178186
required this.fullDayEventBuilder,
179-
required this.dayViewScrollController,
180187
required this.scrollPhysics,
181188
required this.scrollListener,
189+
required this.pageIndex,
190+
required this.isCurrentPage,
182191
required this.dayDetectorBuilder,
183192
required this.showHalfHours,
184193
required this.showQuarterHours,
@@ -210,6 +219,17 @@ class _InternalDayViewPageState<T extends Object?>
210219
initialScrollOffset: widget.lastScrollOffset,
211220
);
212221
scrollController.addListener(_scrollControllerListener);
222+
223+
if (widget.isCurrentPage) {
224+
WidgetsBinding.instance.addPostFrameCallback((_) {
225+
if (!mounted) return;
226+
widget.scrollListener(
227+
widget.pageIndex,
228+
scrollController.offset,
229+
scrollController,
230+
);
231+
});
232+
}
213233
}
214234

215235
@override
@@ -225,6 +245,23 @@ class _InternalDayViewPageState<T extends Object?>
225245
(oldWidget.heightPerMinute > 0 ? oldWidget.heightPerMinute : 1.0);
226246
scrollController.prepareZoomJump(scaledOffset);
227247
}
248+
249+
if (!widget.keepScrollOffset &&
250+
widget.isCurrentPage &&
251+
!oldWidget.isCurrentPage &&
252+
scrollController.hasClients) {
253+
scrollController.jumpTo(widget.lastScrollOffset);
254+
}
255+
256+
if (widget.isCurrentPage && !oldWidget.isCurrentPage) {
257+
widget.scrollListener(
258+
widget.pageIndex,
259+
scrollController.hasClients
260+
? scrollController.offset
261+
: widget.lastScrollOffset,
262+
scrollController,
263+
);
264+
}
228265
}
229266

230267
@override
@@ -236,7 +273,11 @@ class _InternalDayViewPageState<T extends Object?>
236273
}
237274

238275
void _scrollControllerListener() {
239-
widget.scrollListener(scrollController);
276+
widget.scrollListener(
277+
widget.pageIndex,
278+
scrollController.offset,
279+
scrollController,
280+
);
240281
}
241282

242283
/// Builds the background color layer for time slots in the day view.
@@ -325,9 +366,7 @@ class _InternalDayViewPageState<T extends Object?>
325366
scrollbars: widget.keepScrollOffset,
326367
),
327368
child: SingleChildScrollView(
328-
controller: widget.keepScrollOffset
329-
? scrollController
330-
: widget.dayViewScrollController,
369+
controller: scrollController,
331370
physics: widget.scrollPhysics,
332371
child: SizedBox(
333372
height: widget.height,

lib/src/day_view/day_view.dart

Lines changed: 87 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -486,13 +486,23 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
486486
/// Provides data for rendering events and tracks event changes.
487487
EventController<T>? _controller;
488488

489-
/// Scroll controller for managing vertical scrolling within the day view.
490-
/// Controls scroll position for time axis (top-to-bottom).
491-
late ZoomScrollController _scrollController;
489+
/// Per-page scroll offset cache keyed by page index.
490+
final Map<int, double> _pageOffsets = <int, double>{};
491+
492+
/// Currently visible page scroll controller.
493+
ZoomScrollController? _activeScrollController;
492494

493495
/// Public getter for accessing the scroll controller.
494496
/// Allows external code to control or listen to scroll events.
495-
ScrollController get scrollController => _scrollController;
497+
ZoomScrollController get scrollController {
498+
final controller = _activeScrollController;
499+
if (controller == null || !controller.hasClients) {
500+
throw StateError(
501+
"ScrollController is not attached to any scroll views yet.",
502+
);
503+
}
504+
return controller;
505+
}
496506

497507
/// Callback function triggered when the controller changes or events are modified.
498508
/// Used to rebuild the view when event data changes.
@@ -505,8 +515,7 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
505515
@override
506516
void initState() {
507517
super.initState();
508-
_lastScrollOffset = widget.scrollOffset ??
509-
widget.startDuration.inMinutes * widget.heightPerMinute;
518+
_lastScrollOffset = _defaultPageOffset;
510519

511520
_reloadCallback = _reload;
512521
_setDateRange();
@@ -516,10 +525,8 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
516525
_regulateCurrentDate();
517526

518527
_calculateHeights();
519-
_scrollController = ZoomScrollController(
520-
initialScrollOffset: _lastScrollOffset,
521-
);
522528
_pageController = PageController(initialPage: _currentIndex);
529+
_pageOffsets[_currentIndex] = _lastScrollOffset;
523530
_eventArranger = widget.eventArranger ?? SideEventArranger<T>();
524531
_assignBuilders();
525532
}
@@ -562,6 +569,8 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
562569
widget.maxDay != oldWidget.maxDay) {
563570
_setDateRange();
564571
_regulateCurrentDate();
572+
_pageOffsets.clear();
573+
_pageOffsets[_currentIndex] = _defaultPageOffset;
565574

566575
_pageController.jumpToPage(_currentIndex);
567576
}
@@ -575,14 +584,18 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
575584
_assignBuilders();
576585

577586
if (widget.heightPerMinute != oldWidget.heightPerMinute) {
578-
final currentOffset = _scrollController.hasClients
579-
? _scrollController.offset
587+
final activeController = _activeScrollController;
588+
final currentOffset = activeController != null && activeController.hasClients
589+
? activeController.offset
580590
: _lastScrollOffset;
581591
final scaledOffset = currentOffset *
582592
widget.heightPerMinute /
583593
(oldWidget.heightPerMinute > 0 ? oldWidget.heightPerMinute : 1.0);
584594
_lastScrollOffset = scaledOffset;
585-
_scrollController.prepareZoomJump(scaledOffset);
595+
_pageOffsets[_currentIndex] = scaledOffset;
596+
if (activeController != null && activeController.hasClients) {
597+
activeController.prepareZoomJump(scaledOffset);
598+
}
586599
}
587600
}
588601

@@ -668,9 +681,12 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
668681
_quarterHourIndicatorSettings,
669682
emulateVerticalOffsetBy:
670683
widget.emulateVerticalOffsetBy,
671-
lastScrollOffset: _lastScrollOffset,
672-
dayViewScrollController: _scrollController,
684+
lastScrollOffset: widget.keepScrollOffset
685+
? (_pageOffsets[index] ?? _defaultPageOffset)
686+
: _defaultPageOffset,
673687
scrollPhysics: widget.scrollPhysics,
688+
pageIndex: index,
689+
isCurrentPage: index == _currentIndex,
674690
scrollListener: _scrollPageListener,
675691
keepScrollOffset: widget.keepScrollOffset,
676692
timeSlotColorBuilder: _timeSlotColorBuilder,
@@ -951,9 +967,15 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
951967
_currentIndex = index;
952968
});
953969
}
970+
_activeScrollController = null;
971+
_lastScrollOffset = widget.keepScrollOffset
972+
? (_pageOffsets[index] ?? _defaultPageOffset)
973+
: _defaultPageOffset;
974+
954975
if (!widget.keepScrollOffset) {
976+
_pageOffsets[index] = _defaultPageOffset;
955977
_jumpToOffsetAfterPageTransition(
956-
_offsetForDuration(widget.startDuration).toDouble(),
978+
_defaultPageOffset,
957979
);
958980
}
959981
widget.onPageChange?.call(_currentDate, _currentIndex);
@@ -990,24 +1012,46 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
9901012

9911013
void _jumpToOffsetAfterPageTransition(double offset) {
9921014
_runAfterPageTransition(() {
993-
if (!_scrollController.hasClients) return;
1015+
_withAttachedScrollController((controller) {
1016+
final clampedOffset = offset.clamp(
1017+
controller.position.minScrollExtent,
1018+
controller.position.maxScrollExtent,
1019+
);
9941020

995-
final clampedOffset = offset.clamp(
996-
_scrollController.position.minScrollExtent,
997-
_scrollController.position.maxScrollExtent,
998-
);
1021+
_lastScrollOffset = clampedOffset.toDouble();
1022+
_pageOffsets[_currentIndex] = _lastScrollOffset;
1023+
controller.jumpTo(clampedOffset.toDouble());
1024+
});
1025+
});
1026+
}
1027+
1028+
void _withAttachedScrollController(
1029+
void Function(ZoomScrollController controller) action,
1030+
) {
1031+
final controller = _activeScrollController;
1032+
if (controller != null && controller.hasClients) {
1033+
action(controller);
1034+
return;
1035+
}
9991036

1000-
_lastScrollOffset = clampedOffset.toDouble();
1001-
_scrollController.jumpTo(clampedOffset.toDouble());
1037+
WidgetsBinding.instance.addPostFrameCallback((_) {
1038+
if (!mounted) return;
1039+
1040+
final updatedController = _activeScrollController;
1041+
if (updatedController != null && updatedController.hasClients) {
1042+
action(updatedController);
1043+
}
10021044
});
10031045
}
10041046

1047+
double get _defaultPageOffset =>
1048+
widget.scrollOffset ?? _offsetForDuration(widget.startDuration);
1049+
10051050
double _offsetForDuration(Duration startDuration) {
1006-
final offSetForSingleMinute = _height / 24 / 60;
10071051
final startDurationInMinutes = startDuration.inMinutes;
10081052
final minuteOffset =
10091053
startDurationInMinutes > 3600 ? 3600 : startDurationInMinutes;
1010-
return offSetForSingleMinute * minuteOffset;
1054+
return widget.heightPerMinute * minuteOffset;
10111055
}
10121056

10131057
/// Animate to next page (next day).
@@ -1208,20 +1252,32 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
12081252
Duration duration = const Duration(milliseconds: 200),
12091253
Curve curve = Curves.linear,
12101254
}) {
1211-
_scrollController.animateTo(
1212-
offset,
1213-
duration: duration,
1214-
curve: curve,
1215-
);
1255+
_withAttachedScrollController((controller) {
1256+
controller.animateTo(
1257+
offset,
1258+
duration: duration,
1259+
curve: curve,
1260+
);
1261+
});
12161262
}
12171263

12181264
/// Returns the current visible date in day view.
12191265
DateTime get currentDate =>
12201266
DateTime(_currentDate.year, _currentDate.month, _currentDate.day);
12211267

12221268
/// Listener for every day page ScrollController
1223-
void _scrollPageListener(ScrollController controller) {
1224-
_lastScrollOffset = controller.offset;
1269+
void _scrollPageListener(
1270+
int pageIndex,
1271+
double offset,
1272+
ZoomScrollController controller,
1273+
) {
1274+
_activeScrollController = controller;
1275+
if (!widget.keepScrollOffset) return;
1276+
1277+
_pageOffsets[pageIndex] = offset;
1278+
if (pageIndex == _currentIndex) {
1279+
_lastScrollOffset = offset;
1280+
}
12251281
}
12261282
}
12271283

0 commit comments

Comments
 (0)