Skip to content

Commit de39e80

Browse files
✨ Add timeSlotColorBuilder in MultiDayView and extract common logic
1 parent 6f1bb12 commit de39e80

8 files changed

Lines changed: 255 additions & 121 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
- Fixed `MonthViewBuilder` to be generic for improved type safety in `MonthView`. [#524](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/524)
44
- Added `DividerSettings` to customize the dividers in `WeekView` and `MultiDayView`. [#374](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/374), [#430](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/430), [#498](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/498)
5-
- Added `timeSlotColorBuilder` in `DayView` and `WeekView` to customize background color. [#470](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/470)
5+
- Added `timeSlotColorBuilder` in `DayView`, `WeekView` and `MultiDayView` to customize background color. [#470](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/470), [#535](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/535)
66
- Added `pageDate` parameter for timeline label customization with current timestamp fallback. [#527](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/527)
77
- Added `selectedDate` control to `MonthView` for external date management. [#233](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/233)
88
- [BREAKING] Added `isSelected` parameter to `CellBuilder` typedef in `MonthView`. Custom cell builders must be updated to accept this new parameter.

example/lib/widgets/multi_day_view_widget.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ class MultiDayViewWidget extends StatelessWidget {
2828
thickness: 0.5,
2929
height: 0.5,
3030
),
31+
timeSlotColorBuilder: (_, slotStartTime, __, ___) {
32+
final hour = slotStartTime.hour;
33+
final isBusinessHours = hour >= 9 && hour < 17;
34+
final isLunchBreak = hour == 12;
35+
36+
return isLunchBreak
37+
? Colors.orange.shade100
38+
: isBusinessHours
39+
? Colors.green.shade50
40+
: Colors.transparent;
41+
},
3142
onTimestampTap: (date) {
3243
SnackBar snackBar = SnackBar(
3344
content: Text("On tap: ${date.hour} Hr : ${date.minute} Min"),

lib/src/components/common_components.dart

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../calendar_event_data.dart';
88
import '../constants.dart';
99
import '../enumerations.dart';
1010
import '../extensions.dart';
11+
import '../painters.dart';
1112
import '../typedefs.dart';
1213
import 'components.dart';
1314

@@ -114,3 +115,109 @@ class DefaultEventTile<T> extends StatelessWidget {
114115
}
115116
}
116117
}
118+
119+
/// Renders time-slot colored backgrounds for calendar views.
120+
///
121+
/// Accepts one or more [dates] (columns). For a single-day view pass a
122+
/// one-element list; for week/multi-day views pass all visible dates.
123+
/// Each column is painted with per-slot colors returned by
124+
/// [timeSlotColorBuilder] and wrapped in a [RepaintBoundary] so repaints
125+
/// are isolated to individual columns.
126+
class TimeSlotBackgrounds extends StatelessWidget {
127+
/// Dates to render (one column per date).
128+
final List<DateTime> dates;
129+
130+
/// Pixel width of each date column.
131+
final double columnWidth;
132+
133+
/// Total pixel height of the scrollable area.
134+
final double height;
135+
136+
/// Pixel height per minute used to compute slot height.
137+
final double heightPerMinute;
138+
139+
/// Duration of each time slot.
140+
final MinuteSlotSize minuteSlotSize;
141+
142+
/// First hour shown in the view.
143+
final int startHour;
144+
145+
/// Last hour shown in the view.
146+
final int endHour;
147+
148+
/// Callback that returns a background [Color] for each slot.
149+
final TimeSlotColorBuilder timeSlotColorBuilder;
150+
151+
const TimeSlotBackgrounds({
152+
Key? key,
153+
required this.dates,
154+
required this.columnWidth,
155+
required this.height,
156+
required this.heightPerMinute,
157+
required this.minuteSlotSize,
158+
required this.startHour,
159+
required this.endHour,
160+
required this.timeSlotColorBuilder,
161+
}) : super(key: key);
162+
163+
@override
164+
Widget build(BuildContext context) {
165+
// Number of minutes each slot occupies (e.g. 15, 30, or 60).
166+
final slotMinutes = minuteSlotSize.minutes;
167+
// Pixel height of a single time slot rectangle.
168+
final heightPerSlot = heightPerMinute * slotMinutes;
169+
// Total number of slots that fit between startHour and endHour.
170+
final totalSlots = ((endHour - startHour) * 60) ~/ slotMinutes;
171+
// Convenience Duration used when computing slot start/end DateTimes.
172+
final slotDuration = Duration(minutes: slotMinutes);
173+
// Whether the ambient text direction is left-to-right.
174+
// Used to align the background layer correctly in RTL layouts.
175+
final isLtr = Directionality.of(context) == TextDirection.ltr;
176+
177+
return Align(
178+
alignment: isLtr ? Alignment.centerRight : Alignment.centerLeft,
179+
child: SizedBox(
180+
width: columnWidth * dates.length,
181+
height: height,
182+
child: Row(
183+
children: List.generate(dates.length, (dayIndex) {
184+
final dayDate = dates[dayIndex];
185+
final dayStart = DateTime(
186+
dayDate.year,
187+
dayDate.month,
188+
dayDate.day,
189+
startHour,
190+
);
191+
final slotColors = List<Color>.generate(
192+
totalSlots,
193+
(slotIndex) {
194+
final slotStartTime = dayStart.add(slotDuration * slotIndex);
195+
final slotEndTime = slotStartTime.add(slotDuration);
196+
return timeSlotColorBuilder(
197+
dayDate,
198+
slotStartTime,
199+
slotEndTime,
200+
slotIndex,
201+
);
202+
},
203+
);
204+
return ClipRect(
205+
child: SizedBox(
206+
width: columnWidth,
207+
height: height,
208+
child: RepaintBoundary(
209+
child: CustomPaint(
210+
painter: TimeSlotBackgroundPainter(
211+
heightPerSlot: heightPerSlot,
212+
slotColors: slotColors,
213+
),
214+
),
215+
),
216+
),
217+
);
218+
}),
219+
),
220+
),
221+
);
222+
}
223+
}

lib/src/day_view/_internal_day_view_page.dart

Lines changed: 10 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
import 'package:flutter/material.dart';
66

77
import '../components/_internal_components.dart';
8+
import '../components/common_components.dart';
89
import '../components/event_scroll_notifier.dart';
910
import '../enumerations.dart';
1011
import '../event_arrangers/event_arrangers.dart';
1112
import '../event_controller.dart';
12-
import '../extensions.dart';
1313
import '../modals.dart';
1414
import '../painters.dart';
1515
import '../typedefs.dart';
@@ -230,59 +230,21 @@ class _InternalDayViewPageState<T extends Object?>
230230
///
231231
/// Returns a [Widget] rendering the colored background for all time slots.
232232
Widget _buildTimeSlotBackground() {
233-
// Extract the minute duration of each time slot (e.g., 15, 30, 60 minutes)
234-
final slotMinutes = widget.minuteSlotSize.minutes;
235-
// Calculate the pixel height occupied by one time slot
236-
final heightPerSlot = widget.heightPerMinute * slotMinutes;
237-
// Calculate the total number of time slots in the day view
238-
// based on start and end hours
239-
final totalSlots =
240-
((widget.endHour - widget.startHour) * 60) ~/ slotMinutes;
241-
final startDateTime = DateTime(
242-
widget.date.year,
243-
widget.date.month,
244-
widget.date.day,
245-
widget.startHour,
246-
);
247-
final slotDuration = Duration(minutes: slotMinutes);
248-
// Generate a list of colors for each time slot of the day
249-
final slotColors = List<Color>.generate(
250-
totalSlots,
251-
(slotIndex) {
252-
final slotStartTime = startDateTime.add(slotDuration * slotIndex);
253-
final slotEndTime = slotStartTime.add(slotDuration);
254-
return widget.timeSlotColorBuilder!(
255-
widget.date,
256-
slotStartTime,
257-
slotEndTime,
258-
slotIndex,
259-
);
260-
},
261-
);
262-
final direction = Directionality.of(context);
263233
// Calculate the width of the content area, excluding the time line and
264234
// hour indicator lines
265235
final contentWidth = widget.width -
266236
widget.timeLineWidth -
267237
widget.hourIndicatorSettings.offset -
268238
widget.verticalLineOffset;
269-
270-
return Align(
271-
alignment: direction == TextDirection.rtl
272-
? Alignment.centerLeft
273-
: Alignment.centerRight,
274-
child: SizedBox(
275-
width: contentWidth,
276-
height: widget.height,
277-
child: RepaintBoundary(
278-
child: CustomPaint(
279-
painter: TimeSlotBackgroundPainter(
280-
heightPerSlot: heightPerSlot,
281-
slotColors: slotColors,
282-
),
283-
),
284-
),
285-
),
239+
return TimeSlotBackgrounds(
240+
dates: [widget.date],
241+
columnWidth: contentWidth,
242+
height: widget.height,
243+
heightPerMinute: widget.heightPerMinute,
244+
minuteSlotSize: widget.minuteSlotSize,
245+
startHour: widget.startHour,
246+
endHour: widget.endHour,
247+
timeSlotColorBuilder: widget.timeSlotColorBuilder!,
286248
);
287249
}
288250

lib/src/multi_day_view/_internal_multi_day_view_page.dart

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:flutter/material.dart';
66

77
import '../components/_internal_components.dart';
8+
import '../components/common_components.dart';
89
import '../components/event_scroll_notifier.dart';
910
import '../components/week_view_components.dart';
1011
import '../enumerations.dart';
@@ -173,6 +174,9 @@ class InternalMultiDayViewPage<T extends Object?> extends StatefulWidget {
173174
/// This method will be called when user taps on timestamp in timeline.
174175
final TimestampCallback? onTimestampTap;
175176

177+
/// A callback for rendering custom time slot background colors.
178+
final TimeSlotColorBuilder? timeSlotColorBuilder;
179+
176180
/// A single page for week view.
177181
const InternalMultiDayViewPage(
178182
{Key? key,
@@ -224,7 +228,8 @@ class InternalMultiDayViewPage<T extends Object?> extends StatefulWidget {
224228
required this.multiDayViewScrollController,
225229
this.lastScrollOffset = 0.0,
226230
this.keepScrollOffset = false,
227-
this.showMutliDayBottomLine = true})
231+
this.showMutliDayBottomLine = true,
232+
this.timeSlotColorBuilder})
228233
: super(key: key);
229234

230235
@override
@@ -257,6 +262,25 @@ class _InternalMultiDayViewPageState<T extends Object?>
257262
widget.scrollListener(scrollController);
258263
}
259264

265+
/// Builds background layers for time slots in the multi-day view.
266+
/// Uses [timeSlotColorBuilder] to determine each slot's color and paints
267+
/// a grid of colored rectangles (one column per day, one row per slot).
268+
///
269+
/// Parameter: [filteredDates] — visible dates for the page.
270+
/// Returns a [Widget] that paints the slot backgrounds.
271+
Widget _buildMultiDayTimeSlotBackgrounds(List<DateTime> filteredDates) {
272+
return TimeSlotBackgrounds(
273+
dates: filteredDates,
274+
columnWidth: widget.weekTitleWidth,
275+
height: widget.height,
276+
heightPerMinute: widget.heightPerMinute,
277+
minuteSlotSize: widget.minuteSlotSize,
278+
startHour: widget.startHour,
279+
endHour: widget.endHour,
280+
timeSlotColorBuilder: widget.timeSlotColorBuilder!,
281+
);
282+
}
283+
260284
@override
261285
Widget build(BuildContext context) {
262286
final filteredDates = _filteredDate();
@@ -372,6 +396,9 @@ class _InternalMultiDayViewPageState<T extends Object?>
372396
width: widget.width,
373397
child: Stack(
374398
children: [
399+
// Render time slot backgrounds if color builder is provided
400+
if (widget.timeSlotColorBuilder != null)
401+
_buildMultiDayTimeSlotBackgrounds(filteredDates),
375402
CustomPaint(
376403
size: Size(widget.width, widget.height),
377404
painter: widget.hourLinePainter(

lib/src/multi_day_view/multi_day_view.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ class MultiDayView<T extends Object?> extends StatefulWidget {
208208
/// Display workday bottom line
209209
final bool showWeekDayBottomLine;
210210

211+
/// A callback that resolves slot background color for each visible time slot.
212+
/// Useful for highlighting unavailable hours, business hours, or blocked time.
213+
final TimeSlotColorBuilder? timeSlotColorBuilder;
214+
211215
/// Main widget for week view.
212216
const MultiDayView({
213217
Key? key,
@@ -268,6 +272,7 @@ class MultiDayView<T extends Object?> extends StatefulWidget {
268272
this.onTimestampTap,
269273
this.daysInView = 3,
270274
this.showWeekDayBottomLine = true,
275+
this.timeSlotColorBuilder,
271276
}) : assert(!(onHeaderTitleTap != null && weekPageHeaderBuilder != null),
272277
"can't use [onHeaderTitleTap] & [weekPageHeaderBuilder] simultaneously"),
273278
assert((timeLineOffset) >= 0,
@@ -322,6 +327,8 @@ class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>> {
322327
late HourIndicatorSettings _quarterHourIndicatorSettings;
323328
late DividerSettings _dividerSettings;
324329

330+
late TimeSlotColorBuilder? _timeSlotColorBuilder;
331+
325332
late PageController _pageController;
326333

327334
late DateWidgetBuilder _timeLineBuilder;
@@ -550,6 +557,7 @@ class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>> {
550557
scrollPhysics: widget.scrollPhysics,
551558
scrollListener: _scrollPageListener,
552559
keepScrollOffset: widget.keepScrollOffset,
560+
timeSlotColorBuilder: _timeSlotColorBuilder,
553561
),
554562
);
555563
},
@@ -665,6 +673,7 @@ class MultiDayViewState<T extends Object?> extends State<MultiDayView<T>> {
665673
_fullDayEventBuilder =
666674
widget.fullDayEventBuilder ?? _defaultFullDayEventBuilder;
667675
_hourLinePainter = widget.hourLinePainter ?? _defaultHourLinePainter;
676+
_timeSlotColorBuilder = widget.timeSlotColorBuilder;
668677
}
669678

670679
Widget _defaultFullDayEventBuilder(

0 commit comments

Comments
 (0)