Last active
February 10, 2025 05:29
-
-
Save Miracle-Blue/34beab7246b61634db7d78d8053e89ab to your computer and use it in GitHub Desktop.
Flutter custom simple calendar
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:intl/intl.dart'; | |
/// {@template calendar} | |
/// CalendarView widget. | |
/// {@endtemplate} | |
class CalendarView extends StatefulWidget { | |
/// {@macro calendar} | |
const CalendarView({ | |
required this.selectedDate, | |
required this.onDatePressed, | |
super.key, // ignore: unused_element | |
}); | |
final DateTime selectedDate; | |
final void Function(DateTime) onDatePressed; | |
@override | |
State<CalendarView> createState() => _CalendarViewState(); | |
} | |
/// State for widget CalendarView. | |
class _CalendarViewState extends State<CalendarView> { | |
late final PageController _pageController; | |
late DateTime selectedDate; | |
late ValueNotifier<DateTime> _focusedDateForMonth; | |
final int minYear = 2020; | |
final int maxYear = 2030; | |
int _getPageIndex(DateTime date) => (date.year - minYear) * 12 + date.month - 1; | |
int _getYearFromPage(int page) => minYear + page ~/ 12; | |
int _getMonthFromPage(int page) => page % 12 + 1; | |
void _initPageController() { | |
_pageController = PageController( | |
initialPage: _getPageIndex(selectedDate), | |
); | |
} | |
void changeFocusedDate(int index) { | |
final year = _getYearFromPage(index); | |
final month = _getMonthFromPage(index); | |
_focusedDateForMonth.value = DateTime(year, month); | |
} | |
void changeSelectedDate(DateTime date) { | |
widget.onDatePressed(date); | |
setState(() { | |
selectedDate = date; | |
}); | |
} | |
/* #region Lifecycle */ | |
@override | |
void initState() { | |
super.initState(); | |
_focusedDateForMonth = ValueNotifier<DateTime>(widget.selectedDate); | |
selectedDate = widget.selectedDate; | |
_initPageController(); | |
} | |
@override | |
void dispose() { | |
_focusedDateForMonth.dispose(); | |
_pageController.dispose(); | |
super.dispose(); | |
} | |
/* #endregion */ | |
@override | |
Widget build(BuildContext context) => Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
// Header | |
SizedBox( | |
height: 40, | |
child: Row( | |
children: [ | |
ValueListenableBuilder( | |
valueListenable: _focusedDateForMonth, | |
builder: (context, value, child) => Text( | |
DateFormat.yMMMM().format(value).capitalize, | |
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), | |
), | |
), | |
const Spacer(), | |
IconButton( | |
onPressed: () { | |
final previousPage = _pageController.page!.round() - 1; | |
changeFocusedDate(previousPage); | |
_pageController.previousPage( | |
duration: const Duration(milliseconds: 1000), | |
curve: Curves.easeInOut, | |
); | |
}, | |
icon: const Icon( | |
Icons.arrow_back_ios_new, | |
size: 14, | |
color: Colors.black, | |
), | |
), | |
IconButton( | |
onPressed: () { | |
final nextPage = _pageController.page!.round() + 1; | |
changeFocusedDate(nextPage); | |
_pageController.nextPage( | |
duration: const Duration(milliseconds: 1000), | |
curve: Curves.easeInOut, | |
); | |
}, | |
icon: const Icon( | |
Icons.arrow_forward_ios, | |
size: 14, | |
color: Colors.black, | |
), | |
), | |
], | |
), | |
), | |
// Calendar | |
SizedBox( | |
height: MediaQuery.sizeOf(context).height < 500 ? 160 : 240, | |
child: PageView.builder( | |
controller: _pageController, | |
onPageChanged: changeFocusedDate, | |
itemBuilder: (context, index) => MonthGrid( | |
focusedDate: DateTime(_getYearFromPage(index), _getMonthFromPage(index)), | |
selectedDate: selectedDate, | |
onDatePressed: changeSelectedDate, | |
), | |
), | |
), | |
], | |
); | |
} | |
/// {@template calendar} | |
/// MonthGrid widget. | |
/// {@endtemplate} | |
class MonthGrid extends StatelessWidget { | |
/// {@macro calendar} | |
const MonthGrid({ | |
required this.focusedDate, | |
required this.selectedDate, | |
required this.onDatePressed, | |
this.weekDays = const ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], | |
super.key, // ignore: unused_element | |
}); | |
final DateTime focusedDate; | |
final DateTime selectedDate; | |
final void Function(DateTime) onDatePressed; | |
final List<String> weekDays; | |
@override | |
Widget build(BuildContext context) { | |
final daysInMonth = DateTime(focusedDate.year, focusedDate.month + 1, 0).day; | |
final firstDayInMonth = DateTime(focusedDate.year, focusedDate.month, 1).weekday % 7; | |
return GridView.builder( | |
physics: const NeverScrollableScrollPhysics(), | |
itemCount: daysInMonth + firstDayInMonth + weekDays.length, | |
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( | |
crossAxisCount: 7, | |
mainAxisSpacing: 2, | |
crossAxisSpacing: 2, | |
childAspectRatio: MediaQuery.sizeOf(context).height < 500 ? 1.8 : 1.2, | |
), | |
itemBuilder: (context, index) { | |
if (index < weekDays.length) { | |
return Center( | |
child: Text( | |
weekDays[index], | |
style: TextStyle( | |
fontSize: MediaQuery.orientationOf(context) == Orientation.portrait ? 16 : 14, | |
fontWeight: FontWeight.bold, | |
), | |
), | |
); | |
} | |
index -= weekDays.length; | |
if (index < firstDayInMonth) return const SizedBox(); | |
final isSelectedDate = selectedDate.year == focusedDate.year && | |
selectedDate.month == focusedDate.month && | |
selectedDate.day == index - firstDayInMonth + 1; | |
return Center( | |
child: DecoratedBox( | |
decoration: BoxDecoration( | |
shape: BoxShape.circle, | |
color: isSelectedDate ? Theme.of(context).colorScheme.primary : null, | |
), | |
child: ClipRRect( | |
borderRadius: const BorderRadius.all(Radius.circular(100)), | |
child: InkWell( | |
borderRadius: const BorderRadius.all(Radius.circular(100)), | |
splashColor: Colors.transparent, | |
onTap: () => onDatePressed(DateTime(focusedDate.year, focusedDate.month, index - firstDayInMonth + 1)), | |
child: Center( | |
child: Text( | |
'${index - firstDayInMonth + 1}', | |
style: TextStyle( | |
color: isSelectedDate ? Colors.white : Colors.black, | |
fontWeight: FontWeight.w500, | |
fontSize: MediaQuery.orientationOf(context) == Orientation.portrait ? 16 : 14, | |
), | |
), | |
), | |
), | |
), | |
), | |
); | |
}, | |
); | |
} | |
} | |
extension StringExtension on String { | |
String get capitalize => this[0].toUpperCase() + substring(1).toLowerCase(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment