Skip to content

Commit ea99aac

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 ea99aac

8 files changed

Lines changed: 310 additions & 64 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(

lib/src/day_view/_internal_day_view_page.dart

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

106-
final ScrollController dayViewScrollController;
106+
final ZoomScrollController dayViewScrollController;
107107

108108
/// Flag to display half hours.
109109
final bool showHalfHours;
@@ -120,8 +120,18 @@ class InternalDayViewPage<T extends Object?> extends StatefulWidget {
120120
/// Settings for half hour indicator lines.
121121
final HourIndicatorSettings quarterHourIndicatorSettings;
122122

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

126136
/// Last scroll offset of day view page.
127137
final double lastScrollOffset;
@@ -179,6 +189,8 @@ class InternalDayViewPage<T extends Object?> extends StatefulWidget {
179189
required this.dayViewScrollController,
180190
required this.scrollPhysics,
181191
required this.scrollListener,
192+
required this.pageIndex,
193+
required this.isCurrentPage,
182194
required this.dayDetectorBuilder,
183195
required this.showHalfHours,
184196
required this.showQuarterHours,
@@ -210,6 +222,17 @@ class _InternalDayViewPageState<T extends Object?>
210222
initialScrollOffset: widget.lastScrollOffset,
211223
);
212224
scrollController.addListener(_scrollControllerListener);
225+
226+
if (widget.isCurrentPage) {
227+
WidgetsBinding.instance.addPostFrameCallback((_) {
228+
if (!mounted) return;
229+
widget.scrollListener(
230+
widget.pageIndex,
231+
scrollController.offset,
232+
scrollController,
233+
);
234+
});
235+
}
213236
}
214237

215238
@override
@@ -225,6 +248,23 @@ class _InternalDayViewPageState<T extends Object?>
225248
(oldWidget.heightPerMinute > 0 ? oldWidget.heightPerMinute : 1.0);
226249
scrollController.prepareZoomJump(scaledOffset);
227250
}
251+
252+
if (!widget.keepScrollOffset &&
253+
widget.isCurrentPage &&
254+
!oldWidget.isCurrentPage &&
255+
scrollController.hasClients) {
256+
scrollController.jumpTo(widget.lastScrollOffset);
257+
}
258+
259+
if (widget.isCurrentPage && !oldWidget.isCurrentPage) {
260+
widget.scrollListener(
261+
widget.pageIndex,
262+
scrollController.hasClients
263+
? scrollController.offset
264+
: widget.lastScrollOffset,
265+
scrollController,
266+
);
267+
}
228268
}
229269

230270
@override
@@ -236,7 +276,11 @@ class _InternalDayViewPageState<T extends Object?>
236276
}
237277

238278
void _scrollControllerListener() {
239-
widget.scrollListener(scrollController);
279+
widget.scrollListener(
280+
widget.pageIndex,
281+
scrollController.offset,
282+
scrollController,
283+
);
240284
}
241285

242286
/// Builds the background color layer for time slots in the day view.
@@ -325,9 +369,7 @@ class _InternalDayViewPageState<T extends Object?>
325369
scrollbars: widget.keepScrollOffset,
326370
),
327371
child: SingleChildScrollView(
328-
controller: widget.keepScrollOffset
329-
? scrollController
330-
: widget.dayViewScrollController,
372+
controller: scrollController,
331373
physics: widget.scrollPhysics,
332374
child: SizedBox(
333375
height: widget.height,

lib/src/day_view/day_view.dart

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -490,9 +490,16 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
490490
/// Controls scroll position for time axis (top-to-bottom).
491491
late ZoomScrollController _scrollController;
492492

493+
/// Per-page scroll offset cache keyed by page index.
494+
final Map<int, double> _pageOffsets = <int, double>{};
495+
496+
/// Currently visible page scroll controller.
497+
ZoomScrollController? _activeScrollController;
498+
493499
/// Public getter for accessing the scroll controller.
494500
/// Allows external code to control or listen to scroll events.
495-
ScrollController get scrollController => _scrollController;
501+
ZoomScrollController get scrollController =>
502+
_activeScrollController ?? _scrollController;
496503

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

511517
_reloadCallback = _reload;
512518
_setDateRange();
@@ -519,7 +525,9 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
519525
_scrollController = ZoomScrollController(
520526
initialScrollOffset: _lastScrollOffset,
521527
);
528+
_activeScrollController = _scrollController;
522529
_pageController = PageController(initialPage: _currentIndex);
530+
_pageOffsets[_currentIndex] = _lastScrollOffset;
523531
_eventArranger = widget.eventArranger ?? SideEventArranger<T>();
524532
_assignBuilders();
525533
}
@@ -562,6 +570,8 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
562570
widget.maxDay != oldWidget.maxDay) {
563571
_setDateRange();
564572
_regulateCurrentDate();
573+
_pageOffsets.clear();
574+
_pageOffsets[_currentIndex] = _defaultPageOffset;
565575

566576
_pageController.jumpToPage(_currentIndex);
567577
}
@@ -575,13 +585,16 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
575585
_assignBuilders();
576586

577587
if (widget.heightPerMinute != oldWidget.heightPerMinute) {
578-
final currentOffset = _scrollController.hasClients
579-
? _scrollController.offset
580-
: _lastScrollOffset;
588+
final activeController = _activeScrollController;
589+
final currentOffset =
590+
activeController != null && activeController.hasClients
591+
? activeController.offset
592+
: _lastScrollOffset;
581593
final scaledOffset = currentOffset *
582594
widget.heightPerMinute /
583595
(oldWidget.heightPerMinute > 0 ? oldWidget.heightPerMinute : 1.0);
584596
_lastScrollOffset = scaledOffset;
597+
_pageOffsets[_currentIndex] = scaledOffset;
585598
_scrollController.prepareZoomJump(scaledOffset);
586599
}
587600
}
@@ -668,9 +681,13 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
668681
_quarterHourIndicatorSettings,
669682
emulateVerticalOffsetBy:
670683
widget.emulateVerticalOffsetBy,
671-
lastScrollOffset: _lastScrollOffset,
684+
lastScrollOffset: widget.keepScrollOffset
685+
? (_pageOffsets[index] ?? _defaultPageOffset)
686+
: _defaultPageOffset,
672687
dayViewScrollController: _scrollController,
673688
scrollPhysics: widget.scrollPhysics,
689+
pageIndex: index,
690+
isCurrentPage: index == _currentIndex,
674691
scrollListener: _scrollPageListener,
675692
keepScrollOffset: widget.keepScrollOffset,
676693
timeSlotColorBuilder: _timeSlotColorBuilder,
@@ -951,9 +968,14 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
951968
_currentIndex = index;
952969
});
953970
}
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,28 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
9901012

9911013
void _jumpToOffsetAfterPageTransition(double offset) {
9921014
_runAfterPageTransition(() {
993-
if (!_scrollController.hasClients) return;
1015+
final controller = _activeScrollController;
1016+
if (controller == null || !controller.hasClients) return;
9941017

9951018
final clampedOffset = offset.clamp(
996-
_scrollController.position.minScrollExtent,
997-
_scrollController.position.maxScrollExtent,
1019+
controller.position.minScrollExtent,
1020+
controller.position.maxScrollExtent,
9981021
);
9991022

10001023
_lastScrollOffset = clampedOffset.toDouble();
1001-
_scrollController.jumpTo(clampedOffset.toDouble());
1024+
_pageOffsets[_currentIndex] = _lastScrollOffset;
1025+
controller.jumpTo(clampedOffset.toDouble());
10021026
});
10031027
}
10041028

1029+
double get _defaultPageOffset =>
1030+
widget.scrollOffset ?? _offsetForDuration(widget.startDuration);
1031+
10051032
double _offsetForDuration(Duration startDuration) {
1006-
final offSetForSingleMinute = _height / 24 / 60;
10071033
final startDurationInMinutes = startDuration.inMinutes;
10081034
final minuteOffset =
10091035
startDurationInMinutes > 3600 ? 3600 : startDurationInMinutes;
1010-
return offSetForSingleMinute * minuteOffset;
1036+
return widget.heightPerMinute * minuteOffset;
10111037
}
10121038

10131039
/// Animate to next page (next day).
@@ -1208,7 +1234,7 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
12081234
Duration duration = const Duration(milliseconds: 200),
12091235
Curve curve = Curves.linear,
12101236
}) {
1211-
_scrollController.animateTo(
1237+
scrollController.animateTo(
12121238
offset,
12131239
duration: duration,
12141240
curve: curve,
@@ -1220,8 +1246,18 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
12201246
DateTime(_currentDate.year, _currentDate.month, _currentDate.day);
12211247

12221248
/// Listener for every day page ScrollController
1223-
void _scrollPageListener(ScrollController controller) {
1224-
_lastScrollOffset = controller.offset;
1249+
void _scrollPageListener(
1250+
int pageIndex,
1251+
double offset,
1252+
ZoomScrollController controller,
1253+
) {
1254+
_activeScrollController = controller;
1255+
if (!widget.keepScrollOffset) return;
1256+
1257+
_pageOffsets[pageIndex] = offset;
1258+
if (pageIndex == _currentIndex) {
1259+
_lastScrollOffset = offset;
1260+
}
12251261
}
12261262
}
12271263

lib/src/multi_day_view/_internal_multi_day_view_page.dart

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ class InternalMultiDayViewPage<T extends Object?> extends StatefulWidget {
130130
/// Display full day events.
131131
final FullDayEventBuilder<T> fullDayEventBuilder;
132132

133-
final ScrollController multiDayViewScrollController;
133+
final ZoomScrollController multiDayViewScrollController;
134134

135135
/// First hour displayed in the layout
136136
final int startHour;
@@ -159,8 +159,18 @@ class InternalMultiDayViewPage<T extends Object?> extends StatefulWidget {
159159
/// Defines full day events header text config
160160
final FullDayHeaderTextConfig fullDayHeaderTextConfig;
161161

162-
/// Scroll listener to set every page's last offset
163-
final void Function(ScrollController) scrollListener;
162+
/// Scroll listener to set every page's last offset.
163+
final void Function(
164+
int pageIndex,
165+
double offset,
166+
ZoomScrollController controller,
167+
) scrollListener;
168+
169+
/// Page index in the parent [PageView].
170+
final int pageIndex;
171+
172+
/// Whether this page is currently visible in parent [PageView].
173+
final bool isCurrentPage;
164174

165175
/// Last scroll offset of week view page.
166176
final double lastScrollOffset;
@@ -222,6 +232,8 @@ class InternalMultiDayViewPage<T extends Object?> extends StatefulWidget {
222232
required this.fullDayHeaderTextConfig,
223233
required this.scrollPhysics,
224234
required this.scrollListener,
235+
required this.pageIndex,
236+
required this.isCurrentPage,
225237
required this.multiDayViewScrollController,
226238
this.lastScrollOffset = 0.0,
227239
this.keepScrollOffset = false,
@@ -244,6 +256,17 @@ class _InternalMultiDayViewPageState<T extends Object?>
244256
initialScrollOffset: widget.lastScrollOffset,
245257
);
246258
scrollController.addListener(_scrollControllerListener);
259+
260+
if (widget.isCurrentPage) {
261+
WidgetsBinding.instance.addPostFrameCallback((_) {
262+
if (!mounted) return;
263+
widget.scrollListener(
264+
widget.pageIndex,
265+
scrollController.offset,
266+
scrollController,
267+
);
268+
});
269+
}
247270
}
248271

249272
@override
@@ -259,6 +282,23 @@ class _InternalMultiDayViewPageState<T extends Object?>
259282
(oldWidget.heightPerMinute > 0 ? oldWidget.heightPerMinute : 1.0);
260283
scrollController.prepareZoomJump(scaledOffset);
261284
}
285+
286+
if (!widget.keepScrollOffset &&
287+
widget.isCurrentPage &&
288+
!oldWidget.isCurrentPage &&
289+
scrollController.hasClients) {
290+
scrollController.jumpTo(widget.lastScrollOffset);
291+
}
292+
293+
if (widget.isCurrentPage && !oldWidget.isCurrentPage) {
294+
widget.scrollListener(
295+
widget.pageIndex,
296+
scrollController.hasClients
297+
? scrollController.offset
298+
: widget.lastScrollOffset,
299+
scrollController,
300+
);
301+
}
262302
}
263303

264304
@override
@@ -270,7 +310,11 @@ class _InternalMultiDayViewPageState<T extends Object?>
270310
}
271311

272312
void _scrollControllerListener() {
273-
widget.scrollListener(scrollController);
313+
widget.scrollListener(
314+
widget.pageIndex,
315+
scrollController.offset,
316+
scrollController,
317+
);
274318
}
275319

276320
@override
@@ -384,9 +428,7 @@ class _InternalMultiDayViewPageState<T extends Object?>
384428
scrollbars: widget.keepScrollOffset,
385429
),
386430
child: SingleChildScrollView(
387-
controller: widget.keepScrollOffset
388-
? scrollController
389-
: widget.multiDayViewScrollController,
431+
controller: scrollController,
390432
physics: widget.scrollPhysics,
391433
child: SizedBox(
392434
height: widget.height,

0 commit comments

Comments
 (0)