Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- 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)
- Fixed `onlyShowToday` parameter in `WeekView` to update `liveTimeIndicator` properly. [#518](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/518)
- 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)
- 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)

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

Expand Down
3 changes: 3 additions & 0 deletions doc/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ DayView(
backgroundColor: Colors.white, // Background color of day view
showLiveTimeLineInAllDays: true, // Display live time line in all pages
scrollOffset: 0, // Initial scroll position
scrollToCurrentTime: true, // Auto-center the timeline on the current time
width: 400, // Width of day view page
timeLineOffset: 0, // Offset for timeline
// Event handling
Expand Down Expand Up @@ -424,6 +425,7 @@ WeekView(
),
// Scroll configuration
scrollOffset: 0.0,
scrollToCurrentTime: true, // Auto-center the timeline on the current time
scrollPhysics: ScrollPhysics(), // ScrollPhysics for vertical scrolling
pageViewPhysics: ScrollPhysics(), // ScrollPhysics for page view
keepScrollOffset: true, // Maintain scroll offset when the page changes
Expand Down Expand Up @@ -543,6 +545,7 @@ MultiDayView(
),
// Scroll configuration
scrollOffset: 0.0,
scrollToCurrentTime: true, // Auto-center the timeline on the current time
scrollPhysics: ScrollPhysics(), // ScrollPhysics for vertical scrolling
pageViewPhysics: ScrollPhysics(), // ScrollPhysics for page view
keepScrollOffset: true, // Maintain scroll offset when the page changes
Expand Down
2 changes: 1 addition & 1 deletion example/lib/widgets/day_view_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class DayViewWidget extends StatelessWidget {
return DayView(
key: state,
width: width,
startDuration: Duration(hours: 8),
scrollToCurrentTime: true,
Comment thread
lavigarg-simform marked this conversation as resolved.
showHalfHours: true,
heightPerMinute: 3,
timeLineBuilder: (date) => _timeLineBuilder(date, isLtr),
Expand Down
1 change: 1 addition & 0 deletions example/lib/widgets/multi_day_view_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class MultiDayViewWidget extends StatelessWidget {
daysInView: 3,
width: width,
showLiveTimeLineInAllDays: true,
scrollToCurrentTime: true,
eventArranger: SideEventArranger(maxWidth: 30),
timeLineWidth: 65,
scrollPhysics: const BouncingScrollPhysics(),
Expand Down
8 changes: 2 additions & 6 deletions example/lib/widgets/week_view_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,15 @@ class WeekViewWidget extends StatelessWidget {
heightPerMinute: heightPerMinute,
showWeekends: true,
showMidnightHour: true,
scrollToCurrentTime: true,
showLiveTimeLineInAllDays: true,
keepScrollOffset: true,
timeSlotColorBuilder: (_, slotStartTime, __, ___) {
final hour = slotStartTime.hour;
final isBusinessHours = hour >= 9 && hour < 17;
final isLunchBreak = hour == 12;
final isWeekend =
slotStartTime.weekday == DateTime.saturday ||
slotStartTime.weekday == DateTime.sunday;

return isWeekend
? Colors.grey.shade100
: isLunchBreak
return isLunchBreak
? Colors.orange.shade100
: isBusinessHours
? Colors.green.shade50
Expand Down
53 changes: 51 additions & 2 deletions lib/src/day_view/day_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import '../../calendar_view.dart';
import '../constants.dart';
import '../extensions.dart';
import '../painters.dart';
import '../scroll_to_current_time_mixin.dart';
import '../zoom_scroll_controller.dart';
import '_internal_day_view_page.dart';

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

/// When true, the timeline automatically scrolls so that the current time is
/// centered in the viewport on the first build.
///
/// The current time honors [LiveTimeIndicatorSettings.currentTimeProvider]
/// when provided and is clamped to the visible [startHour]/[endHour] range.
/// This takes precedence over [scrollOffset] and [startDuration] for the
/// initial scroll position.
///
/// To trigger this imperatively at any time (e.g. from a "Now" button) use
/// [DayViewState.animateToCurrentTime] or [DayViewState.jumpToCurrentTime].
///
/// Default value is false.
final bool scrollToCurrentTime;

/// This method will be called when user taps on timestamp in timeline.
///
/// Called when user taps on a time value in the timeline (left side of view).
Expand Down Expand Up @@ -327,6 +342,7 @@ class DayView<T extends Object?> extends StatefulWidget {
this.verticalLineOffset = 10,
this.backgroundColor,
this.scrollOffset,
this.scrollToCurrentTime = false,
this.onEventTap,
this.onEventLongTap,
this.onDateLongPress,
Expand Down Expand Up @@ -381,7 +397,8 @@ class DayView<T extends Object?> extends StatefulWidget {
DayViewState<T> createState() => DayViewState<T>();
}

class DayViewState<T extends Object?> extends State<DayView<T>> {
class DayViewState<T extends Object?> extends State<DayView<T>>
with ScrollToCurrentTimeMixin<DayView<T>> {
/// Width of the Day View widget in pixels.
/// Calculated from widget width or device constraint width.
late double _width;
Expand Down Expand Up @@ -504,6 +521,30 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
return controller;
}

// --- ScrollToCurrentTimeMixin implementation ---

@override
DateTime Function()? get currentTimeProvider =>
widget.liveTimeIndicatorSettings?.currentTimeProvider;

@override
int get viewStartHour => widget.startHour;

@override
int get viewEndHour => widget.endHour;

@override
double get viewHeightPerMinute => widget.heightPerMinute;

@override
ZoomScrollController? get activeScrollController => _activeScrollController;

@override
void onCurrentTimeJumped(double offset) {
_lastScrollOffset = offset;
_pageOffsets[_currentIndex] = offset;
}

/// Callback function triggered when the controller changes or events are modified.
/// Used to rebuild the view when event data changes.
late VoidCallback _reloadCallback;
Expand All @@ -515,7 +556,9 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
@override
void initState() {
super.initState();
_lastScrollOffset = _defaultPageOffset;
_lastScrollOffset = widget.scrollToCurrentTime
? offsetForTime(currentTime)
: _defaultPageOffset;

_reloadCallback = _reload;
_setDateRange();
Expand All @@ -529,6 +572,12 @@ class DayViewState<T extends Object?> extends State<DayView<T>> {
_pageOffsets[_currentIndex] = _lastScrollOffset;
_eventArranger = widget.eventArranger ?? SideEventArranger<T>();
_assignBuilders();

if (widget.scrollToCurrentTime) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => scrollToCurrentTimeAfterLayout(),
);
}
}

@override
Expand Down
53 changes: 51 additions & 2 deletions lib/src/multi_day_view/multi_day_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../../calendar_view.dart';
import '../constants.dart';
import '../extensions.dart';
import '../painters.dart';
import '../scroll_to_current_time_mixin.dart';
import '../zoom_scroll_controller.dart';
import '_internal_multi_day_view_page.dart';

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

/// When true, the timeline automatically scrolls so that the current time is
/// centered in the viewport on the first build.
///
/// The current time honors [LiveTimeIndicatorSettings.currentTimeProvider]
/// when provided and is clamped to the visible [startHour]/[endHour] range.
/// This takes precedence over [scrollOffset] for the initial scroll position.
///
/// To trigger this imperatively at any time (e.g. from a "Now" button) use
/// [MultiDayViewState.animateToCurrentTime] or
/// [MultiDayViewState.jumpToCurrentTime].
///
/// Default value is false.
final bool scrollToCurrentTime;

/// This method will be called when user taps on timestamp in timeline.
final TimestampCallback? onTimestampTap;

Expand Down Expand Up @@ -241,6 +256,7 @@ class MultiDayView<T extends Object?> extends StatefulWidget {
this.backgroundColor,
this.scrollPhysics,
this.scrollOffset = 0.0,
this.scrollToCurrentTime = false,
this.onEventTap,
this.onEventLongTap,
this.onDateLongPress,
Expand Down Expand Up @@ -298,7 +314,8 @@ class MultiDayView<T extends Object?> extends StatefulWidget {
MultiDayViewState<T> createState() => MultiDayViewState<T>();
}

class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>> {
class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>>
with ScrollToCurrentTimeMixin<MultiDayView<T>> {
late double _width;
late double _height;
late double _timeLineWidth;
Expand Down Expand Up @@ -355,6 +372,30 @@ class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>> {
return controller;
}

// --- ScrollToCurrentTimeMixin implementation ---

@override
DateTime Function()? get currentTimeProvider =>
widget.liveTimeIndicatorSettings?.currentTimeProvider;

@override
int get viewStartHour => widget.startHour;

@override
int get viewEndHour => widget.endHour;

@override
double get viewHeightPerMinute => widget.heightPerMinute;

@override
ZoomScrollController? get activeScrollController => _activeScrollController;

@override
void onCurrentTimeJumped(double offset) {
_lastScrollOffset = offset;
_pageOffsets[_currentIndex] = offset;
}

late List<WeekDays> _weekDays;

late int _startHour;
Expand All @@ -365,7 +406,9 @@ class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>> {
@override
void initState() {
super.initState();
_lastScrollOffset = _defaultPageOffset;
_lastScrollOffset = widget.scrollToCurrentTime
? offsetForTime(currentTime)
: _defaultPageOffset;

_startHour = widget.startHour;
_endHour = widget.endHour;
Expand All @@ -389,6 +432,12 @@ class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>> {
_fullDayHeaderTitle = widget.fullDayHeaderTitle;
_fullDayHeaderTextConfig =
widget.fullDayHeaderTextConfig ?? FullDayHeaderTextConfig();

if (widget.scrollToCurrentTime) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => scrollToCurrentTimeAfterLayout(),
);
}
}

@override
Expand Down
117 changes: 117 additions & 0 deletions lib/src/scroll_to_current_time_mixin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) 2021 Simform Solutions. All rights reserved.
// Use of this source code is governed by a MIT-style license
// that can be found in the LICENSE file.

import 'package:flutter/material.dart';

import 'zoom_scroll_controller.dart';

/// Provides scroll-to-current-time functionality shared by DayView, WeekView,
/// and MultiDayView state classes.
mixin ScrollToCurrentTimeMixin<W extends StatefulWidget> on State<W> {
// --- Abstract interface (implemented by the host state) ---

/// Returns the [currentTimeProvider] from the view's
/// [LiveTimeIndicatorSettings], or null to use [DateTime.now].
DateTime Function()? get currentTimeProvider;

/// First hour shown in the timeline (0–23).
int get viewStartHour;

/// Last hour shown in the timeline (1–24).
int get viewEndHour;

/// Pixels per minute used to calculate scroll offsets.
double get viewHeightPerMinute;

/// Currently active scroll controller, or null if not yet attached.
ZoomScrollController? get activeScrollController;

/// Called by [jumpToCurrentTime] so the host can persist the new offset
/// (e.g. update _lastScrollOffset and _pageOffsets[_currentIndex]).
void onCurrentTimeJumped(double offset);

// --- Shared implementation ---

int _currentTimeScrollAttempts = 0;

/// Current time, honoring [currentTimeProvider] when provided.
DateTime get currentTime => currentTimeProvider?.call() ?? DateTime.now();

/// Top-aligned pixel offset of [time] within the visible timeline range.
///
/// Accounts for [viewStartHour]/[viewEndHour] and clamps the result so that
/// times outside the range map to the nearest edge.
double offsetForTime(DateTime time) {
final minutesFromStart = (time.hour - viewStartHour) * 60 + time.minute;
final visibleMinutes = (viewEndHour - viewStartHour) * 60;
// Guard against an inverted range (endHour < startHour). Asserts that are
// meant to prevent this are stripped in release builds, and clamp() throws
// when its lower bound exceeds the upper bound, so fail gracefully instead.
if (visibleMinutes <= 0) return 0;
final clampedMinutes = minutesFromStart.clamp(0, visibleMinutes);
return viewHeightPerMinute * clampedMinutes;
}

double? _currentTimeScrollOffset({required bool center}) {
final controller = activeScrollController;
if (controller == null || !controller.hasClients) return null;
final position = controller.position;
var offset = offsetForTime(currentTime);
if (center) offset -= position.viewportDimension / 2;
return offset.clamp(
position.minScrollExtent,
position.maxScrollExtent,
);
}

/// Centers the current time once the scrollable is ready.
///
/// Retries for a few frames if the controller is not attached yet, then
/// gives up to avoid an endless frame-scheduling loop.
void scrollToCurrentTimeAfterLayout() {
if (!mounted) return;
final controller = activeScrollController;
if (controller == null || !controller.hasClients) {
if (_currentTimeScrollAttempts++ >= 5) return;
WidgetsBinding.instance.addPostFrameCallback(
(_) => scrollToCurrentTimeAfterLayout(),
);
return;
}
jumpToCurrentTime();
}

/// Instantly positions the timeline so the current time is visible.
///
/// When [center] is true (default) the current time is placed at the
/// vertical center of the viewport; otherwise it aligns to the top.
/// Does nothing if the scrollable is not yet attached.
void jumpToCurrentTime({bool center = true}) {
final offset = _currentTimeScrollOffset(center: center);
if (offset == null) return;
onCurrentTimeJumped(offset);
activeScrollController?.jumpTo(offset);
}

/// Animates the timeline so that the current time becomes visible.
///
/// When [center] is true (default) the current time is positioned at the
/// vertical center of the viewport; otherwise it aligns to the top. The
/// target is clamped to the scrollable range. Does nothing if the view has
/// not been laid out yet.
Future<void> animateToCurrentTime({
bool center = true,
Duration duration = const Duration(milliseconds: 200),
Curve curve = Curves.linear,
}) async {
final offset = _currentTimeScrollOffset(center: center);
if (offset == null) return;
final controller = activeScrollController;
if (controller == null || !controller.hasClients) return;
// Persist the target so a rebuild mid-animation (which seeds the page from
// the stored offset) doesn't reset the position, matching jumpToCurrentTime.
onCurrentTimeJumped(offset);
await controller.animateTo(offset, duration: duration, curve: curve);
}
}
Loading