New Example - Calendar (#91)

This commit is contained in:
Ishaan Kesarwani
2022-10-22 23:59:43 +05:30
committed by GitHub
parent d9cc97a7ea
commit 91d4d1b868
100 changed files with 7300 additions and 0 deletions

View File

@ -0,0 +1,58 @@
## [0.0.6] - 21 November 2019
Added support for custom day decoration.
Added support for custom disabled days.
## [0.1.0] - 31 May 2020
Fixed i18n issue for MonthPicker in case no locale was set.
Fixed selection periods with unselectable dates issue in RangePicker.
Minor changes and fixes.
## [0.1.1] - 20 June 2020
Added scrollPhysics property to DatePickerLayoutSettings.
## [0.1.3] - 23 June 2020
Added day headers style customization.
Added prev/next icon customization.
Added selected period text styles customization.
## [0.1.4] - 2 July 2020
Added firstDayOfWeekIndex customization.
## [0.1.5] - 29 July 2020
Added support of the CupertinoApp ancestor (fixed #29).
## [0.1.6] - 21 August 2020
Added two customizable fields to DatePickerLayoutSettings: showNextMonthStart, showPrevMonthEnd (implemented #28).
## [0.1.7] - 25 August 2020
Added onMonthChange callback for all day based pickers.
Added newPeriod field to UnselectablePeriodError class.
## [0.1.8] - 26 October 2020
Fixed selection in RangePicker which is on the edge of date when time changes (#44).
## [0.1.9] - 23 December 2020
Increased intl dependency version.
Minor changes.
## [0.1.10] - 23 December 2020
Increased intl dependency version according to one used on pub.dev.
## [0.2.0] - 7 March 2021
Migrated to null-safety.
Added DatePickerLayoutSettings.hideMonthNavigationRow option.
## [0.2.1] - 16 March 2021
Used intl for getting short month name for MonthPicker (fixed #54)
## [0.2.2] - 20 March 2021
Added **initiallyShowDate** optional property for DayPicker, WeekPicker and RangePicker.
## [0.2.3] - 05 April 2021
Fixed nextTooltip initializing (#57).
## [0.2.3+1] - 11 April 2021
Fixed defining DayHeaderStyle in DatePickerStyles.fulfillWithTheme.
## [0.2.4] - 29 April 2021
Fixed incorrect new month calculations (#56).

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Maria Melnik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,120 @@
# flutter_date_pickers
[![Actions Status](https://github.com/MariaMelnik/flutter_date_pickers/workflows/Test/badge.svg)](https://github.com/MariaMelnik/flutter_date_pickers/actions)
[![Pub](https://img.shields.io/pub/v/flutter_date_pickers.svg)](https://pub.dev/packages/flutter_date_pickers)
[![Likes](https://img.shields.io/badge/dynamic/json?color=blue&label=likes&query=likes&url=http://www.pubscore.gq/likes?package=flutter_date_pickers&style=flat-square&cacheSeconds=90000)](https://pub.dev/packages/flutter_date_pickers)
[![Health](https://img.shields.io/badge/dynamic/json?color=blue&label=health&query=pub_points&url=http://www.pubscore.gq/pub-points?package=flutter_date_pickers&style=flat-square&cacheSeconds=90000)](https://pub.dev/packages/flutter_date_pickers/score)
[![Code size](https://img.shields.io/github/languages/code-size/MariaMelnik/flutter_date_pickers?logo=github&logoColor=white)](https://github.com/MariaMelnik/flutter_date_pickers)
[![License: MIT](https://img.shields.io/badge/license-MIT-purple.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/MariaMelnik/flutter_date_pickers?style=social)](https://github.com/MariaMelnik/flutter_date_pickers/)
<!--[![Coverage](https://codecov.io/gh/MariaMelnik/flutter_date_pickers/branch/master/graph/badge.svg)](https://codecov.io/gh/MariaMelnik/flutter_date_pickers)
[![effective_dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://github.com/tenhobi/effective_dart)-->
Allows to use date pickers without dialog.
Provides some customizable styles for date pickers.
A set of date pickers:
* `DayPicker` for one day
* `WeekPicker` for whole week
* `RangePicker` for random range
* `MonthPicker` for month
![](demoDatePickers2.gif)
## How to style date picker
Every date picker constructor take a style object as a parameter (if no styles passed - defaults will be used).
For single value pickers (DayPicker, MonthPicker) it is DatePickerStyles object;
For range pickers (WeekPicker, RangePickers) it is DatePickerRangeStyles object;
Customizable styles:
for all date pickers
| Property | Description |
|---|---|
| TextStyle displayedPeriodTitle | title of the date picker |
| TextStyle currentDateStyle | style for current date |
| TextStyle disabledDateStyle | style for disabled dates (before first and after last date user can pick) |
| TextStyle selectedDateStyle | style for selected date |
| BoxDecoration selectedSingleDateDecoration | decoration for selected date in case single value is selected |
| TextStyle defaultDateTextStyle | style for date which is neither current nor disabled nor selected |
only for range date pickers (WeekPicker, RangePicker)
| Property | Description |
|---|---|
| BoxDecoration selectedPeriodStartDecoration | decoration for the first date of the selected range |
| BoxDecoration selectedPeriodLastDecoration | decoration for the first date of the selected range |
| BoxDecoration selectedPeriodMiddleDecoration | Decoration for the date of the selected range which is not first date and not end date of this range |
## How to make some dates not selectable date picker
By default only dates before `firstDate` and after `lastDate` are not selectable. But you can set custom disabled days.
`DayPicker`, `WeekPicker` and `RangePicker` take a `SelectableDayPredicate selectableDayPredicate`
where you can specify function which returns if some date is disabled or not.
If some date is disabled for selection it gets `disabledDateStyle`.
If selected range or week pretends to include such disabled date `UnselectablePeriodException` occurs.
To handle it - pass `onSelectionError` callback to date picker.
## How to make special decorations for some dates
By default cells are decorated with `datePickerStyles` slyles (or default if no styles was passed to date picker).
If you need special decoration for some days use `eventDecorationBuilder`.
Currently only for `DayPicker`, `WeekPicker` and `RangePicker`.
- If date is not selected basic styles will be merged with styles from `eventDecorationBuilder`.
- If date is current date styles from `eventDecorationBuilder` win (if there are).
- Otherwise basic styles (`datePickerStyles`) win.
## What time I will get after selection?
If one day selected:
you will get start of the day (00:00:00) by default. If selected `firstDate` - you will get time of it.
If range/week selected:
for start you will get start of the day (00:00:00) by default. If selected `firstDate` - you will get time of it.
for end you will get end of the day (23:59:59.999) by default. If selected `lastDate` - you will get time of it.
If month selected:
you will get start (00:00:00) of the 1 day of month by default.
If selected month same as month of the `firstDate` - you will get `firstDate`.
## Usage
```dart
// Create week date picker with passed parameters
Widget buildWeekDatePicker (DateTime selectedDate, DateTime firstAllowedDate, DateTime lastAllowedDate, ValueChanged<DatePeriod> onNewSelected) {
// add some colors to default settings
DatePickerRangeStyles styles = DatePickerRangeStyles(
selectedPeriodLastDecoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.only(
topRight: Radius.circular(10.0),
bottomRight: Radius.circular(10.0))),
selectedPeriodStartDecoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10.0), bottomLeft: Radius.circular(10.0)),
),
selectedPeriodMiddleDecoration: BoxDecoration(
color: Colors.yellow, shape: BoxShape.rectangle),
);
return WeekPicker(
selectedDate: selectedDate,
onChanged: onNewSelected,
firstDate: firstAllowedDate,
lastDate: lastAllowedDate,
datePickerStyles: styles
);
}
```
## Example app
Please checkout [example](https://github.com/MariaMelnik/flutter_date_pickers/tree/master/example).
For help getting started with Flutter, view our
[online documentation](https://flutter.io/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@ -0,0 +1,94 @@
linter:
rules:
# STYLE
camel_case_types: true
camel_case_extensions: true
library_names: true
file_names: true
library_prefixes: true
non_constant_identifier_names: true
constant_identifier_names: true
directives_ordering: true
lines_longer_than_80_chars: true
curly_braces_in_flow_control_structures: true
# DOCUMENTATION
slash_for_doc_comments: true
package_api_docs: true
public_member_api_docs: true
comment_references: true
# USAGE
implementation_imports: true
avoid_relative_lib_imports: true
prefer_relative_imports: true
prefer_adjacent_string_concatenation: true
prefer_interpolation_to_compose_strings: true
unnecessary_brace_in_string_interps: true
prefer_collection_literals: true
prefer_is_empty: true
prefer_is_not_empty: true
avoid_function_literals_in_foreach_calls: true
prefer_iterable_whereType: true
prefer_function_declarations_over_variables: true
unnecessary_lambdas: true
prefer_equal_for_default_values: true
avoid_init_to_null: true
unnecessary_getters_setters: true
unnecessary_getters: true
prefer_expression_function_bodies: true
unnecessary_this: true
unnecessary_const: true
avoid_catches_without_on_clauses: true
avoid_catching_errors: true
use_rethrow_when_possible: true
# DESIGN
use_to_and_as_if_applicable: true
one_member_abstracts: true
avoid_classes_with_only_static_members: true
prefer_mixin: true
prefer_final_fields: true
use_setters_to_change_properties: true
avoid_setters_without_getters: true
avoid_returning_null: true
avoid_returning_this: true
type_annotate_public_apis: true
prefer_typing_uninitialized_variables: true
# omit_local_variable_types: true
avoid_types_on_closure_parameters: true
avoid_return_types_on_setters: true
prefer_generic_function_type_aliases: true
avoid_private_typedef_functions: true
use_function_type_syntax_for_parameters: true
avoid_positional_boolean_parameters: true
hash_and_equals: true
avoid_equals_and_hash_code_on_mutable_classes: true
avoid_null_checks_in_equality_operators: true
# PEDANTIC
# (duplicated rules are removed)
always_declare_return_types: true
always_require_non_null_named_parameters: true
annotate_overrides: true
avoid_empty_else: true
avoid_shadowing_type_parameters: true
avoid_types_as_parameter_names: true
empty_catches: true
empty_constructor_bodies: true
no_duplicate_case_values: true
null_closures: true
prefer_conditional_assignment: true
prefer_contains: true
prefer_for_elements_to_map_fromIterable: true
prefer_if_null_operators: true
# prefer_single_quotes: true
prefer_spread_collections: true
recursive_getters: true
type_init_formals: true
unawaited_futures: true
unnecessary_new: true
unnecessary_null_in_if_null_operators: true
unrelated_type_equality_checks: true
valid_regexps: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@ -0,0 +1,61 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
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'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 28
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.mariamelnik.flutter_date_picker"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
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'
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mariamelnik.flutter_date_picker">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,33 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mariamelnik.flutter_date_picker">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="flutter_date_picker"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,13 @@
package com.mariamelnik.flutter_date_picker;
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);
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mariamelnik.flutter_date_picker">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,29 @@
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true

View File

@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
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

View File

@ -0,0 +1,15 @@
include ':app'
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>8.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,491 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
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 */; };
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 */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
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 */,
);
path = Runner;
sourceTree = "<group>";
};
97C146F11CF9000F007C117D /* Supporting Files */ = {
isa = PBXGroup;
children = (
97C146F21CF9000F007C117D /* main.m */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0910;
ORGANIZATIONNAME = "The Chromium Authors";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
97C146F31CF9000F007C117D /* main.m in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = 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_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = S8QB4VV633;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mariamelnik.flutterDatePicker;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = 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_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = 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_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mariamelnik.flutterDatePicker;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mariamelnik.flutterDatePicker;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0910"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,6 @@
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate
@end

View File

@ -0,0 +1,13 @@
#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

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>flutter_date_picker</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,9 @@
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

View File

@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
/// Dialog with some Material colors ([materialColors]) to pick one of them.
class ColorPickerDialog extends StatefulWidget {
/// Initially selected color.
///
/// If pre-selected color is not from [materialColors] [Colors.blue] will be
/// used.
final Color selectedColor;
///
const ColorPickerDialog({
Key? key,
required this.selectedColor
}) : super(key: key);
@override
State<StatefulWidget> createState() => _ColorPickerDialogState();
}
class _ColorPickerDialogState extends State<ColorPickerDialog> {
Color _mainColor = Colors.blue;
@override
void initState() {
super.initState();
bool isSelectedMaterial = materialColors.contains(widget.selectedColor);
if (isSelectedMaterial) {
_mainColor = widget.selectedColor;
}
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: const EdgeInsets.all(6.0),
title: const Text("Color picker"),
content: MaterialColorPicker(
selectedColor: _mainColor,
allowShades: false,
onMainColorChange: _onMainColorChange,
),
actions: [
TextButton(
child: const Text('CANCEL'),
onPressed: () {
Navigator.of(context).pop(null);
},
),
TextButton(
child: const Text('SUBMIT'),
onPressed: () {
Navigator.of(context).pop(_mainColor);
},
),
],
);
}
void _onMainColorChange (Color? newColor) {
if (newColor == null) return;
setState(() {
_mainColor = newColor;
});
}
}

View File

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
// default with and height for the button container
const double _kBtnSize = 24.0;
/// Round colored button with title to select some style color.
class ColorSelectorBtn extends StatelessWidget {
/// Title near color button.
final String title;
/// Color of the button.
final Color color;
/// onTap callback.
final VoidCallback showDialogFunction;
/// Size of the circle.
///
/// By default is [_kBtnSize].
final double colorBtnSize;
///
const ColorSelectorBtn(
{Key? key,
required this.title,
required this.color,
required this.showDialogFunction,
this.colorBtnSize = _kBtnSize})
: super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Expanded(
child: Row(
children: <Widget>[
GestureDetector(
onTap: showDialogFunction,
child: Container(
height: colorBtnSize,
width: colorBtnSize,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
),
const SizedBox(
width: 8.0,
),
Expanded(
child: Text(
title,
overflow: TextOverflow.ellipsis,
)),
],
),
);
}
}

View File

@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import 'package:flutter_date_pickers/flutter_date_pickers.dart' as dp;
import 'package:flutter_date_pickers/flutter_date_pickers.dart';
import '../color_picker_dialog.dart';
import '../color_selector_btn.dart';
import '../event.dart';
/// Page with [dp.DayPicker].
class DayPickerPage extends StatefulWidget {
/// Custom events.
final List<Event> events;
///
const DayPickerPage({
Key? key,
this.events = const []
}) : super(key: key);
@override
State<StatefulWidget> createState() => _DayPickerPageState();
}
class _DayPickerPageState extends State<DayPickerPage> {
DateTime _selectedDate = DateTime.now();
DateTime _firstDate = DateTime.now().subtract(Duration(days: 45));
DateTime _lastDate = DateTime.now().add(Duration(days: 45));
Color selectedDateStyleColor = Colors.blue;
Color selectedSingleDateDecorationColor = Colors.red;
@override
void didChangeDependencies() {
super.didChangeDependencies();
Color? bodyTextColor = Theme.of(context).accentTextTheme.bodyText1?.color;
if (bodyTextColor != null) selectedDateStyleColor = bodyTextColor;
selectedSingleDateDecorationColor = Theme.of(context).accentColor;
}
@override
Widget build(BuildContext context) {
// add selected colors to default settings
dp.DatePickerRangeStyles styles = dp.DatePickerRangeStyles(
selectedDateStyle: Theme.of(context)
.accentTextTheme
.bodyText1
?.copyWith(color: selectedDateStyleColor),
selectedSingleDateDecoration: BoxDecoration(
color: selectedSingleDateDecorationColor,
shape: BoxShape.circle
),
dayHeaderStyle: DayHeaderStyle(
textStyle: TextStyle(
color: Colors.red
)
)
);
return Flex(
direction: MediaQuery.of(context).orientation == Orientation.portrait
? Axis.vertical
: Axis.horizontal,
children: <Widget>[
Expanded(
child: dp.DayPicker.single(
selectedDate: _selectedDate,
onChanged: _onSelectedDateChanged,
firstDate: _firstDate,
lastDate: _lastDate,
datePickerStyles: styles,
datePickerLayoutSettings: dp.DatePickerLayoutSettings(
maxDayPickerRowCount: 2,
showPrevMonthEnd: true,
showNextMonthStart: true
),
selectableDayPredicate: _isSelectableCustom,
eventDecorationBuilder: _eventDecorationBuilder,
),
),
Container(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
"Selected date styles",
style: Theme.of(context).textTheme.subtitle1,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ColorSelectorBtn(
title: "Text",
color: selectedDateStyleColor,
showDialogFunction: _showSelectedDateDialog,
colorBtnSize: 42.0,
),
SizedBox(
width: 12.0,
),
ColorSelectorBtn(
title: "Background",
color: selectedSingleDateDecorationColor,
showDialogFunction: _showSelectedBackgroundColorDialog,
colorBtnSize: 42.0,
),
],
),
),
Text("Selected: $_selectedDate")
],
),
),
),
],
);
}
// select text color of the selected date
void _showSelectedDateDialog() async {
Color? newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedDateStyleColor,
));
if (newSelectedColor != null) {
setState(() {
selectedDateStyleColor = newSelectedColor;
});
}
}
// select background color of the selected date
void _showSelectedBackgroundColorDialog() async {
Color? newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedSingleDateDecorationColor,
));
if (newSelectedColor != null) {
setState(() {
selectedSingleDateDecorationColor = newSelectedColor;
});
}
}
void _onSelectedDateChanged(DateTime newDate) {
setState(() {
_selectedDate = newDate;
});
}
// ignore: prefer_expression_function_bodies
bool _isSelectableCustom (DateTime day) {
return day.weekday < 6;
}
dp.EventDecoration? _eventDecorationBuilder(DateTime date) {
List<DateTime> eventsDates = widget.events
.map<DateTime>((Event e) => e.date)
.toList();
bool isEventDate = eventsDates.any((DateTime d) =>
date.year == d.year
&& date.month == d.month
&& d.day == date.day);
BoxDecoration roundedBorder = BoxDecoration(
border: Border.all(
color: Colors.deepOrange,
),
borderRadius: BorderRadius.all(Radius.circular(3.0))
);
return isEventDate
? dp.EventDecoration(boxDecoration: roundedBorder)
: null;
}
}

View File

@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import 'package:flutter_date_pickers/flutter_date_pickers.dart' as dp;
import '../color_picker_dialog.dart';
import '../color_selector_btn.dart';
import '../event.dart';
/// Page with [dp.DayPicker] where many single days can be selected.
class DaysPickerPage extends StatefulWidget {
/// Custom events.
final List<Event> events;
///
const DaysPickerPage({
Key? key,
this.events = const []
}) : super(key: key);
@override
State<StatefulWidget> createState() => _DaysPickerPageState();
}
class _DaysPickerPageState extends State<DaysPickerPage> {
List<DateTime> _selectedDates = [];
DateTime _firstDate = DateTime.now().subtract(Duration(days: 45));
DateTime _lastDate = DateTime.now().add(Duration(days: 45));
Color selectedDateStyleColor = Colors.blue;
Color selectedSingleDateDecorationColor = Colors.red;
@override
void initState() {
super.initState();
final now = DateTime.now();
_selectedDates = [
now,
now.subtract(Duration(days: 10)),
now.add(Duration(days: 7))
];
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
Color? bodyTextColor = Theme.of(context).accentTextTheme.bodyText1?.color;
if (bodyTextColor != null) selectedDateStyleColor = bodyTextColor;
selectedSingleDateDecorationColor = Theme.of(context).accentColor;
}
@override
Widget build(BuildContext context) {
// add selected colors to default settings
dp.DatePickerRangeStyles styles = dp.DatePickerRangeStyles(
selectedDateStyle: Theme.of(context)
.accentTextTheme
.bodyText1
?.copyWith(color: selectedDateStyleColor),
selectedSingleDateDecoration: BoxDecoration(
color: selectedSingleDateDecorationColor, shape: BoxShape.circle));
return Flex(
direction: MediaQuery.of(context).orientation == Orientation.portrait
? Axis.vertical
: Axis.horizontal,
children: <Widget>[
Expanded(
child: dp.DayPicker.multi(
selectedDates: _selectedDates,
onChanged: _onSelectedDateChanged,
firstDate: _firstDate,
lastDate: _lastDate,
datePickerStyles: styles,
datePickerLayoutSettings: dp.DatePickerLayoutSettings(
maxDayPickerRowCount: 2,
showPrevMonthEnd: true,
showNextMonthStart: true
),
selectableDayPredicate: _isSelectableCustom,
eventDecorationBuilder: _eventDecorationBuilder,
),
),
Container(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
"Selected date styles",
style: Theme.of(context).textTheme.subtitle1,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ColorSelectorBtn(
title: "Text",
color: selectedDateStyleColor,
showDialogFunction: _showSelectedDateDialog,
colorBtnSize: 42.0,
),
SizedBox(
width: 12.0,
),
ColorSelectorBtn(
title: "Background",
color: selectedSingleDateDecorationColor,
showDialogFunction: _showSelectedBackgroundColorDialog,
colorBtnSize: 42.0,
),
],
),
),
Text("Selected: $_selectedDates")
],
),
),
),
],
);
}
// select text color of the selected date
void _showSelectedDateDialog() async {
Color? newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedDateStyleColor,
));
if (newSelectedColor != null) {
setState(() {
selectedDateStyleColor = newSelectedColor;
});
}
}
// select background color of the selected date
void _showSelectedBackgroundColorDialog() async {
Color? newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedSingleDateDecorationColor,
));
if (newSelectedColor != null) {
setState(() {
selectedSingleDateDecorationColor = newSelectedColor;
});
}
}
void _onSelectedDateChanged(List<DateTime> newDates) {
setState(() {
_selectedDates = newDates;
});
}
// ignore: prefer_expression_function_bodies
bool _isSelectableCustom (DateTime day) {
return day.weekday < 6;
}
dp.EventDecoration? _eventDecorationBuilder(DateTime date) {
List<DateTime> eventsDates = widget.events
.map<DateTime>((Event e) => e.date)
.toList();
bool isEventDate = eventsDates.any((DateTime d) =>
date.year == d.year
&& date.month == d.month
&& d.day == date.day);
BoxDecoration roundedBorder = BoxDecoration(
border: Border.all(
color: Colors.deepOrange,
),
borderRadius: BorderRadius.all(Radius.circular(3.0))
);
return isEventDate
? dp.EventDecoration(boxDecoration: roundedBorder)
: null;
}
}

View File

@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:flutter_date_pickers/flutter_date_pickers.dart' as dp;
import '../color_picker_dialog.dart';
import '../color_selector_btn.dart';
/// Page with the [dp.MonthPicker].
class MonthPickerPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => _MonthPickerPageState();
}
class _MonthPickerPageState extends State<MonthPickerPage> {
DateTime _firstDate = DateTime.now().subtract(Duration(days: 350));
DateTime _lastDate = DateTime.now().add(Duration(days: 350));
DateTime _selectedDate = DateTime.now();
Color selectedDateStyleColor = Colors.blue;
Color selectedSingleDateDecorationColor = Colors.red;
@override
void didChangeDependencies() {
super.didChangeDependencies();
Color? bodyTextColor = Theme.of(context).accentTextTheme.bodyText1?.color;
if (bodyTextColor != null) selectedDateStyleColor = bodyTextColor;
selectedSingleDateDecorationColor = Theme.of(context).accentColor;
}
@override
Widget build(BuildContext context) {
// add selected colors to default settings
dp.DatePickerStyles styles = dp.DatePickerStyles(
selectedDateStyle: Theme.of(context)
.accentTextTheme
.bodyText1
?.copyWith(color: selectedDateStyleColor),
selectedSingleDateDecoration: BoxDecoration(
color: selectedSingleDateDecorationColor, shape: BoxShape.circle));
return Flex(
direction: MediaQuery.of(context).orientation == Orientation.portrait
? Axis.vertical
: Axis.horizontal,
children: <Widget>[
Expanded(
child: dp.MonthPicker(
selectedDate: _selectedDate,
onChanged: _onSelectedDateChanged,
firstDate: _firstDate,
lastDate: _lastDate,
datePickerStyles: styles,
),
),
Container(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
"Selected date styles",
style: Theme.of(context).textTheme.subtitle1,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ColorSelectorBtn(
title: "Text",
color: selectedDateStyleColor,
showDialogFunction: _showSelectedDateDialog,
colorBtnSize: 42.0,
),
SizedBox(
width: 12.0,
),
ColorSelectorBtn(
title: "Background",
color: selectedSingleDateDecorationColor,
showDialogFunction: _showSelectedBackgroundColorDialog,
colorBtnSize: 42.0,
),
],
),
),
Text("Selected: $_selectedDate")
],
),
),
),
],
);
}
// select text color of the selected date
void _showSelectedDateDialog() async {
Color? newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedDateStyleColor,
));
if (newSelectedColor != null) {
setState(() {
selectedDateStyleColor = newSelectedColor;
});
}
}
// select background color of the selected date
void _showSelectedBackgroundColorDialog() async {
Color? newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedSingleDateDecorationColor,
));
if (newSelectedColor != null) {
setState(() {
selectedSingleDateDecorationColor = newSelectedColor;
});
}
}
void _onSelectedDateChanged(DateTime newDate) {
setState(() {
_selectedDate = newDate;
});
}
}

View File

@ -0,0 +1,273 @@
import 'package:flutter/material.dart';
import 'package:flutter_date_pickers/flutter_date_pickers.dart';
import '../color_picker_dialog.dart';
import '../color_selector_btn.dart';
import '../event.dart';
/// Page with the [RangePicker].
class RangePickerPage extends StatefulWidget {
/// Custom events.
final List<Event> events;
///
const RangePickerPage({
Key? key,
this.events = const []
}) : super(key: key);
@override
State<StatefulWidget> createState() => _RangePickerPageState();
}
class _RangePickerPageState extends State<RangePickerPage> {
DateTime _firstDate = DateTime.now().subtract(Duration(days: 345));
DateTime _lastDate = DateTime.now().add(Duration(days: 345));
DatePeriod _selectedPeriod = DatePeriod(
DateTime.now().subtract(Duration(days: 30)),
DateTime.now().subtract(Duration(days: 12))
);
Color selectedPeriodStartColor = Colors.blue;
Color selectedPeriodLastColor = Colors.blue;
Color selectedPeriodMiddleColor = Colors.blue;
@override
void didChangeDependencies() {
super.didChangeDependencies();
selectedPeriodLastColor = Theme.of(context).accentColor;
selectedPeriodMiddleColor = Theme.of(context).accentColor;
selectedPeriodStartColor = Theme.of(context).accentColor;
}
@override
Widget build(BuildContext context) {
// add selected colors to default settings
DatePickerRangeStyles styles = DatePickerRangeStyles(
selectedPeriodLastDecoration: BoxDecoration(
color: selectedPeriodLastColor,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(24.0),
bottomRight: Radius.circular(24.0))),
selectedPeriodStartDecoration: BoxDecoration(
color: selectedPeriodStartColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24.0),
bottomLeft: Radius.circular(24.0)
),
),
selectedPeriodMiddleDecoration: BoxDecoration(
color: selectedPeriodMiddleColor, shape: BoxShape.rectangle),
nextIcon: const Icon(Icons.arrow_right),
prevIcon: const Icon(Icons.arrow_left),
dayHeaderStyleBuilder: _dayHeaderStyleBuilder
);
return Flex(
direction: MediaQuery.of(context).orientation == Orientation.portrait
? Axis.vertical
: Axis.horizontal,
children: <Widget>[
Expanded(
child: RangePicker(
initiallyShowDate: DateTime.now(),
selectedPeriod: _selectedPeriod,
onChanged: _onSelectedDateChanged,
firstDate: _firstDate,
lastDate: _lastDate,
datePickerStyles: styles,
eventDecorationBuilder: _eventDecorationBuilder,
selectableDayPredicate: _isSelectableCustom,
onSelectionError: _onSelectionError,
),
),
Container(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
"Selected date styles",
style: Theme.of(context).textTheme.subtitle1,
),
_stylesBlock(),
_selectedBlock()
],
),
),
),
],
);
}
// Block with show information about selected date
// and boundaries of the selected period.
Widget _selectedBlock() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_selectedPeriod != null
? Column(children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 4.0),
child: Text("Selected period boundaries:"),
),
Text(_selectedPeriod.start.toString()),
Text(_selectedPeriod.end.toString()),
])
: Container()
],
);
// block with color buttons inside
Widget _stylesBlock() => Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ColorSelectorBtn(
title: "Start",
color: selectedPeriodStartColor,
showDialogFunction: _showSelectedStartColorDialog),
SizedBox(
width: 12.0,
),
ColorSelectorBtn(
title: "Middle",
color: selectedPeriodMiddleColor,
showDialogFunction: _showSelectedMiddleColorDialog),
SizedBox(
width: 12.0,
),
ColorSelectorBtn(
title: "End",
color: selectedPeriodLastColor,
showDialogFunction: _showSelectedEndColorDialog),
],
),
);
// select background color for the first date of the selected period
void _showSelectedStartColorDialog() async {
Color? newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedPeriodStartColor,
));
if (newSelectedColor != null) {
setState(() {
selectedPeriodStartColor = newSelectedColor;
});
}
}
// select background color for the last date of the selected period
void _showSelectedEndColorDialog() async {
Color? newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedPeriodLastColor,
));
if (newSelectedColor != null) {
setState(() {
selectedPeriodLastColor = newSelectedColor;
});
}
}
// select background color for the middle dates of the selected period
void _showSelectedMiddleColorDialog() async {
Color? newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedPeriodMiddleColor,
));
if (newSelectedColor != null) {
setState(() {
selectedPeriodMiddleColor = newSelectedColor;
});
}
}
void _onSelectedDateChanged(DatePeriod newPeriod) {
setState(() {
_selectedPeriod = newPeriod;
});
}
EventDecoration? _eventDecorationBuilder(DateTime date) {
List<DateTime> eventsDates = widget.events
.map<DateTime>((Event e) => e.date)
.toList();
bool isEventDate = eventsDates.any((DateTime d) =>
date.year == d.year
&& date.month == d.month
&& d.day == date.day);
BoxDecoration roundedBorder = BoxDecoration(
border: Border.all(
color: Colors.green,
),
borderRadius: BorderRadius.all(Radius.circular(3.0))
);
return isEventDate
? EventDecoration(boxDecoration: roundedBorder)
: null;
}
// ignore: prefer_expression_function_bodies
bool _isSelectableCustom (DateTime day) {
DateTime now = DateTime.now();
DateTime yesterday = now.subtract(Duration(days: 1));
DateTime tomorrow = now.add(Duration(days: 1));
bool isYesterday = sameDate(day, yesterday);
bool isTomorrow = sameDate(day, tomorrow);
return !isYesterday && !isTomorrow;
// return true;
// return day.weekday < 6;
// return day.day != DateTime.now().add(Duration(days: 7)).day ;
}
void _onSelectionError(UnselectablePeriodException exception) {
DatePeriod errorPeriod = exception.period;
// If user supposed to set another start of the period.
bool selectStart = _selectedPeriod.start != errorPeriod.start;
DateTime newSelection = selectStart
? errorPeriod.start
: errorPeriod.end;
DatePeriod newPeriod = DatePeriod(newSelection, newSelection);
setState(() {
_selectedPeriod = newPeriod;
});
}
// 0 is Sunday, 6 is Saturday
DayHeaderStyle _dayHeaderStyleBuilder(int weekday) {
bool isWeekend = weekday == 0 || weekday == 6;
return DayHeaderStyle(
textStyle: TextStyle(
color: isWeekend ? Colors.red : Colors.teal
)
);
}
}
bool sameDate(DateTime first, DateTime second) {
return first.year == second.year && first.month == second.month && first.day == second.day;
}

View File

@ -0,0 +1,228 @@
import 'package:flutter/material.dart';
import 'package:flutter_date_pickers/flutter_date_pickers.dart';
import '../event.dart';
/// Page with the [RangePicker] styled according to issue:
/// https://github.com/MariaMelnik/flutter_date_pickers/issues/49
class RangePickerPageStyled extends StatefulWidget {
/// Custom events.
final List<Event> events;
///
const RangePickerPageStyled({
Key? key,
this.events = const []
}) : super(key: key);
@override
State<StatefulWidget> createState() => _RangePickerPageStyledState();
}
class _RangePickerPageStyledState extends State<RangePickerPageStyled> {
DateTime _firstDate = DateTime.now().subtract(Duration(days: 345));
DateTime _lastDate = DateTime.now().add(Duration(days: 345));
DatePeriod _selectedPeriod = DatePeriod(
DateTime.now().subtract(Duration(days: 4)),
DateTime.now().add(Duration(days: 8))
);
Color selectedPeriodStartColor = Colors.blue;
Color selectedPeriodLastColor = Colors.blue;
Color selectedPeriodMiddleColor = Colors.blue;
@override
void initState() {
super.initState();
DateTime selectedPeriodStart = DateTime.now().subtract(Duration(days: 4));
DateTime selectedPeriodEnd = DateTime.now().add(Duration(days: 8));
_selectedPeriod = DatePeriod(selectedPeriodStart, selectedPeriodEnd);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// defaults for styles
selectedPeriodLastColor = Theme.of(context).accentColor;
selectedPeriodMiddleColor = Theme.of(context).accentColor;
selectedPeriodStartColor = Theme.of(context).accentColor;
}
@override
Widget build(BuildContext context) {
Color middleBgColor = Color.fromRGBO(237, 237, 250, 1);
DecorationImage circleImg = DecorationImage(
image: AssetImage('images/bg.png'),
fit: BoxFit.contain
);
// add selected colors to default settings
DatePickerRangeStyles styles = DatePickerRangeStyles(
selectedPeriodLastDecoration: BoxDecoration(
color: middleBgColor,
gradient: LinearGradient(
colors: [middleBgColor, Colors.transparent],
stops: [0.5, 0.5]
),
image: circleImg,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(24.0),
bottomRight: Radius.circular(24.0))
),
selectedPeriodStartDecoration: BoxDecoration(
color: middleBgColor,
gradient: LinearGradient(
colors: [Colors.transparent, middleBgColor,],
stops: [0.5, 0.5]
),
image: circleImg,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24.0),
bottomLeft: Radius.circular(24.0)
),
),
selectedPeriodMiddleDecoration: BoxDecoration(
color: middleBgColor,
shape: BoxShape.rectangle
),
);
return Flex(
direction: MediaQuery.of(context).orientation == Orientation.portrait
? Axis.vertical
: Axis.horizontal,
children: <Widget>[
Expanded(
child: RangePicker(
selectedPeriod: _selectedPeriod,
onChanged: _onSelectedDateChanged,
firstDate: _firstDate,
lastDate: _lastDate,
datePickerStyles: styles,
eventDecorationBuilder: _eventDecorationBuilder,
selectableDayPredicate: _isSelectableCustom,
onSelectionError: _onSelectionError,
datePickerLayoutSettings: DatePickerLayoutSettings(
showNextMonthStart: true,
showPrevMonthEnd: true
),
),
),
Container(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
"Selected date styles",
style: Theme.of(context).textTheme.subtitle1,
),
_selectedBlock()
],
),
),
),
],
);
}
// Block with show information about selected date
// and boundaries of the selected period.
Widget _selectedBlock() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_selectedPeriod != null
? Column(children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 4.0),
child: Text("Selected period boundaries:"),
),
Text(_selectedPeriod.start.toString()),
Text(_selectedPeriod.end.toString()),
])
: Container()
],
);
void _onSelectedDateChanged(DatePeriod newPeriod) {
setState(() {
_selectedPeriod = newPeriod;
});
}
EventDecoration? _eventDecorationBuilder(DateTime date) {
List<DateTime> eventsDates = widget.events
.map<DateTime>((Event e) => e.date)
.toList();
bool isEventDate = eventsDates.any((DateTime d) =>
date.year == d.year
&& date.month == d.month
&& d.day == date.day);
BoxDecoration roundedBorder = BoxDecoration(
border: Border.all(
color: Colors.green,
),
borderRadius: BorderRadius.all(Radius.circular(3.0))
);
return isEventDate
? EventDecoration(boxDecoration: roundedBorder)
: null;
}
// ignore: prefer_expression_function_bodies
bool _isSelectableCustom (DateTime day) {
DateTime now = DateTime.now();
DateTime yesterday = now.subtract(Duration(days: 1));
DateTime tomorrow = now.add(Duration(days: 1));
bool isYesterday = _sameDate(day, yesterday);
bool isTomorrow = _sameDate(day, tomorrow);
return !isYesterday && !isTomorrow;
// return true;
// return day.weekday < 6;
// return day.day != DateTime.now().add(Duration(days: 7)).day ;
}
void _onSelectionError(UnselectablePeriodException exception) {
DatePeriod errorPeriod = exception.period;
// If user supposed to set another start of the period.
bool selectStart = _selectedPeriod.start != errorPeriod.start;
DateTime newSelection = selectStart
? errorPeriod.start
: errorPeriod.end;
DatePeriod newPeriod = DatePeriod(newSelection, newSelection);
setState(() {
_selectedPeriod = newPeriod;
});
}
// 0 is Sunday, 6 is Saturday
DayHeaderStyle _dayHeaderStyleBuilder(int weekday) {
bool isWeekend = weekday == 0 || weekday == 6;
return DayHeaderStyle(
textStyle: TextStyle(
color: isWeekend ? Colors.red : Colors.teal
)
);
}
}
bool _sameDate(DateTime first, DateTime second) =>
first.year == second.year
&& first.month == second.month
&& first.day == second.day;

View File

@ -0,0 +1,239 @@
import 'package:flutter/material.dart';
import 'package:flutter_date_pickers/flutter_date_pickers.dart';
import '../color_picker_dialog.dart';
import '../color_selector_btn.dart';
import '../event.dart';
/// Page with the [WeekPicker].
class WeekPickerPage extends StatefulWidget {
/// Custom events.
final List<Event> events;
///
const WeekPickerPage({
Key? key,
this.events = const []
}) : super(key: key);
@override
State<StatefulWidget> createState() => _WeekPickerPageState();
}
class _WeekPickerPageState extends State<WeekPickerPage> {
DateTime _selectedDate = DateTime.now();
DateTime _firstDate = DateTime.now().subtract(Duration(days: 45));
DateTime _lastDate = DateTime.now().add(Duration(days: 45));
DatePeriod? _selectedPeriod;
Color selectedPeriodStartColor = Colors.blue;
Color selectedPeriodLastColor = Colors.blue;
Color selectedPeriodMiddleColor = Colors.blue;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// defaults for styles
selectedPeriodLastColor = Theme.of(context).accentColor;
selectedPeriodMiddleColor = Theme.of(context).accentColor;
selectedPeriodStartColor = Theme.of(context).accentColor;
}
@override
Widget build(BuildContext context) {
// add selected colors to default settings
DatePickerRangeStyles styles = DatePickerRangeStyles(
selectedPeriodLastDecoration: BoxDecoration(
color: selectedPeriodLastColor,
borderRadius: BorderRadius.only(
topRight: Radius.circular(10.0),
bottomRight: Radius.circular(10.0))),
selectedPeriodStartDecoration: BoxDecoration(
color: selectedPeriodStartColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10.0), bottomLeft: Radius.circular(10.0)),
),
selectedPeriodMiddleDecoration: BoxDecoration(
color: selectedPeriodMiddleColor, shape: BoxShape.rectangle),
);
return Flex(
direction: MediaQuery.of(context).orientation == Orientation.portrait
? Axis.vertical
: Axis.horizontal,
children: <Widget>[
Expanded(
child: WeekPicker(
selectedDate: _selectedDate,
onChanged: _onSelectedDateChanged,
firstDate: _firstDate,
lastDate: _lastDate,
datePickerStyles: styles,
onSelectionError: _onSelectionError,
selectableDayPredicate: _isSelectableCustom,
eventDecorationBuilder: _eventDecorationBuilder,
),
),
Container(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
"Selected date styles",
style: Theme.of(context).textTheme.subtitle1,
),
_stylesBlock(),
_selectedBlock()
],
),
),
),
],
);
}
// block witt color buttons insede
Widget _stylesBlock() => Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ColorSelectorBtn(
title: "Start",
color: selectedPeriodStartColor,
showDialogFunction: _showSelectedStartColorDialog),
SizedBox(
width: 12.0,
),
ColorSelectorBtn(
title: "Middle",
color: selectedPeriodMiddleColor,
showDialogFunction: _showSelectedMiddleColorDialog),
SizedBox(
width: 12.0,
),
ColorSelectorBtn(
title: "End",
color: selectedPeriodLastColor,
showDialogFunction: _showSelectedEndColorDialog),
],
),
);
// Block with information about selected date
// and boundaries of the selected period.
Widget _selectedBlock() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text("Selected: $_selectedDate"),
),
_selectedPeriod != null
? Column(children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 4.0),
child: Text("Selected period boundaries:"),
),
Text(_selectedPeriod!.start.toString()),
Text(_selectedPeriod!.end.toString()),
])
: Container()
],
);
// select background color for the first date of the selected period
void _showSelectedStartColorDialog() async {
Color? newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedPeriodStartColor,
));
if (newSelectedColor != null) {
setState(() {
selectedPeriodStartColor = newSelectedColor;
});
}
}
// select background color for the last date of the selected period
void _showSelectedEndColorDialog() async {
Color? newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedPeriodLastColor,
));
if (newSelectedColor != null) {
setState(() {
selectedPeriodLastColor = newSelectedColor;
});
}
}
// select background color for the middle dates of the selected period
void _showSelectedMiddleColorDialog() async {
Color newSelectedColor = await showDialog(
context: context,
builder: (_) => ColorPickerDialog(
selectedColor: selectedPeriodMiddleColor,
));
if (newSelectedColor != null) {
setState(() {
selectedPeriodMiddleColor = newSelectedColor;
});
}
}
void _onSelectedDateChanged(DatePeriod newPeriod) {
setState(() {
_selectedDate = newPeriod.start;
_selectedPeriod = newPeriod;
});
}
void _onSelectionError(Object e){
if (e is UnselectablePeriodException) print("catch error: $e");
}
// ignore: prefer_expression_function_bodies
bool _isSelectableCustom (DateTime day) {
// return day.weekday < 6;
return day.day != DateTime.now().add(Duration(days: 7)).day ;
}
EventDecoration? _eventDecorationBuilder(DateTime date) {
List<DateTime> eventsDates = widget.events
.map<DateTime>((Event e) => e.date)
.toList();
bool isEventDate = eventsDates.any((DateTime d) => date.year == d.year
&& date.month == d.month
&& d.day == date.day);
if (!isEventDate) return null;
BoxDecoration roundedBorder = BoxDecoration(
color: Colors.blue,
border: Border.all(
color: Colors.blue,
),
borderRadius: BorderRadius.all(Radius.circular(3.0))
);
TextStyle? whiteText = Theme.of(context)
.textTheme
.bodyText2
?.copyWith(color: Colors.white);
return isEventDate
? EventDecoration(boxDecoration: roundedBorder, textStyle: whiteText)
: null;
}
}

View File

@ -0,0 +1,14 @@
/// Event for the date pickers.
class Event {
/// Event's date.
final DateTime date;
/// Event's title.
final String dis;
///
Event(this.date, this.dis)
: assert(date != null),
assert(dis != null);
}

View File

@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'date_pickers_widgets/day_picker_page.dart';
import 'date_pickers_widgets/days_picker_page.dart';
import 'date_pickers_widgets/month_picker_page.dart';
import 'date_pickers_widgets/range_picker_page.dart';
import 'date_pickers_widgets/week_picker_page.dart';
import 'event.dart';
void main() {
runApp(MyApp());
}
///
class MyApp extends StatelessWidget {
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: GlobalMaterialLocalizations.delegates,
supportedLocales: [
const Locale('en', 'US'), // American English
const Locale('ru', 'RU'), // Russian
const Locale("pt") // Portuguese
],
debugShowCheckedModeBanner: false,
title: 'Date pickers demo',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
),
home: MyHomePage(
title: 'flutter_date_pickers Demo',
),
);
}
}
/// Start page.
class MyHomePage extends StatefulWidget {
/// Page title.
final String title;
///
MyHomePage({
required this.title,
Key? key,
}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
DateTime startOfPeriod = DateTime.now().subtract(Duration(days: 10));
DateTime endOfPeriod = DateTime.now().add(Duration(days: 10));
int _selectedTab = 0;
final List<Widget> datePickers = <Widget>[
DayPickerPage(events: events,),
DaysPickerPage(),
WeekPickerPage(events: events,),
RangePickerPage(events: events,),
MonthPickerPage()
];
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
widget.title,
style: TextStyle(letterSpacing: 1.15),
),
),
body: datePickers[_selectedTab],
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
canvasColor: Colors.blueGrey,
textTheme: Theme.of(context).textTheme.copyWith(
caption: TextStyle(color: Colors.white.withOpacity(0.5)))),
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.date_range), label: "Day"),
BottomNavigationBarItem(
icon: Icon(Icons.date_range), label: "Days"),
BottomNavigationBarItem(
icon: Icon(Icons.date_range), label: "Week"),
BottomNavigationBarItem(
icon: Icon(Icons.date_range), label: "Range"),
BottomNavigationBarItem(
icon: Icon(Icons.date_range), label: "Month"),
],
fixedColor: Colors.yellow,
currentIndex: _selectedTab,
onTap: (newIndex) {
setState(() {
_selectedTab = newIndex;
});
},
),
),
);
}
}
/// Mock events.
final List<Event> events = [
Event(DateTime.now(), "Today event"),
Event(DateTime.now().subtract(Duration(days: 3)), "Ev1"),
Event(DateTime.now().subtract(Duration(days: 13)), "Ev2"),
Event(DateTime.now().subtract(Duration(days: 30)), "Ev3"),
Event(DateTime.now().add(Duration(days: 3)), "Ev4"),
Event(DateTime.now().add(Duration(days: 13)), "Ev5"),
Event(DateTime.now().add(Duration(days: 30)), "Ev6"),
];

View File

@ -0,0 +1,181 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_date_pickers:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.2.3+1"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_material_color_picker:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: "286596b1e0167877bcdb87011bff77364338ba57"
url: "git://github.com/Bwolfs2/flutter_material_color_picker.git"
source: git
version: "1.0.5-nullsafety.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
sdks:
dart: ">=2.12.0 <3.0.0"

View File

@ -0,0 +1,80 @@
name: flutter_date_picker
description: A new Flutter application.
# 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.
# 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.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
flutter_material_color_picker:
# null-safety fork
git:
url: git://github.com/Bwolfs2/flutter_material_color_picker.git
flutter_date_pickers:
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
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
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets:
- images/bg.png
# - 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.
# For details regarding adding assets from package dependencies, see
# https://flutter.io/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
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.io/custom-fonts/#from-packages

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.idea" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/build" />
<excludeFolder url="file://$MODULE_DIR$/example/ios/Flutter/App.framework/flutter_assets/packages" />
</content>
<orderEntry type="jdk" jdkName="Android API 25 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart Packages" level="project" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
</component>
</module>

View File

@ -0,0 +1,12 @@
export 'package:flutter_date_pickers/src/date_period.dart';
export 'package:flutter_date_pickers/src/date_picker_keys.dart';
export 'package:flutter_date_pickers/src/layout_settings.dart';
export 'package:flutter_date_pickers/src/date_picker_styles.dart';
export 'package:flutter_date_pickers/src/event_decoration.dart';
export 'package:flutter_date_pickers/src/day_picker.dart';
export 'package:flutter_date_pickers/src/week_picker.dart';
export 'package:flutter_date_pickers/src/month_picker.dart';
export 'package:flutter_date_pickers/src/range_picker.dart';
export 'package:flutter_date_pickers/src/unselectable_period_error.dart';

View File

@ -0,0 +1,347 @@
import 'package:flutter/material.dart';
import 'date_picker_mixin.dart';
import 'date_picker_styles.dart';
import 'day_type.dart';
import 'event_decoration.dart';
import 'i_selectable_picker.dart';
import 'layout_settings.dart';
import 'utils.dart';
/// Widget for date pickers based on days and cover entire month.
/// Each cell of this picker is day.
class DayBasedPicker<T> extends StatelessWidget with CommonDatePickerFunctions {
/// Selection logic.
final ISelectablePicker selectablePicker;
/// The current date at the time the picker is displayed.
final DateTime currentDate;
/// The earliest date the user is permitted to pick.
/// (only year, month and day matter, time doesn't matter)
final DateTime firstDate;
/// The latest date the user is permitted to pick.
/// (only year, month and day matter, time doesn't matter)
final DateTime lastDate;
/// The month whose days are displayed by this picker.
final DateTime displayedMonth;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Key fo selected month (useful for integration tests)
final Key? selectedPeriodKey;
/// Styles what can be customized by user
final DatePickerRangeStyles datePickerStyles;
/// Builder to get event decoration for each date.
///
/// All event styles are overridden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
/// Localizations used to get strings for prev/next button tooltips,
/// weekday headers and display values for days numbers.
final MaterialLocalizations localizations;
/// Creates main date picker view where every cell is day.
DayBasedPicker(
{Key? key,
required this.currentDate,
required this.firstDate,
required this.lastDate,
required this.displayedMonth,
required this.datePickerLayoutSettings,
required this.datePickerStyles,
required this.selectablePicker,
required this.localizations,
this.selectedPeriodKey,
this.eventDecorationBuilder})
: assert(!firstDate.isAfter(lastDate)),
super(key: key);
@override
Widget build(BuildContext context) {
final List<Widget> labels = <Widget>[];
List<Widget> headers = _buildHeaders(localizations);
List<Widget> daysBeforeMonthStart = _buildCellsBeforeStart(localizations);
List<Widget> monthDays = _buildMonthCells(localizations);
List<Widget> daysAfterMonthEnd = _buildCellsAfterEnd(localizations);
labels.addAll(headers);
labels.addAll(daysBeforeMonthStart);
labels.addAll(monthDays);
labels.addAll(daysAfterMonthEnd);
return Padding(
padding: datePickerLayoutSettings.contentPadding,
child: Column(
children: <Widget>[
Flexible(
child: GridView.custom(
physics: datePickerLayoutSettings.scrollPhysics,
gridDelegate: datePickerLayoutSettings.dayPickerGridDelegate,
childrenDelegate:
SliverChildListDelegate(labels, addRepaintBoundaries: false),
),
),
],
),
);
}
List<Widget> _buildHeaders(MaterialLocalizations localizations) {
final int firstDayOfWeekIndex = datePickerStyles.firstDayOfeWeekIndex ??
localizations.firstDayOfWeekIndex;
DayHeaderStyleBuilder dayHeaderStyleBuilder =
datePickerStyles.dayHeaderStyleBuilder ??
// ignore: avoid_types_on_closure_parameters
(int i) => datePickerStyles.dayHeaderStyle;
List<Widget> headers = getDayHeaders(dayHeaderStyleBuilder,
localizations.narrowWeekdays, firstDayOfWeekIndex);
return headers;
}
List<Widget> _buildCellsBeforeStart(MaterialLocalizations localizations) {
List<Widget> result = [];
final int year = displayedMonth.year;
final int month = displayedMonth.month;
final int firstDayOfWeekIndex = datePickerStyles.firstDayOfeWeekIndex ??
localizations.firstDayOfWeekIndex;
final int firstDayOffset =
computeFirstDayOffset(year, month, firstDayOfWeekIndex);
final bool showDates = datePickerLayoutSettings.showPrevMonthEnd;
if (showDates) {
int prevMonth = month - 1;
if (prevMonth < 1) prevMonth = 12;
int prevYear = prevMonth == 12 ? year - 1 : year;
int daysInPrevMonth = DatePickerUtils.getDaysInMonth(prevYear, prevMonth);
List<Widget> days = List.generate(firstDayOffset, (index) => index)
.reversed
.map((i) => daysInPrevMonth - i)
.map((day) => _buildCell(prevYear, prevMonth, day))
.toList();
result = days;
} else {
result = List.generate(firstDayOffset, (_) => const SizedBox.shrink());
}
return result;
}
List<Widget> _buildMonthCells(MaterialLocalizations localizations) {
List<Widget> result = [];
final int year = displayedMonth.year;
final int month = displayedMonth.month;
final int daysInMonth = DatePickerUtils.getDaysInMonth(year, month);
for (int i = 1; i <= daysInMonth; i += 1) {
Widget dayWidget = _buildCell(year, month, i);
result.add(dayWidget);
}
return result;
}
List<Widget> _buildCellsAfterEnd(MaterialLocalizations localizations) {
List<Widget> result = [];
final bool showDates = datePickerLayoutSettings.showNextMonthStart;
if (!showDates) return result;
final int year = displayedMonth.year;
final int month = displayedMonth.month;
final int firstDayOfWeekIndex = datePickerStyles.firstDayOfeWeekIndex ??
localizations.firstDayOfWeekIndex;
final int firstDayOffset =
computeFirstDayOffset(year, month, firstDayOfWeekIndex);
final int daysInMonth = DatePickerUtils.getDaysInMonth(year, month);
final int totalFilledDays = firstDayOffset + daysInMonth;
int reminder = totalFilledDays % 7;
if (reminder == 0) return result;
final int emptyCellsNum = 7 - reminder;
int nextMonth = month + 1;
result = List.generate(emptyCellsNum, (i) => i + 1)
.map((day) => _buildCell(year, nextMonth, day))
.toList();
return result;
}
Widget _buildCell(int year, int month, int day) {
DateTime dayToBuild = DateTime(year, month, day);
dayToBuild = _checkDateTime(dayToBuild);
DayType dayType = selectablePicker.getDayType(dayToBuild);
Widget dayWidget = _DayCell(
day: dayToBuild,
currentDate: currentDate,
selectablePicker: selectablePicker,
datePickerStyles: datePickerStyles,
eventDecorationBuilder: eventDecorationBuilder,
localizations: localizations,
);
if (dayType != DayType.disabled) {
dayWidget = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => selectablePicker.onDayTapped(dayToBuild),
child: dayWidget,
);
}
return dayWidget;
}
/// Checks if [DateTime] is same day as [lastDate] or [firstDate]
/// and returns dt corrected (with time of [lastDate] or [firstDate]).
DateTime _checkDateTime(DateTime dt) {
DateTime result = dt;
// If dayToBuild is the first day we need to save original time for it.
if (DatePickerUtils.sameDate(dt, firstDate)) result = firstDate;
// If dayToBuild is the last day we need to save original time for it.
if (DatePickerUtils.sameDate(dt, lastDate)) result = lastDate;
return result;
}
}
class _DayCell extends StatelessWidget {
/// Day for this cell.
final DateTime day;
/// Selection logic.
final ISelectablePicker selectablePicker;
/// Styles what can be customized by user
final DatePickerRangeStyles datePickerStyles;
/// The current date at the time the picker is displayed.
final DateTime currentDate;
/// Builder to get event decoration for each date.
///
/// All event styles are overridden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
final MaterialLocalizations localizations;
const _DayCell(
{Key? key,
required this.day,
required this.selectablePicker,
required this.datePickerStyles,
required this.currentDate,
required this.localizations,
this.eventDecorationBuilder})
: super(key: key);
@override
Widget build(BuildContext context) {
DayType dayType = selectablePicker.getDayType(day);
BoxDecoration? decoration;
TextStyle? itemStyle;
if (dayType != DayType.disabled && dayType != DayType.notSelected) {
itemStyle = _getSelectedTextStyle(dayType);
decoration = _getSelectedDecoration(dayType);
} else if (dayType == DayType.disabled) {
itemStyle = datePickerStyles.disabledDateStyle;
} else if (DatePickerUtils.sameDate(currentDate, day)) {
itemStyle = datePickerStyles.currentDateStyle;
} else {
itemStyle = datePickerStyles.defaultDateTextStyle;
}
// Merges decoration and textStyle with [EventDecoration].
//
// Merges only in cases if [dayType] is DayType.notSelected.
// If day is current day it is also gets event decoration
// instead of decoration for current date.
if (dayType == DayType.notSelected && eventDecorationBuilder != null) {
EventDecoration? eDecoration = eventDecorationBuilder != null
? eventDecorationBuilder!.call(day)
: null;
decoration = eDecoration?.boxDecoration ?? decoration;
itemStyle = eDecoration?.textStyle ?? itemStyle;
}
String semanticLabel = '${localizations.formatDecimal(day.day)}, '
'${localizations.formatFullDate(day)}';
bool daySelected =
dayType != DayType.disabled && dayType != DayType.notSelected;
Widget dayWidget = Container(
decoration: decoration,
child: Center(
child: Semantics(
// We want the day of month to be spoken first irrespective of the
// locale-specific preferences or TextDirection. This is because
// an accessibility user is more likely to be interested in the
// day of month before the rest of the date, as they are looking
// for the day of month. To do that we prepend day of month to the
// formatted full date.
label: semanticLabel,
selected: daySelected,
child: ExcludeSemantics(
child: Text(localizations.formatDecimal(day.day), style: itemStyle),
),
),
),
);
return dayWidget;
}
BoxDecoration? _getSelectedDecoration(DayType dayType) {
BoxDecoration? result;
if (dayType == DayType.single) {
result = datePickerStyles.selectedSingleDateDecoration;
} else if (dayType == DayType.start) {
result = datePickerStyles.selectedPeriodStartDecoration;
} else if (dayType == DayType.end) {
result = datePickerStyles.selectedPeriodLastDecoration;
} else {
result = datePickerStyles.selectedPeriodMiddleDecoration;
}
return result;
}
TextStyle? _getSelectedTextStyle(DayType dayType) {
TextStyle? result;
if (dayType == DayType.single) {
result = datePickerStyles.selectedDateStyle;
} else if (dayType == DayType.start) {
result = datePickerStyles.selectedPeriodStartTextStyle;
} else if (dayType == DayType.end) {
result = datePickerStyles.selectedPeriodEndTextStyle;
} else {
result = datePickerStyles.selectedPeriodMiddleTextStyle;
}
return result;
}
}

View File

@ -0,0 +1,13 @@
/// Date period.
class DatePeriod {
/// Start of this period.
final DateTime start;
/// End of this period.
final DateTime end;
///
const DatePeriod(this.start, this.end)
: assert(start != null),
assert(end != null);
}

View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
/// Keys for some date picker's widgets.
///
/// Useful for integration tests to find widgets.
class DatePickerKeys {
/// Key for the previous page icon widget.
final Key previousPageIconKey;
/// Key for the next page icon widget.
final Key nextPageIconKey;
/// Key for showing month.
final Key selectedPeriodKeys;
///
DatePickerKeys(
this.previousPageIconKey, this.nextPageIconKey, this.selectedPeriodKeys);
}

View File

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'date_picker_styles.dart';
///
mixin CommonDatePickerFunctions {
/// Builds widgets showing abbreviated days of week. The first widget in the
/// returned list corresponds to the first day of week for the current locale.
///
/// Examples:
///
/// ```
/// ┌ Sunday is the first day of week in the US (en_US)
/// |
/// S M T W T F S <-- the returned list contains these widgets
/// _ _ _ _ _ 1 2
/// 3 4 5 6 7 8 9
///
/// ┌ But it's Monday in the UK (en_GB)
/// |
/// M T W T F S S <-- the returned list contains these widgets
/// _ _ _ _ 1 2 3
/// 4 5 6 7 8 9 10
/// ```
List<Widget> getDayHeaders(
DayHeaderStyleBuilder headerStyleBuilder,
List<String> narrowWeekdays,
int firstDayOfWeekIndex) {
final List<Widget> result = <Widget>[];
for (int i = firstDayOfWeekIndex; true; i = (i + 1) % 7) {
DayHeaderStyle? headerStyle = headerStyleBuilder(i);
final String weekday = narrowWeekdays[i];
Widget header = ExcludeSemantics(
child: Container(
decoration: headerStyle?.decoration,
child: Center(
child: Text(
weekday,
style: headerStyle?.textStyle
)
),
),
);
result.add(header);
if (i == (firstDayOfWeekIndex - 1) % 7) {
break;
}
}
return result;
}
/// Computes the offset from the first day of week that the first day of the
/// [month] falls on.
///
/// For example, September 1, 2017 falls on a Friday, which in the calendar
/// localized for United States English appears as:
///
/// ```
/// S M T W T F S
/// _ _ _ _ _ 1 2
/// ```
///
/// The offset for the first day of the months is the number of leading blanks
/// in the calendar, i.e. 5.
///
/// The same date localized for the Russian calendar has a different offset,
/// because the first day of week is Monday rather than Sunday:
///
/// ```
/// M T W T F S S
/// _ _ _ _ 1 2 3
/// ```
///
/// So the offset is 4, rather than 5.
///
/// This code consolidates the following:
///
/// - [DateTime.weekday] provides a 1-based index into days of week, with 1
/// falling on Monday.
/// - [MaterialLocalizations.firstDayOfWeekIndex] provides a 0-based index
/// into the [MaterialLocalizations.narrowWeekdays] list.
/// - [MaterialLocalizations.narrowWeekdays] list provides localized names of
/// days of week, always starting with Sunday and ending with Saturday.
int computeFirstDayOffset(
int year, int month, int firstDayOfWeekFromSunday) {
// 0-based day of week, with 0 representing Monday.
final int weekdayFromMonday = DateTime(year, month).weekday - 1;
// firstDayOfWeekFromSunday recomputed to be Monday-based
final int firstDayOfWeekFromMonday = (firstDayOfWeekFromSunday - 1) % 7;
// Number of days between the first day of week appearing on the calendar,
// and the day corresponding to the 1-st of the month.
return (weekdayFromMonday - firstDayOfWeekFromMonday) % 7;
}
}

View File

@ -0,0 +1,386 @@
import 'package:flutter/material.dart';
import 'range_picker.dart';
import 'week_picker.dart';
/// 0 points to Sunday, and 6 points to Saturday.
typedef DayHeaderStyleBuilder = DayHeaderStyle? Function(int dayOfTheWeek);
/// Common styles for date pickers.
///
/// To define more styles for date pickers which allow select some range
/// (e.g. [RangePicker], [WeekPicker]) use [DatePickerRangeStyles].
@immutable
class DatePickerStyles {
/// Styles for title of displayed period
/// (e.g. month for day picker and year for month picker).
final TextStyle? displayedPeriodTitle;
/// Style for the number of current date.
final TextStyle? currentDateStyle;
/// Style for the numbers of disabled dates.
final TextStyle? disabledDateStyle;
/// Style for the number of selected date.
final TextStyle? selectedDateStyle;
/// Used for date which is neither current nor disabled nor selected.
final TextStyle? defaultDateTextStyle;
/// Day cell decoration for selected date in case only one date is selected.
final BoxDecoration? selectedSingleDateDecoration;
/// Style for the day header.
///
/// If you need to customize day header's style depends on day of the week
/// use [dayHeaderStyleBuilder] instead.
final DayHeaderStyle? dayHeaderStyle;
/// Builder to customize styles for day headers depends on day of the week.
/// Where 0 points to Sunday and 6 points to Saturday.
///
/// Builder must return not null value for every weekday from 0 to 6.
///
/// If styles should be the same for any day of the week
/// use [dayHeaderStyle] instead.
final DayHeaderStyleBuilder? dayHeaderStyleBuilder;
/// Widget which will be shown left side of the shown page title.
/// User goes to previous data period by click on it.
final Widget prevIcon;
/// Widget which will be shown right side of the shown page title.
/// User goes to next data period by click on it.
final Widget nextIcon;
/// Index of the first day of week, where 0 points to Sunday, and 6 points to
/// Saturday. Must not be less 0 or more then 6.
///
/// Can be null. In this case value from current locale will be used.
final int? firstDayOfeWeekIndex;
/// Styles for date picker.
DatePickerStyles({
this.displayedPeriodTitle,
this.currentDateStyle,
this.disabledDateStyle,
this.selectedDateStyle,
this.selectedSingleDateDecoration,
this.defaultDateTextStyle,
this.dayHeaderStyleBuilder,
this.dayHeaderStyle,
this.firstDayOfeWeekIndex,
this.prevIcon = const Icon(Icons.chevron_left),
this.nextIcon = const Icon(Icons.chevron_right)
}) : assert(!(dayHeaderStyle != null && dayHeaderStyleBuilder != null),
"Should be only one from: dayHeaderStyleBuilder, dayHeaderStyle."),
assert(dayHeaderStyleBuilder == null
|| _validateDayHeaderStyleBuilder(dayHeaderStyleBuilder),
"dayHeaderStyleBuilder must return not null value from every weekday "
"(from 0 to 6)."),
assert(_validateFirstDayOfWeek(firstDayOfeWeekIndex),
"firstDayOfeWeekIndex must be null or in correct range (from 0 to 6).");
/// Return new [DatePickerStyles] object where fields
/// with null values set with defaults from theme.
DatePickerStyles fulfillWithTheme(ThemeData theme) {
Color accentColor = theme.accentColor;
TextStyle? _displayedPeriodTitle =
displayedPeriodTitle ?? theme.textTheme.subtitle1;
TextStyle? _currentDateStyle = currentDateStyle ??
theme.textTheme.bodyText1?.copyWith(color: theme.accentColor);
TextStyle? _disabledDateStyle = disabledDateStyle ??
theme.textTheme.bodyText2?.copyWith(color: theme.disabledColor);
TextStyle? _selectedDateStyle =
selectedDateStyle ?? theme.accentTextTheme.bodyText1;
TextStyle? _defaultDateTextStyle =
defaultDateTextStyle ?? theme.textTheme.bodyText2;
BoxDecoration _selectedSingleDateDecoration =
selectedSingleDateDecoration ??
BoxDecoration(
color: accentColor,
borderRadius: const BorderRadius.all(Radius.circular(10.0)));
DayHeaderStyle? _dayHeaderStyle = dayHeaderStyle;
if (dayHeaderStyleBuilder == null && _dayHeaderStyle == null) {
_dayHeaderStyle = DayHeaderStyle(textStyle: theme.textTheme.caption);
}
return DatePickerStyles(
disabledDateStyle: _disabledDateStyle,
currentDateStyle: _currentDateStyle,
displayedPeriodTitle: _displayedPeriodTitle,
selectedDateStyle: _selectedDateStyle,
selectedSingleDateDecoration: _selectedSingleDateDecoration,
defaultDateTextStyle: _defaultDateTextStyle,
dayHeaderStyle: _dayHeaderStyle,
dayHeaderStyleBuilder: dayHeaderStyleBuilder,
nextIcon: nextIcon,
prevIcon: prevIcon
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is DatePickerStyles
&& other.displayedPeriodTitle == displayedPeriodTitle
&& other.currentDateStyle == currentDateStyle
&& other.disabledDateStyle == disabledDateStyle
&& other.selectedDateStyle == selectedDateStyle
&& other.defaultDateTextStyle == defaultDateTextStyle
&& other.selectedSingleDateDecoration == selectedSingleDateDecoration
&& other.dayHeaderStyle == dayHeaderStyle
&& other.dayHeaderStyleBuilder == dayHeaderStyleBuilder
&& other.prevIcon == prevIcon
&& other.nextIcon == nextIcon
&& other.firstDayOfeWeekIndex == firstDayOfeWeekIndex;
}
@override
int get hashCode =>
hashValues(
displayedPeriodTitle,
currentDateStyle,
disabledDateStyle,
selectedDateStyle,
defaultDateTextStyle,
selectedSingleDateDecoration,
dayHeaderStyle,
dayHeaderStyleBuilder,
prevIcon,
nextIcon,
firstDayOfeWeekIndex
);
static bool _validateDayHeaderStyleBuilder(DayHeaderStyleBuilder builder) {
List<int> weekdays = const [0, 1, 2, 3, 4, 5, 6];
// ignore: avoid_types_on_closure_parameters
bool valid = weekdays.every((int weekday) => builder(weekday) != null);
return valid;
}
static bool _validateFirstDayOfWeek(int? index) {
if (index == null) return true;
bool valid = index >= 0 && index <= 6;
return valid;
}
}
/// Styles for date pickers which allow select some range
/// (e.g. RangePicker, WeekPicker).
@immutable
class DatePickerRangeStyles extends DatePickerStyles {
/// Decoration for the first date of the selected range.
final BoxDecoration? selectedPeriodStartDecoration;
/// Text style for the first date of the selected range.
///
/// If null - default [DatePickerStyles.selectedDateStyle] will be used.
final TextStyle? selectedPeriodStartTextStyle;
/// Decoration for the last date of the selected range.
final BoxDecoration? selectedPeriodLastDecoration;
/// Text style for the last date of the selected range.
///
/// If null - default [DatePickerStyles.selectedDateStyle] will be used.
final TextStyle? selectedPeriodEndTextStyle;
/// Decoration for the date of the selected range
/// which is not first date and not end date of this range.
///
/// If there is only one date selected
/// [DatePickerStyles.selectedSingleDateDecoration] will be used.
final BoxDecoration? selectedPeriodMiddleDecoration;
/// Text style for the middle date of the selected range.
///
/// If null - default [DatePickerStyles.selectedDateStyle] will be used.
final TextStyle? selectedPeriodMiddleTextStyle;
/// Return new [DatePickerRangeStyles] object
/// where fields with null values set with defaults from given theme.
@override
DatePickerRangeStyles fulfillWithTheme(ThemeData theme) {
Color accentColor = theme.accentColor;
DatePickerStyles commonStyles = super.fulfillWithTheme(theme);
final BoxDecoration _selectedPeriodStartDecoration =
selectedPeriodStartDecoration ??
BoxDecoration(
color: accentColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10.0),
bottomLeft: Radius.circular(10.0)),
);
final BoxDecoration _selectedPeriodLastDecoration =
selectedPeriodLastDecoration ??
BoxDecoration(
color: accentColor,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10.0),
bottomRight: Radius.circular(10.0)),
);
final BoxDecoration _selectedPeriodMiddleDecoration =
selectedPeriodMiddleDecoration ??
BoxDecoration(
color: accentColor,
shape: BoxShape.rectangle,
);
final TextStyle? _selectedPeriodStartTextStyle =
selectedPeriodStartTextStyle ?? commonStyles.selectedDateStyle;
final TextStyle? _selectedPeriodMiddleTextStyle =
selectedPeriodMiddleTextStyle ?? commonStyles.selectedDateStyle;
final TextStyle? _selectedPeriodEndTextStyle =
selectedPeriodEndTextStyle ?? commonStyles.selectedDateStyle;
return DatePickerRangeStyles(
disabledDateStyle: commonStyles.disabledDateStyle,
currentDateStyle: commonStyles.currentDateStyle,
displayedPeriodTitle: commonStyles.displayedPeriodTitle,
selectedDateStyle: commonStyles.selectedDateStyle,
selectedSingleDateDecoration: commonStyles.selectedSingleDateDecoration,
defaultDateTextStyle: commonStyles.defaultDateTextStyle,
dayHeaderStyle: commonStyles.dayHeaderStyle,
dayHeaderStyleBuilder: commonStyles.dayHeaderStyleBuilder,
firstDayOfWeekIndex: firstDayOfeWeekIndex,
selectedPeriodStartDecoration: _selectedPeriodStartDecoration,
selectedPeriodMiddleDecoration: _selectedPeriodMiddleDecoration,
selectedPeriodLastDecoration: _selectedPeriodLastDecoration,
selectedPeriodStartTextStyle: _selectedPeriodStartTextStyle,
selectedPeriodMiddleTextStyle: _selectedPeriodMiddleTextStyle,
selectedPeriodEndTextStyle: _selectedPeriodEndTextStyle,
);
}
/// Styles for the pickers that allows to select range ([RangePicker],
/// [WeekPicker]).
DatePickerRangeStyles({
displayedPeriodTitle,
currentDateStyle,
disabledDateStyle,
selectedDateStyle,
selectedSingleDateDecoration,
defaultDateTextStyle,
dayHeaderStyle,
dayHeaderStyleBuilder,
Widget nextIcon = const Icon(Icons.chevron_right),
Widget prevIcon = const Icon(Icons.chevron_left),
firstDayOfWeekIndex,
this.selectedPeriodLastDecoration,
this.selectedPeriodMiddleDecoration,
this.selectedPeriodStartDecoration,
this.selectedPeriodStartTextStyle,
this.selectedPeriodMiddleTextStyle,
this.selectedPeriodEndTextStyle,
}) : super(
displayedPeriodTitle: displayedPeriodTitle,
currentDateStyle: currentDateStyle,
disabledDateStyle: disabledDateStyle,
selectedDateStyle: selectedDateStyle,
selectedSingleDateDecoration: selectedSingleDateDecoration,
defaultDateTextStyle: defaultDateTextStyle,
dayHeaderStyle: dayHeaderStyle,
dayHeaderStyleBuilder: dayHeaderStyleBuilder,
nextIcon: nextIcon,
prevIcon: prevIcon,
firstDayOfeWeekIndex: firstDayOfWeekIndex
);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is DatePickerRangeStyles
&& other.selectedPeriodStartDecoration == selectedPeriodStartDecoration
&& other.selectedPeriodStartTextStyle == selectedPeriodStartTextStyle
&& other.selectedPeriodLastDecoration == selectedPeriodLastDecoration
&& other.selectedPeriodEndTextStyle == selectedPeriodEndTextStyle
&& other.selectedPeriodMiddleDecoration ==selectedPeriodMiddleDecoration
&& other.selectedPeriodMiddleTextStyle == selectedPeriodMiddleTextStyle
&& other.displayedPeriodTitle == displayedPeriodTitle
&& other.currentDateStyle == currentDateStyle
&& other.disabledDateStyle == disabledDateStyle
&& other.selectedDateStyle == selectedDateStyle
&& other.defaultDateTextStyle == defaultDateTextStyle
&& other.selectedSingleDateDecoration == selectedSingleDateDecoration
&& other.dayHeaderStyle == dayHeaderStyle
&& other.dayHeaderStyleBuilder == dayHeaderStyleBuilder
&& other.prevIcon == prevIcon
&& other.nextIcon == nextIcon
&& other.firstDayOfeWeekIndex == firstDayOfeWeekIndex;
}
@override
int get hashCode =>
hashValues(
selectedPeriodStartDecoration,
selectedPeriodStartTextStyle,
selectedPeriodLastDecoration,
selectedPeriodEndTextStyle,
selectedPeriodMiddleDecoration,
selectedPeriodMiddleTextStyle,
displayedPeriodTitle,
currentDateStyle,
disabledDateStyle,
selectedDateStyle,
defaultDateTextStyle,
selectedSingleDateDecoration,
dayHeaderStyle,
dayHeaderStyleBuilder,
prevIcon,
nextIcon,
firstDayOfeWeekIndex
);
}
/// User styles for the day header in date picker.
@immutable
class DayHeaderStyle {
/// If null - textTheme.caption from the Theme will be used.
final TextStyle? textStyle;
/// If null - no decoration will be applied for the day header;
final BoxDecoration? decoration;
/// Creates styles for the day headers in date pickers.
///
/// See also:
/// * [DatePickerStyles.dayHeaderStyleBuilder]
const DayHeaderStyle({
this.textStyle,
this.decoration
});
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is DayHeaderStyle
&& other.textStyle == textStyle
&& other.decoration == decoration;
}
@override
int get hashCode => hashValues(
textStyle,
decoration
);
}

View File

@ -0,0 +1,363 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'basic_day_based_widget.dart';
import 'date_picker_keys.dart';
import 'date_picker_styles.dart';
import 'day_based_changeable_picker_presenter.dart';
import 'day_picker_selection.dart';
import 'day_type.dart';
import 'event_decoration.dart';
import 'i_selectable_picker.dart';
import 'layout_settings.dart';
import 'month_navigation_row.dart';
import 'semantic_sorting.dart';
import 'typedefs.dart';
import 'utils.dart';
const Locale _defaultLocale = Locale('en', 'US');
/// Date picker based on [DayBasedPicker] picker (for days, weeks, ranges).
/// Allows select previous/next month.
class DayBasedChangeablePicker<T> extends StatefulWidget {
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DayPickerSelection selection;
/// Called when the user picks a new T.
final ValueChanged<T> onChanged;
/// Called when the error was thrown after user selection.
final OnSelectionError? onSelectionError;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// Date for defining what month should be shown initially.
///
/// Default value is [selection.earliest].
final DateTime initiallyShowDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Styles what can be customized by user
final DatePickerRangeStyles datePickerStyles;
/// Some keys useful for integration tests
final DatePickerKeys? datePickerKeys;
/// Logic for date selections.
final ISelectablePicker<T> selectablePicker;
/// Builder to get event decoration for each date.
///
/// All event styles are overridden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
/// Called when the user changes the month
final ValueChanged<DateTime>? onMonthChanged;
/// Create picker with option to change month.
DayBasedChangeablePicker(
{Key? key,
required this.selection,
required this.onChanged,
required this.firstDate,
required this.lastDate,
required this.datePickerLayoutSettings,
required this.datePickerStyles,
required this.selectablePicker,
DateTime? initiallyShownDate,
this.datePickerKeys,
this.onSelectionError,
this.eventDecorationBuilder,
this.onMonthChanged})
: initiallyShowDate = initiallyShownDate ?? selection.earliest,
super(key: key);
@override
State<DayBasedChangeablePicker<T>> createState() =>
_DayBasedChangeablePickerState<T>();
}
// todo: Check initial selection and call onSelectionError in case it has error
// todo: (ISelectablePicker.curSelectionIsCorrupted);
class _DayBasedChangeablePickerState<T>
extends State<DayBasedChangeablePicker<T>> {
DateTime _todayDate = DateTime.now();
Locale curLocale = _defaultLocale;
MaterialLocalizations localizations = _defaultLocalizations;
PageController _dayPickerController = PageController();
// Styles from widget fulfilled with current Theme.
DatePickerRangeStyles _resultStyles = DatePickerRangeStyles();
DayBasedChangeablePickerPresenter _presenter = _defaultPresenter;
Timer? _timer;
StreamSubscription<T>? _changesSubscription;
@override
void initState() {
super.initState();
// Initially display the pre-selected date.
final int monthPage = _getInitPage();
_dayPickerController = PageController(initialPage: monthPage);
_changesSubscription = widget.selectablePicker.onUpdate
.listen((newSelectedDate) => widget.onChanged(newSelectedDate))
..onError((e) => widget.onSelectionError != null
? widget.onSelectionError!.call(e)
: print(e.toString()));
_updateCurrentDate();
_initPresenter();
}
@override
void didUpdateWidget(DayBasedChangeablePicker<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.datePickerStyles != oldWidget.datePickerStyles) {
final ThemeData theme = Theme.of(context);
_resultStyles = widget.datePickerStyles.fulfillWithTheme(theme);
}
if (widget.selectablePicker != oldWidget.selectablePicker) {
_changesSubscription = widget.selectablePicker.onUpdate
.listen((newSelectedDate) => widget.onChanged(newSelectedDate))
..onError((e) => widget.onSelectionError != null
? widget.onSelectionError!.call(e)
: print(e.toString()));
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
curLocale = Localizations.localeOf(context);
MaterialLocalizations? curLocalizations =
Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
if (curLocalizations != null && localizations != curLocalizations) {
localizations = curLocalizations;
_initPresenter();
}
final ThemeData theme = Theme.of(context);
_resultStyles = widget.datePickerStyles.fulfillWithTheme(theme);
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return SizedBox(
width: widget.datePickerLayoutSettings.monthPickerPortraitWidth,
height: widget.datePickerLayoutSettings.maxDayPickerHeight,
child: Column(
children: <Widget>[
widget.datePickerLayoutSettings.hideMonthNavigationRow
? const SizedBox()
: SizedBox(
height: widget.datePickerLayoutSettings.dayPickerRowHeight,
child: Padding(
//match _DayPicker main layout padding
padding: widget.datePickerLayoutSettings.contentPadding,
child: _buildMonthNavigationRow()),
),
Expanded(
child: Semantics(
sortKey: MonthPickerSortKey.calendar,
child: _buildDayPickerPageView(),
),
),
],
));
}
@override
void dispose() {
_timer?.cancel();
_dayPickerController.dispose();
_changesSubscription?.cancel();
widget.selectablePicker.dispose();
_presenter.dispose();
super.dispose();
}
void _updateCurrentDate() {
_todayDate = DateTime.now();
final DateTime tomorrow =
DateTime(_todayDate.year, _todayDate.month, _todayDate.day + 1);
Duration timeUntilTomorrow = tomorrow.difference(_todayDate);
timeUntilTomorrow +=
const Duration(seconds: 1); // so we don't miss it by rounding
_timer?.cancel();
_timer = Timer(timeUntilTomorrow, () {
setState(_updateCurrentDate);
});
}
// ignore: prefer_expression_function_bodies
Widget _buildMonthNavigationRow() {
return StreamBuilder<DayBasedChangeablePickerState>(
stream: _presenter.data,
initialData: _presenter.lastVal,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
DayBasedChangeablePickerState state = snapshot.data!;
return MonthNavigationRow(
previousPageIconKey: widget.datePickerKeys?.previousPageIconKey,
nextPageIconKey: widget.datePickerKeys?.nextPageIconKey,
previousMonthTooltip: state.prevTooltip,
nextMonthTooltip: state.nextTooltip,
onPreviousMonthTapped:
state.isFirstMonth ? null : _presenter.gotoPrevMonth,
onNextMonthTapped:
state.isLastMonth ? null : _presenter.gotoNextMonth,
title: Text(
state.curMonthDis,
key: widget.datePickerKeys?.selectedPeriodKeys,
style: _resultStyles.displayedPeriodTitle,
),
nextIcon: widget.datePickerStyles.nextIcon,
prevIcon: widget.datePickerStyles.prevIcon,
);
});
}
Widget _buildDayPickerPageView() => PageView.builder(
controller: _dayPickerController,
scrollDirection: Axis.horizontal,
itemCount:
DatePickerUtils.monthDelta(widget.firstDate, widget.lastDate) + 1,
itemBuilder: _buildCalendar,
onPageChanged: _handleMonthPageChanged,
);
Widget _buildCalendar(BuildContext context, int index) {
final DateTime targetDate =
DatePickerUtils.addMonthsToMonthDate(widget.firstDate, index);
return DayBasedPicker(
key: ValueKey<DateTime>(targetDate),
selectablePicker: widget.selectablePicker,
currentDate: _todayDate,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
displayedMonth: targetDate,
datePickerLayoutSettings: widget.datePickerLayoutSettings,
selectedPeriodKey: widget.datePickerKeys?.selectedPeriodKeys,
datePickerStyles: _resultStyles,
eventDecorationBuilder: widget.eventDecorationBuilder,
localizations: localizations,
);
}
// Returns appropriate date to be shown for init.
// If [widget.initiallyShowDate] is out of bounds [widget.firstDate]
// - [widget.lastDate], nearest bound will be used.
DateTime _getCheckedInitialDate() {
DateTime initiallyShowDateChecked = widget.initiallyShowDate;
if (initiallyShowDateChecked.isBefore(widget.firstDate)) {
initiallyShowDateChecked = widget.firstDate;
}
if (initiallyShowDateChecked.isAfter(widget.lastDate)) {
initiallyShowDateChecked = widget.lastDate;
}
return initiallyShowDateChecked;
}
int _getInitPage() {
final initialDate = _getCheckedInitialDate();
int initPage = DatePickerUtils.monthDelta(
widget.firstDate, initialDate
);
return initPage;
}
void _initPresenter() {
_presenter.dispose();
_presenter = DayBasedChangeablePickerPresenter(
firstDate: widget.firstDate,
lastDate: widget.lastDate,
localizations: localizations,
showPrevMonthDates: widget.datePickerLayoutSettings.showPrevMonthEnd,
showNextMonthDates: widget.datePickerLayoutSettings.showNextMonthStart,
firstDayOfWeekIndex: widget.datePickerStyles.firstDayOfeWeekIndex);
_presenter.data.listen(_onStateChanged);
// date used to define what month should be shown
DateTime initSelection = _getCheckedInitialDate();
// Give information about initial selection to presenter.
// It should be done after first frame when PageView is already created.
// Otherwise event from presenter will cause a error.
WidgetsBinding.instance!.addPostFrameCallback((_) {
_presenter.setSelectedDate(initSelection);
});
}
void _onStateChanged(DayBasedChangeablePickerState newState) {
DateTime newMonth = newState.currentMonth;
final int monthPage =
DatePickerUtils.monthDelta(widget.firstDate, newMonth);
_dayPickerController.animateToPage(monthPage,
duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
}
void _handleMonthPageChanged(int monthPage) {
DateTime firstMonth = widget.firstDate;
DateTime newMonth = DateTime(firstMonth.year, firstMonth.month + monthPage);
_presenter.changeMonth(newMonth);
widget.onMonthChanged?.call(newMonth);
}
static MaterialLocalizations get _defaultLocalizations =>
MaterialLocalizationEn(
twoDigitZeroPaddedFormat:
intl.NumberFormat('00', _defaultLocale.toString()),
fullYearFormat: intl.DateFormat.y(_defaultLocale.toString()),
longDateFormat: intl.DateFormat.yMMMMEEEEd(_defaultLocale.toString()),
shortMonthDayFormat: intl.DateFormat.MMMd(_defaultLocale.toString()),
decimalFormat:
intl.NumberFormat.decimalPattern(_defaultLocale.toString()),
shortDateFormat: intl.DateFormat.yMMMd(_defaultLocale.toString()),
mediumDateFormat: intl.DateFormat.MMMEd(_defaultLocale.toString()),
compactDateFormat: intl.DateFormat.yMd(_defaultLocale.toString()),
yearMonthFormat: intl.DateFormat.yMMMM(_defaultLocale.toString()),
);
static DayBasedChangeablePickerPresenter get _defaultPresenter =>
DayBasedChangeablePickerPresenter(
firstDate: DateTime.now(),
lastDate: DateTime.now(),
localizations: _defaultLocalizations,
showPrevMonthDates: false,
showNextMonthDates: false,
firstDayOfWeekIndex: 1);
}

View File

@ -0,0 +1,177 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'day_based_changable_picker.dart';
import 'utils.dart';
/// Presenter for [DayBasedChangeablePicker] to handle month changes.
class DayBasedChangeablePickerPresenter {
/// First date user can select.
final DateTime firstDate;
/// Last date user can select.
final DateTime lastDate;
/// Localization.
final MaterialLocalizations localizations;
/// If empty day cells before 1st day of showing month should be filled with
/// date from the last week of the previous month.
final bool showPrevMonthDates;
/// If empty day cells after last day of showing month should be filled with
/// date from the first week of the next month.
final bool showNextMonthDates;
/// Index of the first day in week.
/// 0 is Sunday, 6 is Saturday.
final int firstDayOfWeekIndex;
/// View model stream for the [DayBasedChangeablePicker].
Stream<DayBasedChangeablePickerState> get data => _controller.stream;
/// Last view model state of the [DayBasedChangeablePicker].
DayBasedChangeablePickerState? get lastVal => _lastVal;
/// Creates presenter to use for [DayBasedChangeablePicker].
DayBasedChangeablePickerPresenter({
required this.firstDate,
required this.lastDate,
required this.localizations,
required this.showPrevMonthDates,
required this.showNextMonthDates,
int? firstDayOfWeekIndex
}): firstDayOfWeekIndex = firstDayOfWeekIndex
?? localizations.firstDayOfWeekIndex;
/// Update state according to the [selectedDate] if it needs.
void setSelectedDate(DateTime selectedDate) {
// bool firstAndLastNotNull = _firstShownDate != null
// && _lastShownDate != null;
//
// bool selectedOnCurPage = firstAndLastNotNull
// && !selectedDate.isBefore(_firstShownDate)
// && !selectedDate.isAfter(_lastShownDate);
// if (selectedOnCurPage) return;
changeMonth(selectedDate);
}
/// Update state to show previous month.
void gotoPrevMonth() {
DateTime oldCur = _lastVal!.currentMonth;
DateTime newCurDate = DateTime(oldCur.year, oldCur.month - 1);
changeMonth(newCurDate);
}
/// Update state to show next month.
void gotoNextMonth() {
DateTime oldCur = _lastVal!.currentMonth;
DateTime newCurDate = DateTime(oldCur.year, oldCur.month + 1);
changeMonth(newCurDate);
}
/// Update state to change month to the [newMonth].
void changeMonth(DateTime newMonth) {
bool sameMonth = _lastVal != null
&& DatePickerUtils.sameMonth(_lastVal!.currentMonth, newMonth);
if (sameMonth) return;
int monthPage = DatePickerUtils.monthDelta(firstDate, newMonth);
DateTime prevMonth = DatePickerUtils
.addMonthsToMonthDate(firstDate, monthPage - 1);
DateTime curMonth = DatePickerUtils
.addMonthsToMonthDate(firstDate, monthPage);
DateTime nextMonth = DatePickerUtils
.addMonthsToMonthDate(firstDate, monthPage + 1);
String prevMonthStr = localizations.formatMonthYear(prevMonth);
String curMonthStr = localizations.formatMonthYear(curMonth);
String nextMonthStr = localizations.formatMonthYear(nextMonth);
bool isFirstMonth = DatePickerUtils.sameMonth(curMonth, firstDate);
bool isLastMonth = DatePickerUtils.sameMonth(curMonth, lastDate);
String? prevTooltip = isFirstMonth
? null
: "${localizations.previousMonthTooltip} $prevMonthStr";
String? nextTooltip = isLastMonth
? null
: "${localizations.nextMonthTooltip} $nextMonthStr";
DayBasedChangeablePickerState newState = DayBasedChangeablePickerState(
currentMonth: curMonth,
curMonthDis: curMonthStr,
prevMonthDis: prevMonthStr,
nextMonthDis: nextMonthStr,
prevTooltip: prevTooltip,
nextTooltip: nextTooltip,
isFirstMonth: isFirstMonth,
isLastMonth: isLastMonth
);
_updateState(newState);
}
/// Closes controller.
void dispose () {
_controller.close();
}
void _updateState(DayBasedChangeablePickerState newState) {
_lastVal = newState;
_controller.add(newState);
}
final StreamController<DayBasedChangeablePickerState> _controller =
StreamController.broadcast();
DayBasedChangeablePickerState? _lastVal;
}
/// View Model for the [DayBasedChangeablePicker].
class DayBasedChangeablePickerState {
/// Display name of the current month.
final String curMonthDis;
/// Display name of the previous month.
final String prevMonthDis;
/// Display name of the next month.
final String nextMonthDis;
/// Tooltip for the previous month icon.
final String? prevTooltip;
/// Tooltip for the next month icon.
final String? nextTooltip;
/// Tooltip for the current month icon.
final DateTime currentMonth;
/// If selected month is the month contains last date user can select.
final bool isLastMonth;
/// If selected month is the month contains first date user can select.
final bool isFirstMonth;
/// Creates view model for the [DayBasedChangeablePicker].
DayBasedChangeablePickerState({
required this.curMonthDis,
required this.prevMonthDis,
required this.nextMonthDis,
required this.currentMonth,
required this.isLastMonth,
required this.isFirstMonth,
this.prevTooltip,
this.nextTooltip,
});
}

View File

@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
import 'date_picker_keys.dart';
import 'date_picker_styles.dart';
import 'day_based_changable_picker.dart';
import 'day_picker_selection.dart';
import 'day_type.dart';
import 'event_decoration.dart';
import 'i_selectable_picker.dart';
import 'layout_settings.dart';
/// Date picker for selection one day.
class DayPicker<T extends Object> extends StatelessWidget {
DayPicker._({Key? key,
required this.onChanged,
required this.firstDate,
required this.lastDate,
required this.selectionLogic,
required this.selection,
this.initiallyShowDate,
this.datePickerLayoutSettings = const DatePickerLayoutSettings(),
this.datePickerStyles,
this.datePickerKeys,
this.selectableDayPredicate,
this.eventDecorationBuilder,
this.onMonthChanged}) : super(key: key);
/// Creates a day picker where only one single day can be selected.
///
/// See also:
/// * [DayPicker.multi] - day picker where many single days can be selected.
static DayPicker<DateTime> single({
Key? key,
required DateTime selectedDate,
required ValueChanged<DateTime> onChanged,
required DateTime firstDate,
required DateTime lastDate,
DatePickerLayoutSettings datePickerLayoutSettings
= const DatePickerLayoutSettings(),
DateTime? initiallyShowDate,
DatePickerRangeStyles? datePickerStyles,
DatePickerKeys? datePickerKeys,
SelectableDayPredicate? selectableDayPredicate,
EventDecorationBuilder? eventDecorationBuilder,
ValueChanged<DateTime>? onMonthChanged
})
{
assert(!firstDate.isAfter(lastDate));
assert(!lastDate.isBefore(firstDate));
assert(!selectedDate.isBefore(firstDate));
assert(!selectedDate.isAfter(lastDate));
assert(initiallyShowDate == null
|| !initiallyShowDate.isAfter(lastDate));
assert(initiallyShowDate == null
|| !initiallyShowDate.isBefore(firstDate));
final selection = DayPickerSingleSelection(selectedDate);
final selectionLogic = DaySelectable(
selectedDate, firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
return DayPicker<DateTime>._(
onChanged: onChanged,
firstDate: firstDate,
lastDate: lastDate,
initiallyShowDate: initiallyShowDate,
selectionLogic: selectionLogic,
selection: selection,
eventDecorationBuilder: eventDecorationBuilder,
onMonthChanged: onMonthChanged,
selectableDayPredicate: selectableDayPredicate,
datePickerKeys: datePickerKeys,
datePickerStyles: datePickerStyles,
datePickerLayoutSettings: datePickerLayoutSettings,
);
}
/// Creates a day picker where many single days can be selected.
///
/// See also:
/// * [DayPicker.single] - day picker where only one single day
/// can be selected.
static DayPicker<List<DateTime>> multi({Key? key,
required List<DateTime> selectedDates,
required ValueChanged<List<DateTime>> onChanged,
required DateTime firstDate,
required DateTime lastDate,
DatePickerLayoutSettings datePickerLayoutSettings
= const DatePickerLayoutSettings(),
DateTime? initiallyShowDate,
DatePickerRangeStyles? datePickerStyles,
DatePickerKeys? datePickerKeys,
SelectableDayPredicate? selectableDayPredicate,
EventDecorationBuilder? eventDecorationBuilder,
ValueChanged<DateTime>? onMonthChanged})
{
assert(!firstDate.isAfter(lastDate));
assert(!lastDate.isBefore(firstDate));
assert(initiallyShowDate == null
|| !initiallyShowDate.isAfter(lastDate));
assert(initiallyShowDate == null
|| !initiallyShowDate.isBefore(lastDate));
final selection = DayPickerMultiSelection(selectedDates);
final selectionLogic = DayMultiSelectable(
selectedDates, firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
return DayPicker<List<DateTime>>._(
onChanged: onChanged,
firstDate: firstDate,
lastDate: lastDate,
initiallyShowDate: initiallyShowDate,
selectionLogic: selectionLogic,
selection: selection,
eventDecorationBuilder: eventDecorationBuilder,
onMonthChanged: onMonthChanged,
selectableDayPredicate: selectableDayPredicate,
datePickerKeys: datePickerKeys,
datePickerStyles: datePickerStyles,
datePickerLayoutSettings: datePickerLayoutSettings,
);
}
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DayPickerSelection selection;
/// Called when the user picks a day.
final ValueChanged<T> onChanged;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// Date for defining what month should be shown initially.
///
/// In case of null earliest of the [selection] will be shown.
final DateTime? initiallyShowDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Styles what can be customized by user
final DatePickerRangeStyles? datePickerStyles;
/// Some keys useful for integration tests
final DatePickerKeys? datePickerKeys;
/// Function returns if day can be selected or not.
///
/// If null
final SelectableDayPredicate? selectableDayPredicate;
/// Builder to get event decoration for each date.
///
/// All event styles are overriden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
// Called when the user changes the month.
/// New DateTime object represents first day of new month and 00:00 time.
final ValueChanged<DateTime>? onMonthChanged;
/// Logic to handle user's selections.
final ISelectablePicker<T> selectionLogic;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return DayBasedChangeablePicker<T>(
selectablePicker: selectionLogic,
selection: selection,
firstDate: firstDate,
lastDate: lastDate,
initiallyShownDate: initiallyShowDate,
onChanged: onChanged,
datePickerLayoutSettings: datePickerLayoutSettings,
datePickerStyles: datePickerStyles ?? DatePickerRangeStyles(),
datePickerKeys: datePickerKeys,
eventDecorationBuilder: eventDecorationBuilder,
onMonthChanged: onMonthChanged,
);
}
}

View File

@ -0,0 +1,120 @@
import 'date_period.dart';
import 'utils.dart';
/// Base class for day based pickers selection.
abstract class DayPickerSelection {
/// If this is before [dateTime].
bool isBefore(DateTime dateTime);
/// If this is after [dateTime].
bool isAfter(DateTime dateTime);
/// Returns earliest [DateTime] in this selection.
DateTime get earliest;
/// If this selection is empty.
bool get isEmpty;
/// If this selection is not empty.
bool get isNotEmpty;
/// Constructor to allow children to have constant constructor.
const DayPickerSelection();
}
/// Selection with only one selected date.
///
/// See also:
/// * [DayPickerMultiSelection] - selection with one or many single dates.
/// * [DayPickerRangeSelection] - date period selection.
class DayPickerSingleSelection extends DayPickerSelection {
/// Selected date.
final DateTime selectedDate;
/// Creates selection with only one selected date.
const DayPickerSingleSelection(this.selectedDate)
: assert(selectedDate != null);
@override
bool isAfter(DateTime dateTime) => selectedDate.isAfter(dateTime);
@override
bool isBefore(DateTime dateTime) => selectedDate.isAfter(dateTime);
@override
DateTime get earliest => selectedDate;
@override
bool get isEmpty => selectedDate == null;
@override
bool get isNotEmpty => selectedDate != null;
}
/// Selection with one or many single dates.
///
/// See also:
/// * [DayPickerSingleSelection] - selection with only one selected date.
/// * [DayPickerRangeSelection] - date period selection.
class DayPickerMultiSelection extends DayPickerSelection {
/// List of the selected dates.
final List<DateTime> selectedDates;
/// Selection with one or many single dates.
DayPickerMultiSelection(this.selectedDates)
: assert(selectedDates != null);
@override
bool isAfter(DateTime dateTime)
=> selectedDates.every((d) => d.isAfter(dateTime));
@override
bool isBefore(DateTime dateTime)
=> selectedDates.every((d) => d.isBefore(dateTime));
@override
DateTime get earliest => DatePickerUtils.getEarliestFromList(selectedDates);
@override
bool get isEmpty => selectedDates.isEmpty;
@override
bool get isNotEmpty => selectedDates.isNotEmpty;
}
/// Date period selection.
///
/// See also:
/// * [DayPickerSingleSelection] - selection with only one selected date.
/// * [DayPickerMultiSelection] - selection with one or many single dates.
class DayPickerRangeSelection extends DayPickerSelection {
/// Selected period.
final DatePeriod selectedRange;
/// Date period selection.
const DayPickerRangeSelection(this.selectedRange)
: assert(selectedRange != null);
@override
DateTime get earliest => selectedRange.start;
@override
bool isAfter(DateTime dateTime) => selectedRange.start.isAfter(dateTime);
@override
bool isBefore(DateTime dateTime) => selectedRange.end.isBefore(dateTime);
@override
bool get isEmpty => selectedRange == null;
@override
bool get isNotEmpty => selectedRange != null;
}

View File

@ -0,0 +1,20 @@
/// Type of the day in day based date picker.
enum DayType {
/// start of the selected period
start,
/// middle of the selected period
middle,
/// end of the selected period
end,
/// selected single day
single,
/// disabled day
disabled,
/// not selected day (but not disabled)
notSelected
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/widgets.dart';
import 'day_picker.dart';
import 'range_picker.dart';
import 'week_picker.dart';
/// Signature for function which is used to set set specific decoration for
/// some days in [DayPicker], [WeekPicker] and [RangePicker].
///
/// See also:
/// * [DayPicker.eventDecorationBuilder]
/// * [WeekPicker.eventDecorationBuilder]
/// * [RangePicker.eventDecorationBuilder]
typedef EventDecorationBuilder = EventDecoration? Function(DateTime date);
/// Class to store styles for event (specific day in the date picker).
@immutable
class EventDecoration {
/// Cell decoration for the specific day in the date picker (event).
final BoxDecoration? boxDecoration;
/// Style for number of the specific day in the date picker (event).
final TextStyle? textStyle;
/// Creates decoration for special day.
///
/// Used for [EventDecorationBuilder] function which is usually passed to
/// [DayPicker.eventDecorationBuilder], [WeekPicker.eventDecorationBuilder]
/// and [RangePicker.eventDecorationBuilder] to set specific decoration for
/// some days.
const EventDecoration({this.boxDecoration, this.textStyle});
}

View File

@ -0,0 +1,537 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'date_period.dart';
import 'day_picker.dart' as day_picker;
import 'day_type.dart';
import 'range_picker.dart';
import 'unselectable_period_error.dart';
import 'utils.dart';
/// Interface for selection logic of the different date pickers.
///
/// T - is selection type.
abstract class ISelectablePicker<T> {
/// The earliest date the user is permitted to pick.
/// (only year, month and day matter, time doesn't matter)
final DateTime firstDate;
/// The latest date the user is permitted to pick.
/// (only year, month and day matter, time doesn't matter)
final DateTime lastDate;
/// Function returns if day can be selected or not.
final SelectableDayPredicate _selectableDayPredicate;
/// StreamController for new selection (T).
@protected
StreamController<T> onUpdateController = StreamController<T>.broadcast();
/// Stream with new selected (T) event.
///
/// Throws [UnselectablePeriodException]
/// if there is any custom disabled date in selected.
Stream<T> get onUpdate => onUpdateController.stream;
/// Constructor with required fields that used in non-abstract methods
/// ([isDisabled]).
ISelectablePicker(this.firstDate, this.lastDate,
{SelectableDayPredicate? selectableDayPredicate})
: _selectableDayPredicate =
selectableDayPredicate ?? _defaultSelectableDayPredicate;
/// If current selection exists and includes day/days that can't be selected
/// according to the [_selectableDayPredicate]'
bool get curSelectionIsCorrupted;
/// Returns [DayType] for given [day].
DayType getDayType(DateTime day);
/// Call when user tap on the day cell.
void onDayTapped(DateTime selectedDate);
/// Returns if given day is disabled.
///
/// Returns weather given day before the beginning of the [firstDate]
/// or after the end of the [lastDate].
///
/// If [_selectableDayPredicate] is set checks it as well.
@protected
bool isDisabled(DateTime day) {
final DateTime beginOfTheFirstDay =
DatePickerUtils.startOfTheDay(firstDate);
final DateTime endOfTheLastDay = DatePickerUtils.endOfTheDay(lastDate);
final bool customDisabled =
_selectableDayPredicate != null ? !_selectableDayPredicate(day) : false;
return day.isAfter(endOfTheLastDay) ||
day.isBefore(beginOfTheFirstDay) ||
customDisabled;
}
/// Closes [onUpdateController].
/// After it [onUpdateController] can't get new events.
void dispose() {
onUpdateController.close();
}
static bool _defaultSelectableDayPredicate(_) => true;
}
/// Selection logic for WeekPicker.
class WeekSelectable extends ISelectablePicker<DatePeriod> {
/// Initialized in ctor body.
late DateTime _firstDayOfSelectedWeek;
/// Initialized in ctor body.
late DateTime _lastDayOfSelectedWeek;
// It is int from 0 to 6 where 0 points to Sunday and 6 points to Saturday.
// According to MaterialLocalization.firstDayOfWeekIndex.
final int _firstDayOfWeekIndex;
@override
bool get curSelectionIsCorrupted => _checkCurSelection();
/// Creates selection logic for WeekPicker.
///
/// Entire week will be selected if
/// * it is between [firstDate] and [lastDate]
/// * it doesn't include unselectable days according to the
/// [selectableDayPredicate]
///
/// If one or more days of the week are before [firstDate]
/// first selection date will be the same as [firstDate].
///
/// If one or more days of the week are after [lastDate]
/// last selection date will be the same as [lastDate].
///
/// If one or more days of week are not selectable according to the
/// [selectableDayPredicate] nothing will be returned as selection
/// but [UnselectablePeriodException] will be thrown.
WeekSelectable(DateTime selectedDate, this._firstDayOfWeekIndex,
DateTime firstDate, DateTime lastDate,
{SelectableDayPredicate? selectableDayPredicate})
: super(firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate) {
DatePeriod selectedWeek = _getNewSelectedPeriod(selectedDate);
_firstDayOfSelectedWeek = selectedWeek.start;
_lastDayOfSelectedWeek = selectedWeek.end;
_checkCurSelection();
}
@override
DayType getDayType(DateTime date) {
DayType result;
DatePeriod selectedPeriod =
DatePeriod(_firstDayOfSelectedWeek, _lastDayOfSelectedWeek);
bool selectedPeriodIsBroken =
_disabledDatesInPeriod(selectedPeriod).isNotEmpty;
if (isDisabled(date)) {
result = DayType.disabled;
} else if (_isDaySelected(date) && !selectedPeriodIsBroken) {
DateTime firstNotDisabledDayOfSelectedWeek =
_firstDayOfSelectedWeek.isBefore(firstDate)
? firstDate
: _firstDayOfSelectedWeek;
DateTime lastNotDisabledDayOfSelectedWeek =
_lastDayOfSelectedWeek.isAfter(lastDate)
? lastDate
: _lastDayOfSelectedWeek;
if (DatePickerUtils.sameDate(date, firstNotDisabledDayOfSelectedWeek) &&
DatePickerUtils.sameDate(date, lastNotDisabledDayOfSelectedWeek)) {
result = DayType.single;
} else if (DatePickerUtils.sameDate(date, _firstDayOfSelectedWeek) ||
DatePickerUtils.sameDate(date, firstDate)) {
result = DayType.start;
} else if (DatePickerUtils.sameDate(date, _lastDayOfSelectedWeek) ||
DatePickerUtils.sameDate(date, lastDate)) {
result = DayType.end;
} else {
result = DayType.middle;
}
} else {
result = DayType.notSelected;
}
return result;
}
@override
void onDayTapped(DateTime selectedDate) {
DatePeriod newPeriod = _getNewSelectedPeriod(selectedDate);
List<DateTime> customDisabledDays = _disabledDatesInPeriod(newPeriod);
customDisabledDays.isEmpty
? onUpdateController.add(newPeriod)
: onUpdateController.addError(
UnselectablePeriodException(customDisabledDays, newPeriod));
}
// Returns new selected period according to tapped date.
// Doesn't check custom disabled days.
// You have to check it separately if it needs.
DatePeriod _getNewSelectedPeriod(DateTime tappedDay) {
DatePeriod newPeriod;
DateTime firstDayOfTappedWeek =
DatePickerUtils.getFirstDayOfWeek(tappedDay, _firstDayOfWeekIndex);
DateTime lastDayOfTappedWeek =
DatePickerUtils.getLastDayOfWeek(tappedDay, _firstDayOfWeekIndex);
DateTime firstNotDisabledDayOfSelectedWeek =
firstDayOfTappedWeek.isBefore(firstDate)
? firstDate
: firstDayOfTappedWeek;
DateTime lastNotDisabledDayOfSelectedWeek =
lastDayOfTappedWeek.isAfter(lastDate) ? lastDate : lastDayOfTappedWeek;
newPeriod = DatePeriod(
firstNotDisabledDayOfSelectedWeek, lastNotDisabledDayOfSelectedWeek);
return newPeriod;
}
bool _isDaySelected(DateTime date) {
DateTime startOfTheStartDay =
DatePickerUtils.startOfTheDay(_firstDayOfSelectedWeek);
DateTime endOfTheLastDay =
DatePickerUtils.endOfTheDay(_lastDayOfSelectedWeek);
return !(date.isBefore(startOfTheStartDay) ||
date.isAfter(endOfTheLastDay));
}
List<DateTime> _disabledDatesInPeriod(DatePeriod period) {
List<DateTime> result = <DateTime>[];
var date = period.start;
while (!date.isAfter(period.end)) {
if (isDisabled(date)) result.add(date);
date = date.add(Duration(days: 1));
}
return result;
}
// Returns if current selection contains disabled dates.
// Returns false if there is no any selection.
bool _checkCurSelection() {
bool noSelection =
_firstDayOfSelectedWeek == null || _lastDayOfSelectedWeek == null;
if (noSelection) return false;
DatePeriod selectedPeriod =
DatePeriod(_firstDayOfSelectedWeek, _lastDayOfSelectedWeek);
List<DateTime> disabledDates = _disabledDatesInPeriod(selectedPeriod);
bool selectedPeriodIsBroken = disabledDates.isNotEmpty;
return selectedPeriodIsBroken;
}
}
/// Selection logic for [day_picker.DayPicker].
class DaySelectable extends ISelectablePicker<DateTime> {
/// Currently selected date.
DateTime selectedDate;
@override
bool get curSelectionIsCorrupted => _checkCurSelection();
/// Creates selection logic for [day_picker.DayPicker].
///
/// Every day can be selected if it is between [firstDate] and [lastDate]
/// and not unselectable according to the [selectableDayPredicate].
///
/// If day is not selectable according to the [selectableDayPredicate]
/// nothing will be returned as selection
/// but [UnselectablePeriodException] will be thrown.
DaySelectable(this.selectedDate, DateTime firstDate, DateTime lastDate,
{SelectableDayPredicate? selectableDayPredicate})
: super(firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
@override
DayType getDayType(DateTime date) {
DayType result;
if (isDisabled(date)) {
result = DayType.disabled;
} else if (_isDaySelected(date)) {
result = DayType.single;
} else {
result = DayType.notSelected;
}
return result;
}
@override
void onDayTapped(DateTime selectedDate) {
DateTime newSelected = DatePickerUtils.sameDate(firstDate, selectedDate)
? selectedDate
: DateTime(selectedDate.year, selectedDate.month, selectedDate.day);
onUpdateController.add(newSelected);
}
bool _isDaySelected(DateTime date) =>
DatePickerUtils.sameDate(date, selectedDate);
// Returns if current selection is disabled
// according to the [_selectableDayPredicate].
//
// Returns false if there is no any selection.
bool _checkCurSelection() {
if (selectedDate == null) return false;
bool selectedIsBroken = _selectableDayPredicate(selectedDate);
return selectedIsBroken;
}
}
/// Selection logic for [day_picker.DayPicker] where many single days can be
/// selected.
class DayMultiSelectable extends ISelectablePicker<List<DateTime>> {
/// Currently selected dates.
List<DateTime> selectedDates;
/// Creates selection logic for [day_picker.DayPicker].
///
/// Every day can be selected if it is between [firstDate] and [lastDate]
/// and not unselectable according to the [selectableDayPredicate].
///
/// If day is not selectable according to the [selectableDayPredicate]
/// nothing will be returned as selection
/// but [UnselectablePeriodException] will be thrown.
DayMultiSelectable(this.selectedDates, DateTime firstDate, DateTime lastDate,
{SelectableDayPredicate? selectableDayPredicate})
: super(firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
@override
bool get curSelectionIsCorrupted => _checkCurSelection();
@override
DayType getDayType(DateTime date) {
DayType result;
if (isDisabled(date)) {
result = DayType.disabled;
} else if (_isDaySelected(date)) {
result = DayType.single;
} else {
result = DayType.notSelected;
}
return result;
}
@override
void onDayTapped(DateTime selectedDate) {
bool alreadyExist =
selectedDates.any((d) => DatePickerUtils.sameDate(d, selectedDate));
if (alreadyExist) {
List<DateTime> newSelectedDates = List.from(selectedDates)
..removeWhere((d) => DatePickerUtils.sameDate(d, selectedDate));
onUpdateController.add(newSelectedDates);
} else {
DateTime newSelected = DatePickerUtils.sameDate(firstDate, selectedDate)
? selectedDate
: DateTime(selectedDate.year, selectedDate.month, selectedDate.day);
List<DateTime> newSelectedDates = List.from(selectedDates)
..add(newSelected);
onUpdateController.add(newSelectedDates);
}
}
bool _isDaySelected(DateTime date) =>
selectedDates.any((d) => DatePickerUtils.sameDate(date, d));
// Returns if current selection is disabled
// according to the [_selectableDayPredicate].
//
// Returns false if there is no any selection.
bool _checkCurSelection() {
if (selectedDates == null || selectedDates.isEmpty) return false;
bool selectedIsBroken = selectedDates.every(_selectableDayPredicate);
return selectedIsBroken;
}
}
/// Selection logic for [RangePicker].
class RangeSelectable extends ISelectablePicker<DatePeriod> {
/// Initially selected period.
DatePeriod selectedPeriod;
@override
bool get curSelectionIsCorrupted => _checkCurSelection();
/// Creates selection logic for [RangePicker].
///
/// Period can be selected if
/// * it is between [firstDate] and [lastDate]
/// * it doesn't include unselectable days according to the
/// [selectableDayPredicate]
///
///
/// If one or more days of the period are not selectable according to the
/// [selectableDayPredicate] nothing will be returned as selection
/// but [UnselectablePeriodException] will be thrown.
RangeSelectable(this.selectedPeriod, DateTime firstDate, DateTime lastDate,
{SelectableDayPredicate? selectableDayPredicate})
: super(firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
@override
DayType getDayType(DateTime date) {
DayType result;
bool selectedPeriodIsBroken =
_disabledDatesInPeriod(selectedPeriod).isNotEmpty;
if (isDisabled(date)) {
result = DayType.disabled;
} else if (_isDaySelected(date) && !selectedPeriodIsBroken) {
if (DatePickerUtils.sameDate(date, selectedPeriod.start) &&
DatePickerUtils.sameDate(date, selectedPeriod.end)) {
result = DayType.single;
} else if (DatePickerUtils.sameDate(date, selectedPeriod.start) ||
DatePickerUtils.sameDate(date, firstDate)) {
result = DayType.start;
} else if (DatePickerUtils.sameDate(date, selectedPeriod.end) ||
DatePickerUtils.sameDate(date, lastDate)) {
result = DayType.end;
} else {
result = DayType.middle;
}
} else {
result = DayType.notSelected;
}
return result;
}
@override
void onDayTapped(DateTime selectedDate) {
DatePeriod newPeriod = _getNewSelectedPeriod(selectedDate);
List<DateTime> customDisabledDays = _disabledDatesInPeriod(newPeriod);
customDisabledDays.isEmpty
? onUpdateController.add(newPeriod)
: onUpdateController.addError(
UnselectablePeriodException(customDisabledDays, newPeriod));
}
// Returns new selected period according to tapped date.
DatePeriod _getNewSelectedPeriod(DateTime tappedDate) {
// check if was selected only one date and we should generate period
bool sameDate =
DatePickerUtils.sameDate(selectedPeriod.start, selectedPeriod.end);
DatePeriod newPeriod;
// Was selected one-day-period.
// With new user tap will be generated 2 dates as a period.
if (sameDate) {
// if user tap on the already selected single day
bool selectedAlreadySelectedDay =
DatePickerUtils.sameDate(tappedDate, selectedPeriod.end);
bool isSelectedFirstDay = DatePickerUtils.sameDate(tappedDate, firstDate);
bool isSelectedLastDay = DatePickerUtils.sameDate(tappedDate, lastDate);
if (selectedAlreadySelectedDay) {
if (isSelectedFirstDay && isSelectedLastDay) {
newPeriod = DatePeriod(firstDate, lastDate);
} else if (isSelectedFirstDay) {
newPeriod =
DatePeriod(firstDate, DatePickerUtils.endOfTheDay(firstDate));
} else if (isSelectedLastDay) {
newPeriod =
DatePeriod(DatePickerUtils.startOfTheDay(lastDate), lastDate);
} else {
newPeriod = DatePeriod(DatePickerUtils.startOfTheDay(tappedDate),
DatePickerUtils.endOfTheDay(tappedDate));
}
} else {
DateTime startOfTheSelectedDay =
DatePickerUtils.startOfTheDay(selectedPeriod.start);
if (!tappedDate.isAfter(startOfTheSelectedDay)) {
newPeriod = DatePickerUtils.sameDate(tappedDate, firstDate)
? DatePeriod(firstDate, selectedPeriod.end)
: DatePeriod(DatePickerUtils.startOfTheDay(tappedDate),
selectedPeriod.end);
} else {
newPeriod = DatePickerUtils.sameDate(tappedDate, lastDate)
? DatePeriod(selectedPeriod.start, lastDate)
: DatePeriod(selectedPeriod.start,
DatePickerUtils.endOfTheDay(tappedDate));
}
}
// Was selected 2 dates as a period.
// With new user tap new one-day-period will be generated.
} else {
bool sameAsFirst = DatePickerUtils.sameDate(tappedDate, firstDate);
bool sameAsLast = DatePickerUtils.sameDate(tappedDate, lastDate);
if (sameAsFirst && sameAsLast) {
newPeriod = DatePeriod(firstDate, lastDate);
} else if (sameAsFirst) {
newPeriod =
DatePeriod(firstDate, DatePickerUtils.endOfTheDay(firstDate));
} else if (sameAsLast) {
newPeriod =
DatePeriod(DatePickerUtils.startOfTheDay(tappedDate), lastDate);
} else {
newPeriod = DatePeriod(DatePickerUtils.startOfTheDay(tappedDate),
DatePickerUtils.endOfTheDay(tappedDate));
}
}
return newPeriod;
}
// Returns if current selection contains disabled dates.
// Returns false if there is no any selection.
bool _checkCurSelection() {
if (selectedPeriod == null) return false;
List<DateTime> disabledDates = _disabledDatesInPeriod(selectedPeriod);
bool selectedPeriodIsBroken = disabledDates.isNotEmpty;
return selectedPeriodIsBroken;
}
List<DateTime> _disabledDatesInPeriod(DatePeriod period) {
List<DateTime> result = <DateTime>[];
var date = period.start;
while (!date.isAfter(period.end)) {
if (isDisabled(date)) result.add(date);
date = date.add(Duration(days: 1));
}
return result;
}
bool _isDaySelected(DateTime date) {
DateTime startOfTheStartDay =
DatePickerUtils.startOfTheDay(selectedPeriod.start);
DateTime endOfTheLastDay = DatePickerUtils.endOfTheDay(selectedPeriod.end);
return !(date.isBefore(startOfTheStartDay) ||
date.isAfter(endOfTheLastDay));
}
}

View File

@ -0,0 +1,54 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
/// Icon button widget built different
/// depends on [MaterialApp] or [CupertinoApp] ancestor.
class IconBtn extends StatelessWidget {
/// Widget to use inside button.
///
/// Typically [Icon] widget.
final Widget icon;
/// Function called when user tap on the button.
///
/// Can be null. In this case button will be disabled.
final VoidCallback? onTap;
/// Tooltip for button.
///
/// Applied only for material style buttons.
/// It means only if widget has [MaterialApp] ancestor.
final String? tooltip;
/// Creates button with [icon] different
/// depends on [MaterialApp] or [CupertinoApp] ancestor.
const IconBtn({
Key? key,
required this.icon,
this.onTap,
this.tooltip
}) : super(key: key);
@override
Widget build(BuildContext context) {
bool isMaterial = Material.of(context) != null;
return isMaterial
? _materialBtn()
: _cupertinoBtn();
}
Widget _cupertinoBtn() =>
CupertinoButton(
padding: const EdgeInsets.all(0.0),
child: icon,
onPressed: onTap,
);
Widget _materialBtn() =>
IconButton(
icon: icon,
tooltip: tooltip ?? "",
onPressed: onTap,
);
}

View File

@ -0,0 +1,115 @@
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'day_picker.dart';
import 'month_picker.dart';
import 'range_picker.dart';
import 'week_picker.dart';
// layout defaults
const Duration _kPageScrollDuration = Duration(milliseconds: 200);
const double _kDayPickerRowHeight = 42.0;
const int _kMaxDayPickerRowCount = 6; // A 31 day month that starts on Saturday.
const double _kMonthPickerPortraitWidth = 330.0;
const EdgeInsetsGeometry _kContentPadding =
EdgeInsets.symmetric(horizontal: 8.0);
/// Settings for the layout of the [DayPicker], [WeekPicker], [RangePicker]
/// and [MonthPicker].
class DatePickerLayoutSettings {
/// Duration for scroll to previous or next page.
final Duration pagesScrollDuration;
/// Determines the scroll physics of a date picker widget.
///
/// Can be null. In this case default physics for [ScrollView] will be used.
final ScrollPhysics? scrollPhysics;
/// Height of the one row in picker including headers.
///
/// Default is [_kDayPickerRowHeight].
final double dayPickerRowHeight;
/// Width of the day based pickers.
final double monthPickerPortraitWidth;
///
final int maxDayPickerRowCount;
/// Padding for the entire picker.
final EdgeInsetsGeometry contentPadding;
/// If the first dates from the next month should be shown
/// to complete last week of the selected month.
///
/// false by default.
final bool showNextMonthStart;
/// If the last dates from the previous month should be shown
/// to complete first week of the selected month.
///
/// false by default.
final bool showPrevMonthEnd;
/// Hide Month navigation row
/// false by default.
final bool hideMonthNavigationRow;
/// Grid delegate for the picker according to [dayPickerRowHeight] and
/// [maxDayPickerRowCount].
SliverGridDelegate get dayPickerGridDelegate =>
_DayPickerGridDelegate(dayPickerRowHeight, maxDayPickerRowCount);
/// Maximum height of the day based picker according to [dayPickerRowHeight]
/// and [maxDayPickerRowCount].
///
/// Two extra rows:
/// one for the day-of-week header and one for the month header.
double get maxDayPickerHeight =>
dayPickerRowHeight * (maxDayPickerRowCount + 2);
/// Creates layout settings for the date picker.
///
/// Usually used in [DayPicker], [WeekPicker], [RangePicker]
/// and [MonthPicker].
const DatePickerLayoutSettings({
this.pagesScrollDuration = _kPageScrollDuration,
this.dayPickerRowHeight = _kDayPickerRowHeight,
this.monthPickerPortraitWidth = _kMonthPickerPortraitWidth,
this.maxDayPickerRowCount = _kMaxDayPickerRowCount,
this.contentPadding = _kContentPadding,
this.showNextMonthStart = false,
this.showPrevMonthEnd = false,
this.hideMonthNavigationRow = false,
this.scrollPhysics
});
}
class _DayPickerGridDelegate extends SliverGridDelegate {
final double _dayPickerRowHeight;
final int _maxDayPickerRowCount;
const _DayPickerGridDelegate(
this._dayPickerRowHeight, this._maxDayPickerRowCount);
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
const int columnCount = DateTime.daysPerWeek;
final double tileWidth = constraints.crossAxisExtent / columnCount;
final double tileHeight = math.min(_dayPickerRowHeight,
constraints.viewportMainAxisExtent / (_maxDayPickerRowCount + 1));
return SliverGridRegularTileLayout(
crossAxisCount: columnCount,
mainAxisStride: tileHeight,
crossAxisStride: tileWidth,
childMainAxisExtent: tileHeight,
childCrossAxisExtent: tileWidth,
reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
);
}
@override
bool shouldRelayout(SliverGridDelegate oldDelegate) => false;
}

View File

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'day_picker.dart' as day_picker;
import 'icon_btn.dart';
import 'range_picker.dart';
import 'semantic_sorting.dart';
import 'week_picker.dart';
/// Month navigation widget for day based date pickers like
/// [day_picker.DayPicker],
/// [WeekPicker],
/// [RangePicker].
///
/// It is row with [title] of showing month in the center and icons to selects
/// previous and next month around it.
class MonthNavigationRow extends StatelessWidget {
/// Key for previous page icon.
///
/// Can be useful in integration tests to find icon.
final Key? previousPageIconKey;
/// Key for next page icon.
///
/// Can be useful in integration tests to find icon.
final Key? nextPageIconKey;
/// Function called when [nextIcon] is tapped.
final VoidCallback? onNextMonthTapped;
/// Function called when [prevIcon] is tapped.
final VoidCallback? onPreviousMonthTapped;
/// Tooltip for the [nextIcon].
final String? nextMonthTooltip;
/// Tooltip for the [prevIcon].
final String? previousMonthTooltip;
/// Widget to use at the end of this row (after title).
final Widget nextIcon;
/// Widget to use at the beginning of this row (before title).
final Widget prevIcon;
/// Usually [Text] widget.
final Widget? title;
/// Creates month navigation row.
const MonthNavigationRow({
Key? key,
this.previousPageIconKey,
this.nextPageIconKey,
this.onNextMonthTapped,
this.onPreviousMonthTapped,
this.nextMonthTooltip,
this.previousMonthTooltip,
this.title,
required this.nextIcon,
required this.prevIcon
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Semantics(
sortKey: MonthPickerSortKey.previousMonth,
child: IconBtn(
key: previousPageIconKey,
icon: prevIcon,
tooltip: previousMonthTooltip,
onTap: onPreviousMonthTapped,
),
),
Expanded(
child: Container(
alignment: Alignment.center,
child: Center(
child: ExcludeSemantics(
child: title,
),
),
),
),
Semantics(
sortKey: MonthPickerSortKey.nextMonth,
child: IconBtn(
key: nextPageIconKey,
icon: nextIcon,
tooltip: nextMonthTooltip,
onTap: onNextMonthTapped,
),
),
],
);
}
}

View File

@ -0,0 +1,440 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'date_picker_keys.dart';
import 'date_picker_styles.dart';
import 'layout_settings.dart';
import 'semantic_sorting.dart';
import 'utils.dart';
const Locale _defaultLocale = Locale('en', 'US');
/// Month picker widget.
class MonthPicker extends StatefulWidget {
/// Month picker widget.
MonthPicker(
{Key? key,
required this.selectedDate,
required this.onChanged,
required this.firstDate,
required this.lastDate,
this.datePickerLayoutSettings = const DatePickerLayoutSettings(),
this.datePickerKeys,
required this.datePickerStyles})
: assert(!firstDate.isAfter(lastDate)),
assert(!selectedDate.isBefore(firstDate)),
assert(!selectedDate.isAfter(lastDate)),
super(key: key);
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DateTime selectedDate;
/// Called when the user picks a month.
final ValueChanged<DateTime> onChanged;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Some keys useful for integration tests
final DatePickerKeys? datePickerKeys;
/// Styles what can be customized by user
final DatePickerStyles datePickerStyles;
@override
State<StatefulWidget> createState() => _MonthPickerState();
}
class _MonthPickerState extends State<MonthPicker> {
PageController _monthPickerController = PageController();
Locale locale = _defaultLocale;
MaterialLocalizations localizations = _defaultLocalizations;
TextDirection textDirection = TextDirection.ltr;
DateTime _todayDate = DateTime.now();
DateTime _previousYearDate = DateTime(DateTime.now().year - 1);
DateTime _nextYearDate = DateTime(DateTime.now().year + 1);
DateTime _currentDisplayedYearDate = DateTime.now();
Timer? _timer;
/// True if the earliest allowable year is displayed.
bool get _isDisplayingFirstYear =>
!_currentDisplayedYearDate.isAfter(DateTime(widget.firstDate.year));
/// True if the latest allowable year is displayed.
bool get _isDisplayingLastYear =>
!_currentDisplayedYearDate.isBefore(DateTime(widget.lastDate.year));
@override
void initState() {
super.initState();
// Initially display the pre-selected date.
final int yearPage =
DatePickerUtils.yearDelta(widget.firstDate, widget.selectedDate);
_monthPickerController.dispose();
_monthPickerController = PageController(initialPage: yearPage);
_handleYearPageChanged(yearPage);
_updateCurrentDate();
}
@override
void didUpdateWidget(MonthPicker oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedDate != oldWidget.selectedDate) {
final int yearPage =
DatePickerUtils.yearDelta(widget.firstDate, widget.selectedDate);
_monthPickerController = PageController(initialPage: yearPage);
_handleYearPageChanged(yearPage);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
try {
locale = Localizations.localeOf(context);
MaterialLocalizations? curLocalizations =
Localizations.of<MaterialLocalizations>(
context, MaterialLocalizations);
if (curLocalizations != null && localizations != curLocalizations) {
localizations = curLocalizations;
}
textDirection = Directionality.of(context);
// No MaterialLocalizations or Directionality or Locale was found
// and ".of" method throws error
// trying to cast null to MaterialLocalizations.
} on TypeError catch (_) {}
}
void _updateCurrentDate() {
_todayDate = DateTime.now();
final DateTime tomorrow =
DateTime(_todayDate.year, _todayDate.month, _todayDate.day + 1);
Duration timeUntilTomorrow = tomorrow.difference(_todayDate);
timeUntilTomorrow +=
const Duration(seconds: 1); // so we don't miss it by rounding
_timer?.cancel();
_timer = Timer(timeUntilTomorrow, () {
setState(_updateCurrentDate);
});
}
/// Add years to a year truncated date.
DateTime _addYearsToYearDate(DateTime yearDate, int yearsToAdd) =>
DateTime(yearDate.year + yearsToAdd);
Widget _buildItems(BuildContext context, int index) {
final DateTime year = _addYearsToYearDate(widget.firstDate, index);
final ThemeData theme = Theme.of(context);
DatePickerStyles styles = widget.datePickerStyles;
styles = styles.fulfillWithTheme(theme);
return _MonthPicker(
key: ValueKey<DateTime>(year),
selectedDate: widget.selectedDate,
currentDate: _todayDate,
onChanged: widget.onChanged,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
datePickerLayoutSettings: widget.datePickerLayoutSettings,
displayedYear: year,
selectedPeriodKey: widget.datePickerKeys?.selectedPeriodKeys,
datePickerStyles: styles,
locale: locale,
localizations: localizations,
);
}
void _handleNextYear() {
if (!_isDisplayingLastYear) {
String yearStr = localizations.formatYear(_nextYearDate);
SemanticsService.announce(yearStr, textDirection);
_monthPickerController.nextPage(
duration: widget.datePickerLayoutSettings.pagesScrollDuration,
curve: Curves.ease);
}
}
void _handlePreviousYear() {
if (!_isDisplayingFirstYear) {
String yearStr = localizations.formatYear(_previousYearDate);
SemanticsService.announce(yearStr, textDirection);
_monthPickerController.previousPage(
duration: widget.datePickerLayoutSettings.pagesScrollDuration,
curve: Curves.ease);
}
}
void _handleYearPageChanged(int yearPage) {
setState(() {
_previousYearDate = _addYearsToYearDate(widget.firstDate, yearPage - 1);
_currentDisplayedYearDate =
_addYearsToYearDate(widget.firstDate, yearPage);
_nextYearDate = _addYearsToYearDate(widget.firstDate, yearPage + 1);
});
}
@override
Widget build(BuildContext context) {
int yearsCount =
DatePickerUtils.yearDelta(widget.firstDate, widget.lastDate) + 1;
return SizedBox(
width: widget.datePickerLayoutSettings.monthPickerPortraitWidth,
height: widget.datePickerLayoutSettings.maxDayPickerHeight,
child: Stack(
children: <Widget>[
Semantics(
sortKey: YearPickerSortKey.calendar,
child: PageView.builder(
key: ValueKey<DateTime>(widget.selectedDate),
controller: _monthPickerController,
scrollDirection: Axis.horizontal,
itemCount: yearsCount,
itemBuilder: _buildItems,
onPageChanged: _handleYearPageChanged,
),
),
PositionedDirectional(
top: 0.0,
start: 8.0,
child: Semantics(
sortKey: YearPickerSortKey.previousYear,
child: IconButton(
key: widget.datePickerKeys?.previousPageIconKey,
icon: widget.datePickerStyles.prevIcon,
tooltip: _isDisplayingFirstYear
? null
: '${localizations.formatYear(_previousYearDate)}',
onPressed: _isDisplayingFirstYear ? null : _handlePreviousYear,
),
),
),
PositionedDirectional(
top: 0.0,
end: 8.0,
child: Semantics(
sortKey: YearPickerSortKey.nextYear,
child: IconButton(
key: widget.datePickerKeys?.nextPageIconKey,
icon: widget.datePickerStyles.nextIcon,
tooltip: _isDisplayingLastYear
? null
: '${localizations.formatYear(_nextYearDate)}',
onPressed: _isDisplayingLastYear ? null : _handleNextYear,
),
),
),
],
),
);
}
static MaterialLocalizations get _defaultLocalizations =>
MaterialLocalizationEn(
twoDigitZeroPaddedFormat:
intl.NumberFormat('00', _defaultLocale.toString()),
fullYearFormat: intl.DateFormat.y(_defaultLocale.toString()),
longDateFormat: intl.DateFormat.yMMMMEEEEd(_defaultLocale.toString()),
shortMonthDayFormat: intl.DateFormat.MMMd(_defaultLocale.toString()),
decimalFormat:
intl.NumberFormat.decimalPattern(_defaultLocale.toString()),
shortDateFormat: intl.DateFormat.yMMMd(_defaultLocale.toString()),
mediumDateFormat: intl.DateFormat.MMMEd(_defaultLocale.toString()),
compactDateFormat: intl.DateFormat.yMd(_defaultLocale.toString()),
yearMonthFormat: intl.DateFormat.yMMMM(_defaultLocale.toString()),
);
}
class _MonthPicker extends StatelessWidget {
/// The month whose days are displayed by this picker.
final DateTime displayedYear;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DateTime selectedDate;
/// The current date at the time the picker is displayed.
final DateTime currentDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Called when the user picks a day.
final ValueChanged<DateTime> onChanged;
/// Key fo selected month (useful for integration tests)
final Key? selectedPeriodKey;
/// Styles what can be customized by user
final DatePickerStyles datePickerStyles;
final MaterialLocalizations localizations;
final Locale locale;
_MonthPicker(
{required this.displayedYear,
required this.firstDate,
required this.lastDate,
required this.selectedDate,
required this.currentDate,
required this.onChanged,
required this.datePickerLayoutSettings,
required this.datePickerStyles,
required this.localizations,
required this.locale,
this.selectedPeriodKey,
Key? key})
: assert(!firstDate.isAfter(lastDate)),
assert(selectedDate.isAfter(firstDate) ||
selectedDate.isAtSameMomentAs(firstDate)),
super(key: key);
// We only need to know if month of passed day
// before the month of the firstDate or after the month of the lastDate.
//
// Don't need to compare day and time.
bool _isDisabled(DateTime month) {
DateTime beginningOfTheFirstDateMonth =
DateTime(firstDate.year, firstDate.month);
DateTime endOfTheLastDateMonth = DateTime(lastDate.year, lastDate.month + 1)
.subtract(Duration(microseconds: 1));
return month.isAfter(endOfTheLastDateMonth) ||
month.isBefore(beginningOfTheFirstDateMonth);
}
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final int monthsInYear = 12;
final int year = displayedYear.year;
final int day = 1;
final List<Widget> labels = <Widget>[];
for (int i = 0; i < monthsInYear; i += 1) {
final int month = i + 1;
final DateTime monthToBuild = DateTime(year, month, day);
final bool disabled = _isDisabled(monthToBuild);
final bool isSelectedMonth =
selectedDate.year == year && selectedDate.month == month;
BoxDecoration? decoration;
TextStyle? itemStyle = themeData.textTheme.bodyText2;
if (isSelectedMonth) {
itemStyle = datePickerStyles.selectedDateStyle;
decoration = datePickerStyles.selectedSingleDateDecoration;
} else if (disabled) {
itemStyle = datePickerStyles.disabledDateStyle;
} else if (currentDate.year == year && currentDate.month == month) {
// The current month gets a different text color.
itemStyle = datePickerStyles.currentDateStyle;
} else {
itemStyle = datePickerStyles.defaultDateTextStyle;
}
String monthStr = _getMonthStr(monthToBuild);
Widget monthWidget = Container(
decoration: decoration,
child: Center(
child: Semantics(
// We want the day of month to be spoken first irrespective of the
// locale-specific preferences or TextDirection. This is because
// an accessibility user is more likely to be interested in the
// day of month before the rest of the date, as they are looking
// for the day of month. To do that we prepend day of month to the
// formatted full date.
label: '${localizations.formatDecimal(month)}, '
'${localizations.formatFullDate(monthToBuild)}',
selected: isSelectedMonth,
child: ExcludeSemantics(
child: Text(monthStr, style: itemStyle),
),
),
),
);
if (!disabled) {
monthWidget = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
DatePickerUtils.sameMonth(firstDate, monthToBuild)
? onChanged(firstDate)
: onChanged(monthToBuild);
},
child: monthWidget,
);
}
labels.add(monthWidget);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
children: <Widget>[
Container(
height: datePickerLayoutSettings.dayPickerRowHeight,
child: Center(
child: ExcludeSemantics(
child: Text(
localizations.formatYear(displayedYear),
key: selectedPeriodKey,
style: datePickerStyles.displayedPeriodTitle,
),
),
),
),
Flexible(
child: GridView.count(
physics: datePickerLayoutSettings.scrollPhysics,
crossAxisCount: 4,
children: labels,
),
),
],
),
);
}
// Returns only month made with intl.DateFormat.MMM() for current [locale].
// We can'r use [localizations] here because MaterialLocalizations doesn't
// provide short month string.
String _getMonthStr(DateTime date) {
String month = intl.DateFormat.MMM(locale.toString()).format(date);
return month;
}
}

View File

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'date_period.dart';
import 'date_picker_keys.dart';
import 'date_picker_styles.dart';
import 'day_based_changable_picker.dart';
import 'day_picker_selection.dart';
import 'day_type.dart';
import 'event_decoration.dart';
import 'i_selectable_picker.dart';
import 'layout_settings.dart';
import 'typedefs.dart';
/// Date picker for range selection.
class RangePicker extends StatelessWidget {
/// Creates a range picker.
RangePicker(
{Key? key,
required this.selectedPeriod,
required this.onChanged,
required this.firstDate,
required this.lastDate,
this.initiallyShowDate,
this.datePickerLayoutSettings = const DatePickerLayoutSettings(),
this.datePickerStyles,
this.datePickerKeys,
this.selectableDayPredicate,
this.onSelectionError,
this.eventDecorationBuilder,
this.onMonthChanged})
: assert(!firstDate.isAfter(lastDate)),
assert(!lastDate.isBefore(firstDate)),
assert(!selectedPeriod.start.isBefore(firstDate)),
assert(!selectedPeriod.end.isAfter(lastDate)),
assert(initiallyShowDate == null
|| !initiallyShowDate.isAfter(lastDate)),
assert(initiallyShowDate == null
|| !initiallyShowDate.isBefore(firstDate)),
super(key: key);
/// The currently selected period.
///
/// This date is highlighted in the picker.
final DatePeriod selectedPeriod;
/// Called when the user picks a week.
final ValueChanged<DatePeriod> onChanged;
/// Called when the error was thrown after user selection.
/// (e.g. when user selected a range with one or more days
/// that can't be selected)
final OnSelectionError? onSelectionError;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// Date for defining what month should be shown initially.
///
/// In case of null start of the [selectedPeriod] will be shown.
final DateTime? initiallyShowDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Some keys useful for integration tests
final DatePickerKeys? datePickerKeys;
/// Styles what can be customized by user
final DatePickerRangeStyles? datePickerStyles;
/// Function returns if day can be selected or not.
final SelectableDayPredicate? selectableDayPredicate;
/// Builder to get event decoration for each date.
///
/// All event styles are overridden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
/// Called when the user changes the month.
/// New DateTime object represents first day of new month and 00:00 time.
final ValueChanged<DateTime>? onMonthChanged;
@override
Widget build(BuildContext context) {
ISelectablePicker<DatePeriod> rangeSelectablePicker = RangeSelectable(
selectedPeriod, firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
return DayBasedChangeablePicker<DatePeriod>(
selectablePicker: rangeSelectablePicker,
selection: DayPickerRangeSelection(selectedPeriod),
firstDate: firstDate,
lastDate: lastDate,
initiallyShownDate: initiallyShowDate,
onChanged: onChanged,
onSelectionError: onSelectionError,
datePickerLayoutSettings: datePickerLayoutSettings,
datePickerStyles: datePickerStyles ?? DatePickerRangeStyles(),
datePickerKeys: datePickerKeys,
eventDecorationBuilder: eventDecorationBuilder,
onMonthChanged: onMonthChanged,
);
}
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/semantics.dart';
/// Defines semantic traversal order of the top-level widgets
/// inside the day or week picker.
class MonthPickerSortKey extends OrdinalSortKey {
/// Previous month key.
static const MonthPickerSortKey previousMonth = MonthPickerSortKey(1.0);
/// Next month key.
static const MonthPickerSortKey nextMonth = MonthPickerSortKey(2.0);
/// Calendar key.
static const MonthPickerSortKey calendar = MonthPickerSortKey(3.0);
///
const MonthPickerSortKey(double order) : super(order);
}
/// Defines semantic traversal order of the top-level widgets
/// inside the month picker.
class YearPickerSortKey extends OrdinalSortKey {
/// Previous year key.
static const YearPickerSortKey previousYear = YearPickerSortKey(1.0);
/// Next year key.
static const YearPickerSortKey nextYear = YearPickerSortKey(2.0);
/// Calendar key.
static const YearPickerSortKey calendar = YearPickerSortKey(3.0);
///
const YearPickerSortKey(double order) : super(order);
}

View File

@ -0,0 +1,11 @@
import 'range_picker.dart';
import 'unselectable_period_error.dart';
import 'week_picker.dart';
/// Signature for function that can be used to handle incorrect selections.
///
/// See also:
/// * [WeekPicker.onSelectionError]
/// * [RangePicker.onSelectionError]
typedef OnSelectionError = void Function(UnselectablePeriodException e);

View File

@ -0,0 +1,30 @@
import 'date_period.dart';
import 'range_picker.dart';
import 'week_picker.dart';
/// Exception thrown when selected period contains custom disabled days.
class UnselectablePeriodException implements Exception {
/// Dates inside selected period what can't be selected
/// according custom rules.
final List<DateTime> customDisabledDates;
/// Selected period wanted by the user.
final DatePeriod period;
/// Creates exception that stores dates that can not be selected.
///
/// See also:
/// *[WeekPicker.onSelectionError]
/// *[RangePicker.onSelectionError]
UnselectablePeriodException(this.customDisabledDates, this.period);
@override
String toString() =>
"UnselectablePeriodException:"
" ${customDisabledDates.length} dates inside selected period "
"(${period.start} - ${period.end}) "
"can't be selected according custom rules (selectable pridicate). "
"Check 'customDisabledDates' property "
"to get entire list of such dates.";
}

View File

@ -0,0 +1,251 @@
/// Bunch of useful functions for date pickers.
class DatePickerUtils {
/// Returns if two objects have same year, month and day.
/// Time doesn't matter.
static bool sameDate(DateTime dateTimeOne, DateTime dateTimeTwo) =>
dateTimeOne.year == dateTimeTwo.year &&
dateTimeOne.month == dateTimeTwo.month &&
dateTimeOne.day == dateTimeTwo.day;
/// Returns if two objects have same year and month.
/// Day and time don't matter/
static bool sameMonth(DateTime dateTimeOne, DateTime dateTimeTwo) =>
dateTimeOne.year == dateTimeTwo.year
&& dateTimeOne.month == dateTimeTwo.month;
// Do not use this directly - call getDaysInMonth instead.
static const List<int> _daysInMonth = <int>[
31,
-1,
31,
30,
31,
30,
31,
31,
30,
31,
30,
31
];
/// Returns the number of days in a month, according to the proleptic
/// Gregorian calendar.
///
/// This applies the leap year logic introduced by the Gregorian reforms of
/// 1582. It will not give valid results for dates prior to that time.
static int getDaysInMonth(int year, int month) {
if (month == DateTime.february) {
final bool isLeapYear =
(year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0);
return isLeapYear ? 29 : 28;
}
return _daysInMonth[month - 1];
}
/// Returns number of months between [startDate] and [endDate]
static int monthDelta(DateTime startDate, DateTime endDate) =>
(endDate.year - startDate.year) * 12 +
endDate.month -
startDate.month;
/// Add months to a month truncated date.
static DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) =>
// year is switched automatically if new month > 12
DateTime(monthDate.year, monthDate.month + monthsToAdd);
/// Returns number of years between [startDate] and [endDate]
static int yearDelta(DateTime startDate, DateTime endDate) =>
endDate.year - startDate.year;
/// Returns start of the first day of the week with given day.
///
/// Start of the week calculated using firstDayIndex which is int from 0 to 6
/// where 0 points to Sunday and 6 points to Saturday.
/// (according to MaterialLocalization.firstDayIfWeekIndex)
static DateTime getFirstDayOfWeek(DateTime day, int firstDayIndex) {
// from 1 to 7 where 1 points to Monday and 7 points to Sunday
int weekday = day.weekday;
// to match weekdays where Sunday is 7 not 0
if (firstDayIndex == 0) firstDayIndex = 7;
int diff = weekday - firstDayIndex;
if (diff < 0) diff = 7 + diff;
DateTime firstDayOfWeek = day.subtract(Duration(days: diff));
firstDayOfWeek = startOfTheDay(firstDayOfWeek);
return firstDayOfWeek;
}
/// Returns end of the last day of the week with given day.
///
/// Start of the week calculated using firstDayIndex which is int from 0 to 6
/// where 0 points to Sunday and 6 points to Saturday.
/// (according to MaterialLocalization.firstDayIfWeekIndex)
static DateTime getLastDayOfWeek(DateTime day, int firstDayIndex) {
// from 1 to 7 where 1 points to Monday and 7 points to Sunday
int weekday = day.weekday;
// to match weekdays where Sunday is 7 not 0
if (firstDayIndex == 0) firstDayIndex = 7;
int lastDayIndex = firstDayIndex - 1;
if (lastDayIndex == 0) lastDayIndex = 7;
int diff = lastDayIndex - weekday;
if (diff < 0) diff = 7 + diff;
DateTime lastDayOfWeek = day.add(Duration(days: diff));
lastDayOfWeek = endOfTheDay(lastDayOfWeek);
return lastDayOfWeek;
}
/// Returns end of the given day.
///
/// End time is 1 millisecond before start of the next day.
static DateTime endOfTheDay(DateTime date) {
DateTime tomorrowStart = DateTime(date.year, date.month, date.day + 1);
DateTime result = tomorrowStart.subtract(const Duration(milliseconds: 1));
return result;
}
/// Returns start of the given day.
///
/// Start time is 00:00:00.
static DateTime startOfTheDay(DateTime date) =>
DateTime(date.year, date.month, date.day);
/// Returns first shown date for the [curMonth].
///
/// First shown date is not always 1st day of the [curMonth].
/// It can be day from previous month if [showEndOfPrevMonth] is true.
///
/// If [showEndOfPrevMonth] is true empty day cells before 1st [curMonth]
/// are filled with days from the previous month.
static DateTime firstShownDate({
required DateTime curMonth,
required bool showEndOfPrevMonth,
required int firstDayOfWeekFromSunday}) {
DateTime result = DateTime(curMonth.year, curMonth.month, 1);
if (showEndOfPrevMonth) {
int firstDayOffset = computeFirstDayOffset(curMonth.year, curMonth.month,
firstDayOfWeekFromSunday);
if (firstDayOffset == 0) return result;
int prevMonth = curMonth.month - 1;
if (prevMonth < 1) prevMonth = 12;
int prevYear = prevMonth == 12
? curMonth.year - 1
: curMonth.year;
int daysInPrevMonth = getDaysInMonth(prevYear, prevMonth);
int firstShownDay = daysInPrevMonth - firstDayOffset + 1;
result = DateTime(prevYear, prevMonth, firstShownDay);
}
return result;
}
/// Returns last shown date for the [curMonth].
///
/// Last shown date is not always last day of the [curMonth].
/// It can be day from next month if [showStartNextMonth] is true.
///
/// If [showStartNextMonth] is true empty day cells after last day
/// of [curMonth] are filled with days from the next month.
static DateTime lastShownDate({
required DateTime curMonth,
required bool showStartNextMonth,
required int firstDayOfWeekFromSunday}) {
int daysInCurMonth = getDaysInMonth(curMonth.year, curMonth.month);
DateTime result = DateTime(curMonth.year, curMonth.month, daysInCurMonth);
if (showStartNextMonth) {
int firstDayOffset = computeFirstDayOffset(curMonth.year, curMonth.month,
firstDayOfWeekFromSunday);
int totalDays = firstDayOffset + daysInCurMonth;
int trailingDaysCount = 7 - totalDays % 7;
bool fullWeekTrailing = trailingDaysCount == 7;
if (fullWeekTrailing) return result;
result = DateTime(curMonth.year, curMonth.month + 1, trailingDaysCount);
}
return result;
}
/// Computes the offset from the first day of week that the first day of the
/// [month] falls on.
///
/// For example, September 1, 2017 falls on a Friday, which in the calendar
/// localized for United States English appears as:
///
/// ```
/// S M T W T F S
/// _ _ _ _ _ 1 2
/// ```
///
/// The offset for the first day of the months is the number of leading blanks
/// in the calendar, i.e. 5.
///
/// The same date localized for the Russian calendar has a different offset,
/// because the first day of week is Monday rather than Sunday:
///
/// ```
/// M T W T F S S
/// _ _ _ _ 1 2 3
/// ```
///
/// So the offset is 4, rather than 5.
///
/// This code consolidates the following:
///
/// - [DateTime.weekday] provides a 1-based index into days of week, with 1
/// falling on Monday.
/// - MaterialLocalizations.firstDayOfWeekIndex provides a 0-based index
/// into the MaterialLocalizations.narrowWeekdays list.
/// - MaterialLocalizations.narrowWeekdays list provides localized names of
/// days of week, always starting with Sunday and ending with Saturday.
static int computeFirstDayOffset(
int year, int month, int firstDayOfWeekFromSunday) {
// 0-based day of week, with 0 representing Monday.
final int weekdayFromMonday = DateTime(year, month).weekday - 1;
// firstDayOfWeekFromSunday recomputed to be Monday-based
final int firstDayOfWeekFromMonday = (firstDayOfWeekFromSunday - 1) % 7;
// Number of days between the first day of week appearing on the calendar,
// and the day corresponding to the 1-st of the month.
return (weekdayFromMonday - firstDayOfWeekFromMonday) % 7;
}
/// Returns earliest [DateTime] from the list.
///
/// [dates] must not be null.
/// In case it is null, [ArgumentError] will be thrown.
static DateTime getEarliestFromList(List<DateTime> dates) {
ArgumentError.checkNotNull(dates, "dates");
return dates.fold(dates[0], getEarliest);
}
/// Returns earliest [DateTime] from two.
///
/// If two [DateTime]s is the same moment first ([a]) will be return.
static DateTime getEarliest(DateTime a, DateTime b)
=> a.isBefore(b) ? a : b;
}

View File

@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'date_period.dart';
import 'date_picker_keys.dart';
import 'date_picker_styles.dart';
import 'day_based_changable_picker.dart';
import 'day_picker_selection.dart';
import 'day_type.dart';
import 'event_decoration.dart';
import 'i_selectable_picker.dart';
import 'layout_settings.dart';
import 'typedefs.dart';
/// Date picker for selection a week.
class WeekPicker extends StatelessWidget {
/// Creates a month picker.
WeekPicker(
{Key? key,
required this.selectedDate,
required this.onChanged,
required this.firstDate,
required this.lastDate,
this.initiallyShowDate,
this.datePickerLayoutSettings = const DatePickerLayoutSettings(),
this.datePickerStyles,
this.datePickerKeys,
this.selectableDayPredicate,
this.onSelectionError,
this.eventDecorationBuilder,
this.onMonthChanged})
: assert(!firstDate.isAfter(lastDate)),
assert(!lastDate.isBefore(firstDate)),
assert(!selectedDate.isBefore(firstDate)),
assert(!selectedDate.isAfter(lastDate)),
assert(initiallyShowDate == null
|| !initiallyShowDate.isAfter(lastDate)),
assert(initiallyShowDate == null
|| !initiallyShowDate.isBefore(firstDate)),
super(key: key);
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DateTime selectedDate;
/// Called when the user picks a week.
final ValueChanged<DatePeriod> onChanged;
/// Called when the error was thrown after user selection.
/// (e.g. when user selected a week with one or more days
/// what can't be selected)
final OnSelectionError? onSelectionError;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// Date for defining what month should be shown initially.
///
/// In case of null month with earliest date of the selected week
/// will be shown.
final DateTime? initiallyShowDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Some keys useful for integration tests
final DatePickerKeys? datePickerKeys;
/// Styles what can be customized by user
final DatePickerRangeStyles? datePickerStyles;
/// Function returns if day can be selected or not.
final SelectableDayPredicate? selectableDayPredicate;
/// Builder to get event decoration for each date.
///
/// All event styles are overriden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
/// Called when the user changes the month.
/// New DateTime object represents first day of new month and 00:00 time.
final ValueChanged<DateTime>? onMonthChanged;
@override
Widget build(BuildContext context) {
MaterialLocalizations localizations = MaterialLocalizations.of(context);
int firstDayOfWeekIndex = datePickerStyles?.firstDayOfeWeekIndex ??
localizations.firstDayOfWeekIndex;
ISelectablePicker<DatePeriod> weekSelectablePicker = WeekSelectable(
selectedDate, firstDayOfWeekIndex, firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
return DayBasedChangeablePicker<DatePeriod>(
selectablePicker: weekSelectablePicker,
// todo: maybe create selection for week
// todo: and change logic here to work with it
selection: DayPickerSingleSelection(selectedDate),
firstDate: firstDate,
lastDate: lastDate,
initiallyShownDate: initiallyShowDate,
onChanged: onChanged,
onSelectionError: onSelectionError,
datePickerLayoutSettings: datePickerLayoutSettings,
datePickerStyles: datePickerStyles ?? DatePickerRangeStyles(),
datePickerKeys: datePickerKeys,
eventDecorationBuilder: eventDecorationBuilder,
onMonthChanged: onMonthChanged,
);
}
}

View File

@ -0,0 +1,158 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
intl:
dependency: "direct main"
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
sdks:
dart: ">=2.12.0 <3.0.0"

View File

@ -0,0 +1,20 @@
name: flutter_date_pickers
description: Flutter package for day, week, range and month date pickers.
version: 0.2.4
author: Maria Melnik <melnikmk@gmail.com>
homepage: https://github.com/MariaMelnik/flutter_date_pickers
issue_tracker: https://github.com/MariaMelnik/flutter_date_pickers/issues
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
flutter:
sdk: flutter
intl: ">=0.17.0 <1.0.0"
flutter_localizations:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter

View File

@ -0,0 +1,152 @@
/// Bunch of useful functions for date pickers.
class DateTimeUtils {
/// Returns if two objects have same year, month and day.
/// Time doesn't matter.
static bool sameDate(DateTime dateTimeOne, DateTime dateTimeTwo) =>
dateTimeOne.year == dateTimeTwo.year &&
dateTimeOne.month == dateTimeTwo.month &&
dateTimeOne.day == dateTimeTwo.day;
/// Returns if two objects have same year and month.
/// Day and time don't matter/
static bool sameMonth(DateTime dateTimeOne, DateTime dateTimeTwo) =>
dateTimeOne.year == dateTimeTwo.year
&& dateTimeOne.month == dateTimeTwo.month;
/// Returns number of months between [startDate] and [endDate]
static int monthDelta(DateTime startDate, DateTime endDate) =>
(endDate.year - startDate.year) * 12 +
endDate.month -
startDate.month;
/// Add months to a month truncated date.
static DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) =>
// year is switched automatically if new month > 12
DateTime(monthDate.year, monthDate.month + monthsToAdd);
/// Returns number of years between [startDate] and [endDate]
static int yearDelta(DateTime startDate, DateTime endDate) =>
endDate.year - startDate.year;
/// Returns start of the first day of the week with given day.
///
/// Start of the week calculated using firstDayIndex which is int from 0 to 6
/// where 0 points to Sunday and 6 points to Saturday.
/// (according to MaterialLocalization.firstDayIfWeekIndex)
static DateTime getFirstDayOfWeek(DateTime day, int firstDayIndex) {
// from 1 to 7 where 1 points to Monday and 7 points to Sunday
int weekday = day.weekday;
// to match weekdays where Sunday is 7 not 0
if (firstDayIndex == 0) firstDayIndex = 7;
int diff = weekday - firstDayIndex;
if (diff < 0) diff = 7 + diff;
DateTime firstDayOfWeek = day.subtract(Duration(days: diff));
firstDayOfWeek = startOfTheDay(firstDayOfWeek);
return firstDayOfWeek;
}
/// Returns end of the last day of the week with given day.
///
/// Start of the week calculated using firstDayIndex which is int from 0 to 6
/// where 0 points to Sunday and 6 points to Saturday.
/// (according to MaterialLocalization.firstDayIfWeekIndex)
static DateTime getLastDayOfWeek(DateTime day, int firstDayIndex) {
// from 1 to 7 where 1 points to Monday and 7 points to Sunday
int weekday = day.weekday;
// to match weekdays where Sunday is 7 not 0
if (firstDayIndex == 0) firstDayIndex = 7;
int lastDayIndex = firstDayIndex - 1;
if (lastDayIndex == 0) lastDayIndex = 7;
int diff = lastDayIndex - weekday;
if (diff < 0) diff = 7 + diff;
DateTime lastDayOfWeek = day.add(Duration(days: diff));
lastDayOfWeek = endOfTheDay(lastDayOfWeek);
return lastDayOfWeek;
}
/// Returns end of the given day.
///
/// End time is 1 millisecond before start of the next day.
static DateTime endOfTheDay(DateTime date) =>
DateTime(date.year, date.month, date.day)
.add(const Duration(days: 1))
.subtract(const Duration(milliseconds: 1));
/// Returns start of the given day.
///
/// Start time is 00:00:00.
static DateTime startOfTheDay(DateTime date) =>
DateTime(date.year, date.month, date.day);
/// Computes the offset from the first day of week that the first day of the
/// [month] falls on.
///
/// For example, September 1, 2017 falls on a Friday, which in the calendar
/// localized for United States English appears as:
///
/// ```
/// S M T W T F S
/// _ _ _ _ _ 1 2
/// ```
///
/// The offset for the first day of the months is the number of leading blanks
/// in the calendar, i.e. 5.
///
/// The same date localized for the Russian calendar has a different offset,
/// because the first day of week is Monday rather than Sunday:
///
/// ```
/// M T W T F S S
/// _ _ _ _ 1 2 3
/// ```
///
/// So the offset is 4, rather than 5.
///
/// This code consolidates the following:
///
/// - [DateTime.weekday] provides a 1-based index into days of week, with 1
/// falling on Monday.
/// - MaterialLocalizations.firstDayOfWeekIndex provides a 0-based index
/// into the MaterialLocalizations.narrowWeekdays list.
/// - MaterialLocalizations.narrowWeekdays list provides localized names of
/// days of week, always starting with Sunday and ending with Saturday.
static int computeFirstDayOffset(
int year, int month, int firstDayOfWeekFromSunday) {
// 0-based day of week, with 0 representing Monday.
final int weekdayFromMonday = DateTime(year, month).weekday - 1;
// firstDayOfWeekFromSunday recomputed to be Monday-based
final int firstDayOfWeekFromMonday = (firstDayOfWeekFromSunday - 1) % 7;
// Number of days between the first day of week appearing on the calendar,
// and the day corresponding to the 1-st of the month.
return (weekdayFromMonday - firstDayOfWeekFromMonday) % 7;
}
/// Returns earliest [DateTime] from the list.
///
/// [dates] must not be null.
/// In case it is null, [ArgumentError] will be thrown.
static DateTime getEarliestFromList(List<DateTime> dates) {
ArgumentError.checkNotNull(dates, "dates");
return dates.fold(dates[0], getEarliest);
}
/// Returns earliest [DateTime] from two.
///
/// If two [DateTime]s is the same moment first ([a]) will be return.
static DateTime getEarliest(DateTime a, DateTime b)
=> a.isBefore(b) ? a : b;
}

View File

@ -0,0 +1,42 @@
import 'package:flutter_date_pickers/src/day_type.dart';
import 'package:flutter_date_pickers/src/i_selectable_picker.dart';
import 'package:flutter_test/flutter_test.dart';
import 'date_time_utils.dart';
void main() {
group("DayMultiSelectable test.", () {
test("getDayType() returns correct type for different dates", () {
final now = DateTime.now();
final selectedDates = [
now.subtract(const Duration(days: 1)),
now,
now.add(const Duration(days: 1)),
];
final firstDate = now.subtract(const Duration(days: 10));
final lastDate = now.add(const Duration(days: 10));
final disabledDate = now.subtract(const Duration(days: 5));
// ignore: prefer_function_declarations_over_variables
final selectablePredicate = (DateTime d)
=> !DateTimeUtils.sameDate(d, disabledDate);
final selectableLogic = DayMultiSelectable(
selectedDates, firstDate, lastDate,
selectableDayPredicate: selectablePredicate);
final notSelectedEnabledDateType = selectableLogic.getDayType(firstDate);
expect(notSelectedEnabledDateType, DayType.notSelected);
final disabledDateType = selectableLogic.getDayType(disabledDate);
expect(disabledDateType, DayType.disabled);
for (DateTime d in selectedDates) {
final selectedDateType = selectableLogic.getDayType(d);
expect(selectedDateType, DayType.single,
reason: "Incorrect DayType for the date ${d.toString()}");
}
});
});
}

View File

@ -0,0 +1,31 @@
import 'package:flutter_date_pickers/src/day_type.dart';
import 'package:flutter_date_pickers/src/i_selectable_picker.dart';
import 'package:flutter_test/flutter_test.dart';
import 'date_time_utils.dart';
void main() {
group("DaySelectable test.", () {
test("getDayType() returns correct type for different dates", () {
final selectedDate = DateTime.now();
final firstDate = selectedDate.subtract(const Duration(days: 10));
final lastDate = selectedDate.add(const Duration(days: 10));
final disabledDate = selectedDate.subtract(const Duration(days: 1));
// ignore: prefer_function_declarations_over_variables
final selectablePredicate = (DateTime d)
=> !DateTimeUtils.sameDate(d, disabledDate);
final selectableLogic = DaySelectable(
selectedDate, firstDate, lastDate,
selectableDayPredicate: selectablePredicate);
final selectedDateType = selectableLogic.getDayType(selectedDate);
final notSelectedEnabledDateType = selectableLogic.getDayType(firstDate);
final disabledDateType = selectableLogic.getDayType(disabledDate);
expect(selectedDateType, DayType.single);
expect(notSelectedEnabledDateType, DayType.notSelected);
expect(disabledDateType, DayType.disabled);
});
});
}

View File

@ -0,0 +1,55 @@
import 'package:flutter_date_pickers/flutter_date_pickers.dart';
import 'package:flutter_date_pickers/src/day_type.dart';
import 'package:flutter_date_pickers/src/i_selectable_picker.dart';
import 'package:flutter_test/flutter_test.dart';
import 'date_time_utils.dart';
void main() {
group("RangeSelectable test.", () {
test("getDayType() returns correct type for different dates", () {
final now = DateTime.now();
final startPeriod = now.subtract(const Duration(days: 3));
final endPeriod = now.add(const Duration(days: 3));
final selectedPeriod = DatePeriod(startPeriod, endPeriod);
final firstDate = now.subtract(const Duration(days: 10));
final lastDate = now.add(const Duration(days: 10));
final disabledDate = now.subtract(const Duration(days: 5));
// ignore: prefer_function_declarations_over_variables
final selectablePredicate = (DateTime d)
=> !DateTimeUtils.sameDate(d, disabledDate);
final selectableLogic = RangeSelectable(
selectedPeriod, firstDate, lastDate,
selectableDayPredicate: selectablePredicate);
final notSelectedEnabledDateType = selectableLogic.getDayType(firstDate);
expect(notSelectedEnabledDateType, DayType.notSelected);
final disabledDateType = selectableLogic.getDayType(disabledDate);
expect(disabledDateType, DayType.disabled);
final startPeriodDateType = selectableLogic.getDayType(startPeriod);
expect(startPeriodDateType, DayType.start);
final endPeriodDateType = selectableLogic.getDayType(endPeriod);
expect(endPeriodDateType, DayType.end);
final periodDays = endPeriod.difference(startPeriod).inDays;
// Count of the day period which is not start and not end.
final middleDaysCount = periodDays - 2;
final middleDates = List.generate(middleDaysCount,
(i) => startPeriod.add(Duration(days: i + 1)));
for (DateTime date in middleDates) {
final middlePeriodDateType = selectableLogic.getDayType(date);
expect(middlePeriodDateType, DayType.middle,
reason: "Incorrect DayType for the date ${date.toString()} "
"in period ${startPeriod.toString()} - ${endPeriod.toString()}");
}
});
});
}

View File

@ -0,0 +1,56 @@
import 'package:flutter_date_pickers/src/day_type.dart';
import 'package:flutter_date_pickers/src/i_selectable_picker.dart';
import 'package:flutter_test/flutter_test.dart';
import 'date_time_utils.dart';
void main() {
group("WeekSelectable test.", () {
test("getDayType() returns correct type for different dates", () {
final selectedDate = DateTime(2020, 10, 5); // Monday
final firstDayOfWeekIndex = 1; // Week starts from Monday
final firstDate = selectedDate.subtract(const Duration(days: 10));
final lastDate = selectedDate.add(const Duration(days: 10));
final disabledDate = selectedDate.subtract(const Duration(days: 5));
// ignore: prefer_function_declarations_over_variables
final selectablePredicate = (DateTime d)
=> !DateTimeUtils.sameDate(d, disabledDate);
final selectableLogic = WeekSelectable(
selectedDate, firstDayOfWeekIndex, firstDate, lastDate,
selectableDayPredicate: selectablePredicate);
final notSelectedEnabledDateType = selectableLogic.getDayType(firstDate);
expect(notSelectedEnabledDateType, DayType.notSelected);
final disabledDateType = selectableLogic.getDayType(disabledDate);
expect(disabledDateType, DayType.disabled);
final weekStart = DateTimeUtils
.getFirstDayOfWeek(selectedDate, firstDayOfWeekIndex);
final weekEnd = DateTimeUtils
.getLastDayOfWeek(selectedDate, firstDayOfWeekIndex);
final startPeriodDateType = selectableLogic.getDayType(weekStart);
expect(startPeriodDateType, DayType.start);
final endPeriodDateType = selectableLogic.getDayType(weekEnd);
expect(endPeriodDateType, DayType.end);
// Count of the week days which is not start and not end (7 - 2).
final middleDaysCount = 5;
final middleDates = List.generate(middleDaysCount,
(i) => weekStart.add(Duration(days: i + 1)));
for (DateTime date in middleDates) {
final middlePeriodDateType = selectableLogic.getDayType(date);
expect(middlePeriodDateType, DayType.middle,
reason: "Incorrect DayType for the date ${date.toString()} "
"in week ${weekStart.toString()} - ${weekEnd.toString()}");
}
});
});
}

View File

@ -0,0 +1,12 @@
#!/bin/sh
OUTPUT="$(flutter analyze)"
echo "$OUTPUT"
echo
if grep -q "error •" echo "$OUTPUT"; then
echo "flutter analyze found errors"
exit 1
else
echo "flutter analyze didn't find any errors"
exit 0
fi