Skip to content

Commit 3746f5f

Browse files
committed
feat: add vertical week mode to WeekView for improved layout and scrolling
1 parent 0cc89e4 commit 3746f5f

9 files changed

Lines changed: 1599 additions & 17 deletions

File tree

example/lib/pages/week_view_page.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ class _WeekViewDemoState extends State<WeekViewDemo> {
6565
),
6666
],
6767
),
68-
body: WeekViewWidget(heightPerMinute: _heightPerMinute),
68+
body: WeekViewWidget(
69+
heightPerMinute: _heightPerMinute,
70+
enableVerticalWeekMode: true,
71+
),
6972
),
7073
);
7174
}

example/lib/widgets/add_event_form.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ class _AddOrEditEventFormState extends State<AddOrEditEventForm> {
132132
).applyDefaults(Theme.of(context).inputDecorationTheme),
133133
initialDateTime: _startDate,
134134
onSelect: (date) {
135+
final previousStartDate = _startDate.withoutTime;
136+
135137
if (date.withoutTime.withoutTime.isAfter(
136138
_endDate.withoutTime,
137139
)) {
@@ -142,6 +144,18 @@ class _AddOrEditEventFormState extends State<AddOrEditEventForm> {
142144
if (_isRecurring) {
143145
_endDate = _startDate;
144146

147+
if (mounted) {
148+
WidgetsBinding.instance.addPostFrameCallback((_) {
149+
setState(() {});
150+
});
151+
}
152+
} else if (_endDate.withoutTime.isAtSameMomentAs(
153+
previousStartDate,
154+
)) {
155+
// Keep single-day events aligned when the user changes
156+
// only start date and has not explicitly changed end date.
157+
_endDate = _startDate;
158+
145159
if (mounted) {
146160
WidgetsBinding.instance.addPostFrameCallback((_) {
147161
setState(() {});
@@ -521,6 +535,29 @@ class _AddOrEditEventFormState extends State<AddOrEditEventForm> {
521535

522536
_form.currentState?.save();
523537

538+
final hasOnlyOneTime =
539+
(_startTime == null && _endTime != null) ||
540+
(_startTime != null && _endTime == null);
541+
542+
if (hasOnlyOneTime) {
543+
ScaffoldMessenger.of(context).showSnackBar(
544+
const SnackBar(
545+
content: Text('Please select both start time and end time.'),
546+
),
547+
);
548+
return;
549+
}
550+
551+
if (_startTime != null &&
552+
_endTime != null &&
553+
_startDate.withoutTime.isAtSameMomentAs(_endDate.withoutTime) &&
554+
_endTime!.getTotalMinutes <= _startTime!.getTotalMinutes) {
555+
ScaffoldMessenger.of(context).showSnackBar(
556+
const SnackBar(content: Text('End time must be after start time.')),
557+
);
558+
return;
559+
}
560+
524561
DateTime? combinedStartTime;
525562
DateTime? combinedEndTime;
526563

example/lib/widgets/calendar_views.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ class CalendarViews extends StatelessWidget {
2626
color: AppColors.grey,
2727
child: Center(
2828
child: view == CalendarView.month
29-
? MonthViewWidget(width: width)
29+
? MonthViewWidget(width: width, enableVerticalMonthMode: true)
3030
: view == CalendarView.day
3131
? DayViewWidget(width: width)
32-
: WeekViewWidget(width: width),
32+
: WeekViewWidget(width: width, enableVerticalWeekMode: true),
3333
),
3434
);
3535
}

example/lib/widgets/month_view_widget.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ import '../pages/event_details_page.dart';
88
class MonthViewWidget extends StatefulWidget {
99
final GlobalKey<MonthViewState>? state;
1010
final double? width;
11+
final bool enableVerticalMonthMode;
1112

12-
const MonthViewWidget({super.key, this.state, this.width});
13+
const MonthViewWidget({
14+
super.key,
15+
this.state,
16+
this.width,
17+
this.enableVerticalMonthMode = false,
18+
});
1319

1420
@override
1521
State<MonthViewWidget> createState() => _MonthViewWidgetState();
@@ -34,6 +40,9 @@ class _MonthViewWidgetState extends State<MonthViewWidget> {
3440
MonthView(
3541
key: widget.state,
3642
width: widget.width,
43+
monthViewMode: widget.enableVerticalMonthMode
44+
? MonthViewMode.verticalMonth
45+
: MonthViewMode.standard,
3746
selectedDate: _selectedDate,
3847
multiDateSelectionRange: _multiSelectedDateRange,
3948
multiDateSelectionColor: Colors.blue.withValues(alpha: 0.1),

example/lib/widgets/week_view_widget.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,30 @@ import '../pages/event_details_page.dart';
66
class WeekViewWidget extends StatelessWidget {
77
final GlobalKey<WeekViewState>? state;
88
final double? width;
9+
final bool enableVerticalWeekMode;
910
final double heightPerMinute;
1011

1112
const WeekViewWidget({
1213
super.key,
1314
this.state,
1415
this.width,
1516
this.heightPerMinute = 1.0,
17+
this.enableVerticalWeekMode = false,
1618
});
1719

1820
@override
1921
Widget build(BuildContext context) {
2022
return WeekView(
2123
key: state,
2224
width: width,
25+
26+
// Set this to WeekViewMode.verticalWeek to enable:
27+
// 1) fixed days on the left,
28+
// 2) horizontal hour scrolling,
29+
// 3) vertical week paging.
30+
weekViewMode: enableVerticalWeekMode
31+
? WeekViewMode.verticalWeek
32+
: WeekViewMode.standard,
2333
heightPerMinute: heightPerMinute,
2434
showWeekends: true,
2535
showMidnightHour: true,
@@ -69,6 +79,7 @@ class WeekViewWidget extends StatelessWidget {
6979
SnackBar snackBar = SnackBar(content: Text("on LongTap"));
7080
ScaffoldMessenger.of(context).showSnackBar(snackBar);
7181
},
82+
weekTitleHeight: 72,
7283
);
7384
}
7485
}

lib/src/month_view/month_view.dart

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import 'package:flutter/material.dart';
77
import '../../calendar_view.dart';
88
import '../extensions.dart';
99

10+
/// Controls how [MonthView] pages are scrolled.
11+
enum MonthViewMode {
12+
/// Existing behavior: month pages scroll horizontally.
13+
standard,
14+
15+
/// Month pages scroll vertically.
16+
verticalMonth,
17+
}
18+
1019
class MonthView<T extends Object?> extends StatefulWidget {
1120
/// Main [Widget] to display month view.
1221
const MonthView({
@@ -19,6 +28,7 @@ class MonthView<T extends Object?> extends StatefulWidget {
1928
this.selectedDate,
2029
this.multiDateSelectionRange = const {},
2130
this.multiDateSelectionColor,
31+
this.monthViewMode = MonthViewMode.standard,
2232
}) : super(key: key);
2333

2434
/// A required parameters that controls events for month view.
@@ -58,6 +68,9 @@ class MonthView<T extends Object?> extends StatefulWidget {
5868
/// Color of the date cells selected via [MonthViewBuilders.onDateLongPressMoveUpdate]
5969
final Color? multiDateSelectionColor;
6070

71+
/// Controls scroll direction and layout behavior for the month pages.
72+
final MonthViewMode monthViewMode;
73+
6174
@override
6275
MonthViewState<T> createState() => MonthViewState<T>();
6376
}
@@ -217,21 +230,23 @@ class MonthViewState<T extends Object?> extends State<MonthView<T>> {
217230
super.dispose();
218231
}
219232

220-
void onHorizontalDragEnd(
233+
void _onBoundaryDragEnd(
221234
DragEndDetails dragEndDetails, {
222235
required bool isFirstPage,
223236
required bool isLastPage,
237+
required Axis scrollDirection,
224238
TextDirection textDirection = TextDirection.ltr,
225239
}) {
226240
final velocity = dragEndDetails.primaryVelocity ?? 0;
227241
if (velocity == 0) return;
228242

229243
final isRtl = textDirection == TextDirection.rtl;
230-
231-
// In LTR: swipe right (velocity > 0) = previous, swipe left (velocity < 0) = next
232-
// In RTL: swipe right (velocity > 0) = next, swipe left (velocity < 0) = previous
233-
final isSwipingToPrevious = isRtl ? velocity < 0 : velocity > 0;
234-
final isSwipingToNext = isRtl ? velocity > 0 : velocity < 0;
244+
final isSwipingToPrevious = scrollDirection == Axis.horizontal
245+
? (isRtl ? velocity < 0 : velocity > 0)
246+
: velocity > 0;
247+
final isSwipingToNext = scrollDirection == Axis.horizontal
248+
? (isRtl ? velocity > 0 : velocity < 0)
249+
: velocity < 0;
235250

236251
if (isFirstPage && isLastPage) {
237252
// Only one page - trigger both callbacks based on swipe direction
@@ -281,6 +296,9 @@ class MonthViewState<T extends Object?> extends State<MonthView<T>> {
281296
),
282297
Expanded(
283298
child: PageView.builder(
299+
scrollDirection: widget.monthViewMode == MonthViewMode.standard
300+
? Axis.horizontal
301+
: Axis.vertical,
284302
controller: _pageController,
285303
physics: _isMultiDateSelectionInProgress
286304
? const NeverScrollableScrollPhysics()
@@ -370,13 +388,29 @@ class MonthViewState<T extends Object?> extends State<MonthView<T>> {
370388
final isFirstPage = index == 0;
371389
final isLastPage = index == _totalMonths - 1;
372390
if (isFirstPage || isLastPage) {
391+
final axis =
392+
widget.monthViewMode == MonthViewMode.standard
393+
? Axis.horizontal
394+
: Axis.vertical;
373395
return GestureDetector(
374-
onHorizontalDragEnd: (details) => onHorizontalDragEnd(
375-
details,
376-
isFirstPage: isFirstPage,
377-
isLastPage: isLastPage,
378-
textDirection: textDirection,
379-
),
396+
onHorizontalDragEnd: axis == Axis.horizontal
397+
? (details) => _onBoundaryDragEnd(
398+
details,
399+
isFirstPage: isFirstPage,
400+
isLastPage: isLastPage,
401+
scrollDirection: axis,
402+
textDirection: textDirection,
403+
)
404+
: null,
405+
onVerticalDragEnd: axis == Axis.vertical
406+
? (details) => _onBoundaryDragEnd(
407+
details,
408+
isFirstPage: isFirstPage,
409+
isLastPage: isLastPage,
410+
scrollDirection: axis,
411+
textDirection: textDirection,
412+
)
413+
: null,
380414
child: monthPageContent,
381415
);
382416
}

0 commit comments

Comments
 (0)