Skip to content

Commit 78d29f0

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 78d29f0

9 files changed

Lines changed: 476 additions & 116 deletions

File tree

doc/documentation.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,64 @@ CalendarControllerProvider(
603603

604604
When a view does not receive `controller` directly, it reads the controller from `CalendarControllerProvider`.
605605

606+
## Advanced Internals: `ZoomScrollController` (Optional)
607+
608+
`ZoomScrollController` is an internal utility used by `DayView`, `WeekView`, and
609+
`MultiDayView` to keep the same time range visible when `heightPerMinute`
610+
changes.
611+
612+
> This is **not part of the stable public package API** exported by
613+
> `package:calendar_view/calendar_view.dart`.
614+
>
615+
> Use this only when building a custom calendar/scroll implementation and when
616+
> you are okay with potential breaking changes in internal files.
617+
618+
## Why it exists
619+
620+
When zoom changes (for example from `heightPerMinute: 1.0` to `1.4`), a normal
621+
`ScrollController` update done after build can produce a brief visual jump.
622+
`ZoomScrollController` avoids that by preparing the next offset before layout
623+
and applying it during content-dimension calculation.
624+
625+
## Internal contract
626+
627+
1. Read the current vertical offset.
628+
2. Compute scaled offset:
629+
`scaledOffset = (currentOffset / oldHeightPerMinute) * newHeightPerMinute`
630+
3. Call `prepareZoomJump(scaledOffset)` before rebuild.
631+
4. Rebuild with new `heightPerMinute`.
632+
633+
Because the correction is applied during layout, viewport size and scroll
634+
position update together in the same frame.
635+
636+
## Minimal internal example
637+
638+
```dart
639+
import 'package:calendar_view/src/zoom_scroll_controller.dart';
640+
641+
class MyZoomState {
642+
final controller = ZoomScrollController();
643+
double heightPerMinute = 1.0;
644+
645+
void onZoomChange(double nextHeightPerMinute) {
646+
final currentOffset = controller.hasClients ? controller.offset : 0.0;
647+
648+
final scaledOffset =
649+
(currentOffset / heightPerMinute) * nextHeightPerMinute;
650+
651+
controller.prepareZoomJump(scaledOffset);
652+
heightPerMinute = nextHeightPerMinute;
653+
}
654+
}
655+
```
656+
657+
## Recommendation
658+
659+
Prefer the built-in `DayView`, `WeekView`, and `MultiDayView` behavior unless
660+
you are implementing a custom view layer that needs zoom-aware scroll
661+
correction.
662+
663+
606664
# Localization Guide
607665

608666
This guide covers localization support in `calendar_view` and how to keep localized strings aligned with the package API.
@@ -1159,7 +1217,6 @@ dependencies:
11591217
bool hideDaysNotInMonth,
11601218
);
11611219
```
1162-
11631220
# Contributors
11641221

11651222
## Main Contributors

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,

0 commit comments

Comments
 (0)