diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..82082166
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Report unexpected behavior
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To reproduce**
+Please include a short code sample that can be used to reproduce the problem.
+
+Code sample
+
+
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Output of `flutter doctor`**
+Paste the result of this command here.
+
+Output
+
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..876ae2a9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for table_calendar
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.gitignore b/.gitignore
index 446ed0d1..6395d38a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,70 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
.DS_Store
-.dart_tool/
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
.packages
+.pub-cache/
.pub/
-
build/
-ios/.generated/
-ios/Flutter/Generated.xcconfig
-ios/Runner/GeneratedPluginRegistrant.*
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/ephemeral
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml
deleted file mode 100644
index 98ceccb1..00000000
--- a/.idea/libraries/Dart_SDK.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index dcbe982e..00000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
deleted file mode 100644
index 5b3388cc..00000000
--- a/.idea/workspace.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.metadata b/.metadata
index 6209bc9f..30b175a6 100644
--- a/.metadata
+++ b/.metadata
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
- revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b
- channel: stable
+ revision: f30b7f4db93ee747cd727df747941a28ead25ff5
+ channel: beta
project_type: package
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 3287bb67..4566494e 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -5,9 +5,24 @@
"version": "0.2.0",
"configurations": [
{
- "name": "Flutter",
+ "name": "example",
+ "cwd": "example",
"request": "launch",
"type": "dart"
+ },
+ {
+ "name": "example (profile mode)",
+ "cwd": "example",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "example (release mode)",
+ "cwd": "example",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
}
]
}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..8d4d525e
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "dart.lineLength": 80,
+}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e025dc0..af61d0b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,109 @@
+## [3.2.0]
+
+* Added loadEventsForDisabledDays property to enable loading events for disabled days as well
+* Fixed empty weekendDays assertion issue
+* Updated intl version to 0.20.0
+
+## [3.1.3]
+
+* Updated gradle config for example project
+* Added and applied lint rules, refactored code
+
+## [3.1.2]
+
+* Added dayTextFormatter property to CalendarStyle that allows to customize the text within day cells
+* Reverted the default day cell's text formatting to just the day's number
+
+## [3.1.1]
+
+* Added cell text localization based on current locale
+
+## [3.1.0]
+
+* Upgraded to Dart 3
+* Updated intl version to 0.19.0
+
+## [3.0.9]
+
+* Updated intl version to 0.18.0
+* Added explicit android:exported value to AndroidManifest
+
+## [3.0.8]
+
+* Added tablePadding property to CalendarStyle
+
+## [3.0.7]
+
+* Added week numbering feature
+
+## [3.0.6]
+
+* Fixed issue with missing Flutter Web platform tag
+
+## [3.0.5]
+
+* Added a visual indicator to FormatButton
+* Header buttons are now platform-aware
+
+## [3.0.4]
+
+* Updated dependencies
+* Removed deprecated fields
+
+## [3.0.3]
+
+* Added semantic label to prioritizedBuilder
+* Added tableBorder property to CalendarStyle
+* Added cellAlignment property to CalendarStyle
+* Added cellPadding property to CalendarStyle
+
+## [3.0.2]
+
+* Improved semantic labels for screen readers
+
+## [3.0.1]
+
+* Added pageAnimationEnabled property
+* Added currentDay property to improve widget testability
+
+## [3.0.0]
+
+* Migrated to null safety
+* Removed CalendarController
+* Improved horizontal scrolling
+* Improved widget performance
+* Improved documentation
+* Added date range selection
+* Added multiple date selection
+* Added selective CalendarBuilders
+* Added firstDay and lastDay scroll boundaries
+* Added shouldFillViewport property
+* Added sixWeekMonthsEnforced property
+* Added more options to customize calendar's behavior
+
+## [2.3.3]
+
+* Updated dependencies
+
+## [2.3.2]
+
+* Added previousPage and nextPage methods to CalendarController
+
+## [2.3.1]
+
+* Added chevron visibility properties to HeaderStyle
+* Added cellMargin property to CalendarStyle
+* Added eventDayStyle property to CalendarStyle
+* Added availableCalendarFormats dynamic update
+* Added optional BoxDecoration for each calendar row
+* Added optional BoxDecoration for days of week row
+
+## [2.3.0]
+
+* Migrated to AndroidX
+* Added holidays to onDaySelected callback
+* Replaced deprecated overflow property with clipBehavior
+
## [2.2.3]
* Added onCalendarCreated callback
diff --git a/README.md b/README.md
index 2d63eb40..67f8fe98 100644
--- a/README.md
+++ b/README.md
@@ -1,86 +1,201 @@
-# Table Calendar
+# TableCalendar
[](https://pub.dartlang.org/packages/table_calendar)
[](https://github.com/Solido/awesome-flutter)
-Highly customizable, feature-packed Flutter Calendar with gestures, animations and multiple formats.
+Highly customizable, feature-packed calendar widget for Flutter.
|  |  |
| :------------: | :------------: |
-| **Table Calendar** with custom styles | **Table Calendar** with Builders |
+| **TableCalendar** with custom styles | **TableCalendar** with custom builders |
## Features
* Extensive, yet easy to use API
-* Custom Builders for truly flexible UI
-* Complete programmatic control with CalendarController
-* Dynamic events
-* Interface for holidays
+* Preconfigured UI with customizable styling
+* Custom selective builders for unlimited UI design
* Locale support
-* Vertical autosizing
-* Beautiful animations
-* Gesture handling
-* Multiple Calendar formats
-* Multiple days of the week formats
-* Specifying available date range
-* Nice, configurable UI out of the box
+* Range selection support
+* Multiple selection support
+* Dynamic events and holidays
+* Vertical autosizing - fit the content, or fill the viewport
+* Multiple calendar formats (month, two weeks, week)
+* Horizontal swipe boundaries (first day, last day)
## Usage
-Make sure to check out [example project](https://github.com/aleksanderwozniak/table_calendar/tree/master/example).
-For additional info please refer to [API docs](https://pub.dartlang.org/documentation/table_calendar/latest/table_calendar/table_calendar-library.html).
+Make sure to check out [examples](https://github.com/aleksanderwozniak/table_calendar/tree/master/example/lib/pages) and [API docs](https://pub.dev/documentation/table_calendar/latest/) for more details.
### Installation
-Add to pubspec.yaml:
+Add the following line to `pubspec.yaml`:
```yaml
dependencies:
- table_calendar: ^2.2.3
+ table_calendar: ^3.2.0
```
-Then import it to your project:
+### Basic setup
+
+*The complete example is available [here](https://github.com/aleksanderwozniak/table_calendar/blob/master/example/lib/pages/basics_example.dart).*
+
+**TableCalendar** requires you to provide `firstDay`, `lastDay` and `focusedDay`:
+* `firstDay` is the first available day for the calendar. Users will not be able to access days before it.
+* `lastDay` is the last available day for the calendar. Users will not be able to access days after it.
+* `focusedDay` is the currently targeted day. Use this property to determine which month should be currently visible.
```dart
-import 'package:table_calendar/table_calendar.dart';
+TableCalendar(
+ firstDay: DateTime.utc(2010, 10, 16),
+ lastDay: DateTime.utc(2030, 3, 14),
+ focusedDay: DateTime.now(),
+);
```
-And finally create the **TableCalendar** with a `CalendarController`:
+#### Adding interactivity
+
+You will surely notice that previously set up calendar widget isn't quite interactive - you can only swipe it horizontally, to change the currently visible month. While it may be sufficient in certain situations, you can easily bring it to life by specifying a couple of callbacks.
+
+Adding the following code to the calendar widget will allow it to respond to user's taps, marking the tapped day as selected:
```dart
-@override
-void initState() {
- super.initState();
- _calendarController = CalendarController();
-}
+selectedDayPredicate: (day) {
+ return isSameDay(_selectedDay, day);
+},
+onDaySelected: (selectedDay, focusedDay) {
+ setState(() {
+ _selectedDay = selectedDay;
+ _focusedDay = focusedDay; // update `_focusedDay` here as well
+ });
+},
+```
+
+In order to dynamically update visible calendar format, add those lines to the widget:
+
+```dart
+calendarFormat: _calendarFormat,
+onFormatChanged: (format) {
+ setState(() {
+ _calendarFormat = format;
+ });
+},
+```
+
+Those two changes will make the calendar interactive and responsive to user's input.
+
+#### Updating focusedDay
+
+Setting `focusedDay` to a static value means that whenever **TableCalendar** widget rebuilds, it will use that specific `focusedDay`. You can quickly test it by using hot reload: set `focusedDay` to `DateTime.now()`, swipe to next month and trigger a hot reload - the calendar will "reset" to its initial state. To prevent this from happening, you should store and update `focusedDay` whenever any callback exposes it.
+
+Add this one callback to complete the basic setup:
+
+```dart
+onPageChanged: (focusedDay) {
+ _focusedDay = focusedDay;
+},
+```
+
+It is worth noting that you don't need to call `setState()` inside `onPageChanged()` callback. You should just update the stored value, so that if the widget gets rebuilt later on, it will use the proper `focusedDay`.
+
+*The complete example is available [here](https://github.com/aleksanderwozniak/table_calendar/blob/master/example/lib/pages/basics_example.dart). You can find other examples [here](https://github.com/aleksanderwozniak/table_calendar/tree/master/example/lib/pages).*
-@override
-void dispose() {
- _calendarController.dispose();
- super.dispose();
+### Events
+
+*The complete example is available [here](https://github.com/aleksanderwozniak/table_calendar/blob/master/example/lib/pages/events_example.dart).*
+
+You can supply custom events to **TableCalendar** widget. To do so, use `eventLoader` property - you will be given a `DateTime` object, to which you need to assign a list of events.
+
+```dart
+eventLoader: (day) {
+ return _getEventsForDay(day);
+},
+```
+
+`_getEventsForDay()` can be of any implementation. For example, a `Map>` can be used:
+
+```dart
+List _getEventsForDay(DateTime day) {
+ return events[day] ?? [];
}
+```
+
+One thing worth remembering is that `DateTime` objects consist of both date and time parts. In many cases this time part is redundant for calendar related aspects.
+
+If you decide to use a `Map`, I suggest making it a `LinkedHashMap` - this will allow you to override equality comparison for two `DateTime` objects, comparing them just by their date parts:
+
+```dart
+final events = LinkedHashMap(
+ equals: isSameDay,
+ hashCode: getHashCode,
+)..addAll(eventSource);
+```
+
+#### Cyclic events
+
+`eventLoader` allows you to easily add events that repeat in a pattern. For example, this will add an event to every Monday:
+
+```dart
+eventLoader: (day) {
+ if (day.weekday == DateTime.monday) {
+ return [Event('Cyclic event')];
+ }
+
+ return [];
+},
+```
+
+#### Events selected on tap
-@override
-Widget build(BuildContext context) {
- return TableCalendar(
- calendarController: _calendarController,
- );
+Often times having a sublist of events that are selected by tapping on a day is desired. You can achieve that by using the same method you provided to `eventLoader` inside of `onDaySelected` callback:
+
+```dart
+void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
+ if (!isSameDay(_selectedDay, selectedDay)) {
+ setState(() {
+ _focusedDay = focusedDay;
+ _selectedDay = selectedDay;
+ _selectedEvents = _getEventsForDay(selectedDay);
+ });
+ }
}
```
+*The complete example is available [here](https://github.com/aleksanderwozniak/table_calendar/blob/master/example/lib/pages/events_example.dart).*
+
+### Custom UI with CalendarBuilders
+
+To customize the UI with your own widgets, use [CalendarBuilders](https://pub.dev/documentation/table_calendar/latest/table_calendar/CalendarBuilders-class.html). Each builder can be used to selectively override the UI, allowing you to implement highly specific designs with minimal hassle.
+
+You can return `null` from any builder to use the default style. For example, the following snippet will override only the Sunday's day of the week label (Sun), leaving other dow labels unchanged:
+
+```dart
+calendarBuilders: CalendarBuilders(
+ dowBuilder: (context, day) {
+ if (day.weekday == DateTime.sunday) {
+ final text = DateFormat.E().format(day);
+
+ return Center(
+ child: Text(
+ text,
+ style: TextStyle(color: Colors.red),
+ ),
+ );
+ }
+ },
+),
+```
+
### Locale
-**Table Calendar** supports locales. To display the Calendar in desired language, use `locale` property.
+To display the calendar in desired language, use `locale` property.
If you don't specify it, a default locale will be used.
#### Initialization
-Before you can use a locale, you need to initialize the i18n formatting.
-
-*This is independent of **Table Calendar** package, so I encourage you to do your own research.*
+Before you can use a locale, you might need to initialize date formatting.
A simple way of doing it is as follows:
-* First of all, add [intl](https://pub.dartlang.org/packages/intl) package to your pubspec.yaml file
+* First of all, add [intl](https://pub.dev/packages/intl) package to your pubspec.yaml file
* Then make modifications to your `main()`:
```dart
@@ -91,13 +206,13 @@ void main() {
}
```
-After those two steps your app should be ready to use **Table Calendar** with different languages.
+After those two steps your app should be ready to use **TableCalendar** with different languages.
#### Specifying a language
To specify a language, simply pass it as a String code to `locale` property.
-For example, this will make **Table Calendar** use Polish language:
+For example, this will make **TableCalendar** use Polish language:
```dart
TableCalendar(
@@ -109,57 +224,6 @@ TableCalendar(
| :------------: | :------------: | :------------: | :------------: |
| `'en_US'` | `'pl_PL'` | `'fr_FR'` | `'zh_CN'` |
-Note, that if you want to change the language of `FormatButton`'s text, you have to do this yourself. Use `availableCalendarFormats` property and pass the translated Strings there.
-Use i18n method of your choice.
+Note, that if you want to change the language of `FormatButton`'s text, you have to do this yourself. Use `availableCalendarFormats` property and pass the translated Strings there. Use i18n method of your choice.
You can also hide the button altogether by setting `formatButtonVisible` to false.
-
-### Holidays
-
-**Table Calendar** provides a simple interface for displaying holidays. Here are a few steps to follow:
-
-* Fetch a map of holidays tied to dates. You can search for it manually, or perhaps use some online API
-* Convert it to a proper format - note that these are lists of holidays, since one date could have a couple of holidays:
-```dart
-{
- `DateTime A`: [`Holiday A1`, `Holiday A2`, ...],
- `DateTime B`: [`Holiday B1`, `Holiday B2`, ...],
- ...
-}
-```
-* Link it to **Table Calendar**. Use `holidays` property
-
-And that's your basic setup! Now you can add some styling:
-
-* By using `CalendarStyle` properties: `holidayStyle` and `outsideHolidayStyle`
-* By using `CalendarBuilders` for complete UI control over calendar cell
-
-You can also add custom holiday markers thanks to improved marker API. Check out [example project](https://github.com/aleksanderwozniak/table_calendar/tree/master/example) for more details.
-
-```dart
-markersBuilder: (context, date, events, holidays) {
- final children = [];
-
- if (events.isNotEmpty) {
- children.add(
- Positioned(
- right: 1,
- bottom: 1,
- child: _buildEventsMarker(date, events),
- ),
- );
- }
-
- if (holidays.isNotEmpty) {
- children.add(
- Positioned(
- right: -2,
- top: -2,
- child: _buildHolidaysMarker(),
- ),
- );
- }
-
- return children;
-},
-```
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 00000000..d7855022
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,25 @@
+# This file configures the analyzer to use the lint rule set from `package:lint`
+
+# include: package:lint/strict.yaml # For production apps
+# include: package:lint/casual.yaml # For code samples, hackathons and other non-production code
+include: package:lint/package.yaml # Use this for packages with public API
+
+# You might want to exclude auto-generated files from dart analysis
+analyzer:
+ exclude:
+ #- '**.freezed.dart'
+ #- '**.g.dart'
+
+# You can customize the lint rules set to your own liking. A list of all rules
+# can be found at https://dart-lang.github.io/linter/lints/options/options.html
+linter:
+ rules:
+ # Util classes are awesome!
+ # avoid_classes_with_only_static_members: false
+
+ # Make constructors the first thing in every class
+ # sort_constructors_first: true
+
+ # Choose wisely, but you don't have to
+ # prefer_double_quotes: true
+ # prefer_single_quotes: true
diff --git a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
deleted file mode 100644
index d007606a..00000000
--- a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.flutter.plugins;
-
-import io.flutter.plugin.common.PluginRegistry;
-
-/**
- * Generated file. Do not edit.
- */
-public final class GeneratedPluginRegistrant {
- public static void registerWith(PluginRegistry registry) {
- if (alreadyRegisteredWith(registry)) {
- return;
- }
- }
-
- private static boolean alreadyRegisteredWith(PluginRegistry registry) {
- final String key = GeneratedPluginRegistrant.class.getCanonicalName();
- if (registry.hasPlugin(key)) {
- return true;
- }
- registry.registrarFor(key);
- return false;
- }
-}
diff --git a/example/.gitignore b/example/.gitignore
index 01f4ac79..b4e3d223 100644
--- a/example/.gitignore
+++ b/example/.gitignore
@@ -1,6 +1,5 @@
# Miscellaneous
*.class
-*.lock
*.log
*.pyc
*.swp
@@ -21,52 +20,20 @@
# Flutter/Dart/Pub related
**/doc/api/
+**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
+.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
-build/
+/build/
-# Android related
-**/android/**/gradle-wrapper.jar
-**/android/.gradle
-**/android/captures/
-**/android/gradlew
-**/android/gradlew.bat
-**/android/local.properties
-**/android/**/GeneratedPluginRegistrant.java
+# Web related
+lib/generated_plugin_registrant.dart
-# iOS/XCode related
-**/ios/**/*.mode1v3
-**/ios/**/*.mode2v3
-**/ios/**/*.moved-aside
-**/ios/**/*.pbxuser
-**/ios/**/*.perspectivev3
-**/ios/**/*sync/
-**/ios/**/.sconsign.dblite
-**/ios/**/.tags*
-**/ios/**/.vagrant/
-**/ios/**/DerivedData/
-**/ios/**/Icon?
-**/ios/**/Pods/
-**/ios/**/.symlinks/
-**/ios/**/profile
-**/ios/**/xcuserdata
-**/ios/.generated/
-**/ios/Flutter/App.framework
-**/ios/Flutter/Flutter.framework
-**/ios/Flutter/Generated.xcconfig
-**/ios/Flutter/app.flx
-**/ios/Flutter/app.zip
-**/ios/Flutter/flutter_assets/
-**/ios/Flutter/flutter_export_environment.sh
-**/ios/ServiceDefinitions.json
-**/ios/Runner/GeneratedPluginRegistrant.*
+# Symbolication related
+app.*.symbols
-# Exceptions to above rules.
-!**/ios/**/default.mode1v3
-!**/ios/**/default.mode2v3
-!**/ios/**/default.pbxuser
-!**/ios/**/default.perspectivev3
-!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
+# Obfuscation related
+app.*.map.json
diff --git a/example/.metadata b/example/.metadata
index 460bc20b..f6caaefd 100644
--- a/example/.metadata
+++ b/example/.metadata
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
- revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b
- channel: stable
+ revision: f30b7f4db93ee747cd727df747941a28ead25ff5
+ channel: beta
project_type: app
diff --git a/example/android/.gitignore b/example/android/.gitignore
new file mode 100644
index 00000000..0a741cb4
--- /dev/null
+++ b/example/android/.gitignore
@@ -0,0 +1,11 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 4ed2e254..60a550dc 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -1,3 +1,9 @@
+plugins {
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
}
}
-def flutterRoot = localProperties.getProperty('flutter.sdk')
-if (flutterRoot == null) {
- throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
-}
-
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@@ -21,11 +22,13 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
-apply plugin: 'com.android.application'
-apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
-
android {
- compileSdkVersion 28
+ compileSdkVersion 34
+ namespace "com.example.example"
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
lintOptions {
disable 'InvalidPackage'
@@ -34,11 +37,10 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example"
- minSdkVersion 16
- targetSdkVersion 28
+ minSdkVersion flutter.minSdkVersion
+ targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -48,14 +50,18 @@ android {
signingConfig signingConfigs.debug
}
}
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
}
flutter {
source '../..'
}
-dependencies {
- testImplementation 'junit:junit:4.12'
- androidTestImplementation 'com.android.support.test:runner:1.0.2'
- androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
-}
diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 00000000..c208884f
--- /dev/null
+++ b/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 1b515f8b..e8825bfa 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -1,39 +1,39 @@
-
-
-
-
-
+
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme"
+ />
+
+
diff --git a/example/android/app/src/main/java/com/example/example/MainActivity.java b/example/android/app/src/main/java/com/example/example/MainActivity.java
deleted file mode 100644
index 84f8920f..00000000
--- a/example/android/app/src/main/java/com/example/example/MainActivity.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.example;
-
-import android.os.Bundle;
-import io.flutter.app.FlutterActivity;
-import io.flutter.plugins.GeneratedPluginRegistrant;
-
-public class MainActivity extends FlutterActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- GeneratedPluginRegistrant.registerWith(this);
- }
-}
diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
new file mode 100644
index 00000000..e793a000
--- /dev/null
+++ b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.example
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 00000000..f74085f3
--- /dev/null
+++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 00000000..3db14bb5
--- /dev/null
+++ b/example/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml
index 00fa4417..1f83a33f 100644
--- a/example/android/app/src/main/res/values/styles.xml
+++ b/example/android/app/src/main/res/values/styles.xml
@@ -1,8 +1,18 @@
+
+
+
diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 00000000..c208884f
--- /dev/null
+++ b/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/example/android/build.gradle b/example/android/build.gradle
index bb8a3038..8f31e8ca 100644
--- a/example/android/build.gradle
+++ b/example/android/build.gradle
@@ -1,18 +1,7 @@
-buildscript {
- repositories {
- google()
- jcenter()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:3.2.1'
- }
-}
-
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
@@ -24,6 +13,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
-task clean(type: Delete) {
+tasks.register("clean", Delete) {
delete rootProject.buildDir
-}
+}
\ No newline at end of file
diff --git a/example/android/gradle.properties b/example/android/gradle.properties
index 7be3d8b4..a6738207 100644
--- a/example/android/gradle.properties
+++ b/example/android/gradle.properties
@@ -1,2 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
android.enableR8=true
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
index 2819f022..fe63c6c4 100644
--- a/example/android/gradle/wrapper/gradle-wrapper.properties
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
diff --git a/example/android/settings.gradle b/example/android/settings.gradle
index 5a2f14fb..9301c074 100644
--- a/example/android/settings.gradle
+++ b/example/android/settings.gradle
@@ -1,15 +1,25 @@
-include ':app'
+pluginManagement {
+ def flutterSdkPath = {
+ def properties = new Properties()
+ file("local.properties").withInputStream { properties.load(it) }
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ return flutterSdkPath
+ }()
-def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
-def plugins = new Properties()
-def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
-if (pluginsFile.exists()) {
- pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
}
-plugins.each { name, path ->
- def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
- include ":$name"
- project(":$name").projectDir = pluginDirectory
+plugins {
+ id "dev.flutter.flutter-plugin-loader" version "1.0.0"
+ id "com.android.application" version "8.4.1" apply false
+ id "org.jetbrains.kotlin.android" version "2.0.0" apply false
}
+
+include ":app"
\ No newline at end of file
diff --git a/example/ios/.gitignore b/example/ios/.gitignore
new file mode 100644
index 00000000..e96ef602
--- /dev/null
+++ b/example/ios/.gitignore
@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
index 9367d483..4f8d4d24 100644
--- a/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -3,7 +3,7 @@
CFBundleDevelopmentRegion
- en
+ $(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
App
CFBundleIdentifier
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 8.0
+ 11.0
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
index 5229fcae..bb9a61d7 100644
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -3,20 +3,13 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 46;
+ objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
- 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
- 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
- 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
- 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
- 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
- 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -29,8 +22,6 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
- 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
- 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -40,17 +31,13 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
- 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
- 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
- 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
- 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
- 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
@@ -62,8 +49,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
- 3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -73,10 +58,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
- 2D5378251FAA1A9400D5DBA9 /* flutter_assets */,
- 3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
- 9740EEBA1CF902C7004384FC /* Flutter.framework */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
@@ -90,7 +72,6 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
- CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
);
sourceTree = "";
};
@@ -105,27 +86,18 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
- 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
- 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
- 97C146F11CF9000F007C117D /* Supporting Files */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "";
};
- 97C146F11CF9000F007C117D /* Supporting Files */ = {
- isa = PBXGroup;
- children = (
- 97C146F21CF9000F007C117D /* main.m */,
- );
- name = "Supporting Files";
- sourceTree = "";
- };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -155,17 +127,18 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 0910;
- ORGANIZATIONNAME = "The Chromium Authors";
+ LastUpgradeCheck = 1300;
+ ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
- compatibilityVersion = "Xcode 3.2";
- developmentRegion = English;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
@@ -188,9 +161,7 @@
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
- 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
- 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -210,7 +181,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
@@ -233,8 +204,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
- 97C146F31CF9000F007C117D /* main.m in Sources */,
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -263,7 +233,6 @@
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -275,12 +244,14 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@@ -301,9 +272,10 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
@@ -314,8 +286,8 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = S8QB4VV633;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@@ -329,13 +301,14 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -347,12 +320,14 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@@ -379,7 +354,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -389,7 +364,6 @@
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -401,12 +375,14 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@@ -427,9 +403,11 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
@@ -440,6 +418,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -454,6 +433,9 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -463,6 +445,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -477,6 +460,8 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
index 1d526a16..919434a6 100644
--- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -2,6 +2,6 @@
+ location = "self:">
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 00000000..f9b0d7c5
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 786d6aad..3db53b6e 100644
--- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
@@ -46,7 +45,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
- language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 00000000..f9b0d7c5
--- /dev/null
+++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h
deleted file mode 100644
index 36e21bbf..00000000
--- a/example/ios/Runner/AppDelegate.h
+++ /dev/null
@@ -1,6 +0,0 @@
-#import
-#import
-
-@interface AppDelegate : FlutterAppDelegate
-
-@end
diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m
deleted file mode 100644
index 59a72e90..00000000
--- a/example/ios/Runner/AppDelegate.m
+++ /dev/null
@@ -1,13 +0,0 @@
-#include "AppDelegate.h"
-#include "GeneratedPluginRegistrant.h"
-
-@implementation AppDelegate
-
-- (BOOL)application:(UIApplication *)application
- didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
- [GeneratedPluginRegistrant registerWithRegistry:self];
- // Override point for customization after application launch.
- return [super application:application didFinishLaunchingWithOptions:launchOptions];
-}
-
-@end
diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 00000000..70693e4a
--- /dev/null
+++ b/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index 3d43d11e..dc9ada47 100644
Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist
index 0513117f..8ac112d9 100644
--- a/example/ios/Runner/Info.plist
+++ b/example/ios/Runner/Info.plist
@@ -3,7 +3,7 @@
CFBundleDevelopmentRegion
- en
+ $(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
@@ -11,7 +11,7 @@
CFBundleInfoDictionaryVersion
6.0
CFBundleName
- example
+ table_calendar example
CFBundlePackageType
APPL
CFBundleShortVersionString
@@ -41,5 +41,7 @@
UIViewControllerBasedStatusBarAppearance
+ CADisableMinimumFrameDurationOnPhone
+
diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 00000000..308a2a56
--- /dev/null
+++ b/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/example/ios/Runner/main.m b/example/ios/Runner/main.m
deleted file mode 100644
index dff6597e..00000000
--- a/example/ios/Runner/main.m
+++ /dev/null
@@ -1,9 +0,0 @@
-#import
-#import
-#import "AppDelegate.h"
-
-int main(int argc, char* argv[]) {
- @autoreleasepool {
- return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
- }
-}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index e812dbf1..6cb6ced5 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,342 +1,95 @@
-// Copyright (c) 2019 Aleksander Woźniak
-// Licensed under Apache License v2.0
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
-import 'package:table_calendar/table_calendar.dart';
-
-// Example holidays
-final Map _holidays = {
- DateTime(2019, 1, 1): ['New Year\'s Day'],
- DateTime(2019, 1, 6): ['Epiphany'],
- DateTime(2019, 2, 14): ['Valentine\'s Day'],
- DateTime(2019, 4, 21): ['Easter Sunday'],
- DateTime(2019, 4, 22): ['Easter Monday'],
-};
+import 'package:table_calendar_example/pages/basics_example.dart';
+import 'package:table_calendar_example/pages/complex_example.dart';
+import 'package:table_calendar_example/pages/events_example.dart';
+import 'package:table_calendar_example/pages/multi_example.dart';
+import 'package:table_calendar_example/pages/range_example.dart';
void main() {
- initializeDateFormatting().then((_) => runApp(MyApp()));
+ initializeDateFormatting().then((_) => runApp(const MyApp()));
}
class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
@override
Widget build(BuildContext context) {
return MaterialApp(
- title: 'Table Calendar Demo',
+ title: 'TableCalendar Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
- home: MyHomePage(title: 'Table Calendar Demo'),
+ home: const StartPage(),
);
}
}
-class MyHomePage extends StatefulWidget {
- MyHomePage({Key key, this.title}) : super(key: key);
-
- final String title;
+class StartPage extends StatefulWidget {
+ const StartPage({super.key});
@override
- _MyHomePageState createState() => _MyHomePageState();
+ State createState() => _StartPageState();
}
-class _MyHomePageState extends State with TickerProviderStateMixin {
- Map _events;
- List _selectedEvents;
- AnimationController _animationController;
- CalendarController _calendarController;
-
- @override
- void initState() {
- super.initState();
- final _selectedDay = DateTime.now();
-
- _events = {
- _selectedDay.subtract(Duration(days: 30)): ['Event A0', 'Event B0', 'Event C0'],
- _selectedDay.subtract(Duration(days: 27)): ['Event A1'],
- _selectedDay.subtract(Duration(days: 20)): ['Event A2', 'Event B2', 'Event C2', 'Event D2'],
- _selectedDay.subtract(Duration(days: 16)): ['Event A3', 'Event B3'],
- _selectedDay.subtract(Duration(days: 10)): ['Event A4', 'Event B4', 'Event C4'],
- _selectedDay.subtract(Duration(days: 4)): ['Event A5', 'Event B5', 'Event C5'],
- _selectedDay.subtract(Duration(days: 2)): ['Event A6', 'Event B6'],
- _selectedDay: ['Event A7', 'Event B7', 'Event C7', 'Event D7'],
- _selectedDay.add(Duration(days: 1)): ['Event A8', 'Event B8', 'Event C8', 'Event D8'],
- _selectedDay.add(Duration(days: 3)): Set.from(['Event A9', 'Event A9', 'Event B9']).toList(),
- _selectedDay.add(Duration(days: 7)): ['Event A10', 'Event B10', 'Event C10'],
- _selectedDay.add(Duration(days: 11)): ['Event A11', 'Event B11'],
- _selectedDay.add(Duration(days: 17)): ['Event A12', 'Event B12', 'Event C12', 'Event D12'],
- _selectedDay.add(Duration(days: 22)): ['Event A13', 'Event B13'],
- _selectedDay.add(Duration(days: 26)): ['Event A14', 'Event B14', 'Event C14'],
- };
-
- _selectedEvents = _events[_selectedDay] ?? [];
- _calendarController = CalendarController();
-
- _animationController = AnimationController(
- vsync: this,
- duration: const Duration(milliseconds: 400),
- );
-
- _animationController.forward();
- }
-
- @override
- void dispose() {
- _animationController.dispose();
- _calendarController.dispose();
- super.dispose();
- }
-
- void _onDaySelected(DateTime day, List events, List holidays) {
- print('CALLBACK: _onDaySelected');
- setState(() {
- _selectedEvents = events;
- });
- }
-
- void _onVisibleDaysChanged(DateTime first, DateTime last, CalendarFormat format) {
- print('CALLBACK: _onVisibleDaysChanged');
- }
-
- void _onCalendarCreated(DateTime first, DateTime last, CalendarFormat format) {
- print('CALLBACK: _onCalendarCreated');
- }
-
+class _StartPageState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
- title: Text(widget.title),
- ),
- body: Column(
- mainAxisSize: MainAxisSize.max,
- children: [
- // Switch out 2 lines below to play with TableCalendar's settings
- //-----------------------
- _buildTableCalendar(),
- // _buildTableCalendarWithBuilders(),
- const SizedBox(height: 8.0),
- _buildButtons(),
- const SizedBox(height: 8.0),
- Expanded(child: _buildEventList()),
- ],
- ),
- );
- }
-
- // Simple TableCalendar configuration (using Styles)
- Widget _buildTableCalendar() {
- return TableCalendar(
- calendarController: _calendarController,
- events: _events,
- holidays: _holidays,
- startingDayOfWeek: StartingDayOfWeek.monday,
- calendarStyle: CalendarStyle(
- selectedColor: Colors.deepOrange[400],
- todayColor: Colors.deepOrange[200],
- markersColor: Colors.brown[700],
- outsideDaysVisible: false,
- ),
- headerStyle: HeaderStyle(
- formatButtonTextStyle: TextStyle().copyWith(color: Colors.white, fontSize: 15.0),
- formatButtonDecoration: BoxDecoration(
- color: Colors.deepOrange[400],
- borderRadius: BorderRadius.circular(16.0),
- ),
- ),
- onDaySelected: _onDaySelected,
- onVisibleDaysChanged: _onVisibleDaysChanged,
- onCalendarCreated: _onCalendarCreated,
- );
- }
-
- // More advanced TableCalendar configuration (using Builders & Styles)
- Widget _buildTableCalendarWithBuilders() {
- return TableCalendar(
- locale: 'pl_PL',
- calendarController: _calendarController,
- events: _events,
- holidays: _holidays,
- initialCalendarFormat: CalendarFormat.month,
- formatAnimation: FormatAnimation.slide,
- startingDayOfWeek: StartingDayOfWeek.sunday,
- availableGestures: AvailableGestures.all,
- availableCalendarFormats: const {
- CalendarFormat.month: '',
- CalendarFormat.week: '',
- },
- calendarStyle: CalendarStyle(
- outsideDaysVisible: false,
- weekendStyle: TextStyle().copyWith(color: Colors.blue[800]),
- holidayStyle: TextStyle().copyWith(color: Colors.blue[800]),
- ),
- daysOfWeekStyle: DaysOfWeekStyle(
- weekendStyle: TextStyle().copyWith(color: Colors.blue[600]),
- ),
- headerStyle: HeaderStyle(
- centerHeaderTitle: true,
- formatButtonVisible: false,
+ title: const Text('TableCalendar Example'),
),
- builders: CalendarBuilders(
- selectedDayBuilder: (context, date, _) {
- return FadeTransition(
- opacity: Tween(begin: 0.0, end: 1.0).animate(_animationController),
- child: Container(
- margin: const EdgeInsets.all(4.0),
- padding: const EdgeInsets.only(top: 5.0, left: 6.0),
- color: Colors.deepOrange[300],
- width: 100,
- height: 100,
- child: Text(
- '${date.day}',
- style: TextStyle().copyWith(fontSize: 16.0),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const SizedBox(height: 20.0),
+ ElevatedButton(
+ child: const Text('Basics'),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(builder: (_) => const TableBasicsExample()),
),
),
- );
- },
- todayDayBuilder: (context, date, _) {
- return Container(
- margin: const EdgeInsets.all(4.0),
- padding: const EdgeInsets.only(top: 5.0, left: 6.0),
- color: Colors.amber[400],
- width: 100,
- height: 100,
- child: Text(
- '${date.day}',
- style: TextStyle().copyWith(fontSize: 16.0),
- ),
- );
- },
- markersBuilder: (context, date, events, holidays) {
- final children = [];
-
- if (events.isNotEmpty) {
- children.add(
- Positioned(
- right: 1,
- bottom: 1,
- child: _buildEventsMarker(date, events),
+ const SizedBox(height: 12.0),
+ ElevatedButton(
+ child: const Text('Range Selection'),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(builder: (_) => const TableRangeExample()),
),
- );
- }
-
- if (holidays.isNotEmpty) {
- children.add(
- Positioned(
- right: -2,
- top: -2,
- child: _buildHolidaysMarker(),
+ ),
+ const SizedBox(height: 12.0),
+ ElevatedButton(
+ child: const Text('Events'),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(builder: (_) => const TableEventsExample()),
),
- );
- }
-
- return children;
- },
- ),
- onDaySelected: (date, events, holidays) {
- _onDaySelected(date, events, holidays);
- _animationController.forward(from: 0.0);
- },
- onVisibleDaysChanged: _onVisibleDaysChanged,
- onCalendarCreated: _onCalendarCreated,
- );
- }
-
- Widget _buildEventsMarker(DateTime date, List events) {
- return AnimatedContainer(
- duration: const Duration(milliseconds: 300),
- decoration: BoxDecoration(
- shape: BoxShape.rectangle,
- color: _calendarController.isSelected(date)
- ? Colors.brown[500]
- : _calendarController.isToday(date) ? Colors.brown[300] : Colors.blue[400],
- ),
- width: 16.0,
- height: 16.0,
- child: Center(
- child: Text(
- '${events.length}',
- style: TextStyle().copyWith(
- color: Colors.white,
- fontSize: 12.0,
- ),
- ),
- ),
- );
- }
-
- Widget _buildHolidaysMarker() {
- return Icon(
- Icons.add_box,
- size: 20.0,
- color: Colors.blueGrey[800],
- );
- }
-
- Widget _buildButtons() {
- final dateTime = _events.keys.elementAt(_events.length - 2);
-
- return Column(
- children: [
- Row(
- mainAxisSize: MainAxisSize.max,
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- RaisedButton(
- child: Text('Month'),
- onPressed: () {
- setState(() {
- _calendarController.setCalendarFormat(CalendarFormat.month);
- });
- },
),
- RaisedButton(
- child: Text('2 weeks'),
- onPressed: () {
- setState(() {
- _calendarController.setCalendarFormat(CalendarFormat.twoWeeks);
- });
- },
+ const SizedBox(height: 12.0),
+ ElevatedButton(
+ child: const Text('Multiple Selection'),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(builder: (_) => const TableMultiExample()),
+ ),
),
- RaisedButton(
- child: Text('Week'),
- onPressed: () {
- setState(() {
- _calendarController.setCalendarFormat(CalendarFormat.week);
- });
- },
+ const SizedBox(height: 12.0),
+ ElevatedButton(
+ child: const Text('Complex'),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(builder: (_) => const TableComplexExample()),
+ ),
),
+ const SizedBox(height: 20.0),
],
),
- const SizedBox(height: 8.0),
- RaisedButton(
- child: Text('Set day ${dateTime.day}-${dateTime.month}-${dateTime.year}'),
- onPressed: () {
- _calendarController.setSelectedDay(
- DateTime(dateTime.year, dateTime.month, dateTime.day),
- runCallback: true,
- );
- },
- ),
- ],
- );
- }
-
- Widget _buildEventList() {
- return ListView(
- children: _selectedEvents
- .map((event) => Container(
- decoration: BoxDecoration(
- border: Border.all(width: 0.8),
- borderRadius: BorderRadius.circular(12.0),
- ),
- margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
- child: ListTile(
- title: Text(event.toString()),
- onTap: () => print('$event tapped!'),
- ),
- ))
- .toList(),
+ ),
);
}
}
diff --git a/example/lib/pages/basics_example.dart b/example/lib/pages/basics_example.dart
new file mode 100644
index 00000000..fda26a75
--- /dev/null
+++ b/example/lib/pages/basics_example.dart
@@ -0,0 +1,63 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/material.dart';
+import 'package:table_calendar/table_calendar.dart';
+import 'package:table_calendar_example/utils.dart';
+
+class TableBasicsExample extends StatefulWidget {
+ const TableBasicsExample({super.key});
+
+ @override
+ State createState() => _TableBasicsExampleState();
+}
+
+class _TableBasicsExampleState extends State {
+ CalendarFormat _calendarFormat = CalendarFormat.month;
+ DateTime _focusedDay = DateTime.now();
+ DateTime? _selectedDay;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('TableCalendar - Basics'),
+ ),
+ body: TableCalendar(
+ firstDay: kFirstDay,
+ lastDay: kLastDay,
+ focusedDay: _focusedDay,
+ calendarFormat: _calendarFormat,
+ selectedDayPredicate: (day) {
+ // Use `selectedDayPredicate` to determine which day is currently selected.
+ // If this returns true, then `day` will be marked as selected.
+
+ // Using `isSameDay` is recommended to disregard
+ // the time-part of compared DateTime objects.
+ return isSameDay(_selectedDay, day);
+ },
+ onDaySelected: (selectedDay, focusedDay) {
+ if (!isSameDay(_selectedDay, selectedDay)) {
+ // Call `setState()` when updating the selected day
+ setState(() {
+ _selectedDay = selectedDay;
+ _focusedDay = focusedDay;
+ });
+ }
+ },
+ onFormatChanged: (format) {
+ if (_calendarFormat != format) {
+ // Call `setState()` when updating calendar format
+ setState(() {
+ _calendarFormat = format;
+ });
+ }
+ },
+ onPageChanged: (focusedDay) {
+ // No need to call `setState()` here
+ _focusedDay = focusedDay;
+ },
+ ),
+ );
+ }
+}
diff --git a/example/lib/pages/complex_example.dart b/example/lib/pages/complex_example.dart
new file mode 100644
index 00000000..81c6e4a8
--- /dev/null
+++ b/example/lib/pages/complex_example.dart
@@ -0,0 +1,257 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+// ignore_for_file: avoid_print
+
+import 'dart:collection';
+
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+import 'package:table_calendar/table_calendar.dart';
+import 'package:table_calendar_example/utils.dart';
+
+class TableComplexExample extends StatefulWidget {
+ const TableComplexExample({super.key});
+
+ @override
+ State createState() => _TableComplexExampleState();
+}
+
+class _TableComplexExampleState extends State {
+ late final ValueNotifier> _selectedEvents;
+ final ValueNotifier _focusedDay = ValueNotifier(DateTime.now());
+ final Set _selectedDays = LinkedHashSet(
+ equals: isSameDay,
+ hashCode: getHashCode,
+ );
+
+ late PageController _pageController;
+ CalendarFormat _calendarFormat = CalendarFormat.month;
+ RangeSelectionMode _rangeSelectionMode = RangeSelectionMode.toggledOff;
+ DateTime? _rangeStart;
+ DateTime? _rangeEnd;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _selectedDays.add(_focusedDay.value);
+ _selectedEvents = ValueNotifier(_getEventsForDay(_focusedDay.value));
+ }
+
+ @override
+ void dispose() {
+ _focusedDay.dispose();
+ _selectedEvents.dispose();
+ super.dispose();
+ }
+
+ bool get canClearSelection =>
+ _selectedDays.isNotEmpty || _rangeStart != null || _rangeEnd != null;
+
+ List _getEventsForDay(DateTime day) {
+ return kEvents[day] ?? [];
+ }
+
+ List _getEventsForDays(Iterable days) {
+ return [
+ for (final d in days) ..._getEventsForDay(d),
+ ];
+ }
+
+ List _getEventsForRange(DateTime start, DateTime end) {
+ final days = daysInRange(start, end);
+ return _getEventsForDays(days);
+ }
+
+ void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
+ setState(() {
+ if (_selectedDays.contains(selectedDay)) {
+ _selectedDays.remove(selectedDay);
+ } else {
+ _selectedDays.add(selectedDay);
+ }
+
+ _focusedDay.value = focusedDay;
+ _rangeStart = null;
+ _rangeEnd = null;
+ _rangeSelectionMode = RangeSelectionMode.toggledOff;
+ });
+
+ _selectedEvents.value = _getEventsForDays(_selectedDays);
+ }
+
+ void _onRangeSelected(DateTime? start, DateTime? end, DateTime focusedDay) {
+ setState(() {
+ _focusedDay.value = focusedDay;
+ _rangeStart = start;
+ _rangeEnd = end;
+ _selectedDays.clear();
+ _rangeSelectionMode = RangeSelectionMode.toggledOn;
+ });
+
+ if (start != null && end != null) {
+ _selectedEvents.value = _getEventsForRange(start, end);
+ } else if (start != null) {
+ _selectedEvents.value = _getEventsForDay(start);
+ } else if (end != null) {
+ _selectedEvents.value = _getEventsForDay(end);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('TableCalendar - Complex'),
+ ),
+ body: Column(
+ children: [
+ ValueListenableBuilder(
+ valueListenable: _focusedDay,
+ builder: (context, value, _) {
+ return _CalendarHeader(
+ focusedDay: value,
+ clearButtonVisible: canClearSelection,
+ onTodayButtonTap: () {
+ setState(() => _focusedDay.value = DateTime.now());
+ },
+ onClearButtonTap: () {
+ setState(() {
+ _rangeStart = null;
+ _rangeEnd = null;
+ _selectedDays.clear();
+ _selectedEvents.value = [];
+ });
+ },
+ onLeftArrowTap: () {
+ _pageController.previousPage(
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeOut,
+ );
+ },
+ onRightArrowTap: () {
+ _pageController.nextPage(
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeOut,
+ );
+ },
+ );
+ },
+ ),
+ TableCalendar(
+ firstDay: kFirstDay,
+ lastDay: kLastDay,
+ focusedDay: _focusedDay.value,
+ headerVisible: false,
+ selectedDayPredicate: (day) => _selectedDays.contains(day),
+ rangeStartDay: _rangeStart,
+ rangeEndDay: _rangeEnd,
+ calendarFormat: _calendarFormat,
+ rangeSelectionMode: _rangeSelectionMode,
+ eventLoader: _getEventsForDay,
+ holidayPredicate: (day) {
+ // Every 20th day of the month will be treated as a holiday
+ return day.day == 20;
+ },
+ onDaySelected: _onDaySelected,
+ onRangeSelected: _onRangeSelected,
+ onCalendarCreated: (controller) => _pageController = controller,
+ onPageChanged: (focusedDay) => _focusedDay.value = focusedDay,
+ onFormatChanged: (format) {
+ if (_calendarFormat != format) {
+ setState(() => _calendarFormat = format);
+ }
+ },
+ ),
+ const SizedBox(height: 8.0),
+ Expanded(
+ child: ValueListenableBuilder>(
+ valueListenable: _selectedEvents,
+ builder: (context, value, _) {
+ return ListView.builder(
+ itemCount: value.length,
+ itemBuilder: (context, index) {
+ return Container(
+ margin: const EdgeInsets.symmetric(
+ horizontal: 12.0,
+ vertical: 4.0,
+ ),
+ decoration: BoxDecoration(
+ border: Border.all(),
+ borderRadius: BorderRadius.circular(12.0),
+ ),
+ child: ListTile(
+ onTap: () => print('${value[index]}'),
+ title: Text('${value[index]}'),
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _CalendarHeader extends StatelessWidget {
+ final DateTime focusedDay;
+ final VoidCallback onLeftArrowTap;
+ final VoidCallback onRightArrowTap;
+ final VoidCallback onTodayButtonTap;
+ final VoidCallback onClearButtonTap;
+ final bool clearButtonVisible;
+
+ const _CalendarHeader({
+ required this.focusedDay,
+ required this.onLeftArrowTap,
+ required this.onRightArrowTap,
+ required this.onTodayButtonTap,
+ required this.onClearButtonTap,
+ required this.clearButtonVisible,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final headerText = DateFormat.yMMM().format(focusedDay);
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: Row(
+ children: [
+ const SizedBox(width: 16.0),
+ SizedBox(
+ width: 120.0,
+ child: Text(
+ headerText,
+ style: const TextStyle(fontSize: 26.0),
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.calendar_today, size: 20.0),
+ visualDensity: VisualDensity.compact,
+ onPressed: onTodayButtonTap,
+ ),
+ if (clearButtonVisible)
+ IconButton(
+ icon: const Icon(Icons.clear, size: 20.0),
+ visualDensity: VisualDensity.compact,
+ onPressed: onClearButtonTap,
+ ),
+ const Spacer(),
+ IconButton(
+ icon: const Icon(Icons.chevron_left),
+ onPressed: onLeftArrowTap,
+ ),
+ IconButton(
+ icon: const Icon(Icons.chevron_right),
+ onPressed: onRightArrowTap,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/example/lib/pages/events_example.dart b/example/lib/pages/events_example.dart
new file mode 100644
index 00000000..0d5d8143
--- /dev/null
+++ b/example/lib/pages/events_example.dart
@@ -0,0 +1,155 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+// ignore_for_file: avoid_print
+
+import 'package:flutter/material.dart';
+import 'package:table_calendar/table_calendar.dart';
+import 'package:table_calendar_example/utils.dart';
+
+class TableEventsExample extends StatefulWidget {
+ const TableEventsExample({super.key});
+
+ @override
+ State createState() => _TableEventsExampleState();
+}
+
+class _TableEventsExampleState extends State {
+ late final ValueNotifier> _selectedEvents;
+ CalendarFormat _calendarFormat = CalendarFormat.month;
+ RangeSelectionMode _rangeSelectionMode = RangeSelectionMode
+ .toggledOff; // Can be toggled on/off by longpressing a date
+ DateTime _focusedDay = DateTime.now();
+ DateTime? _selectedDay;
+ DateTime? _rangeStart;
+ DateTime? _rangeEnd;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _selectedDay = _focusedDay;
+ _selectedEvents = ValueNotifier(_getEventsForDay(_selectedDay!));
+ }
+
+ @override
+ void dispose() {
+ _selectedEvents.dispose();
+ super.dispose();
+ }
+
+ List _getEventsForDay(DateTime day) {
+ // Implementation example
+ return kEvents[day] ?? [];
+ }
+
+ List _getEventsForRange(DateTime start, DateTime end) {
+ // Implementation example
+ final days = daysInRange(start, end);
+
+ return [
+ for (final d in days) ..._getEventsForDay(d),
+ ];
+ }
+
+ void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
+ if (!isSameDay(_selectedDay, selectedDay)) {
+ setState(() {
+ _selectedDay = selectedDay;
+ _focusedDay = focusedDay;
+ _rangeStart = null; // Important to clean those
+ _rangeEnd = null;
+ _rangeSelectionMode = RangeSelectionMode.toggledOff;
+ });
+
+ _selectedEvents.value = _getEventsForDay(selectedDay);
+ }
+ }
+
+ void _onRangeSelected(DateTime? start, DateTime? end, DateTime focusedDay) {
+ setState(() {
+ _selectedDay = null;
+ _focusedDay = focusedDay;
+ _rangeStart = start;
+ _rangeEnd = end;
+ _rangeSelectionMode = RangeSelectionMode.toggledOn;
+ });
+
+ // `start` or `end` could be null
+ if (start != null && end != null) {
+ _selectedEvents.value = _getEventsForRange(start, end);
+ } else if (start != null) {
+ _selectedEvents.value = _getEventsForDay(start);
+ } else if (end != null) {
+ _selectedEvents.value = _getEventsForDay(end);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('TableCalendar - Events'),
+ ),
+ body: Column(
+ children: [
+ TableCalendar(
+ firstDay: kFirstDay,
+ lastDay: kLastDay,
+ focusedDay: _focusedDay,
+ selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
+ rangeStartDay: _rangeStart,
+ rangeEndDay: _rangeEnd,
+ calendarFormat: _calendarFormat,
+ rangeSelectionMode: _rangeSelectionMode,
+ eventLoader: _getEventsForDay,
+ startingDayOfWeek: StartingDayOfWeek.monday,
+ calendarStyle: const CalendarStyle(
+ // Use `CalendarStyle` to customize the UI
+ outsideDaysVisible: false,
+ ),
+ onDaySelected: _onDaySelected,
+ onRangeSelected: _onRangeSelected,
+ onFormatChanged: (format) {
+ if (_calendarFormat != format) {
+ setState(() {
+ _calendarFormat = format;
+ });
+ }
+ },
+ onPageChanged: (focusedDay) {
+ _focusedDay = focusedDay;
+ },
+ ),
+ const SizedBox(height: 8.0),
+ Expanded(
+ child: ValueListenableBuilder>(
+ valueListenable: _selectedEvents,
+ builder: (context, value, _) {
+ return ListView.builder(
+ itemCount: value.length,
+ itemBuilder: (context, index) {
+ return Container(
+ margin: const EdgeInsets.symmetric(
+ horizontal: 12.0,
+ vertical: 4.0,
+ ),
+ decoration: BoxDecoration(
+ border: Border.all(),
+ borderRadius: BorderRadius.circular(12.0),
+ ),
+ child: ListTile(
+ onTap: () => print('${value[index]}'),
+ title: Text('${value[index]}'),
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/example/lib/pages/multi_example.dart b/example/lib/pages/multi_example.dart
new file mode 100644
index 00000000..bf3b570a
--- /dev/null
+++ b/example/lib/pages/multi_example.dart
@@ -0,0 +1,135 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+// ignore_for_file: avoid_print
+
+import 'dart:collection';
+
+import 'package:flutter/material.dart';
+import 'package:table_calendar/table_calendar.dart';
+import 'package:table_calendar_example/utils.dart';
+
+class TableMultiExample extends StatefulWidget {
+ const TableMultiExample({super.key});
+
+ @override
+ State createState() => _TableMultiExampleState();
+}
+
+class _TableMultiExampleState extends State {
+ final ValueNotifier> _selectedEvents = ValueNotifier([]);
+
+ // Using a `LinkedHashSet` is recommended due to equality comparison override
+ final Set _selectedDays = LinkedHashSet(
+ equals: isSameDay,
+ hashCode: getHashCode,
+ );
+
+ CalendarFormat _calendarFormat = CalendarFormat.month;
+ DateTime _focusedDay = DateTime.now();
+
+ @override
+ void dispose() {
+ _selectedEvents.dispose();
+ super.dispose();
+ }
+
+ List _getEventsForDay(DateTime day) {
+ // Implementation example
+ return kEvents[day] ?? [];
+ }
+
+ List _getEventsForDays(Set days) {
+ // Implementation example
+ // Note that days are in selection order (same applies to events)
+ return [
+ for (final d in days) ..._getEventsForDay(d),
+ ];
+ }
+
+ void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
+ setState(() {
+ _focusedDay = focusedDay;
+ // Update values in a Set
+ if (_selectedDays.contains(selectedDay)) {
+ _selectedDays.remove(selectedDay);
+ } else {
+ _selectedDays.add(selectedDay);
+ }
+ });
+
+ _selectedEvents.value = _getEventsForDays(_selectedDays);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('TableCalendar - Multi'),
+ ),
+ body: Column(
+ children: [
+ TableCalendar(
+ firstDay: kFirstDay,
+ lastDay: kLastDay,
+ focusedDay: _focusedDay,
+ calendarFormat: _calendarFormat,
+ eventLoader: _getEventsForDay,
+ startingDayOfWeek: StartingDayOfWeek.monday,
+ selectedDayPredicate: (day) {
+ // Use values from Set to mark multiple days as selected
+ return _selectedDays.contains(day);
+ },
+ onDaySelected: _onDaySelected,
+ onFormatChanged: (format) {
+ if (_calendarFormat != format) {
+ setState(() {
+ _calendarFormat = format;
+ });
+ }
+ },
+ onPageChanged: (focusedDay) {
+ _focusedDay = focusedDay;
+ },
+ ),
+ ElevatedButton(
+ child: const Text('Clear selection'),
+ onPressed: () {
+ setState(() {
+ _selectedDays.clear();
+ _selectedEvents.value = [];
+ });
+ },
+ ),
+ const SizedBox(height: 8.0),
+ Expanded(
+ child: ValueListenableBuilder>(
+ valueListenable: _selectedEvents,
+ builder: (context, value, _) {
+ return ListView.builder(
+ itemCount: value.length,
+ itemBuilder: (context, index) {
+ return Container(
+ margin: const EdgeInsets.symmetric(
+ horizontal: 12.0,
+ vertical: 4.0,
+ ),
+ decoration: BoxDecoration(
+ border: Border.all(),
+ borderRadius: BorderRadius.circular(12.0),
+ ),
+ child: ListTile(
+ onTap: () => print('${value[index]}'),
+ title: Text('${value[index]}'),
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/example/lib/pages/range_example.dart b/example/lib/pages/range_example.dart
new file mode 100644
index 00000000..6243fa5f
--- /dev/null
+++ b/example/lib/pages/range_example.dart
@@ -0,0 +1,72 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/material.dart';
+import 'package:table_calendar/table_calendar.dart';
+import 'package:table_calendar_example/utils.dart';
+
+class TableRangeExample extends StatefulWidget {
+ const TableRangeExample({super.key});
+
+ @override
+ State createState() => _TableRangeExampleState();
+}
+
+class _TableRangeExampleState extends State {
+ CalendarFormat _calendarFormat = CalendarFormat.month;
+ RangeSelectionMode _rangeSelectionMode = RangeSelectionMode
+ .toggledOn; // Can be toggled on/off by longpressing a date
+ DateTime _focusedDay = DateTime.now();
+ DateTime? _selectedDay;
+ DateTime? _rangeStart;
+ DateTime? _rangeEnd;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('TableCalendar - Range'),
+ ),
+ body: TableCalendar(
+ firstDay: kFirstDay,
+ lastDay: kLastDay,
+ focusedDay: _focusedDay,
+ selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
+ rangeStartDay: _rangeStart,
+ rangeEndDay: _rangeEnd,
+ calendarFormat: _calendarFormat,
+ rangeSelectionMode: _rangeSelectionMode,
+ onDaySelected: (selectedDay, focusedDay) {
+ if (!isSameDay(_selectedDay, selectedDay)) {
+ setState(() {
+ _selectedDay = selectedDay;
+ _focusedDay = focusedDay;
+ _rangeStart = null; // Important to clean those
+ _rangeEnd = null;
+ _rangeSelectionMode = RangeSelectionMode.toggledOff;
+ });
+ }
+ },
+ onRangeSelected: (start, end, focusedDay) {
+ setState(() {
+ _selectedDay = null;
+ _focusedDay = focusedDay;
+ _rangeStart = start;
+ _rangeEnd = end;
+ _rangeSelectionMode = RangeSelectionMode.toggledOn;
+ });
+ },
+ onFormatChanged: (format) {
+ if (_calendarFormat != format) {
+ setState(() {
+ _calendarFormat = format;
+ });
+ }
+ },
+ onPageChanged: (focusedDay) {
+ _focusedDay = focusedDay;
+ },
+ ),
+ );
+ }
+}
diff --git a/example/lib/utils.dart b/example/lib/utils.dart
new file mode 100644
index 00000000..f75484c9
--- /dev/null
+++ b/example/lib/utils.dart
@@ -0,0 +1,54 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'dart:collection';
+
+import 'package:table_calendar/table_calendar.dart';
+
+/// Example event class.
+class Event {
+ final String title;
+
+ const Event(this.title);
+
+ @override
+ String toString() => title;
+}
+
+/// Example events.
+///
+/// Using a [LinkedHashMap] is highly recommended if you decide to use a map.
+final kEvents = LinkedHashMap>(
+ equals: isSameDay,
+ hashCode: getHashCode,
+)..addAll(_kEventSource);
+
+final _kEventSource = {
+ for (var item in List.generate(50, (index) => index))
+ DateTime.utc(kFirstDay.year, kFirstDay.month, item * 5): List.generate(
+ item % 4 + 1,
+ (index) => Event('Event $item | ${index + 1}'),
+ ),
+}..addAll({
+ kToday: [
+ const Event("Today's Event 1"),
+ const Event("Today's Event 2"),
+ ],
+ });
+
+int getHashCode(DateTime key) {
+ return key.day * 1000000 + key.month * 10000 + key.year;
+}
+
+/// Returns a list of [DateTime] objects from [first] to [last], inclusive.
+List daysInRange(DateTime first, DateTime last) {
+ final dayCount = last.difference(first).inDays + 1;
+ return List.generate(
+ dayCount,
+ (index) => DateTime.utc(first.year, first.month, first.day + index),
+ );
+}
+
+final kToday = DateTime.now();
+final kFirstDay = DateTime(kToday.year, kToday.month - 3, kToday.day);
+final kLastDay = DateTime(kToday.year, kToday.month + 3, kToday.day);
diff --git a/example/pubspec.lock b/example/pubspec.lock
new file mode 100644
index 00000000..ede0d144
--- /dev/null
+++ b/example/pubspec.lock
@@ -0,0 +1,244 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ async:
+ dependency: transitive
+ description:
+ name: async
+ sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.11.0"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.1"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.18.0"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.1"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.2"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.2"
+ intl:
+ dependency: "direct main"
+ description:
+ name: intl
+ sha256: "00f33b908655e606b86d2ade4710a231b802eec6f11e87e4ea3783fd72077a50"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.20.1"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.0.5"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.5"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.12.16+1"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.1"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.15.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.9.0"
+ simple_gesture_detector:
+ dependency: transitive
+ description:
+ name: simple_gesture_detector
+ sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.99"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.10.0"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.11.1"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.0"
+ table_calendar:
+ dependency: "direct main"
+ description:
+ path: ".."
+ relative: true
+ source: path
+ version: "3.2.0"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.2"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.0"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "14.2.5"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+sdks:
+ dart: ">=3.5.0 <4.0.0"
+ flutter: ">=3.18.0-18.0.pre.54"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index b164eef7..cd6a009f 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -1,25 +1,30 @@
name: table_calendar_example
description: A short demo of table_calendar package.
+# The following line prevents the package from being accidentally published to
+# pub.dev using `pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
-# Read more about versioning at semver.org.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
environment:
- sdk: ">=2.1.0 <3.0.0"
+ sdk: ">=3.0.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
-
- # The following adds the Cupertino Icons font to your application.
- # Use with the CupertinoIcons class for iOS style icons.
- cupertino_icons: ^0.1.2
-
+
+ intl: any
table_calendar:
path: ../
@@ -27,9 +32,8 @@ dev_dependencies:
flutter_test:
sdk: flutter
-
# For information on the generic Dart part of this file, see the
-# following page: https://www.dartlang.org/tools/pub/pubspec
+# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
@@ -41,14 +45,14 @@ flutter:
# To add assets to your application, add an assets section, like this:
# assets:
- # - images/a_dot_burr.jpeg
- # - images/a_dot_ham.jpeg
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
- # https://flutter.io/assets-and-images/#resolution-aware.
+ # https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
- # https://flutter.io/assets-and-images/#from-packages
+ # https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
@@ -68,4 +72,4 @@ flutter:
# weight: 700
#
# For details regarding fonts from package dependencies,
- # see https://flutter.io/custom-fonts/#from-packages
+ # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/example/web/favicon.png b/example/web/favicon.png
new file mode 100644
index 00000000..8aaa46ac
Binary files /dev/null and b/example/web/favicon.png differ
diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png
new file mode 100644
index 00000000..b749bfef
Binary files /dev/null and b/example/web/icons/Icon-192.png differ
diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png
new file mode 100644
index 00000000..88cfd48d
Binary files /dev/null and b/example/web/icons/Icon-512.png differ
diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png
new file mode 100644
index 00000000..eb9b4d76
Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ
diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png
new file mode 100644
index 00000000..d69c5669
Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ
diff --git a/example/web/index.html b/example/web/index.html
new file mode 100644
index 00000000..b6b9dd23
--- /dev/null
+++ b/example/web/index.html
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ example
+
+
+
+
+
+
+
diff --git a/example/web/manifest.json b/example/web/manifest.json
new file mode 100644
index 00000000..096edf8f
--- /dev/null
+++ b/example/web/manifest.json
@@ -0,0 +1,35 @@
+{
+ "name": "example",
+ "short_name": "example",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "A new Flutter project.",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-maskable-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "icons/Icon-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/lib/src/calendar.dart b/lib/src/calendar.dart
deleted file mode 100644
index 49119ad8..00000000
--- a/lib/src/calendar.dart
+++ /dev/null
@@ -1,719 +0,0 @@
-// Copyright (c) 2019 Aleksander Woźniak
-// Licensed under Apache License v2.0
-
-part of table_calendar;
-
-/// Callback exposing currently selected day.
-typedef void OnDaySelected(DateTime day, List events, List holidays);
-
-/// Callback exposing currently visible days (first and last of them), as well as current `CalendarFormat`.
-typedef void OnVisibleDaysChanged(DateTime first, DateTime last, CalendarFormat format);
-
-/// Callback exposing initially visible days (first and last of them), as well as initial `CalendarFormat`.
-typedef void OnCalendarCreated(DateTime first, DateTime last, CalendarFormat format);
-
-/// Signature for reacting to header gestures. Exposes current month and year as a `DateTime` object.
-typedef void HeaderGestureCallback(DateTime focusedDay);
-
-/// Builder signature for any text that can be localized and formatted with `DateFormat`.
-typedef String TextBuilder(DateTime date, dynamic locale);
-
-/// Signature for enabling days.
-typedef bool EnabledDayPredicate(DateTime day);
-
-/// Format to display the `TableCalendar` with.
-enum CalendarFormat { month, twoWeeks, week }
-
-/// Available animations to update the `CalendarFormat` with.
-enum FormatAnimation { slide, scale }
-
-/// Available day of week formats. `TableCalendar` will start the week with chosen day.
-/// * `StartingDayOfWeek.monday`: Monday - Sunday
-/// * `StartingDayOfWeek.tuesday`: Tuesday - Monday
-/// * `StartingDayOfWeek.wednesday`: Wednesday - Tuesday
-/// * `StartingDayOfWeek.thursday`: Thursday - Wednesday
-/// * `StartingDayOfWeek.friday`: Friday - Thursday
-/// * `StartingDayOfWeek.saturday`: Saturday - Friday
-/// * `StartingDayOfWeek.sunday`: Sunday - Saturday
-enum StartingDayOfWeek { monday, tuesday, wednesday, thursday, friday, saturday, sunday }
-
-int _getWeekdayNumber(StartingDayOfWeek weekday) {
- return StartingDayOfWeek.values.indexOf(weekday) + 1;
-}
-
-/// Gestures available to interal `TableCalendar`'s logic.
-enum AvailableGestures { none, verticalSwipe, horizontalSwipe, all }
-
-/// Highly customizable, feature-packed Flutter Calendar with gestures, animations and multiple formats.
-class TableCalendar extends StatefulWidget {
- /// Controller required for `TableCalendar`.
- /// Use it to update `events`, `holidays`, etc.
- final CalendarController calendarController;
-
- /// Locale to format `TableCalendar` dates with, for example: `'en_US'`.
- ///
- /// If nothing is provided, a default locale will be used.
- final dynamic locale;
-
- /// `Map` of events.
- /// Each `DateTime` inside this `Map` should get its own `List` of objects (i.e. events).
- final Map events;
-
- /// `Map` of holidays.
- /// This property allows you to provide custom holiday rules.
- final Map holidays;
-
- /// Called whenever any day gets tapped.
- final OnDaySelected onDaySelected;
-
- /// Called whenever any day gets long pressed.
- final OnDaySelected onDayLongPressed;
-
- /// Called whenever any unavailable day gets tapped.
- /// Replaces `onDaySelected` for those days.
- final VoidCallback onUnavailableDaySelected;
-
- /// Called whenever any unavailable day gets long pressed.
- /// Replaces `onDaySelected` for those days.
- final VoidCallback onUnavailableDayLongPressed;
-
- /// Called whenever header gets tapped.
- final HeaderGestureCallback onHeaderTapped;
-
- /// Called whenever header gets long pressed.
- final HeaderGestureCallback onHeaderLongPressed;
-
- /// Called whenever the range of visible days changes.
- final OnVisibleDaysChanged onVisibleDaysChanged;
-
- /// Called once when the CalendarController gets initialized.
- final OnCalendarCreated onCalendarCreated;
-
- /// Initially selected DateTime. Usually it will be `DateTime.now()`.
- final DateTime initialSelectedDay;
-
- /// The first day of `TableCalendar`.
- /// Days before it will use `unavailableStyle` and run `onUnavailableDaySelected` callback.
- final DateTime startDay;
-
- /// The last day of `TableCalendar`.
- /// Days after it will use `unavailableStyle` and run `onUnavailableDaySelected` callback.
- final DateTime endDay;
-
- /// List of days treated as weekend days.
- /// Use built-in `DateTime` weekday constants (e.g. `DateTime.monday`) instead of `int` literals (e.q. `1`).
- final List weekendDays;
-
- /// `CalendarFormat` which will be displayed first.
- final CalendarFormat initialCalendarFormat;
-
- /// `Map` of `CalendarFormat`s and `String` names associated with them.
- /// Those `CalendarFormat`s will be used by internal logic to manage displayed format.
- ///
- /// To ensure proper vertical Swipe behavior, `CalendarFormat`s should be in descending order (eg. from biggest to smallest).
- ///
- /// For example:
- /// ```dart
- /// availableCalendarFormats: const {
- /// CalendarFormat.month: 'Month',
- /// CalendarFormat.week: 'Week',
- /// }
- /// ```
- final Map availableCalendarFormats;
-
- /// Used to show/hide Header.
- final bool headerVisible;
-
- /// Function deciding whether given day should be enabled or not.
- /// If `false` is returned, this day will be unavailable.
- final EnabledDayPredicate enabledDayPredicate;
-
- /// Used for setting the height of `TableCalendar`'s rows.
- final double rowHeight;
-
- /// Animation to run when `CalendarFormat` gets changed.
- final FormatAnimation formatAnimation;
-
- /// `TableCalendar` will start weeks with provided day.
- /// Use `StartingDayOfWeek.monday` for Monday - Sunday week format.
- /// Use `StartingDayOfWeek.sunday` for Sunday - Saturday week format.
- final StartingDayOfWeek startingDayOfWeek;
-
- /// `HitTestBehavior` for every day cell inside `TableCalendar`.
- final HitTestBehavior dayHitTestBehavior;
-
- /// Specify Gestures available to `TableCalendar`.
- /// If `AvailableGestures.none` is used, the Calendar will only be interactive via buttons.
- final AvailableGestures availableGestures;
-
- /// Configuration for vertical Swipe detector.
- final SimpleSwipeConfig simpleSwipeConfig;
-
- /// Style for `TableCalendar`'s content.
- final CalendarStyle calendarStyle;
-
- /// Style for DaysOfWeek displayed between `TableCalendar`'s Header and content.
- final DaysOfWeekStyle daysOfWeekStyle;
-
- /// Style for `TableCalendar`'s Header.
- final HeaderStyle headerStyle;
-
- /// Set of Builders for `TableCalendar` to work with.
- final CalendarBuilders builders;
-
- TableCalendar({
- Key key,
- @required this.calendarController,
- this.locale,
- this.events = const {},
- this.holidays = const {},
- this.onDaySelected,
- this.onDayLongPressed,
- this.onUnavailableDaySelected,
- this.onUnavailableDayLongPressed,
- this.onHeaderTapped,
- this.onHeaderLongPressed,
- this.onVisibleDaysChanged,
- this.onCalendarCreated,
- this.initialSelectedDay,
- this.startDay,
- this.endDay,
- this.weekendDays = const [DateTime.saturday, DateTime.sunday],
- this.initialCalendarFormat = CalendarFormat.month,
- this.availableCalendarFormats = const {
- CalendarFormat.month: 'Month',
- CalendarFormat.twoWeeks: '2 weeks',
- CalendarFormat.week: 'Week',
- },
- this.headerVisible = true,
- this.enabledDayPredicate,
- this.rowHeight,
- this.formatAnimation = FormatAnimation.slide,
- this.startingDayOfWeek = StartingDayOfWeek.sunday,
- this.dayHitTestBehavior = HitTestBehavior.deferToChild,
- this.availableGestures = AvailableGestures.all,
- this.simpleSwipeConfig = const SimpleSwipeConfig(
- verticalThreshold: 25.0,
- swipeDetectionBehavior: SwipeDetectionBehavior.continuousDistinct,
- ),
- this.calendarStyle = const CalendarStyle(),
- this.daysOfWeekStyle = const DaysOfWeekStyle(),
- this.headerStyle = const HeaderStyle(),
- this.builders = const CalendarBuilders(),
- }) : assert(calendarController != null),
- assert(availableCalendarFormats.keys.contains(initialCalendarFormat)),
- assert(availableCalendarFormats.length <= CalendarFormat.values.length),
- assert(weekendDays != null),
- assert(weekendDays.isNotEmpty
- ? weekendDays.every((day) => day >= DateTime.monday && day <= DateTime.sunday)
- : true),
- super(key: key);
-
- @override
- _TableCalendarState createState() => _TableCalendarState();
-}
-
-class _TableCalendarState extends State with SingleTickerProviderStateMixin {
- @override
- void initState() {
- super.initState();
-
- widget.calendarController._init(
- events: widget.events,
- holidays: widget.holidays,
- initialDay: widget.initialSelectedDay,
- initialFormat: widget.initialCalendarFormat,
- availableCalendarFormats: widget.availableCalendarFormats,
- useNextCalendarFormat: widget.headerStyle.formatButtonShowsNext,
- startingDayOfWeek: widget.startingDayOfWeek,
- selectedDayCallback: _selectedDayCallback,
- onVisibleDaysChanged: widget.onVisibleDaysChanged,
- onCalendarCreated: widget.onCalendarCreated,
- includeInvisibleDays: widget.calendarStyle.outsideDaysVisible,
- );
- }
-
- @override
- void didUpdateWidget(TableCalendar oldWidget) {
- super.didUpdateWidget(oldWidget);
-
- if (oldWidget.events != widget.events) {
- widget.calendarController._events = widget.events;
- }
-
- if (oldWidget.holidays != widget.holidays) {
- widget.calendarController._holidays = widget.holidays;
- }
-
- if (oldWidget.availableCalendarFormats != widget.availableCalendarFormats) {
- widget.calendarController._availableCalendarFormats = widget.availableCalendarFormats;
- }
- }
-
- void _selectedDayCallback(DateTime day) {
- if (widget.onDaySelected != null) {
- widget.onDaySelected(
- day,
- widget.calendarController.visibleEvents[_getEventKey(day)] ?? [],
- widget.calendarController.visibleHolidays[_getHolidayKey(day)] ?? [],
- );
- }
- }
-
- void _selectPrevious() {
- setState(() {
- widget.calendarController._selectPrevious();
- });
- }
-
- void _selectNext() {
- setState(() {
- widget.calendarController._selectNext();
- });
- }
-
- void _selectDay(DateTime day) {
- setState(() {
- widget.calendarController.setSelectedDay(day, isProgrammatic: false);
- _selectedDayCallback(day);
- });
- }
-
- void _onDayLongPressed(DateTime day) {
- if (widget.onDayLongPressed != null) {
- widget.onDayLongPressed(
- day,
- widget.calendarController.visibleEvents[_getEventKey(day)] ?? [],
- widget.calendarController.visibleHolidays[_getHolidayKey(day)] ?? [],
- );
- }
- }
-
- void _toggleCalendarFormat() {
- setState(() {
- widget.calendarController.toggleCalendarFormat();
- });
- }
-
- void _onHorizontalSwipe(DismissDirection direction) {
- if (direction == DismissDirection.startToEnd) {
- // Swipe right
- _selectPrevious();
- } else {
- // Swipe left
- _selectNext();
- }
- }
-
- void _onUnavailableDaySelected() {
- if (widget.onUnavailableDaySelected != null) {
- widget.onUnavailableDaySelected();
- }
- }
-
- void _onUnavailableDayLongPressed() {
- if (widget.onUnavailableDayLongPressed != null) {
- widget.onUnavailableDayLongPressed();
- }
- }
-
- void _onHeaderTapped() {
- if (widget.onHeaderTapped != null) {
- widget.onHeaderTapped(widget.calendarController.focusedDay);
- }
- }
-
- void _onHeaderLongPressed() {
- if (widget.onHeaderLongPressed != null) {
- widget.onHeaderLongPressed(widget.calendarController.focusedDay);
- }
- }
-
- bool _isDayUnavailable(DateTime day) {
- return (widget.startDay != null && day.isBefore(widget.calendarController._normalizeDate(widget.startDay))) ||
- (widget.endDay != null && day.isAfter(widget.calendarController._normalizeDate(widget.endDay))) ||
- (!_isDayEnabled(day));
- }
-
- bool _isDayEnabled(DateTime day) {
- return widget.enabledDayPredicate == null ? true : widget.enabledDayPredicate(day);
- }
-
- DateTime _getEventKey(DateTime day) {
- return widget.calendarController._getEventKey(day);
- }
-
- DateTime _getHolidayKey(DateTime day) {
- return widget.calendarController._getHolidayKey(day);
- }
-
- @override
- Widget build(BuildContext context) {
- return ClipRect(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- if (widget.headerVisible) _buildHeader(),
- Padding(
- padding: widget.calendarStyle.contentPadding,
- child: _buildCalendarContent(),
- ),
- ],
- ),
- );
- }
-
- Widget _buildHeader() {
- final children = [
- widget.headerStyle.showLeftChevron ?
- _CustomIconButton(
- icon: widget.headerStyle.leftChevronIcon,
- onTap: _selectPrevious,
- margin: widget.headerStyle.leftChevronMargin,
- padding: widget.headerStyle.leftChevronPadding,
- ) : Container(),
- Expanded(
- child: GestureDetector(
- onTap: _onHeaderTapped,
- onLongPress: _onHeaderLongPressed,
- child: Text(
- widget.headerStyle.titleTextBuilder != null
- ? widget.headerStyle.titleTextBuilder(widget.calendarController.focusedDay, widget.locale)
- : DateFormat.yMMMM(widget.locale).format(widget.calendarController.focusedDay),
- style: widget.headerStyle.titleTextStyle,
- textAlign: widget.headerStyle.centerHeaderTitle ? TextAlign.center : TextAlign.start,
- ),
- ),
- ),
- widget.headerStyle.showRightChevron ?
- _CustomIconButton(
- icon: widget.headerStyle.rightChevronIcon,
- onTap: _selectNext,
- margin: widget.headerStyle.rightChevronMargin,
- padding: widget.headerStyle.rightChevronPadding,
- ) : Container()
- ];
-
- if (widget.headerStyle.formatButtonVisible && widget.availableCalendarFormats.length > 1) {
- children.insert(2, const SizedBox(width: 8.0));
- children.insert(3, _buildFormatButton());
- }
-
- return Container(
- decoration: widget.headerStyle.decoration,
- margin: widget.headerStyle.headerMargin,
- padding: widget.headerStyle.headerPadding,
- child: Row(
- mainAxisSize: MainAxisSize.max,
- children: children,
- ),
- );
- }
-
- Widget _buildFormatButton() {
- return GestureDetector(
- onTap: _toggleCalendarFormat,
- child: Container(
- decoration: widget.headerStyle.formatButtonDecoration,
- padding: widget.headerStyle.formatButtonPadding,
- child: Text(
- widget.calendarController._getFormatButtonText(),
- style: widget.headerStyle.formatButtonTextStyle,
- ),
- ),
- );
- }
-
- Widget _buildCalendarContent() {
- if (widget.formatAnimation == FormatAnimation.slide) {
- return AnimatedSize(
- duration: Duration(milliseconds: widget.calendarController.calendarFormat == CalendarFormat.month ? 330 : 220),
- curve: Curves.fastOutSlowIn,
- alignment: Alignment(0, -1),
- vsync: this,
- child: _buildWrapper(),
- );
- } else {
- return AnimatedSwitcher(
- duration: const Duration(milliseconds: 350),
- transitionBuilder: (child, animation) {
- return SizeTransition(
- sizeFactor: animation,
- child: ScaleTransition(
- scale: animation,
- child: child,
- ),
- );
- },
- child: _buildWrapper(
- key: ValueKey(widget.calendarController.calendarFormat),
- ),
- );
- }
- }
-
- Widget _buildWrapper({Key key}) {
- Widget wrappedChild = _buildTable();
-
- switch (widget.availableGestures) {
- case AvailableGestures.all:
- wrappedChild = _buildVerticalSwipeWrapper(
- child: _buildHorizontalSwipeWrapper(
- child: wrappedChild,
- ),
- );
- break;
- case AvailableGestures.verticalSwipe:
- wrappedChild = _buildVerticalSwipeWrapper(
- child: wrappedChild,
- );
- break;
- case AvailableGestures.horizontalSwipe:
- wrappedChild = _buildHorizontalSwipeWrapper(
- child: wrappedChild,
- );
- break;
- case AvailableGestures.none:
- break;
- }
-
- return Container(
- key: key,
- child: wrappedChild,
- );
- }
-
- Widget _buildVerticalSwipeWrapper({Widget child}) {
- return SimpleGestureDetector(
- child: child,
- onVerticalSwipe: (direction) {
- setState(() {
- widget.calendarController.swipeCalendarFormat(isSwipeUp: direction == SwipeDirection.up);
- });
- },
- swipeConfig: widget.simpleSwipeConfig,
- );
- }
-
- Widget _buildHorizontalSwipeWrapper({Widget child}) {
- return AnimatedSwitcher(
- duration: const Duration(milliseconds: 350),
- switchInCurve: Curves.decelerate,
- transitionBuilder: (child, animation) {
- return SlideTransition(
- position:
- Tween(begin: Offset(widget.calendarController._dx, 0), end: Offset(0, 0)).animate(animation),
- child: child,
- );
- },
- layoutBuilder: (currentChild, _) => currentChild,
- child: Dismissible(
- key: ValueKey(widget.calendarController._pageId),
- resizeDuration: null,
- onDismissed: _onHorizontalSwipe,
- direction: DismissDirection.horizontal,
- child: child,
- ),
- );
- }
-
- Widget _buildTable() {
- final daysInWeek = 7;
- final children = [
- if (widget.calendarStyle.renderDaysOfWeek) _buildDaysOfWeek(),
- ];
-
- int x = 0;
- while (x < widget.calendarController._visibleDays.value.length) {
- children.add(_buildTableRow(widget.calendarController._visibleDays.value.skip(x).take(daysInWeek).toList()));
- x += daysInWeek;
- }
-
- return Table(
- // Makes this Table fill its parent horizontally
- defaultColumnWidth: FractionColumnWidth(1.0 / daysInWeek),
- children: children,
- );
- }
-
- TableRow _buildDaysOfWeek() {
- return TableRow(
- decoration: widget.daysOfWeekStyle.decoration,
- children: widget.calendarController._visibleDays.value.take(7).map((date) {
- final weekdayString = widget.daysOfWeekStyle.dowTextBuilder != null
- ? widget.daysOfWeekStyle.dowTextBuilder(date, widget.locale)
- : DateFormat.E(widget.locale).format(date);
- final isWeekend = widget.calendarController._isWeekend(date, widget.weekendDays);
-
- if (isWeekend && widget.builders.dowWeekendBuilder != null) {
- return widget.builders.dowWeekendBuilder(context, weekdayString);
- }
- if (widget.builders.dowWeekdayBuilder != null) {
- return widget.builders.dowWeekdayBuilder(context, weekdayString);
- }
- return Center(
- child: Text(
- weekdayString,
- style: isWeekend ? widget.daysOfWeekStyle.weekendStyle : widget.daysOfWeekStyle.weekdayStyle,
- ),
- );
- }).toList(),
- );
- }
-
- TableRow _buildTableRow(List days) {
- return TableRow(
- decoration: widget.calendarStyle.contentDecoration,
- children: days.map((date) => _buildTableCell(date)).toList(),
- );
- }
-
- // TableCell will have equal width and height
- Widget _buildTableCell(DateTime date) {
- return LayoutBuilder(
- builder: (context, constraints) => ConstrainedBox(
- constraints: BoxConstraints(
- maxHeight: widget.rowHeight ?? constraints.maxWidth,
- minHeight: widget.rowHeight ?? constraints.maxWidth,
- ),
- child: _buildCell(date),
- ),
- );
- }
-
- Widget _buildCell(DateTime date) {
- if (!widget.calendarStyle.outsideDaysVisible &&
- widget.calendarController._isExtraDay(date) &&
- widget.calendarController.calendarFormat == CalendarFormat.month) {
- return Container();
- }
-
- Widget content = _buildCellContent(date);
-
- final eventKey = _getEventKey(date);
- final holidayKey = _getHolidayKey(date);
- final key = eventKey ?? holidayKey;
-
- if (key != null) {
- final children = [content];
- final events = eventKey != null ? widget.calendarController.visibleEvents[eventKey] : [];
- final holidays = holidayKey != null ? widget.calendarController.visibleHolidays[holidayKey] : [];
-
- if (!_isDayUnavailable(date)) {
- if (widget.builders.markersBuilder != null) {
- children.addAll(
- widget.builders.markersBuilder(
- context,
- key,
- events,
- holidays,
- ),
- );
- } else {
- children.add(
- Positioned(
- top: widget.calendarStyle.markersPositionTop,
- bottom: widget.calendarStyle.markersPositionBottom,
- left: widget.calendarStyle.markersPositionLeft,
- right: widget.calendarStyle.markersPositionRight,
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: events
- .take(widget.calendarStyle.markersMaxAmount)
- .map((event) => _buildMarker(eventKey, event))
- .toList(),
- ),
- ),
- );
- }
- }
-
- if (children.length > 1) {
- content = Stack(
- alignment: widget.calendarStyle.markersAlignment,
- children: children,
- overflow: widget.calendarStyle.canEventMarkersOverflow ? Overflow.visible : Overflow.clip,
- );
- }
- }
-
- return GestureDetector(
- behavior: widget.dayHitTestBehavior,
- onTap: () => _isDayUnavailable(date) ? _onUnavailableDaySelected() : _selectDay(date),
- onLongPress: () => _isDayUnavailable(date) ? _onUnavailableDayLongPressed() : _onDayLongPressed(date),
- child: content,
- );
- }
-
- Widget _buildCellContent(DateTime date) {
- final eventKey = _getEventKey(date);
-
- final tIsUnavailable = _isDayUnavailable(date);
- final tIsSelected = widget.calendarController.isSelected(date);
- final tIsToday = widget.calendarController.isToday(date);
- final tIsOutside = widget.calendarController._isExtraDay(date);
- final tIsHoliday = widget.calendarController.visibleHolidays.containsKey(_getHolidayKey(date));
- final tIsWeekend = widget.calendarController._isWeekend(date, widget.weekendDays);
- final tIsEventDay = widget.calendarController.visibleEvents.containsKey(eventKey);
-
- final isUnavailable = widget.builders.unavailableDayBuilder != null && tIsUnavailable;
- final isSelected = widget.builders.selectedDayBuilder != null && tIsSelected;
- final isToday = widget.builders.todayDayBuilder != null && tIsToday;
- final isOutsideHoliday = widget.builders.outsideHolidayDayBuilder != null && tIsOutside && tIsHoliday;
- final isHoliday = widget.builders.holidayDayBuilder != null && !tIsOutside && tIsHoliday;
- final isOutsideWeekend =
- widget.builders.outsideWeekendDayBuilder != null && tIsOutside && tIsWeekend && !tIsHoliday;
- final isOutside = widget.builders.outsideDayBuilder != null && tIsOutside && !tIsWeekend && !tIsHoliday;
- final isWeekend = widget.builders.weekendDayBuilder != null && !tIsOutside && tIsWeekend && !tIsHoliday;
-
- if (isUnavailable) {
- return widget.builders.unavailableDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]);
- } else if (isSelected && widget.calendarStyle.renderSelectedFirst) {
- return widget.builders.selectedDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]);
- } else if (isToday) {
- return widget.builders.todayDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]);
- } else if (isSelected) {
- return widget.builders.selectedDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]);
- } else if (isOutsideHoliday) {
- return widget.builders.outsideHolidayDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]);
- } else if (isHoliday) {
- return widget.builders.holidayDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]);
- } else if (isOutsideWeekend) {
- return widget.builders.outsideWeekendDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]);
- } else if (isOutside) {
- return widget.builders.outsideDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]);
- } else if (isWeekend) {
- return widget.builders.weekendDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]);
- } else if (widget.builders.dayBuilder != null) {
- return widget.builders.dayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]);
- } else {
- return _CellWidget(
- text: '${date.day}',
- isUnavailable: tIsUnavailable,
- isSelected: tIsSelected,
- isToday: tIsToday,
- isWeekend: tIsWeekend,
- isOutsideMonth: tIsOutside,
- isHoliday: tIsHoliday,
- isEventDay: tIsEventDay,
- calendarStyle: widget.calendarStyle,
- );
- }
- }
-
- Widget _buildMarker(DateTime date, dynamic event) {
- if (widget.builders.singleMarkerBuilder != null) {
- return widget.builders.singleMarkerBuilder(context, date, event);
- } else {
- return Container(
- width: 8.0,
- height: 8.0,
- margin: const EdgeInsets.symmetric(horizontal: 0.3),
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- color: widget.calendarStyle.markersColor,
- ),
- );
- }
- }
-}
diff --git a/lib/src/calendar_controller.dart b/lib/src/calendar_controller.dart
deleted file mode 100644
index 7539268a..00000000
--- a/lib/src/calendar_controller.dart
+++ /dev/null
@@ -1,485 +0,0 @@
-// Copyright (c) 2019 Aleksander Woźniak
-// Licensed under Apache License v2.0
-
-part of table_calendar;
-
-const double _dxMax = 1.2;
-const double _dxMin = -1.2;
-
-typedef void _SelectedDayCallback(DateTime day);
-
-/// Controller required for `TableCalendar`.
-///
-/// Should be created in `initState()`, and then disposed in `dispose()`:
-/// ```dart
-/// @override
-/// void initState() {
-/// super.initState();
-/// _calendarController = CalendarController();
-/// }
-///
-/// @override
-/// void dispose() {
-/// _calendarController.dispose();
-/// super.dispose();
-/// }
-/// ```
-class CalendarController {
- /// Currently focused day (used to determine which year/month should be visible).
- DateTime get focusedDay => _focusedDay;
-
- /// Currently selected day.
- DateTime get selectedDay => _selectedDay;
-
- /// Currently visible calendar format.
- CalendarFormat get calendarFormat => _calendarFormat.value;
-
- /// List of currently visible days.
- List get visibleDays => calendarFormat == CalendarFormat.month && !_includeInvisibleDays
- ? _visibleDays.value.where((day) => !_isExtraDay(day)).toList()
- : _visibleDays.value;
-
- /// `Map` of currently visible events.
- Map get visibleEvents {
- if (_events == null) {
- return {};
- }
-
- return Map.fromEntries(
- _events.entries.where((entry) {
- for (final day in visibleDays) {
- if (_isSameDay(day, entry.key)) {
- return true;
- }
- }
-
- return false;
- }),
- );
- }
-
- /// `Map` of currently visible holidays.
- Map get visibleHolidays {
- if (_holidays == null) {
- return {};
- }
-
- return Map.fromEntries(
- _holidays.entries.where((entry) {
- for (final day in visibleDays) {
- if (_isSameDay(day, entry.key)) {
- return true;
- }
- }
-
- return false;
- }),
- );
- }
-
- Map _events;
- Map _holidays;
- DateTime _focusedDay;
- DateTime _selectedDay;
- StartingDayOfWeek _startingDayOfWeek;
- ValueNotifier _calendarFormat;
- ValueNotifier> _visibleDays;
- Map _availableCalendarFormats;
- DateTime _previousFirstDay;
- DateTime _previousLastDay;
- int _pageId;
- double _dx;
- bool _useNextCalendarFormat;
- bool _includeInvisibleDays;
- _SelectedDayCallback _selectedDayCallback;
-
- void _init({
- @required Map events,
- @required Map holidays,
- @required DateTime initialDay,
- @required CalendarFormat initialFormat,
- @required Map availableCalendarFormats,
- @required bool useNextCalendarFormat,
- @required StartingDayOfWeek startingDayOfWeek,
- @required _SelectedDayCallback selectedDayCallback,
- @required OnVisibleDaysChanged onVisibleDaysChanged,
- @required OnCalendarCreated onCalendarCreated,
- @required bool includeInvisibleDays,
- }) {
- _events = events;
- _holidays = holidays;
- _availableCalendarFormats = availableCalendarFormats;
- _startingDayOfWeek = startingDayOfWeek;
- _useNextCalendarFormat = useNextCalendarFormat;
- _selectedDayCallback = selectedDayCallback;
- _includeInvisibleDays = includeInvisibleDays;
-
- _pageId = 0;
- _dx = 0;
-
- final now = DateTime.now();
- _focusedDay = initialDay ?? _normalizeDate(now);
- _selectedDay = _focusedDay;
- _calendarFormat = ValueNotifier(initialFormat);
- _visibleDays = ValueNotifier(_getVisibleDays());
- _previousFirstDay = _visibleDays.value.first;
- _previousLastDay = _visibleDays.value.last;
-
- _calendarFormat.addListener(() {
- _visibleDays.value = _getVisibleDays();
- });
-
- if (onVisibleDaysChanged != null) {
- _visibleDays.addListener(() {
- if (!_isSameDay(_visibleDays.value.first, _previousFirstDay) ||
- !_isSameDay(_visibleDays.value.last, _previousLastDay)) {
- _previousFirstDay = _visibleDays.value.first;
- _previousLastDay = _visibleDays.value.last;
- onVisibleDaysChanged(
- _getFirstDay(includeInvisible: _includeInvisibleDays),
- _getLastDay(includeInvisible: _includeInvisibleDays),
- _calendarFormat.value,
- );
- }
- });
- }
-
- if (onCalendarCreated != null) {
- onCalendarCreated(
- _getFirstDay(includeInvisible: _includeInvisibleDays),
- _getLastDay(includeInvisible: _includeInvisibleDays),
- _calendarFormat.value,
- );
- }
- }
-
- /// Disposes the controller.
- /// ```dart
- /// @override
- /// void dispose() {
- /// _calendarController.dispose();
- /// super.dispose();
- /// }
- /// ```
- void dispose() {
- _calendarFormat?.dispose();
- _visibleDays?.dispose();
- }
-
- /// Toggles calendar format. Same as using `FormatButton`.
- void toggleCalendarFormat() {
- _calendarFormat.value = _nextFormat();
- }
-
- /// Sets calendar format by emulating swipe.
- void swipeCalendarFormat({@required bool isSwipeUp}) {
- assert(isSwipeUp != null);
-
- final formats = _availableCalendarFormats.keys.toList();
- int id = formats.indexOf(_calendarFormat.value);
-
- // Order of CalendarFormats must be from biggest to smallest,
- // eg.: [month, twoWeeks, week]
- if (isSwipeUp) {
- id = _clamp(0, formats.length - 1, id + 1);
- } else {
- id = _clamp(0, formats.length - 1, id - 1);
- }
- _calendarFormat.value = formats[id];
- }
-
- /// Sets calendar format to a given `value`.
- void setCalendarFormat(CalendarFormat value) {
- _calendarFormat.value = value;
- }
-
- /// Sets selected day to a given `value`.
- /// Use `runCallback: true` if this should trigger `OnDaySelected` callback.
- void setSelectedDay(
- DateTime value, {
- bool isProgrammatic = true,
- bool animate = true,
- bool runCallback = false,
- }) {
- final normalizedDate = _normalizeDate(value);
-
- if (animate) {
- if (normalizedDate.isBefore(_getFirstDay(includeInvisible: false))) {
- _decrementPage();
- } else if (normalizedDate.isAfter(_getLastDay(includeInvisible: false))) {
- _incrementPage();
- }
- }
-
- _selectedDay = normalizedDate;
- _focusedDay = normalizedDate;
- _updateVisibleDays(isProgrammatic);
-
- if (isProgrammatic && runCallback && _selectedDayCallback != null) {
- _selectedDayCallback(normalizedDate);
- }
- }
-
- /// Sets displayed month/year without changing the currently selected day.
- void setFocusedDay(DateTime value) {
- _focusedDay = _normalizeDate(value);
- _updateVisibleDays(true);
- }
-
- void _updateVisibleDays(bool isProgrammatic) {
- if (calendarFormat != CalendarFormat.twoWeeks || isProgrammatic) {
- _visibleDays.value = _getVisibleDays();
- }
- }
-
- CalendarFormat _nextFormat() {
- final formats = _availableCalendarFormats.keys.toList();
- int id = formats.indexOf(_calendarFormat.value);
- id = (id + 1) % formats.length;
-
- return formats[id];
- }
-
- String _getFormatButtonText() =>
- _useNextCalendarFormat ? _availableCalendarFormats[_nextFormat()] : _availableCalendarFormats[_calendarFormat.value];
-
- void _selectPrevious() {
- if (calendarFormat == CalendarFormat.month) {
- _selectPreviousMonth();
- } else if (calendarFormat == CalendarFormat.twoWeeks) {
- _selectPreviousTwoWeeks();
- } else {
- _selectPreviousWeek();
- }
-
- _visibleDays.value = _getVisibleDays();
- _decrementPage();
- }
-
- void _selectNext() {
- if (calendarFormat == CalendarFormat.month) {
- _selectNextMonth();
- } else if (calendarFormat == CalendarFormat.twoWeeks) {
- _selectNextTwoWeeks();
- } else {
- _selectNextWeek();
- }
-
- _visibleDays.value = _getVisibleDays();
- _incrementPage();
- }
-
- void _selectPreviousMonth() {
- _focusedDay = _previousMonth(_focusedDay);
- }
-
- void _selectNextMonth() {
- _focusedDay = _nextMonth(_focusedDay);
- }
-
- void _selectPreviousTwoWeeks() {
- if (_visibleDays.value.take(7).contains(_focusedDay)) {
- // in top row
- _focusedDay = _previousWeek(_focusedDay);
- } else {
- // in bottom row OR not visible
- _focusedDay = _previousWeek(_focusedDay.subtract(const Duration(days: 7)));
- }
- }
-
- void _selectNextTwoWeeks() {
- if (!_visibleDays.value.skip(7).contains(_focusedDay)) {
- // not in bottom row [eg: in top row OR not visible]
- _focusedDay = _nextWeek(_focusedDay);
- }
- }
-
- void _selectPreviousWeek() {
- _focusedDay = _previousWeek(_focusedDay);
- }
-
- void _selectNextWeek() {
- _focusedDay = _nextWeek(_focusedDay);
- }
-
- DateTime _getFirstDay({@required bool includeInvisible}) {
- if (_calendarFormat.value == CalendarFormat.month && !includeInvisible) {
- return _firstDayOfMonth(_focusedDay);
- } else {
- return _visibleDays.value.first;
- }
- }
-
- DateTime _getLastDay({@required bool includeInvisible}) {
- if (_calendarFormat.value == CalendarFormat.month && !includeInvisible) {
- return _lastDayOfMonth(_focusedDay);
- } else {
- return _visibleDays.value.last;
- }
- }
-
- List _getVisibleDays() {
- if (calendarFormat == CalendarFormat.month) {
- return _daysInMonth(_focusedDay);
- } else if (calendarFormat == CalendarFormat.twoWeeks) {
- return _daysInWeek(_focusedDay)
- ..addAll(_daysInWeek(
- _focusedDay.add(const Duration(days: 7)),
- ));
- } else {
- return _daysInWeek(_focusedDay);
- }
- }
-
- void _decrementPage() {
- _pageId--;
- _dx = _dxMin;
- }
-
- void _incrementPage() {
- _pageId++;
- _dx = _dxMax;
- }
-
- List _daysInMonth(DateTime month) {
- final first = _firstDayOfMonth(month);
- final daysBefore = _getDaysBefore(first);
- final firstToDisplay = first.subtract(Duration(days: daysBefore));
-
- final last = _lastDayOfMonth(month);
- final daysAfter = _getDaysAfter(last);
-
- final lastToDisplay = last.add(Duration(days: daysAfter));
- return _daysInRange(firstToDisplay, lastToDisplay).toList();
- }
-
- int _getDaysBefore(DateTime firstDay) {
- return (firstDay.weekday + 7 - _getWeekdayNumber(_startingDayOfWeek)) % 7;
- }
-
- int _getDaysAfter(DateTime lastDay) {
- int invertedStartingWeekday = 8 - _getWeekdayNumber(_startingDayOfWeek);
-
- int daysAfter = 7 - ((lastDay.weekday + invertedStartingWeekday) % 7) + 1;
- if (daysAfter == 8) {
- daysAfter = 1;
- }
-
- return daysAfter;
- }
-
- List _daysInWeek(DateTime week) {
- final first = _firstDayOfWeek(week);
- final last = _lastDayOfWeek(week);
-
- return _daysInRange(first, last).toList();
- }
-
- DateTime _firstDayOfWeek(DateTime day) {
- day = _normalizeDate(day);
-
- final decreaseNum = _getDaysBefore(day);
- return day.subtract(Duration(days: decreaseNum));
- }
-
- DateTime _lastDayOfWeek(DateTime day) {
- day = _normalizeDate(day);
-
- final increaseNum = _getDaysBefore(day);
- return day.add(Duration(days: 7 - increaseNum));
- }
-
- DateTime _firstDayOfMonth(DateTime month) {
- return DateTime.utc(month.year, month.month, 1, 12);
- }
-
- DateTime _lastDayOfMonth(DateTime month) {
- final date = month.month < 12 ? DateTime.utc(month.year, month.month + 1, 1, 12) : DateTime.utc(month.year + 1, 1, 1, 12);
- return date.subtract(const Duration(days: 1));
- }
-
- DateTime _previousWeek(DateTime week) {
- return week.subtract(const Duration(days: 7));
- }
-
- DateTime _nextWeek(DateTime week) {
- return week.add(const Duration(days: 7));
- }
-
- DateTime _previousMonth(DateTime month) {
- if (month.month == 1) {
- return DateTime(month.year - 1, 12);
- } else {
- return DateTime(month.year, month.month - 1);
- }
- }
-
- DateTime _nextMonth(DateTime month) {
- if (month.month == 12) {
- return DateTime(month.year + 1, 1);
- } else {
- return DateTime(month.year, month.month + 1);
- }
- }
-
- Iterable _daysInRange(DateTime firstDay, DateTime lastDay) sync* {
- var temp = firstDay;
-
- while (temp.isBefore(lastDay)) {
- yield _normalizeDate(temp);
- temp = temp.add(const Duration(days: 1));
- }
- }
-
- DateTime _normalizeDate(DateTime value) {
- return DateTime.utc(value.year, value.month, value.day, 12);
- }
-
- DateTime _getEventKey(DateTime day) {
- return visibleEvents.keys.firstWhere((it) => _isSameDay(it, day), orElse: () => null);
- }
-
- DateTime _getHolidayKey(DateTime day) {
- return visibleHolidays.keys.firstWhere((it) => _isSameDay(it, day), orElse: () => null);
- }
-
- /// Returns true if `day` is currently selected.
- bool isSelected(DateTime day) {
- return _isSameDay(day, selectedDay);
- }
-
- /// Returns true if `day` is the same day as `DateTime.now()`.
- bool isToday(DateTime day) {
- return _isSameDay(day, DateTime.now());
- }
-
- bool _isSameDay(DateTime dayA, DateTime dayB) {
- return dayA.year == dayB.year && dayA.month == dayB.month && dayA.day == dayB.day;
- }
-
- bool _isWeekend(DateTime day, List weekendDays) {
- return weekendDays.contains(day.weekday);
- }
-
- bool _isExtraDay(DateTime day) {
- return _isExtraDayBefore(day) || _isExtraDayAfter(day);
- }
-
- bool _isExtraDayBefore(DateTime day) {
- return day.month < _focusedDay.month;
- }
-
- bool _isExtraDayAfter(DateTime day) {
- return day.month > _focusedDay.month;
- }
-
- int _clamp(int min, int max, int value) {
- if (value > max) {
- return max;
- } else if (value < min) {
- return min;
- } else {
- return value;
- }
- }
-}
diff --git a/lib/src/customization/calendar_builders.dart b/lib/src/customization/calendar_builders.dart
index 90e6d107..cf21d2e4 100644
--- a/lib/src/customization/calendar_builders.dart
+++ b/lib/src/customization/calendar_builders.dart
@@ -1,91 +1,113 @@
-// Copyright (c) 2019 Aleksander Woźniak
-// Licensed under Apache License v2.0
-
-part of table_calendar;
-
-/// Main Builder signature for `TableCalendar`. Contains `date` and list of all `events` associated with that `date`.
-/// Note that most of the time, `events` param will be ommited, however it is there if needed.
-/// `events` param can be null.
-typedef FullBuilder = Widget Function(BuildContext context, DateTime date, List events);
-
-/// Builder signature for a list of event markers. Contains `date` and list of all `events` associated with that `date`.
-/// Both `events` and `holidays` params can be null.
-typedef FullListBuilder = List Function(BuildContext context, DateTime date, List events, List holidays);
-
-/// Builder signature for weekday names row. Contains `weekday` string, which is formatted by `dowTextBuilder`
-/// or by default function (DateFormat.E(widget.locale).format(date)), if `dowTextBuilder` is null.
-typedef DowBuilder = Widget Function(BuildContext context, String weekday);
-
-/// Builder signature for a single event marker. Contains `date` and a single `event` associated with that `date`.
-typedef SingleMarkerBuilder = Widget Function(BuildContext context, DateTime date, dynamic event);
-
-/// Class containing all custom Builders for `TableCalendar`.
-class CalendarBuilders {
- /// The most general custom Builder. Use to provide your own UI for every day cell.
- /// If `dayBuilder` is not specified, a default day cell will be displayed.
- /// Default day cells are customizable with `CalendarStyle`.
- final FullBuilder dayBuilder;
-
- /// Custom Builder for currently selected day. Will overwrite `dayBuilder` on selected day.
- final FullBuilder selectedDayBuilder;
-
- /// Custom Builder for today. Will overwrite `dayBuilder` on today.
- final FullBuilder todayDayBuilder;
-
- /// Custom Builder for holidays. Will overwrite `dayBuilder` on holidays.
- final FullBuilder holidayDayBuilder;
-
- /// Custom Builder for weekends. Will overwrite `dayBuilder` on weekends.
- final FullBuilder weekendDayBuilder;
-
- /// Custom Builder for days outside of current month. Will overwrite `dayBuilder` on days outside of current month.
- final FullBuilder outsideDayBuilder;
-
- /// Custom Builder for weekends outside of current month. Will overwrite `dayBuilder`on weekends outside of current month.
- final FullBuilder outsideWeekendDayBuilder;
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/widgets.dart';
+import 'package:table_calendar/src/shared/utils.dart'
+ show DayBuilder, FocusedDayBuilder;
+
+/// Signature for a function that creates a single event marker for a given `day`.
+/// Contains a single `event` associated with that `day`.
+typedef SingleMarkerBuilder = Widget? Function(
+ BuildContext context,
+ DateTime day,
+ T event,
+);
+
+/// Signature for a function that creates an event marker for a given `day`.
+/// Contains a list of `events` associated with that `day`.
+typedef MarkerBuilder = Widget? Function(
+ BuildContext context,
+ DateTime day,
+ List events,
+);
+
+/// Signature for a function that creates a background highlight for a given `day`.
+///
+/// Used for highlighting current range selection.
+/// Contains a value determining if the given `day` falls within the selected range.
+typedef HighlightBuilder = Widget? Function(
+ BuildContext context,
+ DateTime day,
+ bool isWithinRange,
+);
+
+/// Class containing all custom builders for `TableCalendar`.
+class CalendarBuilders {
+ /// Custom builder for day cells, with a priority over any other builder.
+ final FocusedDayBuilder? prioritizedBuilder;
+
+ /// Custom builder for a day cell that matches the current day.
+ final FocusedDayBuilder? todayBuilder;
+
+ /// Custom builder for day cells that are currently marked as selected by `selectedDayPredicate`.
+ final FocusedDayBuilder? selectedBuilder;
+
+ /// Custom builder for a day cell that is the start of current range selection.
+ final FocusedDayBuilder? rangeStartBuilder;
+
+ /// Custom builder for a day cell that is the end of current range selection.
+ final FocusedDayBuilder? rangeEndBuilder;
+
+ /// Custom builder for day cells that fall within the currently selected range.
+ final FocusedDayBuilder? withinRangeBuilder;
+
+ /// Custom builder for day cells, of which the `day.month` is different than `focusedDay.month`.
+ /// This will affect day cells that do not match the currently focused month.
+ final FocusedDayBuilder? outsideBuilder;
+
+ /// Custom builder for day cells that have been disabled.
+ ///
+ /// This refers to dates disabled by returning false in `enabledDayPredicate`,
+ /// as well as dates that are outside of the bounds set up by `firstDay` and `lastDay`.
+ final FocusedDayBuilder? disabledBuilder;
- /// Custom Builder for holidays outside of current month. Will overwrite `dayBuilder` on holidays outside of current month.
- final FullBuilder outsideHolidayDayBuilder;
+ /// Custom builder for day cells that are marked as holidays by `holidayPredicate`.
+ final FocusedDayBuilder? holidayBuilder;
- /// Custom Builder for days outside of `startDay` - `endDay` Date range. Will overwrite `dayBuilder` for aforementioned days.
- final FullBuilder unavailableDayBuilder;
+ /// Custom builder for day cells that do not match any other builder.
+ final FocusedDayBuilder? defaultBuilder;
- /// Custom Builder for a whole group of event markers. Use to provide your own marker UI for each day cell.
- /// Every `Widget` passed here will be placed in a `Stack`, above the cell content.
- /// Wrap them with `Positioned` to gain more control over their placement.
- ///
- /// If `markersBuilder` is not specified, `TableCalendar` will try to use `singleMarkerBuilder` or default markers (customizable with `CalendarStyle`).
- /// Mutually exclusive with `singleMarkerBuilder`.
- final FullListBuilder markersBuilder;
+ /// Custom builder for background highlight of range selection.
+ /// If `isWithinRange` is true, then `day` is within the selected range.
+ final HighlightBuilder? rangeHighlightBuilder;
- /// Custom Builder for a single event marker. Each of those will be displayed in a `Row` above of the day cell.
+ /// Custom builder for a single event marker. Each of those will be displayed in a `Row` above of the day cell.
/// You can adjust markers' position with `CalendarStyle` properties.
///
/// If `singleMarkerBuilder` is not specified, a default event marker will be displayed (customizable with `CalendarStyle`).
- /// Mutually exclusive with `markersBuilder`.
- final SingleMarkerBuilder singleMarkerBuilder;
+ final SingleMarkerBuilder? singleMarkerBuilder;
+
+ /// Custom builder for event markers. Use to provide your own marker UI for each day cell.
+ /// Using `markerBuilder` will override `singleMarkerBuilder` and default event markers.
+ final MarkerBuilder? markerBuilder;
+
+ /// Custom builder for days of the week labels (Mon, Tue, Wed, etc.).
+ final DayBuilder? dowBuilder;
- /// Custom builder for dow weekday names (displayed between `HeaderRow` and calendar days).
- /// Will overwrite `weekdayStyle` and `weekendStyle` from `DaysOfWeekStyle`.
- final DowBuilder dowWeekdayBuilder;
+ /// Use to customize header's title using different widget
+ final DayBuilder? headerTitleBuilder;
- /// Custom builder for dow weekend names (displayed between `HeaderRow` and calendar days).
- /// Will overwrite `weekendStyle` from `DaysOfWeekStyle` and `dowWeekdayBuilder` for weekends, if it also exists.
- final DowBuilder dowWeekendBuilder;
+ /// Custom builder for number of the week labels.
+ final Widget? Function(BuildContext context, int weekNumber)?
+ weekNumberBuilder;
+ /// Creates `CalendarBuilders` for `TableCalendar` widget.
const CalendarBuilders({
- this.dayBuilder,
- this.selectedDayBuilder,
- this.todayDayBuilder,
- this.holidayDayBuilder,
- this.weekendDayBuilder,
- this.outsideDayBuilder,
- this.outsideWeekendDayBuilder,
- this.outsideHolidayDayBuilder,
- this.unavailableDayBuilder,
- this.markersBuilder,
+ this.prioritizedBuilder,
+ this.todayBuilder,
+ this.selectedBuilder,
+ this.rangeStartBuilder,
+ this.rangeEndBuilder,
+ this.withinRangeBuilder,
+ this.outsideBuilder,
+ this.disabledBuilder,
+ this.holidayBuilder,
+ this.defaultBuilder,
+ this.rangeHighlightBuilder,
this.singleMarkerBuilder,
- this.dowWeekdayBuilder,
- this.dowWeekendBuilder,
- }) : assert(!(singleMarkerBuilder != null && markersBuilder != null));
+ this.markerBuilder,
+ this.dowBuilder,
+ this.headerTitleBuilder,
+ this.weekNumberBuilder,
+ });
}
diff --git a/lib/src/customization/calendar_style.dart b/lib/src/customization/calendar_style.dart
index 6f83eb1c..ecfa578e 100644
--- a/lib/src/customization/calendar_style.dart
+++ b/lib/src/customization/calendar_style.dart
@@ -1,131 +1,260 @@
-// Copyright (c) 2019 Aleksander Woźniak
-// Licensed under Apache License v2.0
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
-part of table_calendar;
+import 'package:flutter/widgets.dart';
+import 'package:table_calendar/table_calendar.dart';
-/// Class containing styling for `TableCalendar`'s content.
+/// Class containing styling and configuration for `TableCalendar`'s content.
class CalendarStyle {
- /// BoxDecoration for each interior row of the table
- final BoxDecoration contentDecoration;
+ /// Maximum amount of single event marker dots to be displayed.
+ final int markersMaxCount;
- /// Style of foreground Text for regular weekdays.
- final TextStyle weekdayStyle;
+ /// Specifies if event markers rendered for a day cell can overflow cell's boundaries.
+ /// * `true` - Event markers will be drawn over the cell boundaries
+ /// * `false` - Event markers will be clipped if they are too big
+ final bool canMarkersOverflow;
- /// Style of foreground Text for regular weekends.
- final TextStyle weekendStyle;
+ /// Determines if single event marker dots should be aligned automatically with `markersAnchor`.
+ /// If `false`, `markersOffset` will be used instead.
+ final bool markersAutoAligned;
- /// Style of foreground Text for holidays.
- final TextStyle holidayStyle;
+ /// Specifies the anchor point of single event markers if `markersAutoAligned` is `true`.
+ /// A value of `0.5` will center the markers at the bottom edge of day cell's decoration.
+ ///
+ /// Includes `cellMargin` for calculations.
+ final double markersAnchor;
+
+ /// The size of single event marker dot.
+ ///
+ /// By default `markerSizeScale` is used. To use `markerSize` instead, simply provide a non-null value.
+ final double? markerSize;
- /// Style of foreground Text for selected day.
- final TextStyle selectedStyle;
+ /// Proportion of single event marker dot size in relation to day cell size.
+ ///
+ /// Includes `cellMargin` for calculations.
+ final double markerSizeScale;
- /// Style of foreground Text for today.
- final TextStyle todayStyle;
+ /// `PositionedOffset` for event markers. Allows to specify `top`, `bottom`, `start` and `end`.
+ final PositionedOffset markersOffset;
- /// Style of foreground Text for weekdays outside of current month.
- final TextStyle outsideStyle;
+ /// General `Alignment` for event markers.
+ /// Will have no effect on markers if `markersAutoAligned` or `markersOffset` is used.
+ final AlignmentGeometry markersAlignment;
- /// Style of foreground Text for weekends outside of current month.
- final TextStyle outsideWeekendStyle;
+ /// Decoration of single event markers. Affects each marker dot.
+ final Decoration markerDecoration;
- /// Style of foreground Text for holidays outside of current month.
- final TextStyle outsideHolidayStyle;
+ /// Margin of single event markers. Affects each marker dot.
+ final EdgeInsets markerMargin;
- /// Style of foreground Text for days outside of `startDay` - `endDay` Date range.
- final TextStyle unavailableStyle;
+ /// Margin of each individual day cell.
+ final EdgeInsets cellMargin;
- /// Style of foreground Text for days that contain events.
- final TextStyle eventDayStyle;
+ /// Padding of each individual day cell.
+ final EdgeInsets cellPadding;
- /// Background Color of selected day.
- final Color selectedColor;
+ /// Alignment of each individual day cell.
+ final AlignmentGeometry cellAlignment;
- /// Background Color of today.
- final Color todayColor;
+ /// Proportion of range selection highlight size in relation to day cell size.
+ ///
+ /// Includes `cellMargin` for calculations.
+ final double rangeHighlightScale;
- /// Color of event markers placed on the bottom of every day containing events.
- final Color markersColor;
+ /// Color of range selection highlight.
+ final Color rangeHighlightColor;
- /// General `Alignment` for event markers.
- /// NOTE: `markersPositionBottom` defaults to `5.0`, so you might want to set it to `null` when using `markersAlignment`.
- final Alignment markersAlignment;
+ /// Determines if day cells that do not match the currently focused month should be visible.
+ ///
+ /// Affects only `CalendarFormat.month`.
+ final bool outsideDaysVisible;
+
+ /// Determines if a day cell that matches the current day should be highlighted.
+ final bool isTodayHighlighted;
+
+ /// TextStyle for a day cell that matches the current day.
+ final TextStyle todayTextStyle;
+
+ /// Decoration for a day cell that matches the current day.
+ final Decoration todayDecoration;
+
+ /// TextStyle for day cells that are currently marked as selected by `selectedDayPredicate`.
+ final TextStyle selectedTextStyle;
+
+ /// Decoration for day cells that are currently marked as selected by `selectedDayPredicate`.
+ final Decoration selectedDecoration;
+
+ /// TextStyle for a day cell that is the start of current range selection.
+ final TextStyle rangeStartTextStyle;
+
+ /// Decoration for a day cell that is the start of current range selection.
+ final Decoration rangeStartDecoration;
+
+ /// TextStyle for a day cell that is the end of current range selection.
+ final TextStyle rangeEndTextStyle;
- /// `top` property of `Positioned` widget used for event markers.
- final double markersPositionTop;
+ /// Decoration for a day cell that is the end of current range selection.
+ final Decoration rangeEndDecoration;
- /// `bottom` property of `Positioned` widget used for event markers.
- /// NOTE: This defaults to `5.0`, so you might occasionally want to set it to `null`.
- final double markersPositionBottom;
+ /// TextStyle for day cells that fall within the currently selected range.
+ final TextStyle withinRangeTextStyle;
- /// `left` property of `Positioned` widget used for event markers.
- final double markersPositionLeft;
+ /// Decoration for day cells that fall within the currently selected range.
+ final Decoration withinRangeDecoration;
- /// `right` property of `Positioned` widget used for event markers.
- final double markersPositionRight;
+ /// TextStyle for day cells, of which the `day.month` is different than `focusedDay.month`.
+ /// This will affect day cells that do not match the currently focused month.
+ final TextStyle outsideTextStyle;
- /// Maximum amount of event markers to be displayed.
- final int markersMaxAmount;
+ /// Decoration for day cells, of which the `day.month` is different than `focusedDay.month`.
+ /// This will affect day cells that do not match the currently focused month.
+ final Decoration outsideDecoration;
- /// Specifies whether or not days outside of current month should be displayed.
+ /// TextStyle for day cells that have been disabled.
///
- /// Sometimes a fragment of previous month's last week (or next month's first week) appears in current month's view.
- /// This property defines if those should be visible (eg. with custom style) or hidden.
- final bool outsideDaysVisible;
+ /// This refers to dates disabled by returning false in `enabledDayPredicate`,
+ /// as well as dates that are outside of the bounds set up by `firstDay` and `lastDay`.
+ final TextStyle disabledTextStyle;
- /// Determines rendering priority for SelectedDay and Today.
- /// * `true` - SelectedDay will have higher priority than Today
- /// * `false` - Today will have higher priority than SelectedDay
- final bool renderSelectedFirst;
+ /// Decoration for day cells that have been disabled.
+ ///
+ /// This refers to dates disabled by returning false in `enabledDayPredicate`,
+ /// as well as dates that are outside of the bounds set up by `firstDay` and `lastDay`.
+ final Decoration disabledDecoration;
- /// Determines whether the row of days of the week should be rendered or not.
- final bool renderDaysOfWeek;
+ /// TextStyle for day cells that are marked as holidays by `holidayPredicate`.
+ final TextStyle holidayTextStyle;
- /// Padding of `TableCalendar`'s content.
- final EdgeInsets contentPadding;
+ /// Decoration for day cells that are marked as holidays by `holidayPredicate`.
+ final Decoration holidayDecoration;
- /// Margin of Cells' decoration.
- final EdgeInsets cellMargin;
+ /// TextStyle for day cells that match `weekendDay` list.
+ final TextStyle weekendTextStyle;
- /// Specifies if event markers rendered for a day cell can overflow cell's boundaries.
- /// * `true` - Event markers will be drawn over the cell boundaries
- /// * `false` - Event markers will not be drawn over the cell boundaries and will be clipped if they are too big
- final bool canEventMarkersOverflow;
+ /// Decoration for day cells that match `weekendDay` list.
+ final Decoration weekendDecoration;
- /// Specifies whether or not SelectedDay should be highlighted.
- final bool highlightSelected;
+ /// TextStyle for week number.
+ final TextStyle weekNumberTextStyle;
- /// Specifies whether or not Today should be highlighted.
- final bool highlightToday;
+ /// TextStyle for day cells that do not match any other styles.
+ final TextStyle defaultTextStyle;
+ /// Decoration for day cells that do not match any other styles.
+ final Decoration defaultDecoration;
+
+ /// Decoration for each interior row of day cells.
+ final Decoration rowDecoration;
+
+ /// Border for the internal `Table` widget.
+ final TableBorder tableBorder;
+
+ /// Padding for the internal `Table` widget.
+ final EdgeInsets tablePadding;
+
+ /// Use to customize the text within each day cell.
+ /// Defaults to `'${date.day}'`, to show just the day number.
+ ///
+ /// Example usage:
+ /// ```dart
+ /// dayTextFormatter: (date, locale) => DateFormat.d(locale).format(date),
+ /// ```
+ final TextFormatter? dayTextFormatter;
+
+ /// Creates a `CalendarStyle` used by `TableCalendar` widget.
const CalendarStyle({
- this.contentDecoration = const BoxDecoration(),
- this.weekdayStyle = const TextStyle(),
- this.weekendStyle = const TextStyle(color: const Color(0xFFF44336)), // Material red[500]
- this.holidayStyle = const TextStyle(color: const Color(0xFFF44336)), // Material red[500]
- this.selectedStyle = const TextStyle(color: const Color(0xFFFAFAFA), fontSize: 16.0), // Material grey[50]
- this.todayStyle = const TextStyle(color: const Color(0xFFFAFAFA), fontSize: 16.0), // Material grey[50]
- this.outsideStyle = const TextStyle(color: const Color(0xFF9E9E9E)), // Material grey[500]
- this.outsideWeekendStyle = const TextStyle(color: const Color(0xFFEF9A9A)), // Material red[200]
- this.outsideHolidayStyle = const TextStyle(color: const Color(0xFFEF9A9A)), // Material red[200]
- this.unavailableStyle = const TextStyle(color: const Color(0xFFBFBFBF)),
- this.eventDayStyle = const TextStyle(),
- this.selectedColor = const Color(0xFF5C6BC0), // Material indigo[400]
- this.todayColor = const Color(0xFF9FA8DA), // Material indigo[200]
- this.markersColor = const Color(0xFF263238), // Material blueGrey[900]
- this.markersAlignment = Alignment.bottomCenter,
- this.markersPositionTop,
- this.markersPositionBottom = 5.0,
- this.markersPositionLeft,
- this.markersPositionRight,
- this.markersMaxAmount = 4,
+ this.isTodayHighlighted = true,
+ this.canMarkersOverflow = true,
this.outsideDaysVisible = true,
- this.renderSelectedFirst = true,
- this.renderDaysOfWeek = true,
- this.contentPadding = const EdgeInsets.only(bottom: 4.0, left: 8.0, right: 8.0),
+ this.markersAutoAligned = true,
+ this.markerSize,
+ this.markerSizeScale = 0.2,
+ this.markersAnchor = 0.7,
+ this.rangeHighlightScale = 1.0,
+ this.markerMargin = const EdgeInsets.symmetric(horizontal: 0.3),
+ this.markersAlignment = Alignment.bottomCenter,
+ this.markersMaxCount = 4,
this.cellMargin = const EdgeInsets.all(6.0),
- this.canEventMarkersOverflow = false,
- this.highlightSelected = true,
- this.highlightToday = true,
+ this.cellPadding = EdgeInsets.zero,
+ this.cellAlignment = Alignment.center,
+ this.markersOffset = const PositionedOffset(),
+ this.rangeHighlightColor = const Color(0xFFBBDDFF),
+ this.markerDecoration = const BoxDecoration(
+ color: Color(0xFF263238),
+ shape: BoxShape.circle,
+ ),
+ this.todayTextStyle = const TextStyle(
+ color: Color(0xFFFAFAFA),
+ fontSize: 16.0,
+ ), //
+ this.todayDecoration = const BoxDecoration(
+ color: Color(0xFF9FA8DA),
+ shape: BoxShape.circle,
+ ),
+ this.selectedTextStyle = const TextStyle(
+ color: Color(0xFFFAFAFA),
+ fontSize: 16.0,
+ ),
+ this.selectedDecoration = const BoxDecoration(
+ color: Color(0xFF5C6BC0),
+ shape: BoxShape.circle,
+ ),
+ this.rangeStartTextStyle = const TextStyle(
+ color: Color(0xFFFAFAFA),
+ fontSize: 16.0,
+ ),
+ this.rangeStartDecoration = const BoxDecoration(
+ color: Color(0xFF6699FF),
+ shape: BoxShape.circle,
+ ),
+ this.rangeEndTextStyle = const TextStyle(
+ color: Color(0xFFFAFAFA),
+ fontSize: 16.0,
+ ),
+ this.rangeEndDecoration = const BoxDecoration(
+ color: Color(0xFF6699FF),
+ shape: BoxShape.circle,
+ ),
+ this.withinRangeTextStyle = const TextStyle(),
+ this.withinRangeDecoration = const BoxDecoration(shape: BoxShape.circle),
+ this.outsideTextStyle = const TextStyle(color: Color(0xFFAEAEAE)),
+ this.outsideDecoration = const BoxDecoration(shape: BoxShape.circle),
+ this.disabledTextStyle = const TextStyle(color: Color(0xFFBFBFBF)),
+ this.disabledDecoration = const BoxDecoration(shape: BoxShape.circle),
+ this.holidayTextStyle = const TextStyle(color: Color(0xFF5C6BC0)),
+ this.holidayDecoration = const BoxDecoration(
+ border: Border.fromBorderSide(
+ BorderSide(color: Color(0xFF9FA8DA), width: 1.4),
+ ),
+ shape: BoxShape.circle,
+ ),
+ this.weekendTextStyle = const TextStyle(color: Color(0xFF5A5A5A)),
+ this.weekendDecoration = const BoxDecoration(shape: BoxShape.circle),
+ this.weekNumberTextStyle =
+ const TextStyle(fontSize: 12, color: Color(0xFFBFBFBF)),
+ this.defaultTextStyle = const TextStyle(),
+ this.defaultDecoration = const BoxDecoration(shape: BoxShape.circle),
+ this.rowDecoration = const BoxDecoration(),
+ this.tableBorder = const TableBorder(),
+ this.tablePadding = EdgeInsets.zero,
+ this.dayTextFormatter,
});
}
+
+/// Helper class containing data for internal `Positioned` widget.
+class PositionedOffset {
+ /// Distance from the top edge.
+ final double? top;
+
+ /// Distance from the bottom edge.
+ final double? bottom;
+
+ /// Distance from the leading edge.
+ final double? start;
+
+ /// Distance from the trailing edge.
+ final double? end;
+
+ /// Creates a `PositionedOffset`. Values are set to `null` by default.
+ const PositionedOffset({this.top, this.bottom, this.start, this.end});
+}
diff --git a/lib/src/customization/days_of_week_style.dart b/lib/src/customization/days_of_week_style.dart
index fe5dcd26..8ec79212 100644
--- a/lib/src/customization/days_of_week_style.dart
+++ b/lib/src/customization/days_of_week_style.dart
@@ -1,33 +1,35 @@
-// Copyright (c) 2019 Aleksander Woźniak
-// Licensed under Apache License v2.0
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
-part of table_calendar;
+import 'package:flutter/widgets.dart';
+import 'package:table_calendar/src/shared/utils.dart' show TextFormatter;
/// Class containing styling for `TableCalendar`'s days of week panel.
class DaysOfWeekStyle {
- /// Use to customize days of week panel text (eg. with different `DateFormat`).
+ /// Use to customize days of week panel text (e.g. with different `DateFormat`).
/// You can use `String` transformations to further customize the text.
- /// Defaults to simple `'E'` format (eg. Mon, Tue, Wed, etc.).
+ /// Defaults to simple `'E'` format (i.e. Mon, Tue, Wed, etc.).
///
/// Example usage:
/// ```dart
- /// dowTextBuilder: (date, locale) => DateFormat.E(locale).format(date)[0],
+ /// dowTextFormatter: (date, locale) => DateFormat.E(locale).format(date)[0],
/// ```
- final TextBuilder dowTextBuilder;
+ final TextFormatter? dowTextFormatter;
- /// BoxDecoration for the top row of the table
- final BoxDecoration decoration;
+ /// Decoration for the top row of the table
+ final Decoration decoration;
- /// Style for weekdays on the top of Calendar.
+ /// Style for weekdays on the top of calendar.
final TextStyle weekdayStyle;
- /// Style for weekend days on the top of Calendar.
+ /// Style for weekend days on the top of calendar.
final TextStyle weekendStyle;
+ /// Creates a `DaysOfWeekStyle` used by `TableCalendar` widget.
const DaysOfWeekStyle({
- this.dowTextBuilder,
+ this.dowTextFormatter,
this.decoration = const BoxDecoration(),
- this.weekdayStyle = const TextStyle(color: const Color(0xFF616161)), // Material grey[700]
- this.weekendStyle = const TextStyle(color: const Color(0xFFF44336)), // Material red[500]
+ this.weekdayStyle = const TextStyle(color: Color(0xFF4F4F4F)),
+ this.weekendStyle = const TextStyle(color: Color(0xFF6A6A6A)),
});
}
diff --git a/lib/src/customization/header_style.dart b/lib/src/customization/header_style.dart
index 9ef5fee4..71ac67df 100644
--- a/lib/src/customization/header_style.dart
+++ b/lib/src/customization/header_style.dart
@@ -1,12 +1,13 @@
-// Copyright (c) 2019 Aleksander Woźniak
-// Licensed under Apache License v2.0
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
-part of table_calendar;
+import 'package:flutter/material.dart';
+import 'package:table_calendar/src/shared/utils.dart' show TextFormatter;
/// Class containing styling and configuration of `TableCalendar`'s header.
class HeaderStyle {
/// Responsible for making title Text centered.
- final bool centerHeaderTitle;
+ final bool titleCentered;
/// Responsible for FormatButton visibility.
final bool formatButtonVisible;
@@ -16,15 +17,15 @@ class HeaderStyle {
/// * `false` - the button will show current CalendarFormat
final bool formatButtonShowsNext;
- /// Use to customize header's title text (eg. with different `DateFormat`).
+ /// Use to customize header's title text (e.g. with different `DateFormat`).
/// You can use `String` transformations to further customize the text.
- /// Defaults to simple `'yMMMM'` format (eg. January 2019, February 2019, March 2019, etc.).
+ /// Defaults to simple `'yMMMM'` format (i.e. January 2019, February 2019, March 2019, etc.).
///
/// Example usage:
/// ```dart
- /// titleTextBuilder: (date, locale) => DateFormat.yM(locale).format(date),
+ /// titleTextFormatter: (date, locale) => DateFormat.yM(locale).format(date),
/// ```
- final TextBuilder titleTextBuilder;
+ final TextFormatter? titleTextFormatter;
/// Style for title Text (month-year) displayed in header.
final TextStyle titleTextStyle;
@@ -33,68 +34,74 @@ class HeaderStyle {
final TextStyle formatButtonTextStyle;
/// Background `Decoration` for FormatButton.
- final Decoration formatButtonDecoration;
+ final BoxDecoration formatButtonDecoration;
- /// Inside padding of the whole header.
+ /// Internal padding of the whole header.
final EdgeInsets headerPadding;
- /// Outside margin of the whole header.
+ /// External margin of the whole header.
final EdgeInsets headerMargin;
- /// Inside padding for FormatButton.
+ /// Internal padding of FormatButton.
final EdgeInsets formatButtonPadding;
- /// Inside padding for left chevron.
+ /// Internal padding of left chevron.
+ /// Determines how much of ripple animation is visible during taps.
final EdgeInsets leftChevronPadding;
- /// Inside padding for right chevron.
+ /// Internal padding of right chevron.
+ /// Determines how much of ripple animation is visible during taps.
final EdgeInsets rightChevronPadding;
- /// Outside margin for left chevron.
+ /// External margin of left chevron.
final EdgeInsets leftChevronMargin;
- /// Outside margin for right chevron.
+ /// External margin of right chevron.
final EdgeInsets rightChevronMargin;
- /// Icon used for left chevron.
- /// Defaults to black `Icons.chevron_left`.
- final Icon leftChevronIcon;
+ /// Widget used for left chevron.
+ ///
+ /// Tapping on it will navigate to previous calendar page.
+ final Widget leftChevronIcon;
+
+ /// Widget used for right chevron.
+ ///
+ /// Tapping on it will navigate to next calendar page.
+ final Widget rightChevronIcon;
- /// Icon used for right chevron.
- /// Defaults to black `Icons.chevron_right`.
- final Icon rightChevronIcon;
+ /// Determines left chevron's visibility.
+ final bool leftChevronVisible;
- /// Show or hide chevrons.
- /// Defaults to `true`.
- final bool showLeftChevron;
- final bool showRightChevron;
+ /// Determines right chevron's visibility.
+ final bool rightChevronVisible;
- /// Header decoration, used to draw border or shadow or change color of the header
- /// Defaults to empty BoxDecoration.
+ /// Decoration of the header.
final BoxDecoration decoration;
+ /// Creates a `HeaderStyle` used by `TableCalendar` widget.
const HeaderStyle({
- this.centerHeaderTitle = false,
+ this.titleCentered = false,
this.formatButtonVisible = true,
this.formatButtonShowsNext = true,
- this.titleTextBuilder,
+ this.titleTextFormatter,
this.titleTextStyle = const TextStyle(fontSize: 17.0),
- this.formatButtonTextStyle = const TextStyle(),
+ this.formatButtonTextStyle = const TextStyle(fontSize: 14.0),
this.formatButtonDecoration = const BoxDecoration(
- border: const Border(top: BorderSide(), bottom: BorderSide(), left: BorderSide(), right: BorderSide()),
- borderRadius: const BorderRadius.all(Radius.circular(12.0)),
+ border: Border.fromBorderSide(BorderSide()),
+ borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
- this.headerMargin,
+ this.headerMargin = EdgeInsets.zero,
this.headerPadding = const EdgeInsets.symmetric(vertical: 8.0),
- this.formatButtonPadding = const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0),
+ this.formatButtonPadding =
+ const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0),
this.leftChevronPadding = const EdgeInsets.all(12.0),
this.rightChevronPadding = const EdgeInsets.all(12.0),
this.leftChevronMargin = const EdgeInsets.symmetric(horizontal: 8.0),
this.rightChevronMargin = const EdgeInsets.symmetric(horizontal: 8.0),
this.leftChevronIcon = const Icon(Icons.chevron_left),
this.rightChevronIcon = const Icon(Icons.chevron_right),
- this.showLeftChevron = true,
- this.showRightChevron = true,
+ this.leftChevronVisible = true,
+ this.rightChevronVisible = true,
this.decoration = const BoxDecoration(),
});
}
diff --git a/lib/src/shared/utils.dart b/lib/src/shared/utils.dart
new file mode 100644
index 00000000..7e63f1ec
--- /dev/null
+++ b/lib/src/shared/utils.dart
@@ -0,0 +1,57 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/widgets.dart';
+
+/// Signature for a function that creates a widget for a given `day`.
+typedef DayBuilder = Widget? Function(BuildContext context, DateTime day);
+
+/// Signature for a function that creates a widget for a given `day`.
+/// Additionally, contains the currently focused day.
+typedef FocusedDayBuilder = Widget? Function(
+ BuildContext context,
+ DateTime day,
+ DateTime focusedDay,
+);
+
+/// Signature for a function returning text that can be localized and formatted with `DateFormat`.
+typedef TextFormatter = String Function(DateTime date, dynamic locale);
+
+/// Gestures available for the calendar.
+enum AvailableGestures { none, verticalSwipe, horizontalSwipe, all }
+
+/// Formats that the calendar can display.
+enum CalendarFormat { month, twoWeeks, week }
+
+/// Days of the week that the calendar can start with.
+enum StartingDayOfWeek {
+ monday,
+ tuesday,
+ wednesday,
+ thursday,
+ friday,
+ saturday,
+ sunday,
+}
+
+/// Returns a numerical value associated with given `weekday`.
+///
+/// Returns 1 for `StartingDayOfWeek.monday`, all the way to 7 for `StartingDayOfWeek.sunday`.
+int getWeekdayNumber(StartingDayOfWeek weekday) {
+ return StartingDayOfWeek.values.indexOf(weekday) + 1;
+}
+
+/// Returns `date` in UTC format, without its time part.
+DateTime normalizeDate(DateTime date) {
+ return DateTime.utc(date.year, date.month, date.day);
+}
+
+/// Checks if two DateTime objects are the same day.
+/// Returns `false` if either of them is null.
+bool isSameDay(DateTime? a, DateTime? b) {
+ if (a == null || b == null) {
+ return false;
+ }
+
+ return a.year == b.year && a.month == b.month && a.day == b.day;
+}
diff --git a/lib/src/table_calendar.dart b/lib/src/table_calendar.dart
new file mode 100644
index 00000000..aca0a03c
--- /dev/null
+++ b/lib/src/table_calendar.dart
@@ -0,0 +1,787 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'dart:math';
+
+import 'package:flutter/widgets.dart';
+import 'package:intl/intl.dart';
+import 'package:simple_gesture_detector/simple_gesture_detector.dart';
+import 'package:table_calendar/src/customization/calendar_builders.dart';
+import 'package:table_calendar/src/customization/calendar_style.dart';
+import 'package:table_calendar/src/customization/days_of_week_style.dart';
+import 'package:table_calendar/src/customization/header_style.dart';
+import 'package:table_calendar/src/shared/utils.dart';
+import 'package:table_calendar/src/table_calendar_base.dart';
+import 'package:table_calendar/src/widgets/calendar_header.dart';
+import 'package:table_calendar/src/widgets/cell_content.dart';
+
+/// Signature for `onDaySelected` callback. Contains the selected day and focused day.
+typedef OnDaySelected = void Function(
+ DateTime selectedDay,
+ DateTime focusedDay,
+);
+
+/// Signature for `onRangeSelected` callback.
+/// Contains start and end of the selected range, as well as currently focused day.
+typedef OnRangeSelected = void Function(
+ DateTime? start,
+ DateTime? end,
+ DateTime focusedDay,
+);
+
+/// Modes that range selection can operate in.
+enum RangeSelectionMode { disabled, toggledOff, toggledOn, enforced }
+
+/// Highly customizable, feature-packed Flutter calendar with gestures, animations and multiple formats.
+class TableCalendar extends StatefulWidget {
+ /// Locale to format `TableCalendar` dates with, for example: `'en_US'`.
+ ///
+ /// If nothing is provided, a default locale will be used.
+ final dynamic locale;
+
+ /// The start of the selected day range.
+ final DateTime? rangeStartDay;
+
+ /// The end of the selected day range.
+ final DateTime? rangeEndDay;
+
+ /// DateTime that determines which days are currently visible and focused.
+ final DateTime focusedDay;
+
+ /// The first active day of `TableCalendar`.
+ /// Blocks swiping to days before it.
+ ///
+ /// Days before it will use `disabledStyle` and trigger `onDisabledDayTapped` callback.
+ final DateTime firstDay;
+
+ /// The last active day of `TableCalendar`.
+ /// Blocks swiping to days after it.
+ ///
+ /// Days after it will use `disabledStyle` and trigger `onDisabledDayTapped` callback.
+ final DateTime lastDay;
+
+ /// DateTime that will be treated as today. Defaults to `DateTime.now()`.
+ ///
+ /// Overriding this property might be useful for testing.
+ final DateTime? currentDay;
+
+ /// List of days treated as weekend days.
+ /// Use built-in `DateTime` weekday constants (e.g. `DateTime.monday`) instead of `int` literals (e.g. `1`).
+ final List weekendDays;
+
+ /// Specifies `TableCalendar`'s current format.
+ final CalendarFormat calendarFormat;
+
+ /// `Map` of `CalendarFormat`s and `String` names associated with them.
+ /// Those `CalendarFormat`s will be used by internal logic to manage displayed format.
+ ///
+ /// To ensure proper vertical swipe behavior, `CalendarFormat`s should be in descending order (i.e. from biggest to smallest).
+ ///
+ /// For example:
+ /// ```dart
+ /// availableCalendarFormats: const {
+ /// CalendarFormat.month: 'Month',
+ /// CalendarFormat.week: 'Week',
+ /// }
+ /// ```
+ final Map availableCalendarFormats;
+
+ /// Determines the visibility of calendar header.
+ final bool headerVisible;
+
+ /// Determines the visibility of the row of days of the week.
+ final bool daysOfWeekVisible;
+
+ /// When set to true, tapping on an outside day in `CalendarFormat.month` format
+ /// will jump to the calendar page of the tapped month.
+ final bool pageJumpingEnabled;
+
+ /// When set to true, updating the `focusedDay` will display a scrolling animation
+ /// if the currently visible calendar page is changed.
+ final bool pageAnimationEnabled;
+
+ /// When set to true, `CalendarFormat.month` will always display six weeks,
+ /// even if the content would fit in less.
+ final bool sixWeekMonthsEnforced;
+
+ /// When set to true, `TableCalendar` will fill available height.
+ final bool shouldFillViewport;
+
+ /// Whether to display week numbers on calendar.
+ final bool weekNumbersVisible;
+
+ /// Used for setting the height of `TableCalendar`'s rows.
+ final double rowHeight;
+
+ /// Used for setting the height of `TableCalendar`'s days of week row.
+ final double daysOfWeekHeight;
+
+ /// Specifies the duration of size animation that takes place whenever `calendarFormat` is changed.
+ final Duration formatAnimationDuration;
+
+ /// Specifies the curve of size animation that takes place whenever `calendarFormat` is changed.
+ final Curve formatAnimationCurve;
+
+ /// Specifies the duration of scrolling animation that takes place whenever the visible calendar page is changed.
+ final Duration pageAnimationDuration;
+
+ /// Specifies the curve of scrolling animation that takes place whenever the visible calendar page is changed.
+ final Curve pageAnimationCurve;
+
+ /// `TableCalendar` will start weeks with provided day.
+ ///
+ /// Use `StartingDayOfWeek.monday` for Monday - Sunday week format.
+ /// Use `StartingDayOfWeek.sunday` for Sunday - Saturday week format.
+ final StartingDayOfWeek startingDayOfWeek;
+
+ /// `HitTestBehavior` for every day cell inside `TableCalendar`.
+ final HitTestBehavior dayHitTestBehavior;
+
+ /// Specifies swipe gestures available to `TableCalendar`.
+ /// If `AvailableGestures.none` is used, the calendar will only be interactive via buttons.
+ final AvailableGestures availableGestures;
+
+ /// Configuration for vertical swipe detector.
+ final SimpleSwipeConfig simpleSwipeConfig;
+
+ /// Style for `TableCalendar`'s header.
+ final HeaderStyle headerStyle;
+
+ /// Style for days of week displayed between `TableCalendar`'s header and content.
+ final DaysOfWeekStyle daysOfWeekStyle;
+
+ /// Style for `TableCalendar`'s content.
+ final CalendarStyle calendarStyle;
+
+ /// Set of custom builders for `TableCalendar` to work with.
+ /// Use those to fully tailor the UI.
+ final CalendarBuilders calendarBuilders;
+
+ /// Current mode of range selection.
+ ///
+ /// * `RangeSelectionMode.disabled` - range selection is always off.
+ /// * `RangeSelectionMode.toggledOff` - range selection is currently off, can be toggled by longpressing a day cell.
+ /// * `RangeSelectionMode.toggledOn` - range selection is currently on, can be toggled by longpressing a day cell.
+ /// * `RangeSelectionMode.enforced` - range selection is always on.
+ final RangeSelectionMode rangeSelectionMode;
+
+ /// Allows to load events for days that are not enabled
+ /// If `true` it will ignore `enabledDayPredicate` when calling `eventLoader`.
+ /// If `false` then `enabledDayPredicate` will be used to check when to call `eventLoader`
+ final bool loadEventsForDisabledDays;
+
+ /// Function that assigns a list of events to a specified day.
+ final List Function(DateTime day)? eventLoader;
+
+ /// Function deciding whether given day should be enabled or not.
+ /// If `false` is returned, this day will be disabled.
+ final bool Function(DateTime day)? enabledDayPredicate;
+
+ /// Function deciding whether given day should be marked as selected.
+ final bool Function(DateTime day)? selectedDayPredicate;
+
+ /// Function deciding whether given day is treated as a holiday.
+ final bool Function(DateTime day)? holidayPredicate;
+
+ /// Called whenever a day range gets selected.
+ final OnRangeSelected? onRangeSelected;
+
+ /// Called whenever any day gets tapped.
+ final OnDaySelected? onDaySelected;
+
+ /// Called whenever any day gets long pressed.
+ final OnDaySelected? onDayLongPressed;
+
+ /// Called whenever any disabled day gets tapped.
+ final void Function(DateTime day)? onDisabledDayTapped;
+
+ /// Called whenever any disabled day gets long pressed.
+ final void Function(DateTime day)? onDisabledDayLongPressed;
+
+ /// Called whenever header gets tapped.
+ final void Function(DateTime focusedDay)? onHeaderTapped;
+
+ /// Called whenever header gets long pressed.
+ final void Function(DateTime focusedDay)? onHeaderLongPressed;
+
+ /// Called whenever currently visible calendar page is changed.
+ final void Function(DateTime focusedDay)? onPageChanged;
+
+ /// Called whenever `calendarFormat` is changed.
+ final void Function(CalendarFormat format)? onFormatChanged;
+
+ /// Called when the calendar is created. Exposes its PageController.
+ final void Function(PageController pageController)? onCalendarCreated;
+
+ /// Creates a `TableCalendar` widget.
+ TableCalendar({
+ super.key,
+ required DateTime focusedDay,
+ required DateTime firstDay,
+ required DateTime lastDay,
+ DateTime? currentDay,
+ this.locale,
+ this.rangeStartDay,
+ this.rangeEndDay,
+ this.weekendDays = const [DateTime.saturday, DateTime.sunday],
+ this.calendarFormat = CalendarFormat.month,
+ this.availableCalendarFormats = const {
+ CalendarFormat.month: 'Month',
+ CalendarFormat.twoWeeks: '2 weeks',
+ CalendarFormat.week: 'Week',
+ },
+ this.headerVisible = true,
+ this.daysOfWeekVisible = true,
+ this.pageJumpingEnabled = false,
+ this.pageAnimationEnabled = true,
+ this.sixWeekMonthsEnforced = false,
+ this.shouldFillViewport = false,
+ this.weekNumbersVisible = false,
+ this.rowHeight = 52.0,
+ this.daysOfWeekHeight = 16.0,
+ this.formatAnimationDuration = const Duration(milliseconds: 200),
+ this.formatAnimationCurve = Curves.linear,
+ this.pageAnimationDuration = const Duration(milliseconds: 300),
+ this.pageAnimationCurve = Curves.easeOut,
+ this.startingDayOfWeek = StartingDayOfWeek.sunday,
+ this.dayHitTestBehavior = HitTestBehavior.opaque,
+ this.availableGestures = AvailableGestures.all,
+ this.simpleSwipeConfig = const SimpleSwipeConfig(
+ verticalThreshold: 25.0,
+ swipeDetectionBehavior: SwipeDetectionBehavior.continuousDistinct,
+ ),
+ this.headerStyle = const HeaderStyle(),
+ this.daysOfWeekStyle = const DaysOfWeekStyle(),
+ this.calendarStyle = const CalendarStyle(),
+ this.calendarBuilders = const CalendarBuilders(),
+ this.rangeSelectionMode = RangeSelectionMode.toggledOff,
+ this.eventLoader,
+ this.enabledDayPredicate,
+ this.loadEventsForDisabledDays = false,
+ this.selectedDayPredicate,
+ this.holidayPredicate,
+ this.onRangeSelected,
+ this.onDaySelected,
+ this.onDayLongPressed,
+ this.onDisabledDayTapped,
+ this.onDisabledDayLongPressed,
+ this.onHeaderTapped,
+ this.onHeaderLongPressed,
+ this.onPageChanged,
+ this.onFormatChanged,
+ this.onCalendarCreated,
+ }) : assert(availableCalendarFormats.keys.contains(calendarFormat)),
+ assert(availableCalendarFormats.length <= CalendarFormat.values.length),
+ assert(
+ weekendDays.isEmpty ||
+ weekendDays.every(
+ (day) => day >= DateTime.monday && day <= DateTime.sunday,
+ ),
+ ),
+ focusedDay = normalizeDate(focusedDay),
+ firstDay = normalizeDate(firstDay),
+ lastDay = normalizeDate(lastDay),
+ currentDay = currentDay ?? DateTime.now();
+
+ @override
+ State> createState() => _TableCalendarState();
+}
+
+class _TableCalendarState extends State> {
+ late final PageController _pageController;
+ late final ValueNotifier _focusedDay;
+ late RangeSelectionMode _rangeSelectionMode;
+ DateTime? _firstSelectedDay;
+
+ @override
+ void initState() {
+ super.initState();
+ _focusedDay = ValueNotifier(widget.focusedDay);
+ _rangeSelectionMode = widget.rangeSelectionMode;
+ }
+
+ @override
+ void didUpdateWidget(TableCalendar oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ if (_focusedDay.value != widget.focusedDay) {
+ _focusedDay.value = widget.focusedDay;
+ }
+
+ if (_rangeSelectionMode != widget.rangeSelectionMode) {
+ _rangeSelectionMode = widget.rangeSelectionMode;
+ }
+
+ if (widget.rangeStartDay == null && widget.rangeEndDay == null) {
+ _firstSelectedDay = null;
+ }
+ }
+
+ @override
+ void dispose() {
+ _focusedDay.dispose();
+ super.dispose();
+ }
+
+ bool get _isRangeSelectionToggleable =>
+ _rangeSelectionMode == RangeSelectionMode.toggledOn ||
+ _rangeSelectionMode == RangeSelectionMode.toggledOff;
+
+ bool get _isRangeSelectionOn =>
+ _rangeSelectionMode == RangeSelectionMode.toggledOn ||
+ _rangeSelectionMode == RangeSelectionMode.enforced;
+
+ bool get _shouldBlockOutsideDays =>
+ !widget.calendarStyle.outsideDaysVisible &&
+ widget.calendarFormat == CalendarFormat.month;
+
+ void _swipeCalendarFormat(SwipeDirection direction) {
+ if (widget.onFormatChanged != null) {
+ final formats = widget.availableCalendarFormats.keys.toList();
+
+ final isSwipeUp = direction == SwipeDirection.up;
+ int id = formats.indexOf(widget.calendarFormat);
+
+ // Order of CalendarFormats must be from biggest to smallest,
+ // e.g.: [month, twoWeeks, week]
+ if (isSwipeUp) {
+ id = min(formats.length - 1, id + 1);
+ } else {
+ id = max(0, id - 1);
+ }
+
+ widget.onFormatChanged!(formats[id]);
+ }
+ }
+
+ void _onDayTapped(DateTime day) {
+ final isOutside = day.month != _focusedDay.value.month;
+ if (isOutside && _shouldBlockOutsideDays) {
+ return;
+ }
+
+ if (_isDayDisabled(day)) {
+ return widget.onDisabledDayTapped?.call(day);
+ }
+
+ _updateFocusOnTap(day);
+
+ if (_isRangeSelectionOn && widget.onRangeSelected != null) {
+ if (_firstSelectedDay == null) {
+ _firstSelectedDay = day;
+ widget.onRangeSelected!(_firstSelectedDay, null, _focusedDay.value);
+ } else {
+ if (day.isAfter(_firstSelectedDay!)) {
+ widget.onRangeSelected!(_firstSelectedDay, day, _focusedDay.value);
+ _firstSelectedDay = null;
+ } else if (day.isBefore(_firstSelectedDay!)) {
+ widget.onRangeSelected!(day, _firstSelectedDay, _focusedDay.value);
+ _firstSelectedDay = null;
+ }
+ }
+ } else {
+ widget.onDaySelected?.call(day, _focusedDay.value);
+ }
+ }
+
+ void _onDayLongPressed(DateTime day) {
+ final isOutside = day.month != _focusedDay.value.month;
+ if (isOutside && _shouldBlockOutsideDays) {
+ return;
+ }
+
+ if (_isDayDisabled(day)) {
+ return widget.onDisabledDayLongPressed?.call(day);
+ }
+
+ if (widget.onDayLongPressed != null) {
+ _updateFocusOnTap(day);
+ return widget.onDayLongPressed!(day, _focusedDay.value);
+ }
+
+ if (widget.onRangeSelected != null) {
+ if (_isRangeSelectionToggleable) {
+ _updateFocusOnTap(day);
+ _toggleRangeSelection();
+
+ if (_isRangeSelectionOn) {
+ _firstSelectedDay = day;
+ widget.onRangeSelected!(_firstSelectedDay, null, _focusedDay.value);
+ } else {
+ _firstSelectedDay = null;
+ widget.onDaySelected?.call(day, _focusedDay.value);
+ }
+ }
+ }
+ }
+
+ void _updateFocusOnTap(DateTime day) {
+ if (widget.pageJumpingEnabled) {
+ _focusedDay.value = day;
+ return;
+ }
+
+ if (widget.calendarFormat == CalendarFormat.month) {
+ if (_isBeforeMonth(day, _focusedDay.value)) {
+ _focusedDay.value = _firstDayOfMonth(_focusedDay.value);
+ } else if (_isAfterMonth(day, _focusedDay.value)) {
+ _focusedDay.value = _lastDayOfMonth(_focusedDay.value);
+ } else {
+ _focusedDay.value = day;
+ }
+ } else {
+ _focusedDay.value = day;
+ }
+ }
+
+ void _toggleRangeSelection() {
+ if (_rangeSelectionMode == RangeSelectionMode.toggledOn) {
+ _rangeSelectionMode = RangeSelectionMode.toggledOff;
+ } else {
+ _rangeSelectionMode = RangeSelectionMode.toggledOn;
+ }
+ }
+
+ void _onLeftChevronTap() {
+ _pageController.previousPage(
+ duration: widget.pageAnimationDuration,
+ curve: widget.pageAnimationCurve,
+ );
+ }
+
+ void _onRightChevronTap() {
+ _pageController.nextPage(
+ duration: widget.pageAnimationDuration,
+ curve: widget.pageAnimationCurve,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ if (widget.headerVisible)
+ ValueListenableBuilder(
+ valueListenable: _focusedDay,
+ builder: (context, value, _) {
+ return CalendarHeader(
+ headerTitleBuilder: widget.calendarBuilders.headerTitleBuilder,
+ focusedMonth: value,
+ onLeftChevronTap: _onLeftChevronTap,
+ onRightChevronTap: _onRightChevronTap,
+ onHeaderTap: () => widget.onHeaderTapped?.call(value),
+ onHeaderLongPress: () =>
+ widget.onHeaderLongPressed?.call(value),
+ headerStyle: widget.headerStyle,
+ availableCalendarFormats: widget.availableCalendarFormats,
+ calendarFormat: widget.calendarFormat,
+ locale: widget.locale,
+ onFormatButtonTap: (format) {
+ assert(
+ widget.onFormatChanged != null,
+ 'Using `FormatButton` without providing `onFormatChanged` will have no effect.',
+ );
+
+ widget.onFormatChanged?.call(format);
+ },
+ );
+ },
+ ),
+ Flexible(
+ flex: widget.shouldFillViewport ? 1 : 0,
+ child: TableCalendarBase(
+ onCalendarCreated: (pageController) {
+ _pageController = pageController;
+ widget.onCalendarCreated?.call(pageController);
+ },
+ focusedDay: _focusedDay.value,
+ calendarFormat: widget.calendarFormat,
+ availableGestures: widget.availableGestures,
+ firstDay: widget.firstDay,
+ lastDay: widget.lastDay,
+ startingDayOfWeek: widget.startingDayOfWeek,
+ dowDecoration: widget.daysOfWeekStyle.decoration,
+ rowDecoration: widget.calendarStyle.rowDecoration,
+ tableBorder: widget.calendarStyle.tableBorder,
+ tablePadding: widget.calendarStyle.tablePadding,
+ dowVisible: widget.daysOfWeekVisible,
+ dowHeight: widget.daysOfWeekHeight,
+ rowHeight: widget.rowHeight,
+ formatAnimationDuration: widget.formatAnimationDuration,
+ formatAnimationCurve: widget.formatAnimationCurve,
+ pageAnimationEnabled: widget.pageAnimationEnabled,
+ pageAnimationDuration: widget.pageAnimationDuration,
+ pageAnimationCurve: widget.pageAnimationCurve,
+ availableCalendarFormats: widget.availableCalendarFormats,
+ simpleSwipeConfig: widget.simpleSwipeConfig,
+ sixWeekMonthsEnforced: widget.sixWeekMonthsEnforced,
+ onVerticalSwipe: _swipeCalendarFormat,
+ onPageChanged: (focusedDay) {
+ _focusedDay.value = focusedDay;
+ widget.onPageChanged?.call(focusedDay);
+ },
+ weekNumbersVisible: widget.weekNumbersVisible,
+ weekNumberBuilder: (BuildContext context, DateTime day) {
+ final weekNumber = _calculateWeekNumber(day);
+ final cell = widget.calendarBuilders.weekNumberBuilder
+ ?.call(context, weekNumber);
+
+ return cell ??
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4),
+ child: Center(
+ child: Text(
+ weekNumber.toString(),
+ style: widget.calendarStyle.weekNumberTextStyle,
+ ),
+ ),
+ );
+ },
+ dowBuilder: (BuildContext context, DateTime day) {
+ Widget? dowCell =
+ widget.calendarBuilders.dowBuilder?.call(context, day);
+
+ if (dowCell == null) {
+ final weekdayString = widget.daysOfWeekStyle.dowTextFormatter
+ ?.call(day, widget.locale) ??
+ DateFormat.E(widget.locale).format(day);
+
+ final isWeekend =
+ _isWeekend(day, weekendDays: widget.weekendDays);
+
+ dowCell = Center(
+ child: ExcludeSemantics(
+ child: Text(
+ weekdayString,
+ style: isWeekend
+ ? widget.daysOfWeekStyle.weekendStyle
+ : widget.daysOfWeekStyle.weekdayStyle,
+ ),
+ ),
+ );
+ }
+
+ return dowCell;
+ },
+ dayBuilder: (context, day, focusedMonth) {
+ return GestureDetector(
+ behavior: widget.dayHitTestBehavior,
+ onTap: () => _onDayTapped(day),
+ onLongPress: () => _onDayLongPressed(day),
+ child: _buildCell(day, focusedMonth),
+ );
+ },
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildCell(DateTime day, DateTime focusedDay) {
+ final isOutside = day.month != focusedDay.month;
+
+ if (isOutside && _shouldBlockOutsideDays) {
+ return Container();
+ }
+
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ final shorterSide = constraints.maxHeight > constraints.maxWidth
+ ? constraints.maxWidth
+ : constraints.maxHeight;
+
+ final children = [];
+
+ final isWithinRange = widget.rangeStartDay != null &&
+ widget.rangeEndDay != null &&
+ _isWithinRange(day, widget.rangeStartDay!, widget.rangeEndDay!);
+
+ final isRangeStart = isSameDay(day, widget.rangeStartDay);
+ final isRangeEnd = isSameDay(day, widget.rangeEndDay);
+
+ Widget? rangeHighlight = widget.calendarBuilders.rangeHighlightBuilder
+ ?.call(context, day, isWithinRange);
+
+ if (rangeHighlight == null) {
+ if (isWithinRange) {
+ rangeHighlight = Center(
+ child: Container(
+ margin: EdgeInsetsDirectional.only(
+ start: isRangeStart ? constraints.maxWidth * 0.5 : 0.0,
+ end: isRangeEnd ? constraints.maxWidth * 0.5 : 0.0,
+ ),
+ height:
+ (shorterSide - widget.calendarStyle.cellMargin.vertical) *
+ widget.calendarStyle.rangeHighlightScale,
+ color: widget.calendarStyle.rangeHighlightColor,
+ ),
+ );
+ }
+ }
+
+ if (rangeHighlight != null) {
+ children.add(rangeHighlight);
+ }
+
+ final isToday = isSameDay(day, widget.currentDay);
+ final isDisabled = _isDayDisabled(day);
+ final isWeekend = _isWeekend(day, weekendDays: widget.weekendDays);
+
+ final content = CellContent(
+ key: ValueKey('CellContent-${day.year}-${day.month}-${day.day}'),
+ day: day,
+ focusedDay: focusedDay,
+ calendarStyle: widget.calendarStyle,
+ calendarBuilders: widget.calendarBuilders,
+ isTodayHighlighted: widget.calendarStyle.isTodayHighlighted,
+ isToday: isToday,
+ isSelected: widget.selectedDayPredicate?.call(day) ?? false,
+ isRangeStart: isRangeStart,
+ isRangeEnd: isRangeEnd,
+ isWithinRange: isWithinRange,
+ isOutside: isOutside,
+ isDisabled: isDisabled,
+ isWeekend: isWeekend,
+ isHoliday: widget.holidayPredicate?.call(day) ?? false,
+ locale: widget.locale,
+ );
+
+ children.add(content);
+
+ if (widget.loadEventsForDisabledDays || !isDisabled) {
+ final events = widget.eventLoader?.call(day) ?? [];
+ Widget? markerWidget =
+ widget.calendarBuilders.markerBuilder?.call(context, day, events);
+
+ if (events.isNotEmpty && markerWidget == null) {
+ final center = constraints.maxHeight / 2;
+
+ final markerSize = widget.calendarStyle.markerSize ??
+ (shorterSide - widget.calendarStyle.cellMargin.vertical) *
+ widget.calendarStyle.markerSizeScale;
+
+ final markerAutoAlignmentTop = center +
+ (shorterSide - widget.calendarStyle.cellMargin.vertical) / 2 -
+ (markerSize * widget.calendarStyle.markersAnchor);
+
+ markerWidget = PositionedDirectional(
+ top: widget.calendarStyle.markersAutoAligned
+ ? markerAutoAlignmentTop
+ : widget.calendarStyle.markersOffset.top,
+ bottom: widget.calendarStyle.markersAutoAligned
+ ? null
+ : widget.calendarStyle.markersOffset.bottom,
+ start: widget.calendarStyle.markersAutoAligned
+ ? null
+ : widget.calendarStyle.markersOffset.start,
+ end: widget.calendarStyle.markersAutoAligned
+ ? null
+ : widget.calendarStyle.markersOffset.end,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: events
+ .take(widget.calendarStyle.markersMaxCount)
+ .map((event) => _buildSingleMarker(day, event, markerSize))
+ .toList(),
+ ),
+ );
+ }
+
+ if (markerWidget != null) {
+ children.add(markerWidget);
+ }
+ }
+
+ return Stack(
+ alignment: widget.calendarStyle.markersAlignment,
+ clipBehavior: widget.calendarStyle.canMarkersOverflow
+ ? Clip.none
+ : Clip.hardEdge,
+ children: children,
+ );
+ },
+ );
+ }
+
+ Widget _buildSingleMarker(DateTime day, T event, double markerSize) {
+ return widget.calendarBuilders.singleMarkerBuilder
+ ?.call(context, day, event) ??
+ Container(
+ width: markerSize,
+ height: markerSize,
+ margin: widget.calendarStyle.markerMargin,
+ decoration: widget.calendarStyle.markerDecoration,
+ );
+ }
+
+ int _calculateWeekNumber(DateTime date) {
+ final middleDay = date.add(const Duration(days: 3));
+ final dayOfYear = _dayOfYear(middleDay);
+
+ return 1 + ((dayOfYear - 1) / 7).floor();
+ }
+
+ int _dayOfYear(DateTime date) {
+ return normalizeDate(date).difference(DateTime.utc(date.year)).inDays + 1;
+ }
+
+ bool _isWithinRange(DateTime day, DateTime start, DateTime end) {
+ if (isSameDay(day, start) || isSameDay(day, end)) {
+ return true;
+ }
+
+ if (day.isAfter(start) && day.isBefore(end)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ bool _isDayDisabled(DateTime day) {
+ return day.isBefore(widget.firstDay) ||
+ day.isAfter(widget.lastDay) ||
+ !_isDayAvailable(day);
+ }
+
+ bool _isDayAvailable(DateTime day) {
+ if (widget.enabledDayPredicate == null) {
+ return true;
+ }
+
+ return widget.enabledDayPredicate!(day);
+ }
+
+ DateTime _firstDayOfMonth(DateTime month) {
+ return DateTime.utc(month.year, month.month);
+ }
+
+ DateTime _lastDayOfMonth(DateTime month) {
+ final date = month.month < 12
+ ? DateTime.utc(month.year, month.month + 1)
+ : DateTime.utc(month.year + 1);
+ return date.subtract(const Duration(days: 1));
+ }
+
+ bool _isBeforeMonth(DateTime day, DateTime month) {
+ if (day.year == month.year) {
+ return day.month < month.month;
+ } else {
+ return day.isBefore(month);
+ }
+ }
+
+ bool _isAfterMonth(DateTime day, DateTime month) {
+ if (day.year == month.year) {
+ return day.month > month.month;
+ } else {
+ return day.isAfter(month);
+ }
+ }
+
+ bool _isWeekend(
+ DateTime day, {
+ List weekendDays = const [DateTime.saturday, DateTime.sunday],
+ }) {
+ return weekendDays.contains(day.weekday);
+ }
+}
diff --git a/lib/src/table_calendar_base.dart b/lib/src/table_calendar_base.dart
new file mode 100644
index 00000000..a5727160
--- /dev/null
+++ b/lib/src/table_calendar_base.dart
@@ -0,0 +1,356 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/material.dart';
+import 'package:simple_gesture_detector/simple_gesture_detector.dart';
+import 'package:table_calendar/src/shared/utils.dart';
+import 'package:table_calendar/src/widgets/calendar_core.dart';
+
+class TableCalendarBase extends StatefulWidget {
+ final DateTime firstDay;
+ final DateTime lastDay;
+ final DateTime focusedDay;
+ final CalendarFormat calendarFormat;
+ final DayBuilder? dowBuilder;
+ final DayBuilder? weekNumberBuilder;
+ final FocusedDayBuilder dayBuilder;
+ final double? dowHeight;
+ final double rowHeight;
+ final bool sixWeekMonthsEnforced;
+ final bool dowVisible;
+ final bool weekNumbersVisible;
+ final Decoration? dowDecoration;
+ final Decoration? rowDecoration;
+ final TableBorder? tableBorder;
+ final EdgeInsets? tablePadding;
+ final Duration formatAnimationDuration;
+ final Curve formatAnimationCurve;
+ final bool pageAnimationEnabled;
+ final Duration pageAnimationDuration;
+ final Curve pageAnimationCurve;
+ final StartingDayOfWeek startingDayOfWeek;
+ final AvailableGestures availableGestures;
+ final SimpleSwipeConfig simpleSwipeConfig;
+ final Map availableCalendarFormats;
+ final SwipeCallback? onVerticalSwipe;
+ final void Function(DateTime focusedDay)? onPageChanged;
+ final void Function(PageController pageController)? onCalendarCreated;
+
+ TableCalendarBase({
+ super.key,
+ required this.firstDay,
+ required this.lastDay,
+ required this.focusedDay,
+ this.calendarFormat = CalendarFormat.month,
+ this.dowBuilder,
+ required this.dayBuilder,
+ this.dowHeight,
+ required this.rowHeight,
+ this.sixWeekMonthsEnforced = false,
+ this.dowVisible = true,
+ this.weekNumberBuilder,
+ this.weekNumbersVisible = false,
+ this.dowDecoration,
+ this.rowDecoration,
+ this.tableBorder,
+ this.tablePadding,
+ this.formatAnimationDuration = const Duration(milliseconds: 200),
+ this.formatAnimationCurve = Curves.linear,
+ this.pageAnimationEnabled = true,
+ this.pageAnimationDuration = const Duration(milliseconds: 300),
+ this.pageAnimationCurve = Curves.easeOut,
+ this.startingDayOfWeek = StartingDayOfWeek.sunday,
+ this.availableGestures = AvailableGestures.all,
+ this.simpleSwipeConfig = const SimpleSwipeConfig(
+ verticalThreshold: 25.0,
+ swipeDetectionBehavior: SwipeDetectionBehavior.continuousDistinct,
+ ),
+ this.availableCalendarFormats = const {
+ CalendarFormat.month: 'Month',
+ CalendarFormat.twoWeeks: '2 weeks',
+ CalendarFormat.week: 'Week',
+ },
+ this.onVerticalSwipe,
+ this.onPageChanged,
+ this.onCalendarCreated,
+ }) : assert(!dowVisible || (dowHeight != null && dowBuilder != null)),
+ assert(isSameDay(focusedDay, firstDay) || focusedDay.isAfter(firstDay)),
+ assert(isSameDay(focusedDay, lastDay) || focusedDay.isBefore(lastDay));
+
+ @override
+ State createState() => _TableCalendarBaseState();
+}
+
+class _TableCalendarBaseState extends State {
+ late final ValueNotifier _pageHeight;
+ late final PageController _pageController;
+ late DateTime _focusedDay;
+ late int _previousIndex;
+ late bool _pageCallbackDisabled;
+
+ @override
+ void initState() {
+ super.initState();
+ _focusedDay = widget.focusedDay;
+
+ final rowCount = _getRowCount(widget.calendarFormat, _focusedDay);
+ _pageHeight = ValueNotifier(_getPageHeight(rowCount));
+
+ final initialPage = _calculateFocusedPage(
+ widget.calendarFormat,
+ widget.firstDay,
+ _focusedDay,
+ );
+
+ _pageController = PageController(initialPage: initialPage);
+ widget.onCalendarCreated?.call(_pageController);
+
+ _previousIndex = initialPage;
+ _pageCallbackDisabled = false;
+ }
+
+ @override
+ void didUpdateWidget(TableCalendarBase oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ if (_focusedDay != widget.focusedDay ||
+ widget.calendarFormat != oldWidget.calendarFormat ||
+ widget.startingDayOfWeek != oldWidget.startingDayOfWeek) {
+ final shouldAnimate = _focusedDay != widget.focusedDay;
+
+ _focusedDay = widget.focusedDay;
+ _updatePage(shouldAnimate: shouldAnimate);
+ }
+
+ if (widget.rowHeight != oldWidget.rowHeight ||
+ widget.dowHeight != oldWidget.dowHeight ||
+ widget.dowVisible != oldWidget.dowVisible ||
+ widget.sixWeekMonthsEnforced != oldWidget.sixWeekMonthsEnforced) {
+ final rowCount = _getRowCount(widget.calendarFormat, _focusedDay);
+ _pageHeight.value = _getPageHeight(rowCount);
+ }
+ }
+
+ @override
+ void dispose() {
+ _pageController.dispose();
+ _pageHeight.dispose();
+ super.dispose();
+ }
+
+ bool get _canScrollHorizontally =>
+ widget.availableGestures == AvailableGestures.all ||
+ widget.availableGestures == AvailableGestures.horizontalSwipe;
+
+ bool get _canScrollVertically =>
+ widget.availableGestures == AvailableGestures.all ||
+ widget.availableGestures == AvailableGestures.verticalSwipe;
+
+ void _updatePage({bool shouldAnimate = false}) {
+ final currentIndex = _calculateFocusedPage(
+ widget.calendarFormat,
+ widget.firstDay,
+ _focusedDay,
+ );
+
+ final endIndex = _calculateFocusedPage(
+ widget.calendarFormat,
+ widget.firstDay,
+ widget.lastDay,
+ );
+
+ if (currentIndex != _previousIndex ||
+ currentIndex == 0 ||
+ currentIndex == endIndex) {
+ _pageCallbackDisabled = true;
+ }
+
+ if (shouldAnimate && widget.pageAnimationEnabled) {
+ if ((currentIndex - _previousIndex).abs() > 1) {
+ final jumpIndex =
+ currentIndex > _previousIndex ? currentIndex - 1 : currentIndex + 1;
+
+ _pageController.jumpToPage(jumpIndex);
+ }
+
+ _pageController.animateToPage(
+ currentIndex,
+ duration: widget.pageAnimationDuration,
+ curve: widget.pageAnimationCurve,
+ );
+ } else {
+ _pageController.jumpToPage(currentIndex);
+ }
+
+ _previousIndex = currentIndex;
+ final rowCount = _getRowCount(widget.calendarFormat, _focusedDay);
+ _pageHeight.value = _getPageHeight(rowCount);
+
+ _pageCallbackDisabled = false;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ return SimpleGestureDetector(
+ onVerticalSwipe: _canScrollVertically ? widget.onVerticalSwipe : null,
+ swipeConfig: widget.simpleSwipeConfig,
+ child: ValueListenableBuilder(
+ valueListenable: _pageHeight,
+ builder: (context, value, child) {
+ final height =
+ constraints.hasBoundedHeight ? constraints.maxHeight : value;
+
+ return AnimatedSize(
+ duration: widget.formatAnimationDuration,
+ curve: widget.formatAnimationCurve,
+ alignment: Alignment.topCenter,
+ child: SizedBox(
+ height: height,
+ child: child,
+ ),
+ );
+ },
+ child: CalendarCore(
+ constraints: constraints,
+ pageController: _pageController,
+ scrollPhysics: _canScrollHorizontally
+ ? const PageScrollPhysics()
+ : const NeverScrollableScrollPhysics(),
+ firstDay: widget.firstDay,
+ lastDay: widget.lastDay,
+ startingDayOfWeek: widget.startingDayOfWeek,
+ calendarFormat: widget.calendarFormat,
+ previousIndex: _previousIndex,
+ focusedDay: _focusedDay,
+ sixWeekMonthsEnforced: widget.sixWeekMonthsEnforced,
+ dowVisible: widget.dowVisible,
+ dowHeight: widget.dowHeight,
+ rowHeight: widget.rowHeight,
+ weekNumbersVisible: widget.weekNumbersVisible,
+ weekNumberBuilder: widget.weekNumberBuilder,
+ dowDecoration: widget.dowDecoration,
+ rowDecoration: widget.rowDecoration,
+ tableBorder: widget.tableBorder,
+ tablePadding: widget.tablePadding,
+ onPageChanged: (index, focusedMonth) {
+ if (!_pageCallbackDisabled) {
+ if (!isSameDay(_focusedDay, focusedMonth)) {
+ _focusedDay = focusedMonth;
+ }
+
+ if (widget.calendarFormat == CalendarFormat.month &&
+ !widget.sixWeekMonthsEnforced &&
+ !constraints.hasBoundedHeight) {
+ final rowCount = _getRowCount(
+ widget.calendarFormat,
+ focusedMonth,
+ );
+ _pageHeight.value = _getPageHeight(rowCount);
+ }
+
+ _previousIndex = index;
+ widget.onPageChanged?.call(focusedMonth);
+ }
+
+ _pageCallbackDisabled = false;
+ },
+ dowBuilder: widget.dowBuilder,
+ dayBuilder: widget.dayBuilder,
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ double _getPageHeight(int rowCount) {
+ final tablePaddingHeight = widget.tablePadding?.vertical ?? 0.0;
+ final dowHeight = widget.dowVisible ? widget.dowHeight! : 0.0;
+ return dowHeight + rowCount * widget.rowHeight + tablePaddingHeight;
+ }
+
+ int _calculateFocusedPage(
+ CalendarFormat format,
+ DateTime startDay,
+ DateTime focusedDay,
+ ) {
+ switch (format) {
+ case CalendarFormat.month:
+ return _getMonthCount(startDay, focusedDay);
+ case CalendarFormat.twoWeeks:
+ return _getTwoWeekCount(startDay, focusedDay);
+ case CalendarFormat.week:
+ return _getWeekCount(startDay, focusedDay);
+ }
+ }
+
+ int _getMonthCount(DateTime first, DateTime last) {
+ final yearDif = last.year - first.year;
+ final monthDif = last.month - first.month;
+
+ return yearDif * 12 + monthDif;
+ }
+
+ int _getWeekCount(DateTime first, DateTime last) {
+ return last.difference(_firstDayOfWeek(first)).inDays ~/ 7;
+ }
+
+ int _getTwoWeekCount(DateTime first, DateTime last) {
+ return last.difference(_firstDayOfWeek(first)).inDays ~/ 14;
+ }
+
+ int _getRowCount(CalendarFormat format, DateTime focusedDay) {
+ if (format == CalendarFormat.twoWeeks) {
+ return 2;
+ } else if (format == CalendarFormat.week) {
+ return 1;
+ } else if (widget.sixWeekMonthsEnforced) {
+ return 6;
+ }
+
+ final first = _firstDayOfMonth(focusedDay);
+ final daysBefore = _getDaysBefore(first);
+ final firstToDisplay = first.subtract(Duration(days: daysBefore));
+
+ final last = _lastDayOfMonth(focusedDay);
+ final daysAfter = _getDaysAfter(last);
+ final lastToDisplay = last.add(Duration(days: daysAfter));
+
+ return (lastToDisplay.difference(firstToDisplay).inDays + 1) ~/ 7;
+ }
+
+ int _getDaysBefore(DateTime firstDay) {
+ return (firstDay.weekday + 7 - getWeekdayNumber(widget.startingDayOfWeek)) %
+ 7;
+ }
+
+ int _getDaysAfter(DateTime lastDay) {
+ final invertedStartingWeekday =
+ 8 - getWeekdayNumber(widget.startingDayOfWeek);
+
+ final daysAfter = 7 - ((lastDay.weekday + invertedStartingWeekday) % 7);
+ if (daysAfter == 7) {
+ return 0;
+ }
+
+ return daysAfter;
+ }
+
+ DateTime _firstDayOfWeek(DateTime week) {
+ final daysBefore = _getDaysBefore(week);
+ return week.subtract(Duration(days: daysBefore));
+ }
+
+ DateTime _firstDayOfMonth(DateTime month) {
+ return DateTime.utc(month.year, month.month);
+ }
+
+ DateTime _lastDayOfMonth(DateTime month) {
+ final date = month.month < 12
+ ? DateTime.utc(month.year, month.month + 1)
+ : DateTime.utc(month.year + 1);
+ return date.subtract(const Duration(days: 1));
+ }
+}
diff --git a/lib/src/widgets/calendar_core.dart b/lib/src/widgets/calendar_core.dart
new file mode 100644
index 00000000..573ef532
--- /dev/null
+++ b/lib/src/widgets/calendar_core.dart
@@ -0,0 +1,320 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/material.dart';
+import 'package:table_calendar/src/shared/utils.dart';
+import 'package:table_calendar/src/widgets/calendar_page.dart';
+
+class CalendarCore extends StatelessWidget {
+ final DateTime? focusedDay;
+ final DateTime firstDay;
+ final DateTime lastDay;
+ final CalendarFormat calendarFormat;
+ final DayBuilder? dowBuilder;
+ final DayBuilder? weekNumberBuilder;
+ final FocusedDayBuilder dayBuilder;
+ final bool sixWeekMonthsEnforced;
+ final bool dowVisible;
+ final bool weekNumbersVisible;
+ final Decoration? dowDecoration;
+ final Decoration? rowDecoration;
+ final TableBorder? tableBorder;
+ final EdgeInsets? tablePadding;
+ final double? dowHeight;
+ final double? rowHeight;
+ final BoxConstraints constraints;
+ final int? previousIndex;
+ final StartingDayOfWeek startingDayOfWeek;
+ final PageController? pageController;
+ final ScrollPhysics? scrollPhysics;
+ final void Function(int, DateTime) onPageChanged;
+
+ const CalendarCore({
+ super.key,
+ this.dowBuilder,
+ required this.dayBuilder,
+ required this.onPageChanged,
+ required this.firstDay,
+ required this.lastDay,
+ required this.constraints,
+ this.dowHeight,
+ this.rowHeight,
+ this.startingDayOfWeek = StartingDayOfWeek.sunday,
+ this.calendarFormat = CalendarFormat.month,
+ this.pageController,
+ this.focusedDay,
+ this.previousIndex,
+ this.sixWeekMonthsEnforced = false,
+ this.dowVisible = true,
+ this.weekNumberBuilder,
+ required this.weekNumbersVisible,
+ this.dowDecoration,
+ this.rowDecoration,
+ this.tableBorder,
+ this.tablePadding,
+ this.scrollPhysics,
+ }) : assert(!dowVisible || (dowHeight != null && dowBuilder != null));
+
+ @override
+ Widget build(BuildContext context) {
+ return PageView.builder(
+ controller: pageController,
+ physics: scrollPhysics,
+ itemCount: _getPageCount(calendarFormat, firstDay, lastDay),
+ itemBuilder: (context, index) {
+ final baseDay = _getBaseDay(calendarFormat, index);
+ final visibleRange = _getVisibleRange(calendarFormat, baseDay);
+ final visibleDays = _daysInRange(visibleRange.start, visibleRange.end);
+
+ final actualDowHeight = dowVisible ? dowHeight! : 0.0;
+ final constrainedRowHeight = constraints.hasBoundedHeight
+ ? (constraints.maxHeight - actualDowHeight) /
+ _getRowCount(calendarFormat, baseDay)
+ : null;
+
+ return CalendarPage(
+ visibleDays: visibleDays,
+ dowVisible: dowVisible,
+ dowDecoration: dowDecoration,
+ rowDecoration: rowDecoration,
+ tableBorder: tableBorder,
+ tablePadding: tablePadding,
+ dowBuilder: (context, day) {
+ return SizedBox(
+ height: dowHeight,
+ child: dowBuilder?.call(context, day),
+ );
+ },
+ dayBuilder: (context, day) {
+ DateTime baseDay;
+ final previousFocusedDay = focusedDay;
+ if (previousFocusedDay == null || previousIndex == null) {
+ baseDay = _getBaseDay(calendarFormat, index);
+ } else {
+ baseDay =
+ _getFocusedDay(calendarFormat, previousFocusedDay, index);
+ }
+
+ return SizedBox(
+ height: constrainedRowHeight ?? rowHeight,
+ child: dayBuilder(context, day, baseDay),
+ );
+ },
+ dowHeight: dowHeight,
+ weekNumberVisible: weekNumbersVisible,
+ weekNumberBuilder: (context, day) {
+ return SizedBox(
+ height: constrainedRowHeight ?? rowHeight,
+ child: weekNumberBuilder?.call(context, day),
+ );
+ },
+ );
+ },
+ onPageChanged: (index) {
+ DateTime baseDay;
+ final previousFocusedDay = focusedDay;
+ if (previousFocusedDay == null || previousIndex == null) {
+ baseDay = _getBaseDay(calendarFormat, index);
+ } else {
+ baseDay = _getFocusedDay(calendarFormat, previousFocusedDay, index);
+ }
+
+ return onPageChanged(index, baseDay);
+ },
+ );
+ }
+
+ int _getPageCount(CalendarFormat format, DateTime first, DateTime last) {
+ switch (format) {
+ case CalendarFormat.month:
+ return _getMonthCount(first, last) + 1;
+ case CalendarFormat.twoWeeks:
+ return _getTwoWeekCount(first, last) + 1;
+ case CalendarFormat.week:
+ return _getWeekCount(first, last) + 1;
+ }
+ }
+
+ int _getMonthCount(DateTime first, DateTime last) {
+ final yearDif = last.year - first.year;
+ final monthDif = last.month - first.month;
+
+ return yearDif * 12 + monthDif;
+ }
+
+ int _getWeekCount(DateTime first, DateTime last) {
+ return last.difference(_firstDayOfWeek(first)).inDays ~/ 7;
+ }
+
+ int _getTwoWeekCount(DateTime first, DateTime last) {
+ return last.difference(_firstDayOfWeek(first)).inDays ~/ 14;
+ }
+
+ DateTime _getFocusedDay(
+ CalendarFormat format,
+ DateTime prevFocusedDay,
+ int pageIndex,
+ ) {
+ if (pageIndex == previousIndex) {
+ return prevFocusedDay;
+ }
+
+ final pageDif = pageIndex - previousIndex!;
+ DateTime day;
+
+ switch (format) {
+ case CalendarFormat.month:
+ day = DateTime.utc(prevFocusedDay.year, prevFocusedDay.month + pageDif);
+ case CalendarFormat.twoWeeks:
+ day = DateTime.utc(
+ prevFocusedDay.year,
+ prevFocusedDay.month,
+ prevFocusedDay.day + pageDif * 14,
+ );
+ case CalendarFormat.week:
+ day = DateTime.utc(
+ prevFocusedDay.year,
+ prevFocusedDay.month,
+ prevFocusedDay.day + pageDif * 7,
+ );
+ }
+
+ if (day.isBefore(firstDay)) {
+ day = firstDay;
+ } else if (day.isAfter(lastDay)) {
+ day = lastDay;
+ }
+
+ return day;
+ }
+
+ DateTime _getBaseDay(CalendarFormat format, int pageIndex) {
+ DateTime day;
+
+ switch (format) {
+ case CalendarFormat.month:
+ day = DateTime.utc(firstDay.year, firstDay.month + pageIndex);
+ case CalendarFormat.twoWeeks:
+ day = DateTime.utc(
+ firstDay.year,
+ firstDay.month,
+ firstDay.day + pageIndex * 14,
+ );
+ case CalendarFormat.week:
+ day = DateTime.utc(
+ firstDay.year,
+ firstDay.month,
+ firstDay.day + pageIndex * 7,
+ );
+ }
+
+ if (day.isBefore(firstDay)) {
+ day = firstDay;
+ } else if (day.isAfter(lastDay)) {
+ day = lastDay;
+ }
+
+ return day;
+ }
+
+ DateTimeRange _getVisibleRange(CalendarFormat format, DateTime focusedDay) {
+ switch (format) {
+ case CalendarFormat.month:
+ return _daysInMonth(focusedDay);
+ case CalendarFormat.twoWeeks:
+ return _daysInTwoWeeks(focusedDay);
+ case CalendarFormat.week:
+ return _daysInWeek(focusedDay);
+ }
+ }
+
+ DateTimeRange _daysInWeek(DateTime focusedDay) {
+ final daysBefore = _getDaysBefore(focusedDay);
+ final firstToDisplay = focusedDay.subtract(Duration(days: daysBefore));
+ final lastToDisplay = firstToDisplay.add(const Duration(days: 7));
+ return DateTimeRange(start: firstToDisplay, end: lastToDisplay);
+ }
+
+ DateTimeRange _daysInTwoWeeks(DateTime focusedDay) {
+ final daysBefore = _getDaysBefore(focusedDay);
+ final firstToDisplay = focusedDay.subtract(Duration(days: daysBefore));
+ final lastToDisplay = firstToDisplay.add(const Duration(days: 14));
+ return DateTimeRange(start: firstToDisplay, end: lastToDisplay);
+ }
+
+ DateTimeRange _daysInMonth(DateTime focusedDay) {
+ final first = _firstDayOfMonth(focusedDay);
+ final daysBefore = _getDaysBefore(first);
+ final firstToDisplay = first.subtract(Duration(days: daysBefore));
+
+ if (sixWeekMonthsEnforced) {
+ final end = firstToDisplay.add(const Duration(days: 42));
+ return DateTimeRange(start: firstToDisplay, end: end);
+ }
+
+ final last = _lastDayOfMonth(focusedDay);
+ final daysAfter = _getDaysAfter(last);
+ final lastToDisplay = last.add(Duration(days: daysAfter));
+
+ return DateTimeRange(start: firstToDisplay, end: lastToDisplay);
+ }
+
+ List _daysInRange(DateTime first, DateTime last) {
+ final dayCount = last.difference(first).inDays + 1;
+ return List.generate(
+ dayCount,
+ (index) => DateTime.utc(first.year, first.month, first.day + index),
+ );
+ }
+
+ DateTime _firstDayOfWeek(DateTime week) {
+ final daysBefore = _getDaysBefore(week);
+ return week.subtract(Duration(days: daysBefore));
+ }
+
+ DateTime _firstDayOfMonth(DateTime month) {
+ return DateTime.utc(month.year, month.month);
+ }
+
+ DateTime _lastDayOfMonth(DateTime month) {
+ final date = month.month < 12
+ ? DateTime.utc(month.year, month.month + 1)
+ : DateTime.utc(month.year + 1);
+ return date.subtract(const Duration(days: 1));
+ }
+
+ int _getRowCount(CalendarFormat format, DateTime focusedDay) {
+ if (format == CalendarFormat.twoWeeks) {
+ return 2;
+ } else if (format == CalendarFormat.week) {
+ return 1;
+ } else if (sixWeekMonthsEnforced) {
+ return 6;
+ }
+
+ final first = _firstDayOfMonth(focusedDay);
+ final daysBefore = _getDaysBefore(first);
+ final firstToDisplay = first.subtract(Duration(days: daysBefore));
+
+ final last = _lastDayOfMonth(focusedDay);
+ final daysAfter = _getDaysAfter(last);
+ final lastToDisplay = last.add(Duration(days: daysAfter));
+
+ return (lastToDisplay.difference(firstToDisplay).inDays + 1) ~/ 7;
+ }
+
+ int _getDaysBefore(DateTime firstDay) {
+ return (firstDay.weekday + 7 - getWeekdayNumber(startingDayOfWeek)) % 7;
+ }
+
+ int _getDaysAfter(DateTime lastDay) {
+ final invertedStartingWeekday = 8 - getWeekdayNumber(startingDayOfWeek);
+
+ final daysAfter = 7 - ((lastDay.weekday + invertedStartingWeekday) % 7);
+ if (daysAfter == 7) {
+ return 0;
+ }
+
+ return daysAfter;
+ }
+}
diff --git a/lib/src/widgets/calendar_header.dart b/lib/src/widgets/calendar_header.dart
new file mode 100644
index 00000000..6a54336e
--- /dev/null
+++ b/lib/src/widgets/calendar_header.dart
@@ -0,0 +1,97 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/widgets.dart';
+import 'package:intl/intl.dart';
+import 'package:table_calendar/src/customization/header_style.dart';
+import 'package:table_calendar/src/shared/utils.dart'
+ show CalendarFormat, DayBuilder;
+import 'package:table_calendar/src/widgets/custom_icon_button.dart';
+import 'package:table_calendar/src/widgets/format_button.dart';
+
+class CalendarHeader extends StatelessWidget {
+ final dynamic locale;
+ final DateTime focusedMonth;
+ final CalendarFormat calendarFormat;
+ final HeaderStyle headerStyle;
+ final VoidCallback onLeftChevronTap;
+ final VoidCallback onRightChevronTap;
+ final VoidCallback onHeaderTap;
+ final VoidCallback onHeaderLongPress;
+ final ValueChanged onFormatButtonTap;
+ final Map availableCalendarFormats;
+ final DayBuilder? headerTitleBuilder;
+
+ const CalendarHeader({
+ super.key,
+ this.locale,
+ required this.focusedMonth,
+ required this.calendarFormat,
+ required this.headerStyle,
+ required this.onLeftChevronTap,
+ required this.onRightChevronTap,
+ required this.onHeaderTap,
+ required this.onHeaderLongPress,
+ required this.onFormatButtonTap,
+ required this.availableCalendarFormats,
+ this.headerTitleBuilder,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final text = headerStyle.titleTextFormatter?.call(focusedMonth, locale) ??
+ DateFormat.yMMMM(locale).format(focusedMonth);
+
+ return Container(
+ decoration: headerStyle.decoration,
+ margin: headerStyle.headerMargin,
+ padding: headerStyle.headerPadding,
+ child: Row(
+ children: [
+ if (headerStyle.leftChevronVisible)
+ CustomIconButton(
+ icon: headerStyle.leftChevronIcon,
+ onTap: onLeftChevronTap,
+ margin: headerStyle.leftChevronMargin,
+ padding: headerStyle.leftChevronPadding,
+ ),
+ Expanded(
+ child: headerTitleBuilder?.call(context, focusedMonth) ??
+ GestureDetector(
+ onTap: onHeaderTap,
+ onLongPress: onHeaderLongPress,
+ child: Text(
+ text,
+ style: headerStyle.titleTextStyle,
+ textAlign: headerStyle.titleCentered
+ ? TextAlign.center
+ : TextAlign.start,
+ ),
+ ),
+ ),
+ if (headerStyle.formatButtonVisible &&
+ availableCalendarFormats.length > 1)
+ Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: FormatButton(
+ onTap: onFormatButtonTap,
+ availableCalendarFormats: availableCalendarFormats,
+ calendarFormat: calendarFormat,
+ decoration: headerStyle.formatButtonDecoration,
+ padding: headerStyle.formatButtonPadding,
+ textStyle: headerStyle.formatButtonTextStyle,
+ showsNextFormat: headerStyle.formatButtonShowsNext,
+ ),
+ ),
+ if (headerStyle.rightChevronVisible)
+ CustomIconButton(
+ icon: headerStyle.rightChevronIcon,
+ onTap: onRightChevronTap,
+ margin: headerStyle.rightChevronMargin,
+ padding: headerStyle.rightChevronPadding,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/calendar_page.dart b/lib/src/widgets/calendar_page.dart
new file mode 100644
index 00000000..0b3cb08d
--- /dev/null
+++ b/lib/src/widgets/calendar_page.dart
@@ -0,0 +1,97 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/widgets.dart';
+
+class CalendarPage extends StatelessWidget {
+ final Widget Function(BuildContext context, DateTime day)? dowBuilder;
+ final Widget Function(BuildContext context, DateTime day) dayBuilder;
+ final Widget Function(BuildContext context, DateTime day)? weekNumberBuilder;
+ final List visibleDays;
+ final Decoration? dowDecoration;
+ final Decoration? rowDecoration;
+ final TableBorder? tableBorder;
+ final EdgeInsets? tablePadding;
+ final bool dowVisible;
+ final bool weekNumberVisible;
+ final double? dowHeight;
+
+ const CalendarPage({
+ super.key,
+ required this.visibleDays,
+ this.dowBuilder,
+ required this.dayBuilder,
+ this.weekNumberBuilder,
+ this.dowDecoration,
+ this.rowDecoration,
+ this.tableBorder,
+ this.tablePadding,
+ this.dowVisible = true,
+ this.weekNumberVisible = false,
+ this.dowHeight,
+ }) : assert(!dowVisible || (dowHeight != null && dowBuilder != null)),
+ assert(!weekNumberVisible || weekNumberBuilder != null);
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: tablePadding ?? EdgeInsets.zero,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ if (weekNumberVisible) _buildWeekNumbers(context),
+ Expanded(
+ child: Table(
+ border: tableBorder,
+ children: [
+ if (dowVisible) _buildDaysOfWeek(context),
+ ..._buildCalendarDays(context),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildWeekNumbers(BuildContext context) {
+ final rowAmount = visibleDays.length ~/ 7;
+
+ return Column(
+ children: [
+ if (dowVisible) SizedBox(height: dowHeight ?? 0),
+ ...List.generate(
+ rowAmount,
+ (index) => Expanded(
+ child: weekNumberBuilder!(context, visibleDays[index * 7]),
+ ),
+ ),
+ ],
+ );
+ }
+
+ TableRow _buildDaysOfWeek(BuildContext context) {
+ return TableRow(
+ decoration: dowDecoration,
+ children: List.generate(
+ 7,
+ (index) => dowBuilder!(context, visibleDays[index]),
+ ),
+ );
+ }
+
+ List _buildCalendarDays(BuildContext context) {
+ final rowAmount = visibleDays.length ~/ 7;
+
+ return List.generate(
+ rowAmount,
+ (index) => TableRow(
+ decoration: rowDecoration,
+ children: List.generate(
+ 7,
+ (id) => dayBuilder(context, visibleDays[index * 7 + id]),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/cell_content.dart b/lib/src/widgets/cell_content.dart
new file mode 100644
index 00000000..c2b52b4a
--- /dev/null
+++ b/lib/src/widgets/cell_content.dart
@@ -0,0 +1,176 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/widgets.dart';
+import 'package:intl/intl.dart';
+import 'package:table_calendar/src/customization/calendar_builders.dart';
+import 'package:table_calendar/src/customization/calendar_style.dart';
+
+class CellContent extends StatelessWidget {
+ final DateTime day;
+ final DateTime focusedDay;
+ final dynamic locale;
+ final bool isTodayHighlighted;
+ final bool isToday;
+ final bool isSelected;
+ final bool isRangeStart;
+ final bool isRangeEnd;
+ final bool isWithinRange;
+ final bool isOutside;
+ final bool isDisabled;
+ final bool isHoliday;
+ final bool isWeekend;
+ final CalendarStyle calendarStyle;
+ final CalendarBuilders calendarBuilders;
+
+ const CellContent({
+ super.key,
+ required this.day,
+ required this.focusedDay,
+ required this.calendarStyle,
+ required this.calendarBuilders,
+ required this.isTodayHighlighted,
+ required this.isToday,
+ required this.isSelected,
+ required this.isRangeStart,
+ required this.isRangeEnd,
+ required this.isWithinRange,
+ required this.isOutside,
+ required this.isDisabled,
+ required this.isHoliday,
+ required this.isWeekend,
+ this.locale,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final dowLabel = DateFormat.EEEE(locale).format(day);
+ final dayLabel = DateFormat.yMMMMd(locale).format(day);
+ final semanticsLabel = '$dowLabel, $dayLabel';
+
+ Widget? cell =
+ calendarBuilders.prioritizedBuilder?.call(context, day, focusedDay);
+
+ if (cell != null) {
+ return Semantics(
+ label: semanticsLabel,
+ excludeSemantics: true,
+ child: cell,
+ );
+ }
+
+ final text =
+ calendarStyle.dayTextFormatter?.call(day, locale) ?? '${day.day}';
+ final margin = calendarStyle.cellMargin;
+ final padding = calendarStyle.cellPadding;
+ final alignment = calendarStyle.cellAlignment;
+ const duration = Duration(milliseconds: 250);
+
+ if (isDisabled) {
+ cell = calendarBuilders.disabledBuilder?.call(context, day, focusedDay) ??
+ AnimatedContainer(
+ duration: duration,
+ margin: margin,
+ padding: padding,
+ decoration: calendarStyle.disabledDecoration,
+ alignment: alignment,
+ child: Text(text, style: calendarStyle.disabledTextStyle),
+ );
+ } else if (isSelected) {
+ cell = calendarBuilders.selectedBuilder?.call(context, day, focusedDay) ??
+ AnimatedContainer(
+ duration: duration,
+ margin: margin,
+ padding: padding,
+ decoration: calendarStyle.selectedDecoration,
+ alignment: alignment,
+ child: Text(text, style: calendarStyle.selectedTextStyle),
+ );
+ } else if (isRangeStart) {
+ cell =
+ calendarBuilders.rangeStartBuilder?.call(context, day, focusedDay) ??
+ AnimatedContainer(
+ duration: duration,
+ margin: margin,
+ padding: padding,
+ decoration: calendarStyle.rangeStartDecoration,
+ alignment: alignment,
+ child: Text(text, style: calendarStyle.rangeStartTextStyle),
+ );
+ } else if (isRangeEnd) {
+ cell = calendarBuilders.rangeEndBuilder?.call(context, day, focusedDay) ??
+ AnimatedContainer(
+ duration: duration,
+ margin: margin,
+ padding: padding,
+ decoration: calendarStyle.rangeEndDecoration,
+ alignment: alignment,
+ child: Text(text, style: calendarStyle.rangeEndTextStyle),
+ );
+ } else if (isToday && isTodayHighlighted) {
+ cell = calendarBuilders.todayBuilder?.call(context, day, focusedDay) ??
+ AnimatedContainer(
+ duration: duration,
+ margin: margin,
+ padding: padding,
+ decoration: calendarStyle.todayDecoration,
+ alignment: alignment,
+ child: Text(text, style: calendarStyle.todayTextStyle),
+ );
+ } else if (isHoliday) {
+ cell = calendarBuilders.holidayBuilder?.call(context, day, focusedDay) ??
+ AnimatedContainer(
+ duration: duration,
+ margin: margin,
+ padding: padding,
+ decoration: calendarStyle.holidayDecoration,
+ alignment: alignment,
+ child: Text(text, style: calendarStyle.holidayTextStyle),
+ );
+ } else if (isWithinRange) {
+ cell =
+ calendarBuilders.withinRangeBuilder?.call(context, day, focusedDay) ??
+ AnimatedContainer(
+ duration: duration,
+ margin: margin,
+ padding: padding,
+ decoration: calendarStyle.withinRangeDecoration,
+ alignment: alignment,
+ child: Text(text, style: calendarStyle.withinRangeTextStyle),
+ );
+ } else if (isOutside) {
+ cell = calendarBuilders.outsideBuilder?.call(context, day, focusedDay) ??
+ AnimatedContainer(
+ duration: duration,
+ margin: margin,
+ padding: padding,
+ decoration: calendarStyle.outsideDecoration,
+ alignment: alignment,
+ child: Text(text, style: calendarStyle.outsideTextStyle),
+ );
+ } else {
+ cell = calendarBuilders.defaultBuilder?.call(context, day, focusedDay) ??
+ AnimatedContainer(
+ duration: duration,
+ margin: margin,
+ padding: padding,
+ decoration: isWeekend
+ ? calendarStyle.weekendDecoration
+ : calendarStyle.defaultDecoration,
+ alignment: alignment,
+ child: Text(
+ text,
+ style: isWeekend
+ ? calendarStyle.weekendTextStyle
+ : calendarStyle.defaultTextStyle,
+ ),
+ );
+ }
+
+ return Semantics(
+ label: semanticsLabel,
+ excludeSemantics: true,
+ child: cell,
+ );
+ }
+}
diff --git a/lib/src/widgets/cell_widget.dart b/lib/src/widgets/cell_widget.dart
deleted file mode 100644
index eb3d2a40..00000000
--- a/lib/src/widgets/cell_widget.dart
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright (c) 2019 Aleksander Woźniak
-// Licensed under Apache License v2.0
-
-part of table_calendar;
-
-class _CellWidget extends StatelessWidget {
- final String text;
- final bool isUnavailable;
- final bool isSelected;
- final bool isToday;
- final bool isWeekend;
- final bool isOutsideMonth;
- final bool isHoliday;
- final bool isEventDay;
- final CalendarStyle calendarStyle;
-
- const _CellWidget({
- Key key,
- @required this.text,
- this.isUnavailable = false,
- this.isSelected = false,
- this.isToday = false,
- this.isWeekend = false,
- this.isOutsideMonth = false,
- this.isHoliday = false,
- this.isEventDay = false,
- @required this.calendarStyle,
- }) : assert(text != null),
- assert(calendarStyle != null),
- super(key: key);
-
- @override
- Widget build(BuildContext context) {
- return AnimatedContainer(
- duration: const Duration(milliseconds: 250),
- decoration: _buildCellDecoration(),
- margin: calendarStyle.cellMargin,
- alignment: Alignment.center,
- child: Text(
- text,
- style: _buildCellTextStyle(),
- ),
- );
- }
-
- Decoration _buildCellDecoration() {
- if (isSelected && calendarStyle.renderSelectedFirst && calendarStyle.highlightSelected) {
- return BoxDecoration(shape: BoxShape.circle, color: calendarStyle.selectedColor);
- } else if (isToday && calendarStyle.highlightToday) {
- return BoxDecoration(shape: BoxShape.circle, color: calendarStyle.todayColor);
- } else if (isSelected && calendarStyle.highlightSelected) {
- return BoxDecoration(shape: BoxShape.circle, color: calendarStyle.selectedColor);
- } else {
- return BoxDecoration(shape: BoxShape.circle);
- }
- }
-
- TextStyle _buildCellTextStyle() {
- if (isUnavailable) {
- return calendarStyle.unavailableStyle;
- } else if (isSelected && calendarStyle.renderSelectedFirst && calendarStyle.highlightSelected) {
- return calendarStyle.selectedStyle;
- } else if (isToday && calendarStyle.highlightToday) {
- return calendarStyle.todayStyle;
- } else if (isSelected && calendarStyle.highlightSelected) {
- return calendarStyle.selectedStyle;
- } else if (isOutsideMonth && isHoliday) {
- return calendarStyle.outsideHolidayStyle;
- } else if (isHoliday) {
- return calendarStyle.holidayStyle;
- } else if (isOutsideMonth && isWeekend) {
- return calendarStyle.outsideWeekendStyle;
- } else if (isOutsideMonth) {
- return calendarStyle.outsideStyle;
- } else if (isWeekend) {
- return calendarStyle.weekendStyle;
- } else if (isEventDay) {
- return calendarStyle.eventDayStyle;
- } else {
- return calendarStyle.weekdayStyle;
- }
- }
-}
diff --git a/lib/src/widgets/custom_icon_button.dart b/lib/src/widgets/custom_icon_button.dart
index 06be1983..0b31b98a 100644
--- a/lib/src/widgets/custom_icon_button.dart
+++ b/lib/src/widgets/custom_icon_button.dart
@@ -1,36 +1,46 @@
-// Copyright (c) 2019 Aleksander Woźniak
-// Licensed under Apache License v2.0
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
-part of table_calendar;
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
-class _CustomIconButton extends StatelessWidget {
- final Icon icon;
+class CustomIconButton extends StatelessWidget {
+ final Widget icon;
final VoidCallback onTap;
final EdgeInsets margin;
final EdgeInsets padding;
- const _CustomIconButton({
- Key key,
- @required this.icon,
- @required this.onTap,
- this.margin,
- this.padding,
- }) : assert(icon != null),
- assert(onTap != null),
- super(key: key);
+ const CustomIconButton({
+ super.key,
+ required this.icon,
+ required this.onTap,
+ this.margin = EdgeInsets.zero,
+ this.padding = const EdgeInsets.all(8.0),
+ });
@override
Widget build(BuildContext context) {
+ final platform = Theme.of(context).platform;
+
return Padding(
padding: margin,
- child: InkWell(
- onTap: onTap,
- borderRadius: BorderRadius.circular(100.0),
- child: Padding(
- padding: padding,
- child: icon,
- ),
- ),
+ child: !kIsWeb &&
+ (platform == TargetPlatform.iOS ||
+ platform == TargetPlatform.macOS)
+ ? CupertinoButton(
+ onPressed: onTap,
+ padding: padding,
+ child: icon,
+ )
+ : InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(100.0),
+ child: Padding(
+ padding: padding,
+ child: icon,
+ ),
+ ),
);
}
}
diff --git a/lib/src/widgets/format_button.dart b/lib/src/widgets/format_button.dart
new file mode 100644
index 00000000..76bbc523
--- /dev/null
+++ b/lib/src/widgets/format_button.dart
@@ -0,0 +1,68 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:table_calendar/src/shared/utils.dart' show CalendarFormat;
+
+class FormatButton extends StatelessWidget {
+ final CalendarFormat calendarFormat;
+ final ValueChanged onTap;
+ final TextStyle textStyle;
+ final BoxDecoration decoration;
+ final EdgeInsets padding;
+ final bool showsNextFormat;
+ final Map availableCalendarFormats;
+
+ const FormatButton({
+ super.key,
+ required this.calendarFormat,
+ required this.onTap,
+ required this.textStyle,
+ required this.decoration,
+ required this.padding,
+ required this.showsNextFormat,
+ required this.availableCalendarFormats,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final child = Container(
+ decoration: decoration,
+ padding: padding,
+ child: Text(
+ _formatButtonText,
+ style: textStyle,
+ ),
+ );
+
+ final platform = Theme.of(context).platform;
+
+ return !kIsWeb &&
+ (platform == TargetPlatform.iOS || platform == TargetPlatform.macOS)
+ ? CupertinoButton(
+ onPressed: () => onTap(_nextFormat()),
+ padding: EdgeInsets.zero,
+ child: child,
+ )
+ : InkWell(
+ borderRadius:
+ decoration.borderRadius?.resolve(Directionality.of(context)),
+ onTap: () => onTap(_nextFormat()),
+ child: child,
+ );
+ }
+
+ String get _formatButtonText => showsNextFormat
+ ? availableCalendarFormats[_nextFormat()]!
+ : availableCalendarFormats[calendarFormat]!;
+
+ CalendarFormat _nextFormat() {
+ final formats = availableCalendarFormats.keys.toList();
+ int id = formats.indexOf(calendarFormat);
+ id = (id + 1) % formats.length;
+
+ return formats[id];
+ }
+}
diff --git a/lib/table_calendar.dart b/lib/table_calendar.dart
index f0649629..4115dc30 100644
--- a/lib/table_calendar.dart
+++ b/lib/table_calendar.dart
@@ -1,17 +1,10 @@
-// Copyright (c) 2019 Aleksander Woźniak
-// Licensed under Apache License v2.0
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
-library table_calendar;
-
-import 'package:flutter/material.dart';
-import 'package:intl/intl.dart';
-import 'package:simple_gesture_detector/simple_gesture_detector.dart';
-
-part 'src/calendar.dart';
-part 'src/calendar_controller.dart';
-part 'src/customization/calendar_builders.dart';
-part 'src/customization/calendar_style.dart';
-part 'src/customization/days_of_week_style.dart';
-part 'src/customization/header_style.dart';
-part 'src/widgets/cell_widget.dart';
-part 'src/widgets/custom_icon_button.dart';
+export 'src/customization/calendar_builders.dart';
+export 'src/customization/calendar_style.dart';
+export 'src/customization/days_of_week_style.dart';
+export 'src/customization/header_style.dart';
+export 'src/shared/utils.dart';
+export 'src/table_calendar.dart';
+export 'src/table_calendar_base.dart';
diff --git a/pubspec.lock b/pubspec.lock
index cda66d9a..d4a0886f 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1,62 +1,54 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
- archive:
- dependency: transitive
- description:
- name: archive
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.0.11"
- args:
- dependency: transitive
- description:
- name: args
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.5.2"
async:
dependency: transitive
description:
name: async
- url: "https://pub.dartlang.org"
+ sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+ url: "https://pub.dev"
source: hosted
- version: "2.4.0"
+ version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
- url: "https://pub.dartlang.org"
+ sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+ url: "https://pub.dev"
source: hosted
- version: "1.0.5"
- charcode:
+ version: "2.1.1"
+ characters:
dependency: transitive
description:
- name: charcode
- url: "https://pub.dartlang.org"
+ name: characters
+ sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
+ url: "https://pub.dev"
source: hosted
- version: "1.1.2"
- collection:
+ version: "1.3.0"
+ clock:
dependency: transitive
description:
- name: collection
- url: "https://pub.dartlang.org"
+ name: clock
+ sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+ url: "https://pub.dev"
source: hosted
- version: "1.14.11"
- convert:
+ version: "1.1.1"
+ collection:
dependency: transitive
description:
- name: convert
- url: "https://pub.dartlang.org"
+ name: collection
+ sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
+ url: "https://pub.dev"
source: hosted
- version: "2.1.1"
- crypto:
+ version: "1.18.0"
+ fake_async:
dependency: transitive
description:
- name: crypto
- url: "https://pub.dartlang.org"
+ name: fake_async
+ sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
+ url: "https://pub.dev"
source: hosted
- version: "2.1.3"
+ version: "1.3.1"
flutter:
dependency: "direct main"
description: flutter
@@ -67,69 +59,102 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
- image:
+ http:
dependency: transitive
description:
- name: image
- url: "https://pub.dartlang.org"
+ name: http
+ sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
+ url: "https://pub.dev"
source: hosted
- version: "2.1.4"
+ version: "1.2.2"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.2"
intl:
dependency: "direct main"
description:
name: intl
- url: "https://pub.dartlang.org"
+ sha256: "00f33b908655e606b86d2ade4710a231b802eec6f11e87e4ea3783fd72077a50"
+ url: "https://pub.dev"
source: hosted
- version: "0.16.0"
- matcher:
+ version: "0.20.1"
+ leak_tracker:
dependency: transitive
description:
- name: matcher
- url: "https://pub.dartlang.org"
+ name: leak_tracker
+ sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
+ url: "https://pub.dev"
source: hosted
- version: "0.12.6"
- meta:
+ version: "10.0.5"
+ leak_tracker_flutter_testing:
dependency: transitive
description:
- name: meta
- url: "https://pub.dartlang.org"
+ name: leak_tracker_flutter_testing
+ sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
+ url: "https://pub.dev"
source: hosted
- version: "1.1.8"
- path:
+ version: "3.0.5"
+ leak_tracker_testing:
dependency: transitive
description:
- name: path
- url: "https://pub.dartlang.org"
+ name: leak_tracker_testing
+ sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
+ lint:
+ dependency: "direct dev"
+ description:
+ name: lint
+ sha256: d758a5211fce7fd3f5e316f804daefecdc34c7e53559716125e6da7388ae8565
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.0"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+ url: "https://pub.dev"
source: hosted
- version: "1.6.4"
- pedantic:
+ version: "0.12.16+1"
+ material_color_utilities:
dependency: transitive
description:
- name: pedantic
- url: "https://pub.dartlang.org"
+ name: material_color_utilities
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ url: "https://pub.dev"
source: hosted
- version: "1.8.0+1"
- petitparser:
+ version: "0.11.1"
+ meta:
dependency: transitive
description:
- name: petitparser
- url: "https://pub.dartlang.org"
+ name: meta
+ sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+ url: "https://pub.dev"
source: hosted
- version: "2.4.0"
- quiver:
+ version: "1.15.0"
+ path:
dependency: transitive
description:
- name: quiver
- url: "https://pub.dartlang.org"
+ name: path
+ sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+ url: "https://pub.dev"
source: hosted
- version: "2.0.5"
+ version: "1.9.0"
simple_gesture_detector:
dependency: "direct main"
description:
name: simple_gesture_detector
- url: "https://pub.dartlang.org"
+ sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3
+ url: "https://pub.dev"
source: hosted
- version: "0.1.3"
+ version: "0.2.1"
sky_engine:
dependency: transitive
description: flutter
@@ -139,64 +164,82 @@ packages:
dependency: transitive
description:
name: source_span
- url: "https://pub.dartlang.org"
+ sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+ url: "https://pub.dev"
source: hosted
- version: "1.5.5"
+ version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
- url: "https://pub.dartlang.org"
+ sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+ url: "https://pub.dev"
source: hosted
- version: "1.9.3"
+ version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
- url: "https://pub.dartlang.org"
+ sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+ url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
- url: "https://pub.dartlang.org"
+ sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+ url: "https://pub.dev"
source: hosted
- version: "1.0.5"
+ version: "1.2.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
- url: "https://pub.dartlang.org"
+ sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+ url: "https://pub.dev"
source: hosted
- version: "1.1.0"
+ version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
- url: "https://pub.dartlang.org"
+ sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
+ url: "https://pub.dev"
source: hosted
- version: "0.2.11"
+ version: "0.7.2"
typed_data:
dependency: transitive
description:
name: typed_data
- url: "https://pub.dartlang.org"
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+ url: "https://pub.dev"
source: hosted
- version: "1.1.6"
+ version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
- url: "https://pub.dartlang.org"
+ sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+ url: "https://pub.dev"
source: hosted
- version: "2.0.8"
- xml:
+ version: "2.1.4"
+ vm_service:
dependency: transitive
description:
- name: xml
- url: "https://pub.dartlang.org"
+ name: vm_service
+ sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
+ url: "https://pub.dev"
source: hosted
- version: "3.5.0"
+ version: "14.2.5"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
sdks:
- dart: ">=2.4.0 <3.0.0"
+ dart: ">=3.5.0 <4.0.0"
+ flutter: ">=3.18.0-18.0.pre.54"
diff --git a/pubspec.yaml b/pubspec.yaml
index 9f247263..c1fb94c0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,21 +1,22 @@
name: table_calendar
-description: Highly customizable, feature-packed Flutter Calendar with gestures, animations and multiple formats.
-version: 2.2.3
+description: Highly customizable, feature-packed calendar widget for Flutter.
+version: 3.2.0
author: Aleksander Woźniak
homepage: https://github.com/aleksanderwozniak/table_calendar
environment:
- sdk: ">=2.2.2 <3.0.0"
+ sdk: ">=3.0.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
-
- intl: ">=0.15.0 <0.17.0"
- simple_gesture_detector: ^0.1.3
+
+ intl: ^0.20.0
+ simple_gesture_detector: ^0.2.0
dev_dependencies:
flutter_test:
sdk: flutter
+ lint: ^2.0.1
-flutter:
\ No newline at end of file
+flutter:
diff --git a/table_calendar.iml b/table_calendar.iml
deleted file mode 100644
index 499f388f..00000000
--- a/table_calendar.iml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/test/calendar_header_test.dart b/test/calendar_header_test.dart
new file mode 100644
index 00000000..0df34d6d
--- /dev/null
+++ b/test/calendar_header_test.dart
@@ -0,0 +1,201 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:intl/intl.dart' as intl;
+import 'package:table_calendar/src/customization/header_style.dart';
+import 'package:table_calendar/src/shared/utils.dart';
+import 'package:table_calendar/src/widgets/calendar_header.dart';
+import 'package:table_calendar/src/widgets/custom_icon_button.dart';
+import 'package:table_calendar/src/widgets/format_button.dart';
+
+import 'common.dart';
+
+final focusedMonth = DateTime.utc(2021, 7, 15);
+
+Widget setupTestWidget({
+ HeaderStyle headerStyle = const HeaderStyle(),
+ VoidCallback? onLeftChevronTap,
+ VoidCallback? onRightChevronTap,
+ VoidCallback? onHeaderTap,
+ VoidCallback? onHeaderLongPress,
+ Function(CalendarFormat)? onFormatButtonTap,
+ Map availableCalendarFormats = calendarFormatMap,
+}) {
+ return Directionality(
+ textDirection: TextDirection.ltr,
+ child: Material(
+ child: CalendarHeader(
+ focusedMonth: focusedMonth,
+ calendarFormat: CalendarFormat.month,
+ headerStyle: headerStyle,
+ onLeftChevronTap: () => onLeftChevronTap?.call(),
+ onRightChevronTap: () => onRightChevronTap?.call(),
+ onHeaderTap: () => onHeaderTap?.call(),
+ onHeaderLongPress: () => onHeaderLongPress?.call(),
+ onFormatButtonTap: (format) => onFormatButtonTap?.call(format),
+ availableCalendarFormats: availableCalendarFormats,
+ ),
+ ),
+ );
+}
+
+void main() {
+ testWidgets(
+ 'Displays corrent month and year for given focusedMonth',
+ (tester) async {
+ await tester.pumpWidget(setupTestWidget());
+
+ final headerText = intl.DateFormat.yMMMM().format(focusedMonth);
+
+ expect(find.byType(CalendarHeader), findsOneWidget);
+ expect(find.text(headerText), findsOneWidget);
+ },
+ );
+ testWidgets(
+ 'Ensure chevrons and FormatButton are visible by default, test onTap callbacks',
+ (tester) async {
+ bool leftChevronTapped = false;
+ bool rightChevronTapped = false;
+ bool headerTapped = false;
+ bool headerLongPressed = false;
+ bool formatButtonTapped = false;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ onLeftChevronTap: () => leftChevronTapped = true,
+ onRightChevronTap: () => rightChevronTapped = true,
+ onHeaderTap: () => headerTapped = true,
+ onHeaderLongPress: () => headerLongPressed = true,
+ onFormatButtonTap: (_) => formatButtonTapped = true,
+ ),
+ );
+
+ final leftChevron = find.widgetWithIcon(
+ CustomIconButton,
+ Icons.chevron_left,
+ );
+
+ final rightChevron = find.widgetWithIcon(
+ CustomIconButton,
+ Icons.chevron_right,
+ );
+
+ final header = find.byType(CalendarHeader);
+ final formatButton = find.byType(FormatButton);
+
+ expect(leftChevron, findsOneWidget);
+ expect(rightChevron, findsOneWidget);
+ expect(header, findsOneWidget);
+ expect(formatButton, findsOneWidget);
+
+ expect(leftChevronTapped, false);
+ expect(rightChevronTapped, false);
+ expect(headerTapped, false);
+ expect(headerLongPressed, false);
+ expect(formatButtonTapped, false);
+
+ await tester.tap(leftChevron);
+ await tester.pumpAndSettle();
+
+ await tester.tap(rightChevron);
+ await tester.pumpAndSettle();
+
+ await tester.tap(header);
+ await tester.pumpAndSettle();
+
+ await tester.longPress(header);
+ await tester.pumpAndSettle();
+
+ await tester.tap(formatButton);
+ await tester.pumpAndSettle();
+
+ expect(leftChevronTapped, true);
+ expect(rightChevronTapped, true);
+ expect(headerTapped, true);
+ expect(headerLongPressed, true);
+ expect(formatButtonTapped, true);
+ },
+ );
+
+ testWidgets(
+ 'When leftChevronVisible is false, do not show the left chevron',
+ (tester) async {
+ await tester.pumpWidget(
+ setupTestWidget(
+ headerStyle: const HeaderStyle(
+ leftChevronVisible: false,
+ ),
+ ),
+ );
+
+ final leftChevron = find.widgetWithIcon(
+ CustomIconButton,
+ Icons.chevron_left,
+ );
+
+ final rightChevron = find.widgetWithIcon(
+ CustomIconButton,
+ Icons.chevron_right,
+ );
+
+ expect(leftChevron, findsNothing);
+ expect(rightChevron, findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'When rightChevronVisible is false, do not show the right chevron',
+ (tester) async {
+ await tester.pumpWidget(
+ setupTestWidget(
+ headerStyle: const HeaderStyle(
+ rightChevronVisible: false,
+ ),
+ ),
+ );
+
+ final leftChevron = find.widgetWithIcon(
+ CustomIconButton,
+ Icons.chevron_left,
+ );
+
+ final rightChevron = find.widgetWithIcon(
+ CustomIconButton,
+ Icons.chevron_right,
+ );
+
+ expect(leftChevron, findsOneWidget);
+ expect(rightChevron, findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'When availableCalendarFormats has a single format, do not show the FormatButton',
+ (tester) async {
+ await tester.pumpWidget(
+ setupTestWidget(
+ availableCalendarFormats: const {CalendarFormat.month: 'Month'},
+ ),
+ );
+
+ final formatButton = find.byType(FormatButton);
+ expect(formatButton, findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'When formatButtonVisible is false, do not show the FormatButton',
+ (tester) async {
+ await tester.pumpWidget(
+ setupTestWidget(
+ headerStyle: const HeaderStyle(formatButtonVisible: false),
+ ),
+ );
+
+ final formatButton = find.byType(FormatButton);
+ expect(formatButton, findsNothing);
+ },
+ );
+}
diff --git a/test/calendar_page_test.dart b/test/calendar_page_test.dart
new file mode 100644
index 00000000..19fc8267
--- /dev/null
+++ b/test/calendar_page_test.dart
@@ -0,0 +1,152 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:table_calendar/src/widgets/calendar_page.dart';
+
+Widget setupTestWidget(Widget child) {
+ return Directionality(
+ textDirection: TextDirection.ltr,
+ child: child,
+ );
+}
+
+List visibleDays = getDaysInRange(
+ DateTime.utc(2021, 6, 27),
+ DateTime.utc(2021, 7, 31),
+);
+
+List getDaysInRange(DateTime first, DateTime last) {
+ final dayCount = last.difference(first).inDays + 1;
+ return List.generate(
+ dayCount,
+ (index) => DateTime.utc(first.year, first.month, first.day + index),
+ );
+}
+
+void main() {
+ testWidgets(
+ 'CalendarPage lays out all the visible days',
+ (tester) async {
+ await tester.pumpWidget(
+ setupTestWidget(
+ CalendarPage(
+ visibleDays: visibleDays,
+ dayBuilder: (context, day) {
+ return Text('${day.day}');
+ },
+ dowVisible: false,
+ ),
+ ),
+ );
+
+ final expectedCellCount = visibleDays.length;
+ expect(find.byType(Text), findsNWidgets(expectedCellCount));
+ },
+ );
+
+ testWidgets(
+ 'CalendarPage lays out 7 DOW labels',
+ (tester) async {
+ await tester.pumpWidget(
+ setupTestWidget(
+ CalendarPage(
+ visibleDays: visibleDays,
+ dayBuilder: (context, day) {
+ return Text('${day.day}');
+ },
+ dowBuilder: (context, day) {
+ return Text('${day.weekday}');
+ },
+ dowHeight: 5,
+ ),
+ ),
+ );
+
+ final expectedCellCount = visibleDays.length;
+ const expectedDowLabels = 7;
+
+ expect(
+ find.byType(Text),
+ findsNWidgets(expectedCellCount + expectedDowLabels),
+ );
+ },
+ );
+
+ testWidgets(
+ 'Throw AssertionError when CalendarPage is built with dowVisible set to true, but dowBuilder is absent',
+ (tester) async {
+ expect(
+ () async {
+ await tester.pumpWidget(
+ setupTestWidget(
+ CalendarPage(
+ visibleDays: visibleDays,
+ dayBuilder: (context, day) {
+ return Text('${day.day}');
+ },
+ ),
+ ),
+ );
+ },
+ throwsAssertionError,
+ );
+ },
+ );
+
+ testWidgets(
+ 'Week numbers are not visible by default',
+ (tester) async {
+ await tester.pumpWidget(
+ setupTestWidget(
+ CalendarPage(
+ visibleDays: visibleDays,
+ dayBuilder: (context, day) {
+ return Text('${day.day}');
+ },
+ dowBuilder: (context, day) {
+ return Text('${day.weekday}');
+ },
+ dowHeight: 5,
+ ),
+ ),
+ );
+
+ expect(
+ find.byType(Column),
+ findsNWidgets(0),
+ );
+ },
+ );
+
+ testWidgets(
+ 'Week numbers are visible',
+ (tester) async {
+ await tester.pumpWidget(
+ setupTestWidget(
+ CalendarPage(
+ visibleDays: visibleDays,
+ dayBuilder: (context, day) {
+ return Text('${day.day}');
+ },
+ dowBuilder: (context, day) {
+ return Text('${day.weekday}');
+ },
+ dowHeight: 5,
+ weekNumberVisible: true,
+ weekNumberBuilder: (BuildContext context, DateTime day) {
+ return Text(day.weekday.toString());
+ },
+ ),
+ ),
+ );
+
+ expect(
+ find.byType(Column),
+ findsNWidgets(1),
+ );
+ },
+ );
+}
diff --git a/test/cell_content_test.dart b/test/cell_content_test.dart
new file mode 100644
index 00000000..9f43e2b6
--- /dev/null
+++ b/test/cell_content_test.dart
@@ -0,0 +1,390 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:intl/date_symbol_data_local.dart';
+import 'package:intl/intl.dart' hide TextDirection;
+import 'package:table_calendar/src/widgets/cell_content.dart';
+import 'package:table_calendar/table_calendar.dart';
+
+Widget setupTestWidget(
+ DateTime cellDay, {
+ CalendarBuilders calendarBuilders = const CalendarBuilders(),
+ CalendarStyle calendarStyle = const CalendarStyle(),
+ bool isDisabled = false,
+ bool isToday = false,
+ bool isWeekend = false,
+ bool isOutside = false,
+ bool isSelected = false,
+ bool isRangeStart = false,
+ bool isRangeEnd = false,
+ bool isWithinRange = false,
+ bool isHoliday = false,
+ bool isTodayHighlighted = true,
+ String? locale,
+}) {
+ return Directionality(
+ textDirection: TextDirection.ltr,
+ child: CellContent(
+ day: cellDay,
+ focusedDay: cellDay,
+ calendarBuilders: calendarBuilders,
+ calendarStyle: calendarStyle,
+ isDisabled: isDisabled,
+ isToday: isToday,
+ isWeekend: isWeekend,
+ isOutside: isOutside,
+ isSelected: isSelected,
+ isRangeStart: isRangeStart,
+ isRangeEnd: isRangeEnd,
+ isWithinRange: isWithinRange,
+ isHoliday: isHoliday,
+ isTodayHighlighted: isTodayHighlighted,
+ locale: locale,
+ ),
+ );
+}
+
+void main() {
+ group('CalendarBuilders flag test:', () {
+ testWidgets('selectedBuilder', (tester) async {
+ DateTime? builderDay;
+
+ final calendarBuilders = CalendarBuilders(
+ selectedBuilder: (context, day, focusedDay) {
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ );
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ expect(builderDay, isNull);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ calendarBuilders: calendarBuilders,
+ isSelected: true,
+ ),
+ );
+
+ expect(builderDay, cellDay);
+ });
+
+ testWidgets('rangeStartBuilder', (tester) async {
+ DateTime? builderDay;
+
+ final calendarBuilders = CalendarBuilders(
+ rangeStartBuilder: (context, day, focusedDay) {
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ );
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ expect(builderDay, isNull);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ calendarBuilders: calendarBuilders,
+ isRangeStart: true,
+ ),
+ );
+
+ expect(builderDay, cellDay);
+ });
+
+ testWidgets('rangeEndBuilder', (tester) async {
+ DateTime? builderDay;
+
+ final calendarBuilders = CalendarBuilders(
+ rangeEndBuilder: (context, day, focusedDay) {
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ );
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ expect(builderDay, isNull);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ calendarBuilders: calendarBuilders,
+ isRangeEnd: true,
+ ),
+ );
+
+ expect(builderDay, cellDay);
+ });
+
+ testWidgets('withinRangeBuilder', (tester) async {
+ DateTime? builderDay;
+
+ final calendarBuilders = CalendarBuilders(
+ withinRangeBuilder: (context, day, focusedDay) {
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ );
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ expect(builderDay, isNull);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ calendarBuilders: calendarBuilders,
+ isWithinRange: true,
+ ),
+ );
+
+ expect(builderDay, cellDay);
+ });
+
+ testWidgets('todayBuilder', (tester) async {
+ DateTime? builderDay;
+
+ final calendarBuilders = CalendarBuilders(
+ todayBuilder: (context, day, focusedDay) {
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ );
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ expect(builderDay, isNull);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ calendarBuilders: calendarBuilders,
+ isToday: true,
+ ),
+ );
+
+ expect(builderDay, cellDay);
+ });
+
+ testWidgets('holidayBuilder', (tester) async {
+ DateTime? builderDay;
+
+ final calendarBuilders = CalendarBuilders(
+ holidayBuilder: (context, day, focusedDay) {
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ );
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ expect(builderDay, isNull);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ calendarBuilders: calendarBuilders,
+ isHoliday: true,
+ ),
+ );
+
+ expect(builderDay, cellDay);
+ });
+
+ testWidgets('outsideBuilder', (tester) async {
+ DateTime? builderDay;
+
+ final calendarBuilders = CalendarBuilders(
+ outsideBuilder: (context, day, focusedDay) {
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ );
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ expect(builderDay, isNull);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ calendarBuilders: calendarBuilders,
+ isOutside: true,
+ ),
+ );
+
+ expect(builderDay, cellDay);
+ });
+
+ testWidgets(
+ 'defaultBuilder gets triggered when no other flags are active',
+ (tester) async {
+ DateTime? builderDay;
+
+ final calendarBuilders = CalendarBuilders(
+ defaultBuilder: (context, day, focusedDay) {
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ );
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ expect(builderDay, isNull);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ calendarBuilders: calendarBuilders,
+ ),
+ );
+
+ expect(builderDay, cellDay);
+ },
+ );
+
+ testWidgets(
+ 'disabledBuilder has higher build order priority than selectedBuilder',
+ (tester) async {
+ DateTime? builderDay;
+ String builderName = '';
+
+ final calendarBuilders = CalendarBuilders(
+ selectedBuilder: (context, day, focusedDay) {
+ builderName = 'selectedBuilder';
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ disabledBuilder: (context, day, focusedDay) {
+ builderName = 'disabledBuilder';
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ );
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ expect(builderDay, isNull);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ calendarBuilders: calendarBuilders,
+ isDisabled: true,
+ isSelected: true,
+ ),
+ );
+
+ expect(builderDay, cellDay);
+ expect(builderName, 'disabledBuilder');
+ },
+ );
+
+ testWidgets(
+ 'prioritizedBuilder has the highest build order priority',
+ (tester) async {
+ DateTime? builderDay;
+ String builderName = '';
+
+ final calendarBuilders = CalendarBuilders(
+ prioritizedBuilder: (context, day, focusedDay) {
+ builderName = 'prioritizedBuilder';
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ disabledBuilder: (context, day, focusedDay) {
+ builderName = 'disabledBuilder';
+ builderDay = day;
+ return Text('${day.day}');
+ },
+ );
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ expect(builderDay, isNull);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ calendarBuilders: calendarBuilders,
+ isDisabled: true,
+ ),
+ );
+
+ expect(builderDay, cellDay);
+ expect(builderName, 'prioritizedBuilder');
+ },
+ );
+ });
+
+ group('CalendarBuilders Locale test:', () {
+ testWidgets('en locale with default dayTextFormatter', (tester) async {
+ const locale = 'en';
+ initializeDateFormatting(locale);
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ locale: locale,
+ ),
+ );
+
+ final dayFinder = find.text('${cellDay.day}');
+ expect(dayFinder, findsOneWidget);
+ });
+
+ testWidgets('en locale with custom dayTextFormatter', (tester) async {
+ const locale = 'en';
+ initializeDateFormatting(locale);
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ locale: locale,
+ calendarStyle: CalendarStyle(
+ dayTextFormatter: (date, locale) =>
+ DateFormat.d(locale).format(date),
+ ),
+ ),
+ );
+
+ final dayFinder = find.text(DateFormat.d(locale).format(cellDay));
+ expect(dayFinder, findsOneWidget);
+ });
+
+ testWidgets('ar locale with default dayTextFormatter', (tester) async {
+ const locale = 'ar';
+ initializeDateFormatting(locale);
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ locale: locale,
+ ),
+ );
+
+ final dayFinder = find.text('${cellDay.day}');
+ expect(dayFinder, findsOneWidget);
+ });
+
+ testWidgets('ar locale with custom dayTextFormatter', (tester) async {
+ const locale = 'ar';
+ initializeDateFormatting(locale);
+
+ final cellDay = DateTime.utc(2021, 7, 15);
+ await tester.pumpWidget(
+ setupTestWidget(
+ cellDay,
+ locale: locale,
+ calendarStyle: CalendarStyle(
+ dayTextFormatter: (date, locale) =>
+ DateFormat.d(locale).format(date),
+ ),
+ ),
+ );
+
+ final dayFinder = find.text(DateFormat.d(locale).format(cellDay));
+ expect(dayFinder, findsOneWidget);
+ });
+ });
+}
diff --git a/test/common.dart b/test/common.dart
new file mode 100644
index 00000000..7e869196
--- /dev/null
+++ b/test/common.dart
@@ -0,0 +1,15 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/foundation.dart';
+import 'package:table_calendar/src/shared/utils.dart';
+
+ValueKey dateToKey(DateTime date, {String prefix = ''}) {
+ return ValueKey('$prefix${date.year}-${date.month}-${date.day}');
+}
+
+const calendarFormatMap = {
+ CalendarFormat.month: 'Month',
+ CalendarFormat.twoWeeks: 'Two weeks',
+ CalendarFormat.week: 'week',
+};
diff --git a/test/custom_icon_button_test.dart b/test/custom_icon_button_test.dart
new file mode 100644
index 00000000..de53539b
--- /dev/null
+++ b/test/custom_icon_button_test.dart
@@ -0,0 +1,41 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:table_calendar/src/widgets/custom_icon_button.dart';
+
+Widget setupTestWidget(Widget child) {
+ return Directionality(
+ textDirection: TextDirection.ltr,
+ child: Material(child: child),
+ );
+}
+
+void main() {
+ testWidgets(
+ 'onTap gets called when CustomIconButton is tapped',
+ (tester) async {
+ bool buttonTapped = false;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ CustomIconButton(
+ icon: const Icon(Icons.chevron_left),
+ onTap: () {
+ buttonTapped = true;
+ },
+ ),
+ ),
+ );
+
+ final button = find.byType(CustomIconButton);
+ expect(button, findsOneWidget);
+ expect(buttonTapped, false);
+
+ await tester.tap(button);
+ await tester.pumpAndSettle();
+ expect(buttonTapped, true);
+ },
+ );
+}
diff --git a/test/format_button_test.dart b/test/format_button_test.dart
new file mode 100644
index 00000000..61c4bdeb
--- /dev/null
+++ b/test/format_button_test.dart
@@ -0,0 +1,183 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:table_calendar/src/widgets/format_button.dart';
+import 'package:table_calendar/table_calendar.dart';
+
+import 'common.dart';
+
+Widget setupTestWidget(Widget child) {
+ return Directionality(
+ textDirection: TextDirection.ltr,
+ child: Material(child: child),
+ );
+}
+
+void main() {
+ group('onTap callback tests:', () {
+ testWidgets(
+ 'Initial format month returns twoWeeks when tapped',
+ (tester) async {
+ const headerStyle = HeaderStyle();
+ CalendarFormat? calendarFormat;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ FormatButton(
+ availableCalendarFormats: calendarFormatMap,
+ calendarFormat: CalendarFormat.month,
+ decoration: headerStyle.formatButtonDecoration,
+ padding: headerStyle.formatButtonPadding,
+ textStyle: headerStyle.formatButtonTextStyle,
+ showsNextFormat: headerStyle.formatButtonShowsNext,
+ onTap: (format) {
+ calendarFormat = format;
+ },
+ ),
+ ),
+ );
+
+ expect(find.byType(FormatButton), findsOneWidget);
+ expect(calendarFormat, isNull);
+
+ await tester.tap(find.byType(FormatButton));
+ await tester.pumpAndSettle();
+ expect(calendarFormat, CalendarFormat.twoWeeks);
+ },
+ );
+
+ testWidgets(
+ 'Initial format twoWeeks returns week when tapped',
+ (tester) async {
+ const headerStyle = HeaderStyle();
+ CalendarFormat? calendarFormat;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ FormatButton(
+ availableCalendarFormats: calendarFormatMap,
+ calendarFormat: CalendarFormat.twoWeeks,
+ decoration: headerStyle.formatButtonDecoration,
+ padding: headerStyle.formatButtonPadding,
+ textStyle: headerStyle.formatButtonTextStyle,
+ showsNextFormat: headerStyle.formatButtonShowsNext,
+ onTap: (format) {
+ calendarFormat = format;
+ },
+ ),
+ ),
+ );
+
+ expect(find.byType(FormatButton), findsOneWidget);
+ expect(calendarFormat, isNull);
+
+ await tester.tap(find.byType(FormatButton));
+ await tester.pumpAndSettle();
+ expect(calendarFormat, CalendarFormat.week);
+ },
+ );
+
+ testWidgets(
+ 'Initial format week return month when tapped',
+ (tester) async {
+ const headerStyle = HeaderStyle();
+ CalendarFormat? calendarFormat;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ FormatButton(
+ availableCalendarFormats: calendarFormatMap,
+ calendarFormat: CalendarFormat.week,
+ decoration: headerStyle.formatButtonDecoration,
+ padding: headerStyle.formatButtonPadding,
+ textStyle: headerStyle.formatButtonTextStyle,
+ showsNextFormat: headerStyle.formatButtonShowsNext,
+ onTap: (format) {
+ calendarFormat = format;
+ },
+ ),
+ ),
+ );
+
+ expect(find.byType(FormatButton), findsOneWidget);
+ expect(calendarFormat, isNull);
+
+ await tester.tap(find.byType(FormatButton));
+ await tester.pumpAndSettle();
+ expect(calendarFormat, CalendarFormat.month);
+ },
+ );
+ });
+
+ group('showsNextFormat tests:', () {
+ testWidgets(
+ 'true - display next calendar format',
+ (tester) async {
+ const headerStyle = HeaderStyle();
+
+ const currentFormatIndex = 0;
+ final currentFormat =
+ calendarFormatMap.keys.elementAt(currentFormatIndex);
+ final currentFormatText =
+ calendarFormatMap.values.elementAt(currentFormatIndex);
+
+ const nextFormatIndex = 1;
+ final nextFormatText =
+ calendarFormatMap.values.elementAt(nextFormatIndex);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ FormatButton(
+ availableCalendarFormats: calendarFormatMap,
+ calendarFormat: currentFormat,
+ decoration: headerStyle.formatButtonDecoration,
+ padding: headerStyle.formatButtonPadding,
+ textStyle: headerStyle.formatButtonTextStyle,
+ showsNextFormat: headerStyle.formatButtonShowsNext,
+ onTap: (format) {},
+ ),
+ ),
+ );
+
+ expect(find.byType(FormatButton), findsOneWidget);
+ expect(currentFormatText, isNotNull);
+ expect(find.text(currentFormatText), findsNothing);
+ expect(nextFormatText, isNotNull);
+ expect(find.text(nextFormatText), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'false - display current calendar format',
+ (tester) async {
+ const headerStyle = HeaderStyle(formatButtonShowsNext: false);
+
+ const currentFormatIndex = 0;
+ final currentFormat =
+ calendarFormatMap.keys.elementAt(currentFormatIndex);
+ final currentFormatText =
+ calendarFormatMap.values.elementAt(currentFormatIndex);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ FormatButton(
+ availableCalendarFormats: calendarFormatMap,
+ calendarFormat: currentFormat,
+ decoration: headerStyle.formatButtonDecoration,
+ padding: headerStyle.formatButtonPadding,
+ textStyle: headerStyle.formatButtonTextStyle,
+ showsNextFormat: headerStyle.formatButtonShowsNext,
+ onTap: (format) {},
+ ),
+ ),
+ );
+
+ expect(find.byType(FormatButton), findsOneWidget);
+ expect(currentFormatText, isNotNull);
+ expect(find.text(currentFormatText), findsOneWidget);
+ },
+ );
+ });
+}
diff --git a/test/table_calendar_base_test.dart b/test/table_calendar_base_test.dart
new file mode 100644
index 00000000..c0fddf66
--- /dev/null
+++ b/test/table_calendar_base_test.dart
@@ -0,0 +1,378 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:simple_gesture_detector/simple_gesture_detector.dart';
+import 'package:table_calendar/table_calendar.dart';
+
+import 'common.dart';
+
+Widget setupTestWidget(Widget child) {
+ return Directionality(
+ textDirection: TextDirection.ltr,
+ child: child,
+ );
+}
+
+void main() {
+ group('Correct days are displayed for given focusedDay when:', () {
+ testWidgets(
+ 'in month format, starting day is Sunday',
+ (tester) async {
+ final focusedDay = DateTime.utc(2021, 7, 15);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendarBase(
+ firstDay: DateTime.utc(2021, 5, 15),
+ lastDay: DateTime.utc(2021, 8, 18),
+ focusedDay: focusedDay,
+ dayBuilder: (context, day, focusedDay) {
+ return Text(
+ '${day.day}',
+ key: dateToKey(day),
+ );
+ },
+ rowHeight: 52,
+ dowVisible: false,
+ ),
+ ),
+ );
+
+ final firstVisibleDay = DateTime.utc(2021, 6, 27);
+ final lastVisibleDay = DateTime.utc(2021, 7, 31);
+
+ final focusedDayKey = dateToKey(focusedDay);
+ final firstVisibleDayKey = dateToKey(firstVisibleDay);
+ final lastVisibleDayKey = dateToKey(lastVisibleDay);
+
+ final startOOBKey =
+ dateToKey(firstVisibleDay.subtract(const Duration(days: 1)));
+ final endOOBKey =
+ dateToKey(lastVisibleDay.add(const Duration(days: 1)));
+
+ expect(find.byKey(focusedDayKey), findsOneWidget);
+ expect(find.byKey(firstVisibleDayKey), findsOneWidget);
+ expect(find.byKey(lastVisibleDayKey), findsOneWidget);
+
+ expect(find.byKey(startOOBKey), findsNothing);
+ expect(find.byKey(endOOBKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'in two weeks format, starting day is Sunday',
+ (tester) async {
+ final focusedDay = DateTime.utc(2021, 7, 15);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendarBase(
+ firstDay: DateTime.utc(2021, 5, 15),
+ lastDay: DateTime.utc(2021, 8, 18),
+ focusedDay: focusedDay,
+ dayBuilder: (context, day, focusedDay) {
+ return Text(
+ '${day.day}',
+ key: dateToKey(day),
+ );
+ },
+ rowHeight: 52,
+ dowVisible: false,
+ calendarFormat: CalendarFormat.twoWeeks,
+ ),
+ ),
+ );
+
+ final firstVisibleDay = DateTime.utc(2021, 7, 4);
+ final lastVisibleDay = DateTime.utc(2021, 7, 17);
+
+ final focusedDayKey = dateToKey(focusedDay);
+ final firstVisibleDayKey = dateToKey(firstVisibleDay);
+ final lastVisibleDayKey = dateToKey(lastVisibleDay);
+
+ final startOOBKey =
+ dateToKey(firstVisibleDay.subtract(const Duration(days: 1)));
+ final endOOBKey =
+ dateToKey(lastVisibleDay.add(const Duration(days: 1)));
+
+ expect(find.byKey(focusedDayKey), findsOneWidget);
+ expect(find.byKey(firstVisibleDayKey), findsOneWidget);
+ expect(find.byKey(lastVisibleDayKey), findsOneWidget);
+
+ expect(find.byKey(startOOBKey), findsNothing);
+ expect(find.byKey(endOOBKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'in week format, starting day is Sunday',
+ (tester) async {
+ final focusedDay = DateTime.utc(2021, 7, 15);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendarBase(
+ firstDay: DateTime.utc(2021, 5, 15),
+ lastDay: DateTime.utc(2021, 8, 18),
+ focusedDay: focusedDay,
+ dayBuilder: (context, day, focusedDay) {
+ return Text(
+ '${day.day}',
+ key: dateToKey(day),
+ );
+ },
+ rowHeight: 52,
+ dowVisible: false,
+ calendarFormat: CalendarFormat.week,
+ ),
+ ),
+ );
+
+ final firstVisibleDay = DateTime.utc(2021, 7, 11);
+ final lastVisibleDay = DateTime.utc(2021, 7, 17);
+
+ final focusedDayKey = dateToKey(focusedDay);
+ final firstVisibleDayKey = dateToKey(firstVisibleDay);
+ final lastVisibleDayKey = dateToKey(lastVisibleDay);
+
+ final startOOBKey =
+ dateToKey(firstVisibleDay.subtract(const Duration(days: 1)));
+ final endOOBKey =
+ dateToKey(lastVisibleDay.add(const Duration(days: 1)));
+
+ expect(find.byKey(focusedDayKey), findsOneWidget);
+ expect(find.byKey(firstVisibleDayKey), findsOneWidget);
+ expect(find.byKey(lastVisibleDayKey), findsOneWidget);
+
+ expect(find.byKey(startOOBKey), findsNothing);
+ expect(find.byKey(endOOBKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'in month format, starting day is Monday',
+ (tester) async {
+ final focusedDay = DateTime.utc(2021, 7, 15);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendarBase(
+ firstDay: DateTime.utc(2021, 5, 15),
+ lastDay: DateTime.utc(2021, 8, 18),
+ focusedDay: focusedDay,
+ dayBuilder: (context, day, focusedDay) {
+ return Text(
+ '${day.day}',
+ key: dateToKey(day),
+ );
+ },
+ rowHeight: 52,
+ dowVisible: false,
+ startingDayOfWeek: StartingDayOfWeek.monday,
+ ),
+ ),
+ );
+
+ final firstVisibleDay = DateTime.utc(2021, 6, 28);
+ final lastVisibleDay = DateTime.utc(2021, 8);
+
+ final focusedDayKey = dateToKey(focusedDay);
+ final firstVisibleDayKey = dateToKey(firstVisibleDay);
+ final lastVisibleDayKey = dateToKey(lastVisibleDay);
+
+ final startOOBKey =
+ dateToKey(firstVisibleDay.subtract(const Duration(days: 1)));
+ final endOOBKey =
+ dateToKey(lastVisibleDay.add(const Duration(days: 1)));
+
+ expect(find.byKey(focusedDayKey), findsOneWidget);
+ expect(find.byKey(firstVisibleDayKey), findsOneWidget);
+ expect(find.byKey(lastVisibleDayKey), findsOneWidget);
+
+ expect(find.byKey(startOOBKey), findsNothing);
+ expect(find.byKey(endOOBKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'in two weeks format, starting day is Monday',
+ (tester) async {
+ final focusedDay = DateTime.utc(2021, 7, 15);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendarBase(
+ firstDay: DateTime.utc(2021, 5, 15),
+ lastDay: DateTime.utc(2021, 8, 18),
+ focusedDay: focusedDay,
+ dayBuilder: (context, day, focusedDay) {
+ return Text(
+ '${day.day}',
+ key: dateToKey(day),
+ );
+ },
+ rowHeight: 52,
+ dowVisible: false,
+ calendarFormat: CalendarFormat.twoWeeks,
+ startingDayOfWeek: StartingDayOfWeek.monday,
+ ),
+ ),
+ );
+
+ final firstVisibleDay = DateTime.utc(2021, 7, 5);
+ final lastVisibleDay = DateTime.utc(2021, 7, 18);
+
+ final focusedDayKey = dateToKey(focusedDay);
+ final firstVisibleDayKey = dateToKey(firstVisibleDay);
+ final lastVisibleDayKey = dateToKey(lastVisibleDay);
+
+ final startOOBKey =
+ dateToKey(firstVisibleDay.subtract(const Duration(days: 1)));
+ final endOOBKey =
+ dateToKey(lastVisibleDay.add(const Duration(days: 1)));
+
+ expect(find.byKey(focusedDayKey), findsOneWidget);
+ expect(find.byKey(firstVisibleDayKey), findsOneWidget);
+ expect(find.byKey(lastVisibleDayKey), findsOneWidget);
+
+ expect(find.byKey(startOOBKey), findsNothing);
+ expect(find.byKey(endOOBKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'in week format, starting day is Monday',
+ (tester) async {
+ final focusedDay = DateTime.utc(2021, 7, 15);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendarBase(
+ firstDay: DateTime.utc(2021, 5, 15),
+ lastDay: DateTime.utc(2021, 8, 18),
+ focusedDay: focusedDay,
+ dayBuilder: (context, day, focusedDay) {
+ return Text(
+ '${day.day}',
+ key: dateToKey(day),
+ );
+ },
+ rowHeight: 52,
+ dowVisible: false,
+ calendarFormat: CalendarFormat.week,
+ startingDayOfWeek: StartingDayOfWeek.monday,
+ ),
+ ),
+ );
+
+ final firstVisibleDay = DateTime.utc(2021, 7, 12);
+ final lastVisibleDay = DateTime.utc(2021, 7, 18);
+
+ final focusedDayKey = dateToKey(focusedDay);
+ final firstVisibleDayKey = dateToKey(firstVisibleDay);
+ final lastVisibleDayKey = dateToKey(lastVisibleDay);
+
+ final startOOBKey =
+ dateToKey(firstVisibleDay.subtract(const Duration(days: 1)));
+ final endOOBKey =
+ dateToKey(lastVisibleDay.add(const Duration(days: 1)));
+
+ expect(find.byKey(focusedDayKey), findsOneWidget);
+ expect(find.byKey(firstVisibleDayKey), findsOneWidget);
+ expect(find.byKey(lastVisibleDayKey), findsOneWidget);
+
+ expect(find.byKey(startOOBKey), findsNothing);
+ expect(find.byKey(endOOBKey), findsNothing);
+ },
+ );
+ });
+
+ testWidgets(
+ 'Callbacks return expected values',
+ (tester) async {
+ DateTime focusedDay = DateTime.utc(2021, 7, 15);
+ final nextMonth = focusedDay.add(const Duration(days: 31)).month;
+
+ bool calendarCreatedFlag = false;
+ SwipeDirection? verticalSwipeDirection;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendarBase(
+ firstDay: DateTime.utc(2021, 5, 15),
+ lastDay: DateTime.utc(2021, 8, 18),
+ focusedDay: focusedDay,
+ dayBuilder: (context, day, focusedDay) {
+ return Text(
+ '${day.day}',
+ key: dateToKey(day),
+ );
+ },
+ onCalendarCreated: (pageController) {
+ calendarCreatedFlag = true;
+ },
+ onPageChanged: (focusedDay2) {
+ focusedDay = focusedDay2;
+ },
+ onVerticalSwipe: (direction) {
+ verticalSwipeDirection = direction;
+ },
+ rowHeight: 52,
+ dowVisible: false,
+ ),
+ ),
+ );
+
+ expect(calendarCreatedFlag, true);
+
+ // Swipe left
+ await tester.drag(
+ find.byKey(dateToKey(focusedDay)),
+ const Offset(-500, 0),
+ );
+ await tester.pumpAndSettle();
+ expect(focusedDay.month, nextMonth);
+
+ // Swipe up
+ await tester.drag(
+ find.byKey(dateToKey(focusedDay)),
+ const Offset(0, -500),
+ );
+ await tester.pumpAndSettle();
+ expect(verticalSwipeDirection, SwipeDirection.up);
+ },
+ );
+
+ testWidgets(
+ 'Throw AssertionError when TableCalendarBase is built with dowVisible and dowBuilder, but dowHeight is absent',
+ (tester) async {
+ expect(
+ () async {
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendarBase(
+ firstDay: DateTime.utc(2021, 5, 15),
+ lastDay: DateTime.utc(2021, 8, 18),
+ focusedDay: DateTime.utc(2021, 7, 15),
+ dayBuilder: (context, day, focusedDay) {
+ return Text(
+ '${day.day}',
+ key: dateToKey(day),
+ );
+ },
+ rowHeight: 52,
+ dowBuilder: (context, day) {
+ return Text('${day.weekday}');
+ },
+ ),
+ ),
+ );
+ },
+ throwsAssertionError,
+ );
+ },
+ );
+}
diff --git a/test/table_calendar_test.dart b/test/table_calendar_test.dart
new file mode 100644
index 00000000..2889b947
--- /dev/null
+++ b/test/table_calendar_test.dart
@@ -0,0 +1,1464 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:intl/intl.dart' as intl;
+import 'package:table_calendar/src/widgets/calendar_header.dart';
+import 'package:table_calendar/src/widgets/cell_content.dart';
+import 'package:table_calendar/src/widgets/custom_icon_button.dart';
+import 'package:table_calendar/table_calendar.dart';
+
+import 'common.dart';
+
+final initialFocusedDay = DateTime.utc(2021, 7, 15);
+final today = initialFocusedDay;
+final firstDay = DateTime.utc(2021, 5, 15);
+final lastDay = DateTime.utc(2021, 9, 18);
+
+Widget setupTestWidget(Widget child) {
+ return Directionality(
+ textDirection: TextDirection.ltr,
+ child: Material(child: child),
+ );
+}
+
+Widget createTableCalendar({
+ DateTime? focusedDay,
+ CalendarFormat calendarFormat = CalendarFormat.month,
+ Function(DateTime)? onPageChanged,
+ bool sixWeekMonthsEnforced = false,
+}) {
+ return setupTestWidget(
+ TableCalendar(
+ focusedDay: focusedDay ?? initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ calendarFormat: calendarFormat,
+ onPageChanged: onPageChanged,
+ sixWeekMonthsEnforced: sixWeekMonthsEnforced,
+ ),
+ );
+}
+
+ValueKey cellContentKey(DateTime date) {
+ return dateToKey(date, prefix: 'CellContent-');
+}
+
+void main() {
+ group('TableCalendar correctly displays:', () {
+ testWidgets(
+ 'visible day cells for given focusedDay',
+ (tester) async {
+ await tester.pumpWidget(createTableCalendar());
+
+ final firstVisibleDay = DateTime.utc(2021, 6, 27);
+ final lastVisibleDay = DateTime.utc(2021, 7, 31);
+
+ final focusedDayKey = cellContentKey(initialFocusedDay);
+ final firstVisibleDayKey = cellContentKey(firstVisibleDay);
+ final lastVisibleDayKey = cellContentKey(lastVisibleDay);
+
+ final startOOBKey =
+ cellContentKey(firstVisibleDay.subtract(const Duration(days: 1)));
+ final endOOBKey =
+ cellContentKey(lastVisibleDay.add(const Duration(days: 1)));
+
+ expect(find.byKey(focusedDayKey), findsOneWidget);
+ expect(find.byKey(firstVisibleDayKey), findsOneWidget);
+ expect(find.byKey(lastVisibleDayKey), findsOneWidget);
+
+ expect(find.byKey(startOOBKey), findsNothing);
+ expect(find.byKey(endOOBKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'visible day cells after swipe right when in week format',
+ (tester) async {
+ DateTime? updatedFocusedDay;
+
+ await tester.pumpWidget(
+ createTableCalendar(
+ calendarFormat: CalendarFormat.week,
+ onPageChanged: (focusedDay) {
+ updatedFocusedDay = focusedDay;
+ },
+ ),
+ );
+
+ await tester.drag(
+ find.byType(CellContent).first,
+ const Offset(500, 0),
+ );
+ await tester.pumpAndSettle();
+
+ expect(updatedFocusedDay, isNotNull);
+
+ final firstVisibleDay = DateTime.utc(2021, 7, 4);
+ final lastVisibleDay = DateTime.utc(2021, 7, 10);
+
+ final focusedDayKey = cellContentKey(updatedFocusedDay!);
+ final firstVisibleDayKey = cellContentKey(firstVisibleDay);
+ final lastVisibleDayKey = cellContentKey(lastVisibleDay);
+
+ final startOOBKey =
+ cellContentKey(firstVisibleDay.subtract(const Duration(days: 1)));
+ final endOOBKey =
+ cellContentKey(lastVisibleDay.add(const Duration(days: 1)));
+
+ expect(find.byKey(focusedDayKey), findsOneWidget);
+ expect(find.byKey(firstVisibleDayKey), findsOneWidget);
+ expect(find.byKey(lastVisibleDayKey), findsOneWidget);
+
+ expect(find.byKey(startOOBKey), findsNothing);
+ expect(find.byKey(endOOBKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'visible day cells after swipe left when in week format',
+ (tester) async {
+ DateTime? updatedFocusedDay;
+
+ await tester.pumpWidget(
+ createTableCalendar(
+ calendarFormat: CalendarFormat.week,
+ onPageChanged: (focusedDay) {
+ updatedFocusedDay = focusedDay;
+ },
+ ),
+ );
+
+ await tester.drag(
+ find.byType(CellContent).first,
+ const Offset(-500, 0),
+ );
+ await tester.pumpAndSettle();
+
+ expect(updatedFocusedDay, isNotNull);
+
+ final firstVisibleDay = DateTime.utc(2021, 7, 18);
+ final lastVisibleDay = DateTime.utc(2021, 7, 24);
+
+ final focusedDayKey = cellContentKey(updatedFocusedDay!);
+ final firstVisibleDayKey = cellContentKey(firstVisibleDay);
+ final lastVisibleDayKey = cellContentKey(lastVisibleDay);
+
+ final startOOBKey =
+ cellContentKey(firstVisibleDay.subtract(const Duration(days: 1)));
+ final endOOBKey =
+ cellContentKey(lastVisibleDay.add(const Duration(days: 1)));
+
+ expect(find.byKey(focusedDayKey), findsOneWidget);
+ expect(find.byKey(firstVisibleDayKey), findsOneWidget);
+ expect(find.byKey(lastVisibleDayKey), findsOneWidget);
+
+ expect(find.byKey(startOOBKey), findsNothing);
+ expect(find.byKey(endOOBKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'visible day cells after swipe right when in two weeks format',
+ (tester) async {
+ DateTime? updatedFocusedDay;
+
+ await tester.pumpWidget(
+ createTableCalendar(
+ calendarFormat: CalendarFormat.twoWeeks,
+ onPageChanged: (focusedDay) {
+ updatedFocusedDay = focusedDay;
+ },
+ ),
+ );
+
+ await tester.drag(
+ find.byType(CellContent).first,
+ const Offset(500, 0),
+ );
+ await tester.pumpAndSettle();
+
+ expect(updatedFocusedDay, isNotNull);
+
+ final firstVisibleDay = DateTime.utc(2021, 6, 20);
+ final lastVisibleDay = DateTime.utc(2021, 7, 3);
+
+ final focusedDayKey = cellContentKey(updatedFocusedDay!);
+ final firstVisibleDayKey = cellContentKey(firstVisibleDay);
+ final lastVisibleDayKey = cellContentKey(lastVisibleDay);
+
+ final startOOBKey =
+ cellContentKey(firstVisibleDay.subtract(const Duration(days: 1)));
+ final endOOBKey =
+ cellContentKey(lastVisibleDay.add(const Duration(days: 1)));
+
+ expect(find.byKey(focusedDayKey), findsOneWidget);
+ expect(find.byKey(firstVisibleDayKey), findsOneWidget);
+ expect(find.byKey(lastVisibleDayKey), findsOneWidget);
+
+ expect(find.byKey(startOOBKey), findsNothing);
+ expect(find.byKey(endOOBKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'visible day cells after swipe left when in two weeks format',
+ (tester) async {
+ DateTime? updatedFocusedDay;
+
+ await tester.pumpWidget(
+ createTableCalendar(
+ calendarFormat: CalendarFormat.twoWeeks,
+ onPageChanged: (focusedDay) {
+ updatedFocusedDay = focusedDay;
+ },
+ ),
+ );
+
+ await tester.drag(
+ find.byType(CellContent).first,
+ const Offset(-500, 0),
+ );
+ await tester.pumpAndSettle();
+
+ expect(updatedFocusedDay, isNotNull);
+
+ final firstVisibleDay = DateTime.utc(2021, 7, 18);
+ final lastVisibleDay = DateTime.utc(2021, 7, 31);
+
+ final focusedDayKey = cellContentKey(updatedFocusedDay!);
+ final firstVisibleDayKey = cellContentKey(firstVisibleDay);
+ final lastVisibleDayKey = cellContentKey(lastVisibleDay);
+
+ final startOOBKey =
+ cellContentKey(firstVisibleDay.subtract(const Duration(days: 1)));
+ final endOOBKey =
+ cellContentKey(lastVisibleDay.add(const Duration(days: 1)));
+
+ expect(find.byKey(focusedDayKey), findsOneWidget);
+ expect(find.byKey(firstVisibleDayKey), findsOneWidget);
+ expect(find.byKey(lastVisibleDayKey), findsOneWidget);
+
+ expect(find.byKey(startOOBKey), findsNothing);
+ expect(find.byKey(endOOBKey), findsNothing);
+ },
+ );
+
+ testWidgets(
+ '7 day cells in week format',
+ (tester) async {
+ await tester.pumpWidget(
+ createTableCalendar(
+ calendarFormat: CalendarFormat.week,
+ ),
+ );
+
+ final dayCells = tester.widgetList(find.byType(CellContent));
+ expect(dayCells.length, 7);
+ },
+ );
+
+ testWidgets(
+ '14 day cells in two weeks format',
+ (tester) async {
+ await tester.pumpWidget(
+ createTableCalendar(
+ calendarFormat: CalendarFormat.twoWeeks,
+ ),
+ );
+
+ final dayCells = tester.widgetList(find.byType(CellContent));
+ expect(dayCells.length, 14);
+ },
+ );
+
+ testWidgets(
+ '35 day cells in month format for July 2021',
+ (tester) async {
+ await tester.pumpWidget(
+ createTableCalendar(),
+ );
+
+ final dayCells = tester.widgetList(find.byType(CellContent));
+ expect(dayCells.length, 35);
+ },
+ );
+
+ testWidgets(
+ '42 day cells in month format for July 2021, when sixWeekMonthsEnforced is set to true',
+ (tester) async {
+ await tester.pumpWidget(
+ createTableCalendar(
+ sixWeekMonthsEnforced: true,
+ ),
+ );
+
+ final dayCells = tester.widgetList(find.byType(CellContent));
+ expect(dayCells.length, 42);
+ },
+ );
+
+ testWidgets(
+ 'CalendarHeader with updated month and year when focusedDay is changed',
+ (tester) async {
+ await tester.pumpWidget(createTableCalendar());
+
+ String headerText = intl.DateFormat.yMMMM().format(initialFocusedDay);
+ expect(find.byType(CalendarHeader), findsOneWidget);
+ expect(find.text(headerText), findsOneWidget);
+
+ final updatedFocusedDay = DateTime.utc(2021, 8, 4);
+
+ await tester.pumpWidget(
+ createTableCalendar(focusedDay: updatedFocusedDay),
+ );
+
+ headerText = intl.DateFormat.yMMMM().format(updatedFocusedDay);
+ expect(find.byType(CalendarHeader), findsOneWidget);
+ expect(find.text(headerText), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'CalendarHeader with updated month and year when TableCalendar is swiped left',
+ (tester) async {
+ DateTime? updatedFocusedDay;
+
+ await tester.pumpWidget(
+ createTableCalendar(
+ onPageChanged: (focusedDay) {
+ updatedFocusedDay = focusedDay;
+ },
+ ),
+ );
+
+ String headerText = intl.DateFormat.yMMMM().format(initialFocusedDay);
+ expect(find.byType(CalendarHeader), findsOneWidget);
+ expect(find.text(headerText), findsOneWidget);
+
+ await tester.drag(
+ find.byType(CellContent).first,
+ const Offset(-500, 0),
+ );
+ await tester.pumpAndSettle();
+
+ expect(updatedFocusedDay, isNotNull);
+ expect(updatedFocusedDay!.month, initialFocusedDay.month + 1);
+
+ headerText = intl.DateFormat.yMMMM().format(updatedFocusedDay!);
+ expect(find.byType(CalendarHeader), findsOneWidget);
+ expect(find.text(headerText), findsOneWidget);
+
+ updatedFocusedDay = null;
+
+ await tester.drag(
+ find.byType(CellContent).first,
+ const Offset(-500, 0),
+ );
+ await tester.pumpAndSettle();
+
+ expect(updatedFocusedDay, isNotNull);
+ expect(updatedFocusedDay!.month, initialFocusedDay.month + 2);
+
+ headerText = intl.DateFormat.yMMMM().format(updatedFocusedDay!);
+ expect(find.byType(CalendarHeader), findsOneWidget);
+ expect(find.text(headerText), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'CalendarHeader with updated month and year when TableCalendar is swiped right',
+ (tester) async {
+ DateTime? updatedFocusedDay;
+
+ await tester.pumpWidget(
+ createTableCalendar(
+ onPageChanged: (focusedDay) {
+ updatedFocusedDay = focusedDay;
+ },
+ ),
+ );
+
+ String headerText = intl.DateFormat.yMMMM().format(initialFocusedDay);
+ expect(find.byType(CalendarHeader), findsOneWidget);
+ expect(find.text(headerText), findsOneWidget);
+
+ await tester.drag(
+ find.byType(CellContent).first,
+ const Offset(500, 0),
+ );
+ await tester.pumpAndSettle();
+
+ expect(updatedFocusedDay, isNotNull);
+ expect(updatedFocusedDay!.month, initialFocusedDay.month - 1);
+
+ headerText = intl.DateFormat.yMMMM().format(updatedFocusedDay!);
+ expect(find.byType(CalendarHeader), findsOneWidget);
+ expect(find.text(headerText), findsOneWidget);
+
+ updatedFocusedDay = null;
+
+ await tester.drag(
+ find.byType(CellContent).first,
+ const Offset(500, 0),
+ );
+ await tester.pumpAndSettle();
+
+ expect(updatedFocusedDay, isNotNull);
+ expect(updatedFocusedDay!.month, initialFocusedDay.month - 2);
+
+ headerText = intl.DateFormat.yMMMM().format(updatedFocusedDay!);
+ expect(find.byType(CalendarHeader), findsOneWidget);
+ expect(find.text(headerText), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ '3 event markers are visible when 3 events are assigned to a given day',
+ (tester) async {
+ final eventDay = DateTime.utc(2021, 7, 20);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ eventLoader: (day) {
+ if (day.day == eventDay.day && day.month == eventDay.month) {
+ return ['Event 1', 'Event 2', 'Event 3'];
+ }
+
+ return [];
+ },
+ ),
+ ),
+ );
+
+ final eventDayKey = cellContentKey(eventDay);
+ final eventDayCellContent = find.byKey(eventDayKey);
+
+ final eventDayStack = find.ancestor(
+ of: eventDayCellContent,
+ matching: find.byType(Stack),
+ );
+
+ final eventMarkers = tester.widgetList(
+ find.descendant(
+ of: eventDayStack,
+ matching: find.byWidgetPredicate(
+ (Widget marker) => marker is Container && marker.child == null,
+ ),
+ ),
+ );
+
+ expect(eventMarkers.length, 3);
+ },
+ );
+
+ testWidgets(
+ 'Event loader is called for disabled days when loadEventForDisabledDays is set to true',
+ (tester) async {
+ final eventDay = DateTime.utc(2021, 7, 20);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ loadEventsForDisabledDays: true,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ eventLoader: (day) {
+ if (day.day == eventDay.day && day.month == eventDay.month) {
+ return ['Event 1', 'Event 2', 'Event 3'];
+ }
+
+ return [];
+ },
+ enabledDayPredicate: (day) => false,
+ ),
+ ),
+ );
+
+ final eventDayKey = cellContentKey(eventDay);
+ final eventDayCellContent = find.byKey(eventDayKey);
+
+ final eventDayStack = find.ancestor(
+ of: eventDayCellContent,
+ matching: find.byType(Stack),
+ );
+
+ final eventMarkers = tester.widgetList(
+ find.descendant(
+ of: eventDayStack,
+ matching: find.byWidgetPredicate(
+ (Widget marker) => marker is Container && marker.child == null,
+ ),
+ ),
+ );
+
+ expect(eventMarkers.length, 3);
+ },
+ );
+
+ testWidgets(
+ 'currentDay correctly marks given day as today',
+ (tester) async {
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ ),
+ ),
+ );
+
+ final currentDayKey = cellContentKey(today);
+ final currentDayCellContent =
+ tester.widget(find.byKey(currentDayKey)) as CellContent;
+
+ expect(currentDayCellContent.isToday, true);
+ },
+ );
+
+ testWidgets(
+ 'if currentDay is absent, DateTime.now() is marked as today',
+ (tester) async {
+ final now = DateTime.now();
+ final firstDay = DateTime.utc(now.year, now.month - 3, now.day);
+ final lastDay = DateTime.utc(now.year, now.month + 3, now.day);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: now,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ ),
+ ),
+ );
+
+ final currentDayKey = cellContentKey(now);
+ final currentDayCellContent =
+ tester.widget(find.byKey(currentDayKey)) as CellContent;
+
+ expect(currentDayCellContent.isToday, true);
+ },
+ );
+
+ testWidgets(
+ 'selectedDayPredicate correctly marks given day as selected',
+ (tester) async {
+ final selectedDay = DateTime.utc(2021, 7, 20);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ selectedDayPredicate: (day) {
+ return isSameDay(day, selectedDay);
+ },
+ ),
+ ),
+ );
+
+ final selectedDayKey = cellContentKey(selectedDay);
+ final selectedDayCellContent =
+ tester.widget(find.byKey(selectedDayKey)) as CellContent;
+
+ expect(selectedDayCellContent.isSelected, true);
+ },
+ );
+
+ testWidgets(
+ 'holidayPredicate correctly marks given day as holiday',
+ (tester) async {
+ final holiday = DateTime.utc(2021, 7, 20);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ holidayPredicate: (day) {
+ return isSameDay(day, holiday);
+ },
+ ),
+ ),
+ );
+
+ final holidayKey = cellContentKey(holiday);
+ final holidayCellContent =
+ tester.widget(find.byKey(holidayKey)) as CellContent;
+
+ expect(holidayCellContent.isHoliday, true);
+ },
+ );
+ });
+
+ group('CalendarHeader chevrons test:', () {
+ testWidgets(
+ 'tapping on a left chevron navigates to previous calendar page',
+ (tester) async {
+ await tester.pumpWidget(createTableCalendar());
+
+ expect(find.text('July 2021'), findsOneWidget);
+
+ final leftChevron = find.widgetWithIcon(
+ CustomIconButton,
+ Icons.chevron_left,
+ );
+
+ await tester.tap(leftChevron);
+ await tester.pumpAndSettle();
+
+ expect(find.text('June 2021'), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'tapping on a right chevron navigates to next calendar page',
+ (tester) async {
+ await tester.pumpWidget(createTableCalendar());
+
+ expect(find.text('July 2021'), findsOneWidget);
+
+ final rightChevron = find.widgetWithIcon(
+ CustomIconButton,
+ Icons.chevron_right,
+ );
+
+ await tester.tap(rightChevron);
+ await tester.pumpAndSettle();
+
+ expect(find.text('August 2021'), findsOneWidget);
+ },
+ );
+ });
+
+ group('Scrolling boundaries are set up properly:', () {
+ testWidgets('starting scroll boundary works correctly', (tester) async {
+ final focusedDay = DateTime.utc(2021, 6, 15);
+
+ await tester.pumpWidget(createTableCalendar(focusedDay: focusedDay));
+
+ expect(find.byType(TableCalendar), findsOneWidget);
+ expect(find.text('June 2021'), findsOneWidget);
+
+ await tester.drag(find.byType(CellContent).first, const Offset(500, 0));
+ await tester.pumpAndSettle();
+ expect(find.text('May 2021'), findsOneWidget);
+
+ await tester.drag(find.byType(CellContent).first, const Offset(500, 0));
+ await tester.pumpAndSettle();
+ expect(find.text('May 2021'), findsOneWidget);
+ });
+
+ testWidgets('ending scroll boundary works correctly', (tester) async {
+ final focusedDay = DateTime.utc(2021, 8, 15);
+
+ await tester.pumpWidget(createTableCalendar(focusedDay: focusedDay));
+
+ expect(find.byType(TableCalendar), findsOneWidget);
+ expect(find.text('August 2021'), findsOneWidget);
+
+ await tester.drag(find.byType(CellContent).first, const Offset(-500, 0));
+ await tester.pumpAndSettle();
+ expect(find.text('September 2021'), findsOneWidget);
+
+ await tester.drag(find.byType(CellContent).first, const Offset(-500, 0));
+ await tester.pumpAndSettle();
+ expect(find.text('September 2021'), findsOneWidget);
+ });
+ });
+
+ group('onFormatChanged callback returns correct values:', () {
+ testWidgets('when initial format is month', (tester) async {
+ CalendarFormat calendarFormat = CalendarFormat.month;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: today,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ calendarFormat: calendarFormat,
+ onFormatChanged: (format) {
+ calendarFormat = format;
+ },
+ ),
+ ),
+ );
+
+ await tester.drag(find.byType(CellContent).first, const Offset(0, -500));
+ await tester.pumpAndSettle();
+ expect(calendarFormat, CalendarFormat.twoWeeks);
+
+ await tester.drag(find.byType(CellContent).first, const Offset(0, 500));
+ await tester.pumpAndSettle();
+ expect(calendarFormat, CalendarFormat.month);
+ });
+
+ testWidgets('when initial format is two weeks', (tester) async {
+ CalendarFormat calendarFormat = CalendarFormat.twoWeeks;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: today,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ calendarFormat: calendarFormat,
+ onFormatChanged: (format) {
+ calendarFormat = format;
+ },
+ ),
+ ),
+ );
+
+ await tester.drag(find.byType(CellContent).first, const Offset(0, -500));
+ await tester.pumpAndSettle();
+ expect(calendarFormat, CalendarFormat.week);
+
+ await tester.drag(find.byType(CellContent).first, const Offset(0, 500));
+ await tester.pumpAndSettle();
+ expect(calendarFormat, CalendarFormat.month);
+ });
+
+ testWidgets('when initial format is week', (tester) async {
+ CalendarFormat calendarFormat = CalendarFormat.week;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: today,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ calendarFormat: calendarFormat,
+ onFormatChanged: (format) {
+ calendarFormat = format;
+ },
+ ),
+ ),
+ );
+
+ await tester.drag(find.byType(CellContent).first, const Offset(0, -500));
+ await tester.pumpAndSettle();
+ expect(calendarFormat, CalendarFormat.week);
+
+ await tester.drag(find.byType(CellContent).first, const Offset(0, 500));
+ await tester.pumpAndSettle();
+ expect(calendarFormat, CalendarFormat.twoWeeks);
+ });
+ });
+
+ group('onDaySelected callback test:', () {
+ testWidgets(
+ 'selects correct day when tapped',
+ (tester) async {
+ DateTime? selectedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ onDaySelected: (selected, focused) {
+ selectedDay = selected;
+ },
+ ),
+ ),
+ );
+
+ expect(selectedDay, isNull);
+
+ final tappedDay = DateTime.utc(2021, 7, 18);
+ final tappedDayKey = cellContentKey(tappedDay);
+
+ await tester.tap(find.byKey(tappedDayKey));
+ await tester.pumpAndSettle();
+ expect(selectedDay, tappedDay);
+ },
+ );
+
+ testWidgets(
+ 'focuses correct day when tapped',
+ (tester) async {
+ DateTime? focusedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ onDaySelected: (selected, focused) {
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(focusedDay, isNull);
+
+ final tappedDay = DateTime.utc(2021, 7, 18);
+ final tappedDayKey = cellContentKey(tappedDay);
+
+ await tester.tap(find.byKey(tappedDayKey));
+ await tester.pumpAndSettle();
+ expect(focusedDay, tappedDay);
+ },
+ );
+
+ testWidgets(
+ 'properly selects and focuses on outside cell tap - previous month (when in month format)',
+ (tester) async {
+ DateTime? selectedDay;
+ DateTime? focusedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ onDaySelected: (selected, focused) {
+ selectedDay = selected;
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(selectedDay, isNull);
+ expect(focusedDay, isNull);
+
+ final tappedDay = DateTime.utc(2021, 6, 30);
+ final tappedDayKey = cellContentKey(tappedDay);
+
+ final expectedFocusedDay = DateTime.utc(2021, 7);
+
+ await tester.tap(find.byKey(tappedDayKey));
+ await tester.pumpAndSettle();
+ expect(selectedDay, tappedDay);
+ expect(focusedDay, expectedFocusedDay);
+ },
+ );
+
+ testWidgets(
+ 'properly selects and focuses on outside cell tap - next month (when in month format)',
+ (tester) async {
+ DateTime? selectedDay;
+ DateTime? focusedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: DateTime.utc(2021, 8, 16),
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: DateTime.utc(2021, 8, 16),
+ onDaySelected: (selected, focused) {
+ selectedDay = selected;
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(selectedDay, isNull);
+ expect(focusedDay, isNull);
+
+ final tappedDay = DateTime.utc(2021, 9);
+ final tappedDayKey = cellContentKey(tappedDay);
+
+ final expectedFocusedDay = DateTime.utc(2021, 8, 31);
+
+ await tester.tap(find.byKey(tappedDayKey));
+ await tester.pumpAndSettle();
+ expect(selectedDay, tappedDay);
+ expect(focusedDay, expectedFocusedDay);
+ },
+ );
+ });
+
+ group('onDayLongPressed callback test:', () {
+ testWidgets(
+ 'selects correct day when long pressed',
+ (tester) async {
+ DateTime? selectedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ onDayLongPressed: (selected, focused) {
+ selectedDay = selected;
+ },
+ ),
+ ),
+ );
+
+ expect(selectedDay, isNull);
+
+ final longPressedDay = DateTime.utc(2021, 7, 18);
+ final longPressedDayKey = cellContentKey(longPressedDay);
+
+ await tester.longPress(find.byKey(longPressedDayKey));
+ await tester.pumpAndSettle();
+ expect(selectedDay, longPressedDay);
+ },
+ );
+
+ testWidgets(
+ 'focuses correct day when long pressed',
+ (tester) async {
+ DateTime? focusedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ onDayLongPressed: (selected, focused) {
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(focusedDay, isNull);
+
+ final longPressedDay = DateTime.utc(2021, 7, 18);
+ final longPressedDayKey = cellContentKey(longPressedDay);
+
+ await tester.longPress(find.byKey(longPressedDayKey));
+ await tester.pumpAndSettle();
+ expect(focusedDay, longPressedDay);
+ },
+ );
+
+ testWidgets(
+ 'properly selects and focuses on outside cell long press - previous month (when in month format)',
+ (tester) async {
+ DateTime? selectedDay;
+ DateTime? focusedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ onDayLongPressed: (selected, focused) {
+ selectedDay = selected;
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(selectedDay, isNull);
+ expect(focusedDay, isNull);
+
+ final longPressedDay = DateTime.utc(2021, 6, 30);
+ final longPressedDayKey = cellContentKey(longPressedDay);
+
+ final expectedFocusedDay = DateTime.utc(2021, 7);
+
+ await tester.longPress(find.byKey(longPressedDayKey));
+ await tester.pumpAndSettle();
+ expect(selectedDay, longPressedDay);
+ expect(focusedDay, expectedFocusedDay);
+ },
+ );
+
+ testWidgets(
+ 'properly selects and focuses on outside cell long press - next month (when in month format)',
+ (tester) async {
+ DateTime? selectedDay;
+ DateTime? focusedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: DateTime.utc(2021, 8, 16),
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: DateTime.utc(2021, 8, 16),
+ onDayLongPressed: (selected, focused) {
+ selectedDay = selected;
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(selectedDay, isNull);
+ expect(focusedDay, isNull);
+
+ final longPressedDay = DateTime.utc(2021, 9);
+ final longPressedDayKey = cellContentKey(longPressedDay);
+
+ final expectedFocusedDay = DateTime.utc(2021, 8, 31);
+
+ await tester.longPress(find.byKey(longPressedDayKey));
+ await tester.pumpAndSettle();
+ expect(selectedDay, longPressedDay);
+ expect(focusedDay, expectedFocusedDay);
+ },
+ );
+ });
+
+ group('onRangeSelection callback test:', () {
+ testWidgets(
+ 'proper values are returned when second tapped day is after the first one',
+ (tester) async {
+ DateTime? rangeStart;
+ DateTime? rangeEnd;
+ DateTime? focusedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ rangeSelectionMode: RangeSelectionMode.enforced,
+ onRangeSelected: (start, end, focused) {
+ rangeStart = start;
+ rangeEnd = end;
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(rangeStart, isNull);
+ expect(rangeEnd, isNull);
+ expect(focusedDay, isNull);
+
+ final firstTappedDay = DateTime.utc(2021, 7, 8);
+ final secondTappedDay = DateTime.utc(2021, 7, 21);
+
+ final firstTappedDayKey = cellContentKey(firstTappedDay);
+ final secondTappedDayKey = cellContentKey(secondTappedDay);
+
+ final expectedFocusedDay = secondTappedDay;
+
+ await tester.tap(find.byKey(firstTappedDayKey));
+ await tester.pumpAndSettle();
+ await tester.tap(find.byKey(secondTappedDayKey));
+ await tester.pumpAndSettle();
+ expect(rangeStart, firstTappedDay);
+ expect(rangeEnd, secondTappedDay);
+ expect(focusedDay, expectedFocusedDay);
+ },
+ );
+
+ testWidgets(
+ 'proper values are returned when second tapped day is before the first one',
+ (tester) async {
+ DateTime? rangeStart;
+ DateTime? rangeEnd;
+ DateTime? focusedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ rangeSelectionMode: RangeSelectionMode.enforced,
+ onRangeSelected: (start, end, focused) {
+ rangeStart = start;
+ rangeEnd = end;
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(rangeStart, isNull);
+ expect(rangeEnd, isNull);
+ expect(focusedDay, isNull);
+
+ final firstTappedDay = DateTime.utc(2021, 7, 14);
+ final secondTappedDay = DateTime.utc(2021, 7, 7);
+
+ final firstTappedDayKey = cellContentKey(firstTappedDay);
+ final secondTappedDayKey = cellContentKey(secondTappedDay);
+
+ final expectedFocusedDay = secondTappedDay;
+
+ await tester.tap(find.byKey(firstTappedDayKey));
+ await tester.pumpAndSettle();
+ await tester.tap(find.byKey(secondTappedDayKey));
+ await tester.pumpAndSettle();
+ expect(rangeStart, secondTappedDay);
+ expect(rangeEnd, firstTappedDay);
+ expect(focusedDay, expectedFocusedDay);
+ },
+ );
+
+ testWidgets(
+ 'long press toggles rangeSelectionMode when onDayLongPress callback is null - initial mode is toggledOff',
+ (tester) async {
+ DateTime? rangeStart;
+ DateTime? rangeEnd;
+ DateTime? focusedDay;
+ DateTime? selectedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ onDaySelected: (selected, focused) {
+ selectedDay = selected;
+ focusedDay = focused;
+ },
+ onRangeSelected: (start, end, focused) {
+ rangeStart = start;
+ rangeEnd = end;
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(rangeStart, isNull);
+ expect(rangeEnd, isNull);
+ expect(focusedDay, isNull);
+ expect(selectedDay, isNull);
+
+ final firstTappedDay = DateTime.utc(2021, 7, 8);
+ final secondTappedDay = DateTime.utc(2021, 7, 21);
+
+ final firstTappedDayKey = cellContentKey(firstTappedDay);
+ final secondTappedDayKey = cellContentKey(secondTappedDay);
+
+ final expectedFocusedDay = secondTappedDay;
+
+ await tester.longPress(find.byKey(firstTappedDayKey));
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.byKey(firstTappedDayKey));
+ await tester.pumpAndSettle();
+ await tester.tap(find.byKey(secondTappedDayKey));
+ await tester.pumpAndSettle();
+ expect(rangeStart, firstTappedDay);
+ expect(rangeEnd, secondTappedDay);
+ expect(focusedDay, expectedFocusedDay);
+ expect(selectedDay, isNull);
+ },
+ );
+
+ testWidgets(
+ 'long press toggles rangeSelectionMode when onDayLongPress callback is null - initial mode is toggledOn',
+ (tester) async {
+ DateTime? rangeStart;
+ DateTime? rangeEnd;
+ DateTime? focusedDay;
+ DateTime? selectedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ rangeSelectionMode: RangeSelectionMode.toggledOn,
+ onDaySelected: (selected, focused) {
+ selectedDay = selected;
+ focusedDay = focused;
+ },
+ onRangeSelected: (start, end, focused) {
+ rangeStart = start;
+ rangeEnd = end;
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(rangeStart, isNull);
+ expect(rangeEnd, isNull);
+ expect(focusedDay, isNull);
+ expect(selectedDay, isNull);
+
+ final firstTappedDay = DateTime.utc(2021, 7, 8);
+ final secondTappedDay = DateTime.utc(2021, 7, 21);
+
+ final firstTappedDayKey = cellContentKey(firstTappedDay);
+ final secondTappedDayKey = cellContentKey(secondTappedDay);
+
+ final expectedFocusedDay = secondTappedDay;
+
+ await tester.longPress(find.byKey(firstTappedDayKey));
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.byKey(firstTappedDayKey));
+ await tester.pumpAndSettle();
+ await tester.tap(find.byKey(secondTappedDayKey));
+ await tester.pumpAndSettle();
+ expect(rangeStart, isNull);
+ expect(rangeEnd, isNull);
+ expect(focusedDay, expectedFocusedDay);
+ expect(selectedDay, secondTappedDay);
+ },
+ );
+
+ testWidgets(
+ 'rangeSelectionMode.enforced disables onDaySelected callback',
+ (tester) async {
+ DateTime? rangeStart;
+ DateTime? rangeEnd;
+ DateTime? focusedDay;
+ DateTime? selectedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ rangeSelectionMode: RangeSelectionMode.enforced,
+ onDaySelected: (selected, focused) {
+ selectedDay = selected;
+ focusedDay = focused;
+ },
+ onRangeSelected: (start, end, focused) {
+ rangeStart = start;
+ rangeEnd = end;
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(rangeStart, isNull);
+ expect(rangeEnd, isNull);
+ expect(focusedDay, isNull);
+ expect(selectedDay, isNull);
+
+ final firstTappedDay = DateTime.utc(2021, 7, 8);
+ final secondTappedDay = DateTime.utc(2021, 7, 21);
+
+ final firstTappedDayKey = cellContentKey(firstTappedDay);
+ final secondTappedDayKey = cellContentKey(secondTappedDay);
+
+ final expectedFocusedDay = secondTappedDay;
+
+ await tester.longPress(find.byKey(firstTappedDayKey));
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.byKey(firstTappedDayKey));
+ await tester.pumpAndSettle();
+ await tester.tap(find.byKey(secondTappedDayKey));
+ await tester.pumpAndSettle();
+ expect(rangeStart, firstTappedDay);
+ expect(rangeEnd, secondTappedDay);
+ expect(focusedDay, expectedFocusedDay);
+ expect(selectedDay, isNull);
+ },
+ );
+
+ testWidgets(
+ 'rangeSelectionMode.disabled enforces onDaySelected callback',
+ (tester) async {
+ DateTime? rangeStart;
+ DateTime? rangeEnd;
+ DateTime? focusedDay;
+ DateTime? selectedDay;
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ rangeSelectionMode: RangeSelectionMode.disabled,
+ onDaySelected: (selected, focused) {
+ selectedDay = selected;
+ focusedDay = focused;
+ },
+ onRangeSelected: (start, end, focused) {
+ rangeStart = start;
+ rangeEnd = end;
+ focusedDay = focused;
+ },
+ ),
+ ),
+ );
+
+ expect(rangeStart, isNull);
+ expect(rangeEnd, isNull);
+ expect(focusedDay, isNull);
+ expect(selectedDay, isNull);
+
+ final firstTappedDay = DateTime.utc(2021, 7, 8);
+ final secondTappedDay = DateTime.utc(2021, 7, 21);
+
+ final firstTappedDayKey = cellContentKey(firstTappedDay);
+ final secondTappedDayKey = cellContentKey(secondTappedDay);
+
+ final expectedFocusedDay = secondTappedDay;
+
+ await tester.longPress(find.byKey(firstTappedDayKey));
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.byKey(firstTappedDayKey));
+ await tester.pumpAndSettle();
+ await tester.tap(find.byKey(secondTappedDayKey));
+ await tester.pumpAndSettle();
+ expect(rangeStart, isNull);
+ expect(rangeEnd, isNull);
+ expect(focusedDay, expectedFocusedDay);
+ expect(selectedDay, secondTappedDay);
+ },
+ );
+ });
+
+ group('Range selection test:', () {
+ testWidgets(
+ 'range selection has correct start and end point',
+ (tester) async {
+ final rangeStart = DateTime.utc(2021, 7, 8);
+ final rangeEnd = DateTime.utc(2021, 7, 21);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ rangeStartDay: rangeStart,
+ rangeEndDay: rangeEnd,
+ ),
+ ),
+ );
+
+ final rangeStartKey = cellContentKey(rangeStart);
+ final rangeStartCellContent =
+ tester.widget(find.byKey(rangeStartKey)) as CellContent;
+
+ expect(rangeStartCellContent.isRangeStart, true);
+ expect(rangeStartCellContent.isRangeEnd, false);
+ expect(rangeStartCellContent.isWithinRange, true);
+
+ final rangeEndKey = cellContentKey(rangeEnd);
+ final rangeEndCellContent =
+ tester.widget(find.byKey(rangeEndKey)) as CellContent;
+
+ expect(rangeEndCellContent.isRangeStart, false);
+ expect(rangeEndCellContent.isRangeEnd, true);
+ expect(rangeEndCellContent.isWithinRange, true);
+ },
+ );
+
+ testWidgets(
+ 'days within range selection are marked as inWithinRange',
+ (tester) async {
+ final rangeStart = DateTime.utc(2021, 7, 8);
+ final rangeEnd = DateTime.utc(2021, 7, 13);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ rangeStartDay: rangeStart,
+ rangeEndDay: rangeEnd,
+ ),
+ ),
+ );
+
+ final dayCount = rangeEnd.difference(rangeStart).inDays - 1;
+ expect(dayCount, 4);
+
+ for (int i = 1; i <= dayCount; i++) {
+ final testDay = rangeStart.add(Duration(days: i));
+
+ expect(testDay.isAfter(rangeStart), true);
+ expect(testDay.isBefore(rangeEnd), true);
+
+ final testDayKey = cellContentKey(testDay);
+ final testDayCellContent =
+ tester.widget(find.byKey(testDayKey)) as CellContent;
+
+ expect(testDayCellContent.isWithinRange, true);
+ }
+ },
+ );
+
+ testWidgets(
+ 'days outside range selection are not marked as inWithinRange',
+ (tester) async {
+ final rangeStart = DateTime.utc(2021, 7, 8);
+ final rangeEnd = DateTime.utc(2021, 7, 13);
+
+ await tester.pumpWidget(
+ setupTestWidget(
+ TableCalendar(
+ focusedDay: initialFocusedDay,
+ firstDay: firstDay,
+ lastDay: lastDay,
+ currentDay: today,
+ rangeStartDay: rangeStart,
+ rangeEndDay: rangeEnd,
+ ),
+ ),
+ );
+
+ final oobStart = rangeStart.subtract(const Duration(days: 1));
+ final oobEnd = rangeEnd.add(const Duration(days: 1));
+
+ final oobStartKey = cellContentKey(oobStart);
+ final oobStartCellContent =
+ tester.widget(find.byKey(oobStartKey)) as CellContent;
+
+ final oobEndKey = cellContentKey(oobEnd);
+ final oobEndCellContent =
+ tester.widget(find.byKey(oobEndKey)) as CellContent;
+
+ expect(oobStartCellContent.isWithinRange, false);
+ expect(oobEndCellContent.isWithinRange, false);
+ },
+ );
+ });
+}
diff --git a/test/utils_test.dart b/test/utils_test.dart
new file mode 100644
index 00000000..472c0afb
--- /dev/null
+++ b/test/utils_test.dart
@@ -0,0 +1,69 @@
+// Copyright 2019 Aleksander Woźniak
+// SPDX-License-Identifier: Apache-2.0
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:table_calendar/src/shared/utils.dart';
+
+void main() {
+ group('isSameDay() tests:', () {
+ test('Same day, different time', () {
+ final dateA = DateTime(2020, 5, 10, 4, 32, 16);
+ final dateB = DateTime(2020, 5, 10, 8, 21, 44);
+
+ expect(isSameDay(dateA, dateB), true);
+ });
+
+ test('Different day, same time', () {
+ final dateA = DateTime(2020, 5, 10, 4, 32, 16);
+ final dateB = DateTime(2020, 5, 11, 4, 32, 16);
+
+ expect(isSameDay(dateA, dateB), false);
+ });
+
+ test('UTC and local time zone', () {
+ final dateA = DateTime.utc(2020, 5, 10);
+ final dateB = DateTime(2020, 5, 10);
+
+ expect(isSameDay(dateA, dateB), true);
+ });
+ });
+
+ group('normalizeDate() tests:', () {
+ test('Local time zone gets converted to UTC', () {
+ final dateA = DateTime(2020, 5, 10, 4, 32, 16);
+ final dateB = normalizeDate(dateA);
+
+ expect(dateB.isUtc, true);
+ });
+
+ test('Date is unchanged', () {
+ final dateA = DateTime(2020, 5, 10, 4, 32, 16);
+ final dateB = normalizeDate(dateA);
+
+ expect(dateB.year, 2020);
+ expect(dateB.month, 5);
+ expect(dateB.day, 10);
+ });
+
+ test('Time gets trimmed', () {
+ final dateA = DateTime(2020, 5, 10, 4, 32, 16);
+ final dateB = normalizeDate(dateA);
+
+ expect(dateB.hour, 0);
+ expect(dateB.minute, 0);
+ expect(dateB.second, 0);
+ expect(dateB.millisecond, 0);
+ expect(dateB.microsecond, 0);
+ });
+ });
+
+ group('getWeekdayNumber() tests:', () {
+ test('Monday returns number 1', () {
+ expect(getWeekdayNumber(StartingDayOfWeek.monday), 1);
+ });
+
+ test('Sunday returns number 7', () {
+ expect(getWeekdayNumber(StartingDayOfWeek.sunday), 7);
+ });
+ });
+}