Skip to content

Instantly share code, notes, and snippets.

@Miracle-Blue
Last active February 10, 2025 05:29
Show Gist options
  • Save Miracle-Blue/34beab7246b61634db7d78d8053e89ab to your computer and use it in GitHub Desktop.
Save Miracle-Blue/34beab7246b61634db7d78d8053e89ab to your computer and use it in GitHub Desktop.
Flutter custom simple calendar
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