Skip to content

Commit 5cc0298

Browse files
✨ Fixes issue #331: Add auto-scroll to current time for Day, Week, and Multi-Day views
1 parent 78d29f0 commit 5cc0298

10 files changed

Lines changed: 474 additions & 6 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+
- Added `scrollToCurrentTime` to `DayView`, `WeekView` and `MultiDayView` to auto-center the timeline on the current time, along with `animateToCurrentTime()` and `jumpToCurrentTime()` methods to trigger it imperatively. [#331](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/331)
1415

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

doc/documentation.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ DayView(
254254
backgroundColor: Colors.white, // Background color of day view
255255
showLiveTimeLineInAllDays: true, // Display live time line in all pages
256256
scrollOffset: 0, // Initial scroll position
257+
scrollToCurrentTime: true, // Auto-center the timeline on the current time
257258
width: 400, // Width of day view page
258259
timeLineOffset: 0, // Offset for timeline
259260
// Event handling
@@ -424,6 +425,7 @@ WeekView(
424425
),
425426
// Scroll configuration
426427
scrollOffset: 0.0,
428+
scrollToCurrentTime: true, // Auto-center the timeline on the current time
427429
scrollPhysics: ScrollPhysics(), // ScrollPhysics for vertical scrolling
428430
pageViewPhysics: ScrollPhysics(), // ScrollPhysics for page view
429431
keepScrollOffset: true, // Maintain scroll offset when the page changes
@@ -543,6 +545,7 @@ MultiDayView(
543545
),
544546
// Scroll configuration
545547
scrollOffset: 0.0,
548+
scrollToCurrentTime: true, // Auto-center the timeline on the current time
546549
scrollPhysics: ScrollPhysics(), // ScrollPhysics for vertical scrolling
547550
pageViewPhysics: ScrollPhysics(), // ScrollPhysics for page view
548551
keepScrollOffset: true, // Maintain scroll offset when the page changes

example/lib/widgets/day_view_widget.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class DayViewWidget extends StatelessWidget {
1717
key: state,
1818
width: width,
1919
startDuration: Duration(hours: 8),
20+
scrollToCurrentTime: true,
2021
showHalfHours: true,
2122
heightPerMinute: 3,
2223
timeLineBuilder: (date) => _timeLineBuilder(date, isLtr),

example/lib/widgets/multi_day_view_widget.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class MultiDayViewWidget extends StatelessWidget {
1616
daysInView: 3,
1717
width: width,
1818
showLiveTimeLineInAllDays: true,
19+
scrollToCurrentTime: true,
1920
eventArranger: SideEventArranger(maxWidth: 30),
2021
timeLineWidth: 65,
2122
scrollPhysics: const BouncingScrollPhysics(),

example/lib/widgets/week_view_widget.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class WeekViewWidget extends StatelessWidget {
2323
heightPerMinute: heightPerMinute,
2424
showWeekends: true,
2525
showMidnightHour: true,
26+
scrollToCurrentTime: true,
2627
showLiveTimeLineInAllDays: true,
2728
keepScrollOffset: true,
2829
timeSlotColorBuilder: (_, slotStartTime, __, ___) {

lib/src/day_view/day_view.dart

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import '../../calendar_view.dart';
1010
import '../constants.dart';
1111
import '../extensions.dart';
1212
import '../painters.dart';
13+
import '../scroll_to_current_time_mixin.dart';
1314
import '../zoom_scroll_controller.dart';
1415
import '_internal_day_view_page.dart';
1516

@@ -179,6 +180,20 @@ class DayView<T extends Object?> extends StatefulWidget {
179180
/// rather than relying on the [startDuration] parameter.
180181
final double? scrollOffset;
181182

183+
/// When true, the timeline automatically scrolls so that the current time is
184+
/// centered in the viewport on the first build.
185+
///
186+
/// The current time honors [LiveTimeIndicatorSettings.currentTimeProvider]
187+
/// when provided and is clamped to the visible [startHour]/[endHour] range.
188+
/// This takes precedence over [scrollOffset] and [startDuration] for the
189+
/// initial scroll position.
190+
///
191+
/// To trigger this imperatively at any time (e.g. from a "Now" button) use
192+
/// [DayViewState.animateToCurrentTime] or [DayViewState.jumpToCurrentTime].
193+
///
194+
/// Default value is false.
195+
final bool scrollToCurrentTime;
196+
182197
/// This method will be called when user taps on timestamp in timeline.
183198
///
184199
/// Called when user taps on a time value in the timeline (left side of view).
@@ -327,6 +342,7 @@ class DayView<T extends Object?> extends StatefulWidget {
327342
this.verticalLineOffset = 10,
328343
this.backgroundColor,
329344
this.scrollOffset,
345+
this.scrollToCurrentTime = false,
330346
this.onEventTap,
331347
this.onEventLongTap,
332348
this.onDateLongPress,
@@ -381,7 +397,8 @@ class DayView<T extends Object?> extends StatefulWidget {
381397
DayViewState<T> createState() => DayViewState<T>();
382398
}
383399

384-
class DayViewState<T extends Object?> extends State<DayView<T>> {
400+
class DayViewState<T extends Object?> extends State<DayView<T>>
401+
with ScrollToCurrentTimeMixin<DayView<T>> {
385402
/// Width of the Day View widget in pixels.
386403
/// Calculated from widget width or device constraint width.
387404
late double _width;
@@ -504,6 +521,30 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
504521
return controller;
505522
}
506523

524+
// --- ScrollToCurrentTimeMixin implementation ---
525+
526+
@override
527+
DateTime Function()? get currentTimeProvider =>
528+
widget.liveTimeIndicatorSettings?.currentTimeProvider;
529+
530+
@override
531+
int get viewStartHour => widget.startHour;
532+
533+
@override
534+
int get viewEndHour => widget.endHour;
535+
536+
@override
537+
double get viewHeightPerMinute => widget.heightPerMinute;
538+
539+
@override
540+
ZoomScrollController? get activeScrollController => _activeScrollController;
541+
542+
@override
543+
void onCurrentTimeJumped(double offset) {
544+
_lastScrollOffset = offset;
545+
_pageOffsets[_currentIndex] = offset;
546+
}
547+
507548
/// Callback function triggered when the controller changes or events are modified.
508549
/// Used to rebuild the view when event data changes.
509550
late VoidCallback _reloadCallback;
@@ -515,7 +556,8 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
515556
@override
516557
void initState() {
517558
super.initState();
518-
_lastScrollOffset = _defaultPageOffset;
559+
_lastScrollOffset =
560+
widget.scrollToCurrentTime ? offsetForTime(currentTime) : _defaultPageOffset;
519561

520562
_reloadCallback = _reload;
521563
_setDateRange();
@@ -529,6 +571,12 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
529571
_pageOffsets[_currentIndex] = _lastScrollOffset;
530572
_eventArranger = widget.eventArranger ?? SideEventArranger<T>();
531573
_assignBuilders();
574+
575+
if (widget.scrollToCurrentTime) {
576+
WidgetsBinding.instance.addPostFrameCallback(
577+
(_) => scrollToCurrentTimeAfterLayout(),
578+
);
579+
}
532580
}
533581

534582
@override
@@ -1262,6 +1310,8 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
12621310
});
12631311
}
12641312

1313+
/// Animates the timeline so that the current time becomes visible.
1314+
///
12651315
/// Returns the current visible date in day view.
12661316
DateTime get currentDate =>
12671317
DateTime(_currentDate.year, _currentDate.month, _currentDate.day);

lib/src/multi_day_view/multi_day_view.dart

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../../calendar_view.dart';
88
import '../constants.dart';
99
import '../extensions.dart';
1010
import '../painters.dart';
11+
import '../scroll_to_current_time_mixin.dart';
1112
import '../zoom_scroll_controller.dart';
1213
import '_internal_multi_day_view_page.dart';
1314

@@ -129,6 +130,20 @@ class MultiDayView<T extends Object?> extends StatefulWidget {
129130
/// Scroll offset of week view page.
130131
final double scrollOffset;
131132

133+
/// When true, the timeline automatically scrolls so that the current time is
134+
/// centered in the viewport on the first build.
135+
///
136+
/// The current time honors [LiveTimeIndicatorSettings.currentTimeProvider]
137+
/// when provided and is clamped to the visible [startHour]/[endHour] range.
138+
/// This takes precedence over [scrollOffset] for the initial scroll position.
139+
///
140+
/// To trigger this imperatively at any time (e.g. from a "Now" button) use
141+
/// [MultiDayViewState.animateToCurrentTime] or
142+
/// [MultiDayViewState.jumpToCurrentTime].
143+
///
144+
/// Default value is false.
145+
final bool scrollToCurrentTime;
146+
132147
/// This method will be called when user taps on timestamp in timeline.
133148
final TimestampCallback? onTimestampTap;
134149

@@ -241,6 +256,7 @@ class MultiDayView<T extends Object?> extends StatefulWidget {
241256
this.backgroundColor,
242257
this.scrollPhysics,
243258
this.scrollOffset = 0.0,
259+
this.scrollToCurrentTime = false,
244260
this.onEventTap,
245261
this.onEventLongTap,
246262
this.onDateLongPress,
@@ -298,7 +314,8 @@ class MultiDayView<T extends Object?> extends StatefulWidget {
298314
MultiDayViewState<T> createState() => MultiDayViewState<T>();
299315
}
300316

301-
class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>> {
317+
class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>>
318+
with ScrollToCurrentTimeMixin<MultiDayView<T>> {
302319
late double _width;
303320
late double _height;
304321
late double _timeLineWidth;
@@ -355,6 +372,30 @@ class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>> {
355372
return controller;
356373
}
357374

375+
// --- ScrollToCurrentTimeMixin implementation ---
376+
377+
@override
378+
DateTime Function()? get currentTimeProvider =>
379+
widget.liveTimeIndicatorSettings?.currentTimeProvider;
380+
381+
@override
382+
int get viewStartHour => widget.startHour;
383+
384+
@override
385+
int get viewEndHour => widget.endHour;
386+
387+
@override
388+
double get viewHeightPerMinute => widget.heightPerMinute;
389+
390+
@override
391+
ZoomScrollController? get activeScrollController => _activeScrollController;
392+
393+
@override
394+
void onCurrentTimeJumped(double offset) {
395+
_lastScrollOffset = offset;
396+
_pageOffsets[_currentIndex] = offset;
397+
}
398+
358399
late List<WeekDays> _weekDays;
359400

360401
late int _startHour;
@@ -365,7 +406,8 @@ class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>> {
365406
@override
366407
void initState() {
367408
super.initState();
368-
_lastScrollOffset = _defaultPageOffset;
409+
_lastScrollOffset =
410+
widget.scrollToCurrentTime ? offsetForTime(currentTime) : _defaultPageOffset;
369411

370412
_startHour = widget.startHour;
371413
_endHour = widget.endHour;
@@ -389,6 +431,12 @@ class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>> {
389431
_fullDayHeaderTitle = widget.fullDayHeaderTitle;
390432
_fullDayHeaderTextConfig =
391433
widget.fullDayHeaderTextConfig ?? FullDayHeaderTextConfig();
434+
435+
if (widget.scrollToCurrentTime) {
436+
WidgetsBinding.instance.addPostFrameCallback(
437+
(_) => scrollToCurrentTimeAfterLayout(),
438+
);
439+
}
392440
}
393441

394442
@override
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) 2021 Simform Solutions. All rights reserved.
2+
// Use of this source code is governed by a MIT-style license
3+
// that can be found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
7+
import 'zoom_scroll_controller.dart';
8+
9+
/// Provides scroll-to-current-time functionality shared by DayView, WeekView,
10+
/// and MultiDayView state classes.
11+
mixin ScrollToCurrentTimeMixin<W extends StatefulWidget> on State<W> {
12+
// --- Abstract interface (implemented by the host state) ---
13+
14+
/// Returns the [currentTimeProvider] from the view's
15+
/// [LiveTimeIndicatorSettings], or null to use [DateTime.now].
16+
DateTime Function()? get currentTimeProvider;
17+
18+
/// First hour shown in the timeline (0–23).
19+
int get viewStartHour;
20+
21+
/// Last hour shown in the timeline (1–24).
22+
int get viewEndHour;
23+
24+
/// Pixels per minute used to calculate scroll offsets.
25+
double get viewHeightPerMinute;
26+
27+
/// Currently active scroll controller, or null if not yet attached.
28+
ZoomScrollController? get activeScrollController;
29+
30+
/// Called by [jumpToCurrentTime] so the host can persist the new offset
31+
/// (e.g. update _lastScrollOffset and _pageOffsets[_currentIndex]).
32+
void onCurrentTimeJumped(double offset);
33+
34+
// --- Shared implementation ---
35+
36+
int _currentTimeScrollAttempts = 0;
37+
38+
/// Current time, honoring [currentTimeProvider] when provided.
39+
DateTime get currentTime => currentTimeProvider?.call() ?? DateTime.now();
40+
41+
/// Top-aligned pixel offset of [time] within the visible timeline range.
42+
///
43+
/// Accounts for [viewStartHour]/[viewEndHour] and clamps the result so that
44+
/// times outside the range map to the nearest edge.
45+
double offsetForTime(DateTime time) {
46+
final minutesFromStart = (time.hour - viewStartHour) * 60 + time.minute;
47+
final visibleMinutes = (viewEndHour - viewStartHour) * 60;
48+
final clampedMinutes = minutesFromStart.clamp(0, visibleMinutes);
49+
return viewHeightPerMinute * clampedMinutes;
50+
}
51+
52+
double? _currentTimeScrollOffset({required bool center}) {
53+
final controller = activeScrollController;
54+
if (controller == null || !controller.hasClients) return null;
55+
final position = controller.position;
56+
var offset = offsetForTime(currentTime);
57+
if (center) offset -= position.viewportDimension / 2;
58+
return offset.clamp(
59+
position.minScrollExtent,
60+
position.maxScrollExtent,
61+
);
62+
}
63+
64+
/// Centers the current time once the scrollable is ready.
65+
///
66+
/// Retries for a few frames if the controller is not attached yet, then
67+
/// gives up to avoid an endless frame-scheduling loop.
68+
void scrollToCurrentTimeAfterLayout() {
69+
if (!mounted) return;
70+
final controller = activeScrollController;
71+
if (controller == null || !controller.hasClients) {
72+
if (_currentTimeScrollAttempts++ >= 5) return;
73+
WidgetsBinding.instance.addPostFrameCallback(
74+
(_) => scrollToCurrentTimeAfterLayout(),
75+
);
76+
return;
77+
}
78+
jumpToCurrentTime();
79+
}
80+
81+
/// Instantly positions the timeline so the current time is visible.
82+
///
83+
/// When [center] is true (default) the current time is placed at the
84+
/// vertical center of the viewport; otherwise it aligns to the top.
85+
/// Does nothing if the scrollable is not yet attached.
86+
void jumpToCurrentTime({bool center = true}) {
87+
final offset = _currentTimeScrollOffset(center: center);
88+
if (offset == null) return;
89+
onCurrentTimeJumped(offset);
90+
activeScrollController?.jumpTo(offset);
91+
}
92+
93+
/// Animates the timeline so that the current time becomes visible.
94+
///
95+
/// When [center] is true (default) the current time is positioned at the
96+
/// vertical center of the viewport; otherwise it aligns to the top. The
97+
/// target is clamped to the scrollable range. Does nothing if the view has
98+
/// not been laid out yet.
99+
Future<void> animateToCurrentTime({
100+
bool center = true,
101+
Duration duration = const Duration(milliseconds: 200),
102+
Curve curve = Curves.linear,
103+
}) async {
104+
final offset = _currentTimeScrollOffset(center: center);
105+
if (offset == null) return;
106+
final controller = activeScrollController;
107+
if (controller == null || !controller.hasClients) return;
108+
await controller.animateTo(offset, duration: duration, curve: curve);
109+
}
110+
}

0 commit comments

Comments
 (0)