Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
4560ebcf
Unverified
Commit
4560ebcf
authored
May 08, 2020
by
Darren Austin
Committed by
GitHub
May 08, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implementation of the Material Date Range Picker. (#55939)
parent
cd67da26
Changes
10
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
2356 additions
and
65 deletions
+2356
-65
calendar_date_range_picker.dart
.../lib/src/material/pickers/calendar_date_range_picker.dart
+773
-0
date_picker_common.dart
.../flutter/lib/src/material/pickers/date_picker_common.dart
+52
-1
date_picker_dialog.dart
.../flutter/lib/src/material/pickers/date_picker_dialog.dart
+38
-18
date_picker_header.dart
.../flutter/lib/src/material/pickers/date_picker_header.dart
+8
-6
date_range_picker_dialog.dart
...er/lib/src/material/pickers/date_range_picker_dialog.dart
+715
-0
date_utils.dart
packages/flutter/lib/src/material/pickers/date_utils.dart
+35
-0
input_date_picker.dart
...s/flutter/lib/src/material/pickers/input_date_picker.dart
+41
-39
input_date_range_picker.dart
...ter/lib/src/material/pickers/input_date_range_picker.dart
+276
-0
pickers.dart
packages/flutter/lib/src/material/pickers/pickers.dart
+6
-1
date_range_picker_test.dart
packages/flutter/test/material/date_range_picker_test.dart
+412
-0
No files found.
packages/flutter/lib/src/material/pickers/calendar_date_range_picker.dart
0 → 100644
View file @
4560ebcf
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:math'
as
math
;
import
'package:flutter/gestures.dart'
show
DragStartBehavior
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/widgets.dart'
;
import
'../color_scheme.dart'
;
import
'../divider.dart'
;
import
'../material_localizations.dart'
;
import
'../text_theme.dart'
;
import
'../theme.dart'
;
import
'date_utils.dart'
as
utils
;
const
double
_monthItemHeaderHeight
=
58.0
;
const
double
_monthItemFooterHeight
=
12.0
;
const
double
_monthItemRowHeight
=
42.0
;
const
double
_monthItemSpaceBetweenRows
=
8.0
;
const
double
_horizontalPadding
=
8.0
;
const
double
_maxCalendarWidthLandscape
=
384.0
;
const
double
_maxCalendarWidthPortrait
=
480.0
;
/// Displays a scrollable calendar grid that allows a user to select a range
/// of dates.
///
/// Note: this is not publicly exported (see pickers.dart), as it is an
/// internal component used by [showDateRangePicker].
class
CalendarDateRangePicker
extends
StatefulWidget
{
/// Creates a scrollable calendar grid for picking date ranges.
CalendarDateRangePicker
({
Key
key
,
DateTime
initialStartDate
,
DateTime
initialEndDate
,
@required
DateTime
firstDate
,
@required
DateTime
lastDate
,
DateTime
currentDate
,
@required
this
.
onStartDateChanged
,
@required
this
.
onEndDateChanged
,
})
:
initialStartDate
=
initialStartDate
!=
null
?
utils
.
dateOnly
(
initialStartDate
)
:
null
,
initialEndDate
=
initialEndDate
!=
null
?
utils
.
dateOnly
(
initialEndDate
)
:
null
,
assert
(
firstDate
!=
null
),
assert
(
lastDate
!=
null
),
firstDate
=
utils
.
dateOnly
(
firstDate
),
lastDate
=
utils
.
dateOnly
(
lastDate
),
currentDate
=
utils
.
dateOnly
(
currentDate
??
DateTime
.
now
()),
super
(
key:
key
)
{
assert
(
this
.
initialStartDate
==
null
||
this
.
initialEndDate
==
null
||
!
this
.
initialStartDate
.
isAfter
(
initialEndDate
),
'initialStartDate must be on or before initialEndDate.'
);
assert
(
!
this
.
lastDate
.
isBefore
(
this
.
firstDate
),
'firstDate must be on or before lastDate.'
);
}
/// The [DateTime] that represents the start of the initial date range selection.
final
DateTime
initialStartDate
;
/// The [DateTime] that represents the end of the initial date range selection.
final
DateTime
initialEndDate
;
/// The earliest allowable [DateTime] that the user can select.
final
DateTime
firstDate
;
/// The latest allowable [DateTime] that the user can select.
final
DateTime
lastDate
;
/// The [DateTime] representing today. It will be highlighted in the day grid.
final
DateTime
currentDate
;
/// Called when the user changes the start date of the selected range.
final
ValueChanged
<
DateTime
>
onStartDateChanged
;
/// Called when the user changes the end date of the selected range.
final
ValueChanged
<
DateTime
>
onEndDateChanged
;
@override
_CalendarDateRangePickerState
createState
()
=>
_CalendarDateRangePickerState
();
}
class
_CalendarDateRangePickerState
extends
State
<
CalendarDateRangePicker
>
{
DateTime
_startDate
;
DateTime
_endDate
;
int
_initialMonthIndex
=
0
;
ScrollController
_controller
;
bool
_showWeekBottomDivider
;
@override
void
initState
()
{
super
.
initState
();
_controller
=
ScrollController
();
_controller
.
addListener
(
_scrollListener
);
_startDate
=
widget
.
initialStartDate
;
_endDate
=
widget
.
initialEndDate
;
// Calculate the index for the initially displayed month. This is needed to
// divide the list of months into two `SliverList`s.
final
DateTime
initialDate
=
widget
.
initialStartDate
??
widget
.
currentDate
;
if
(
widget
.
firstDate
.
isBefore
(
initialDate
)
&&
widget
.
lastDate
.
isAfter
(
initialDate
))
{
_initialMonthIndex
=
utils
.
monthDelta
(
widget
.
firstDate
,
initialDate
);
}
_showWeekBottomDivider
=
_initialMonthIndex
!=
0
;
}
@override
void
dispose
()
{
_controller
.
dispose
();
super
.
dispose
();
}
void
_scrollListener
()
{
if
(
_controller
.
offset
<=
_controller
.
position
.
minScrollExtent
)
{
setState
(()
{
_showWeekBottomDivider
=
false
;
});
}
else
if
(!
_showWeekBottomDivider
)
{
setState
(()
{
_showWeekBottomDivider
=
true
;
});
}
}
int
get
_numberOfMonths
=>
utils
.
monthDelta
(
widget
.
firstDate
,
widget
.
lastDate
)
+
1
;
void
_vibrate
()
{
switch
(
Theme
.
of
(
context
).
platform
)
{
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
HapticFeedback
.
vibrate
();
break
;
default
:
break
;
}
}
// This updates the selected date range using this logic:
//
// * From the unselected state, selecting one date creates the start date.
// * If the next selection is before the start date, reset date range and
// set the start date to that selection.
// * If the next selection is on or after the start date, set the end date
// to that selection.
// * After both start and end dates are selected, any subsequent selection
// resets the date range and sets start date to that selection.
void
_updateSelection
(
DateTime
date
)
{
_vibrate
();
setState
(()
{
if
(
_startDate
!=
null
&&
_endDate
==
null
&&
!
date
.
isBefore
(
_startDate
))
{
_endDate
=
date
;
widget
.
onEndDateChanged
?.
call
(
_endDate
);
}
else
{
_startDate
=
date
;
widget
.
onStartDateChanged
?.
call
(
_startDate
);
if
(
_endDate
!=
null
)
{
_endDate
=
null
;
widget
.
onEndDateChanged
?.
call
(
_endDate
);
}
}
});
}
Widget
_buildMonthItem
(
BuildContext
context
,
int
index
,
bool
beforeInitialMonth
)
{
final
int
monthIndex
=
beforeInitialMonth
?
_initialMonthIndex
-
index
-
1
:
_initialMonthIndex
+
index
;
final
DateTime
month
=
utils
.
addMonthsToMonthDate
(
widget
.
firstDate
,
monthIndex
);
return
_MonthItem
(
selectedDateStart:
_startDate
,
selectedDateEnd:
_endDate
,
currentDate:
widget
.
currentDate
,
firstDate:
widget
.
firstDate
,
lastDate:
widget
.
lastDate
,
displayedMonth:
month
,
onChanged:
_updateSelection
,
);
}
@override
Widget
build
(
BuildContext
context
)
{
const
Key
sliverAfterKey
=
Key
(
'sliverAfterKey'
);
return
Column
(
children:
<
Widget
>[
_DayHeaders
(),
if
(
_showWeekBottomDivider
)
const
Divider
(
height:
0
),
Expanded
(
// In order to prevent performance issues when displaying the
// correct initial month, 2 `SliverList`s are used to split the
// months. The first item in the second SliverList is the initial
// month to be displayed.
child:
CustomScrollView
(
controller:
_controller
,
center:
sliverAfterKey
,
slivers:
<
Widget
>[
SliverList
(
delegate:
SliverChildBuilderDelegate
((
BuildContext
context
,
int
index
)
=>
_buildMonthItem
(
context
,
index
,
true
),
childCount:
_initialMonthIndex
,
),
),
SliverList
(
key:
sliverAfterKey
,
delegate:
SliverChildBuilderDelegate
((
BuildContext
context
,
int
index
)
=>
_buildMonthItem
(
context
,
index
,
false
),
childCount:
_numberOfMonths
-
_initialMonthIndex
,
),
),
],
),
),
],
);
}
}
class
_DayHeaders
extends
StatelessWidget
{
/// 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
(
TextStyle
headerStyle
,
MaterialLocalizations
localizations
)
{
final
List
<
Widget
>
result
=
<
Widget
>[];
for
(
int
i
=
localizations
.
firstDayOfWeekIndex
;
true
;
i
=
(
i
+
1
)
%
7
)
{
final
String
weekday
=
localizations
.
narrowWeekdays
[
i
];
result
.
add
(
ExcludeSemantics
(
child:
Center
(
child:
Text
(
weekday
,
style:
headerStyle
)),
));
if
(
i
==
(
localizations
.
firstDayOfWeekIndex
-
1
)
%
7
)
break
;
}
return
result
;
}
@override
Widget
build
(
BuildContext
context
)
{
final
ThemeData
themeData
=
Theme
.
of
(
context
);
final
ColorScheme
colorScheme
=
themeData
.
colorScheme
;
final
TextStyle
textStyle
=
themeData
.
textTheme
.
subtitle2
.
apply
(
color:
colorScheme
.
onSurface
);
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
final
List
<
Widget
>
labels
=
_getDayHeaders
(
textStyle
,
localizations
);
// Add leading and trailing containers for edges of the custom grid layout.
labels
.
insert
(
0
,
Container
());
labels
.
add
(
Container
());
return
Container
(
constraints:
BoxConstraints
(
maxWidth:
MediaQuery
.
of
(
context
).
orientation
==
Orientation
.
landscape
?
_maxCalendarWidthLandscape
:
_maxCalendarWidthPortrait
,
maxHeight:
_monthItemRowHeight
,
),
child:
GridView
.
custom
(
shrinkWrap:
true
,
gridDelegate:
_monthItemGridDelegate
,
childrenDelegate:
SliverChildListDelegate
(
labels
,
addRepaintBoundaries:
false
,
),
),
);
}
}
class
_MonthItemGridDelegate
extends
SliverGridDelegate
{
const
_MonthItemGridDelegate
();
@override
SliverGridLayout
getLayout
(
SliverConstraints
constraints
)
{
final
double
tileWidth
=
(
constraints
.
crossAxisExtent
-
2
*
_horizontalPadding
)
/
DateTime
.
daysPerWeek
;
return
_MonthSliverGridLayout
(
crossAxisCount:
DateTime
.
daysPerWeek
+
2
,
dayChildWidth:
tileWidth
,
edgeChildWidth:
_horizontalPadding
,
reverseCrossAxis:
axisDirectionIsReversed
(
constraints
.
crossAxisDirection
),
);
}
@override
bool
shouldRelayout
(
_MonthItemGridDelegate
oldDelegate
)
=>
false
;
}
const
_MonthItemGridDelegate
_monthItemGridDelegate
=
_MonthItemGridDelegate
();
class
_MonthSliverGridLayout
extends
SliverGridLayout
{
/// Creates a layout that uses equally sized and spaced tiles for each day of
/// the week and an additional edge tile for padding at the start and end of
/// each row.
///
/// This is necessary to facilitate the painting of the range highlight
/// correctly.
const
_MonthSliverGridLayout
({
@required
this
.
crossAxisCount
,
@required
this
.
dayChildWidth
,
@required
this
.
edgeChildWidth
,
@required
this
.
reverseCrossAxis
,
})
:
assert
(
crossAxisCount
!=
null
&&
crossAxisCount
>
0
),
assert
(
dayChildWidth
!=
null
&&
dayChildWidth
>=
0
),
assert
(
edgeChildWidth
!=
null
&&
edgeChildWidth
>=
0
),
assert
(
reverseCrossAxis
!=
null
);
/// The number of children in the cross axis.
final
int
crossAxisCount
;
/// The width in logical pixels of the day child widgets.
final
double
dayChildWidth
;
/// The width in logical pixels of the edge child widgets.
final
double
edgeChildWidth
;
/// Whether the children should be placed in the opposite order of increasing
/// coordinates in the cross axis.
///
/// For example, if the cross axis is horizontal, the children are placed from
/// left to right when [reverseCrossAxis] is false and from right to left when
/// [reverseCrossAxis] is true.
///
/// Typically set to the return value of [axisDirectionIsReversed] applied to
/// the [SliverConstraints.crossAxisDirection].
final
bool
reverseCrossAxis
;
/// The number of logical pixels from the leading edge of one row to the
/// leading edge of the next row.
double
get
_rowHeight
{
return
_monthItemRowHeight
+
_monthItemSpaceBetweenRows
;
}
/// The height in logical pixels of the children widgets.
double
get
_childHeight
{
return
_monthItemRowHeight
;
}
@override
int
getMinChildIndexForScrollOffset
(
double
scrollOffset
)
{
return
crossAxisCount
*
(
scrollOffset
~/
_rowHeight
);
}
@override
int
getMaxChildIndexForScrollOffset
(
double
scrollOffset
)
{
final
int
mainAxisCount
=
(
scrollOffset
/
_rowHeight
).
ceil
();
return
math
.
max
(
0
,
crossAxisCount
*
mainAxisCount
-
1
);
}
double
_getCrossAxisOffset
(
double
crossAxisStart
,
bool
isPadding
)
{
if
(
reverseCrossAxis
)
{
return
((
crossAxisCount
-
2
)
*
dayChildWidth
+
2
*
edgeChildWidth
)
-
crossAxisStart
-
(
isPadding
?
edgeChildWidth
:
dayChildWidth
);
}
return
crossAxisStart
;
}
@override
SliverGridGeometry
getGeometryForChildIndex
(
int
index
)
{
final
int
adjustedIndex
=
index
%
crossAxisCount
;
final
bool
isEdge
=
adjustedIndex
==
0
||
adjustedIndex
==
crossAxisCount
-
1
;
final
double
crossAxisStart
=
math
.
max
(
0
,
(
adjustedIndex
-
1
)
*
dayChildWidth
+
edgeChildWidth
);
return
SliverGridGeometry
(
scrollOffset:
(
index
~/
crossAxisCount
)
*
_rowHeight
,
crossAxisOffset:
_getCrossAxisOffset
(
crossAxisStart
,
isEdge
),
mainAxisExtent:
_childHeight
,
crossAxisExtent:
isEdge
?
edgeChildWidth
:
dayChildWidth
,
);
}
@override
double
computeMaxScrollOffset
(
int
childCount
)
{
assert
(
childCount
>=
0
);
final
int
mainAxisCount
=
((
childCount
-
1
)
~/
crossAxisCount
)
+
1
;
final
double
mainAxisSpacing
=
_rowHeight
-
_childHeight
;
return
_rowHeight
*
mainAxisCount
-
mainAxisSpacing
;
}
}
/// Displays the days of a given month and allows choosing a date range.
///
/// The days are arranged in a rectangular grid with one column for each day of
/// the week.
class
_MonthItem
extends
StatelessWidget
{
/// Creates a month item.
_MonthItem
({
Key
key
,
@required
this
.
selectedDateStart
,
@required
this
.
selectedDateEnd
,
@required
this
.
currentDate
,
@required
this
.
onChanged
,
@required
this
.
firstDate
,
@required
this
.
lastDate
,
@required
this
.
displayedMonth
,
this
.
dragStartBehavior
=
DragStartBehavior
.
start
,
})
:
assert
(
firstDate
!=
null
),
assert
(
lastDate
!=
null
),
assert
(!
firstDate
.
isAfter
(
lastDate
)),
assert
(
selectedDateStart
==
null
||
!
selectedDateStart
.
isBefore
(
firstDate
)),
assert
(
selectedDateEnd
==
null
||
!
selectedDateEnd
.
isBefore
(
firstDate
)),
assert
(
selectedDateStart
==
null
||
!
selectedDateStart
.
isAfter
(
lastDate
)),
assert
(
selectedDateEnd
==
null
||
!
selectedDateEnd
.
isAfter
(
lastDate
)),
assert
(
selectedDateStart
==
null
||
selectedDateEnd
==
null
||
!
selectedDateStart
.
isAfter
(
selectedDateEnd
)),
assert
(
currentDate
!=
null
),
assert
(
onChanged
!=
null
),
assert
(
displayedMonth
!=
null
),
assert
(
dragStartBehavior
!=
null
),
super
(
key:
key
);
/// The currently selected start date.
///
/// This date is highlighted in the picker.
final
DateTime
selectedDateStart
;
/// The currently selected end date.
///
/// This date is highlighted in the picker.
final
DateTime
selectedDateEnd
;
/// The current date at the time the picker is displayed.
final
DateTime
currentDate
;
/// Called when the user picks a day.
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
;
/// The month whose days are displayed by this picker.
final
DateTime
displayedMonth
;
/// Determines the way that drag start behavior is handled.
///
/// If set to [DragStartBehavior.start], the drag gesture used to scroll a
/// date picker wheel will begin upon the detection of a drag gesture. If set
/// to [DragStartBehavior.down] it will begin when a down event is first
/// detected.
///
/// In general, setting this to [DragStartBehavior.start] will make drag
/// animation smoother and setting it to [DragStartBehavior.down] will make
/// drag behavior feel slightly more reactive.
///
/// By default, the drag start behavior is [DragStartBehavior.start].
///
/// See also:
///
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for
/// the different behaviors.
final
DragStartBehavior
dragStartBehavior
;
Color
_highlightColor
(
BuildContext
context
)
{
final
ColorScheme
colors
=
Theme
.
of
(
context
).
colorScheme
;
return
Color
.
alphaBlend
(
colors
.
primary
.
withOpacity
(
0.12
),
colors
.
background
);
}
Widget
_buildDayItem
(
BuildContext
context
,
DateTime
dayToBuild
,
int
firstDayOffset
,
int
daysInMonth
)
{
final
ThemeData
theme
=
Theme
.
of
(
context
);
final
ColorScheme
colorScheme
=
theme
.
colorScheme
;
final
TextTheme
textTheme
=
theme
.
textTheme
;
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
final
TextDirection
textDirection
=
Directionality
.
of
(
context
);
final
Color
highlightColor
=
_highlightColor
(
context
);
final
int
day
=
dayToBuild
.
day
;
final
bool
isDisabled
=
dayToBuild
.
isAfter
(
lastDate
)
||
dayToBuild
.
isBefore
(
firstDate
);
BoxDecoration
decoration
;
TextStyle
itemStyle
=
textTheme
.
bodyText2
;
final
bool
isRangeSelected
=
selectedDateStart
!=
null
&&
selectedDateEnd
!=
null
;
final
bool
isSelectedDayStart
=
selectedDateStart
!=
null
&&
dayToBuild
.
isAtSameMomentAs
(
selectedDateStart
);
final
bool
isSelectedDayEnd
=
selectedDateEnd
!=
null
&&
dayToBuild
.
isAtSameMomentAs
(
selectedDateEnd
);
final
bool
isInRange
=
isRangeSelected
&&
dayToBuild
.
isAfter
(
selectedDateStart
)
&&
dayToBuild
.
isBefore
(
selectedDateEnd
);
_HighlightPainter
highlightPainter
;
if
(
isSelectedDayStart
||
isSelectedDayEnd
)
{
// The selected start and end dates gets a circle background
// highlight, and a contrasting text color.
itemStyle
=
textTheme
.
bodyText2
?.
apply
(
color:
colorScheme
.
onPrimary
);
decoration
=
BoxDecoration
(
color:
colorScheme
.
primary
,
shape:
BoxShape
.
circle
,
);
if
(
isRangeSelected
&&
selectedDateStart
!=
selectedDateEnd
)
{
final
_HighlightPainterStyle
style
=
isSelectedDayStart
?
_HighlightPainterStyle
.
highlightTrailing
:
_HighlightPainterStyle
.
highlightLeading
;
highlightPainter
=
_HighlightPainter
(
color:
highlightColor
,
style:
style
,
textDirection:
textDirection
,
);
}
}
else
if
(
isInRange
)
{
// The days within the range get a light background highlight.
highlightPainter
=
_HighlightPainter
(
color:
highlightColor
,
style:
_HighlightPainterStyle
.
highlightAll
,
textDirection:
textDirection
,
);
}
else
if
(
isDisabled
)
{
itemStyle
=
textTheme
.
bodyText2
?.
apply
(
color:
colorScheme
.
onSurface
.
withOpacity
(
0.38
));
}
else
if
(
utils
.
isSameDay
(
currentDate
,
dayToBuild
))
{
// The current day gets a different text color and a circle stroke
// border.
itemStyle
=
textTheme
.
bodyText2
?.
apply
(
color:
colorScheme
.
primary
);
decoration
=
BoxDecoration
(
border:
Border
.
all
(
color:
colorScheme
.
primary
,
width:
1
),
shape:
BoxShape
.
circle
,
);
}
// 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.
String
semanticLabel
=
'
${localizations.formatDecimal(day)}
,
${localizations.formatFullDate(dayToBuild)}
'
;
if
(
isSelectedDayStart
)
{
// TODO(darrenaustin): localize 'Start Date' and 'End Date'
semanticLabel
=
'Start Date '
+
semanticLabel
;
}
else
if
(
isSelectedDayEnd
)
{
semanticLabel
=
'End Date '
+
semanticLabel
;
}
Widget
dayWidget
=
Container
(
decoration:
decoration
,
child:
Center
(
child:
Semantics
(
label:
semanticLabel
,
selected:
isSelectedDayStart
||
isSelectedDayEnd
,
child:
ExcludeSemantics
(
child:
Text
(
localizations
.
formatDecimal
(
day
),
style:
itemStyle
),
),
),
),
);
if
(
highlightPainter
!=
null
)
{
dayWidget
=
CustomPaint
(
painter:
highlightPainter
,
child:
dayWidget
,
);
}
if
(!
isDisabled
)
{
dayWidget
=
GestureDetector
(
behavior:
HitTestBehavior
.
opaque
,
onTap:
()
{
onChanged
(
dayToBuild
);
},
child:
dayWidget
,
dragStartBehavior:
dragStartBehavior
,
);
}
return
dayWidget
;
}
Widget
_buildEdgeContainer
(
BuildContext
context
,
bool
isHighlighted
)
{
return
Container
(
color:
isHighlighted
?
_highlightColor
(
context
)
:
null
);
}
@override
Widget
build
(
BuildContext
context
)
{
final
ThemeData
themeData
=
Theme
.
of
(
context
);
final
TextTheme
textTheme
=
themeData
.
textTheme
;
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
final
int
year
=
displayedMonth
.
year
;
final
int
month
=
displayedMonth
.
month
;
final
int
daysInMonth
=
utils
.
getDaysInMonth
(
year
,
month
);
final
int
dayOffset
=
utils
.
firstDayOffset
(
year
,
month
,
localizations
);
final
int
weeks
=
((
daysInMonth
+
dayOffset
)
/
DateTime
.
daysPerWeek
).
ceil
();
final
double
gridHeight
=
weeks
*
_monthItemRowHeight
+
(
weeks
-
1
)
*
_monthItemSpaceBetweenRows
;
final
List
<
Widget
>
dayItems
=
<
Widget
>[];
for
(
int
i
=
0
;
true
;
i
+=
1
)
{
// 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
// a leap year.
final
int
day
=
i
-
dayOffset
+
1
;
if
(
day
>
daysInMonth
)
break
;
if
(
day
<
1
)
{
dayItems
.
add
(
Container
());
}
else
{
final
DateTime
dayToBuild
=
DateTime
(
year
,
month
,
day
);
final
Widget
dayItem
=
_buildDayItem
(
context
,
dayToBuild
,
dayOffset
,
daysInMonth
,
);
dayItems
.
add
(
dayItem
);
}
}
// Add the leading/trailing edge containers to each week in order to
// correctly extend the range highlight.
final
List
<
Widget
>
paddedDayItems
=
<
Widget
>[];
for
(
int
i
=
0
;
i
<
weeks
;
i
++)
{
final
int
start
=
i
*
DateTime
.
daysPerWeek
;
final
int
end
=
math
.
min
(
start
+
DateTime
.
daysPerWeek
,
dayItems
.
length
,
);
final
List
<
Widget
>
weekList
=
dayItems
.
sublist
(
start
,
end
);
final
DateTime
dateAfterLeadingPadding
=
DateTime
(
year
,
month
,
start
-
dayOffset
+
1
);
// Only color the edge container if it is after the start date and
// on/before the end date.
final
bool
isLeadingInRange
=
!(
dayOffset
>
0
&&
i
==
0
)
&&
selectedDateStart
!=
null
&&
selectedDateEnd
!=
null
&&
dateAfterLeadingPadding
.
isAfter
(
selectedDateStart
)
&&
!
dateAfterLeadingPadding
.
isAfter
(
selectedDateEnd
);
weekList
.
insert
(
0
,
_buildEdgeContainer
(
context
,
isLeadingInRange
));
// Only add a trailing edge container if it is for a full week and not a
// partial week.
if
(
end
<
dayItems
.
length
||
(
end
==
dayItems
.
length
&&
dayItems
.
length
%
DateTime
.
daysPerWeek
==
0
))
{
final
DateTime
dateBeforeTrailingPadding
=
DateTime
(
year
,
month
,
end
-
dayOffset
);
// Only color the edge container if it is on/after the start date and
// before the end date.
final
bool
isTrailingInRange
=
selectedDateStart
!=
null
&&
selectedDateEnd
!=
null
&&
!
dateBeforeTrailingPadding
.
isBefore
(
selectedDateStart
)
&&
dateBeforeTrailingPadding
.
isBefore
(
selectedDateEnd
);
weekList
.
add
(
_buildEdgeContainer
(
context
,
isTrailingInRange
));
}
paddedDayItems
.
addAll
(
weekList
);
}
final
double
maxWidth
=
MediaQuery
.
of
(
context
).
orientation
==
Orientation
.
landscape
?
_maxCalendarWidthLandscape
:
_maxCalendarWidthPortrait
;
return
Column
(
children:
<
Widget
>[
Container
(
constraints:
BoxConstraints
(
maxWidth:
maxWidth
),
height:
_monthItemHeaderHeight
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
alignment:
AlignmentDirectional
.
centerStart
,
child:
ExcludeSemantics
(
child:
Text
(
localizations
.
formatMonthYear
(
displayedMonth
),
style:
textTheme
.
bodyText2
.
apply
(
color:
themeData
.
colorScheme
.
onSurface
),
),
),
),
Container
(
constraints:
BoxConstraints
(
maxWidth:
maxWidth
,
maxHeight:
gridHeight
,
),
child:
GridView
.
custom
(
physics:
const
NeverScrollableScrollPhysics
(),
gridDelegate:
_monthItemGridDelegate
,
childrenDelegate:
SliverChildListDelegate
(
paddedDayItems
,
addRepaintBoundaries:
false
,
),
),
),
const
SizedBox
(
height:
_monthItemFooterHeight
),
],
);
}
}
/// Determines which style to use to paint the highlight.
enum
_HighlightPainterStyle
{
/// Paints nothing.
none
,
/// Paints a rectangle that occupies the leading half of the space.
highlightLeading
,
/// Paints a rectangle that occupies the trailing half of the space.
highlightTrailing
,
/// Paints a rectangle that occupies all available space.
highlightAll
,
}
/// This custom painter will add a background highlight to its child.
///
/// This highlight will be drawn depending on the [style], [color], and
/// [textDirection] supplied. It will either paint a rectangle on the
/// left/right, a full rectangle, or nothing at all. This logic is determined by
/// a combination of the [style] and [textDirection].
class
_HighlightPainter
extends
CustomPainter
{
_HighlightPainter
({
this
.
color
,
this
.
style
=
_HighlightPainterStyle
.
none
,
this
.
textDirection
,
});
final
Color
color
;
final
_HighlightPainterStyle
style
;
final
TextDirection
textDirection
;
@override
void
paint
(
Canvas
canvas
,
Size
size
)
{
if
(
style
==
_HighlightPainterStyle
.
none
)
{
return
;
}
final
Paint
paint
=
Paint
()
..
color
=
color
..
style
=
PaintingStyle
.
fill
;
// This ensures no gaps in the highlight track due to floating point
// division of the available screen width.
final
double
width
=
size
.
width
+
1
;
final
Rect
rectLeft
=
Rect
.
fromLTWH
(
0
,
0
,
width
/
2
,
size
.
height
);
final
Rect
rectRight
=
Rect
.
fromLTWH
(
size
.
width
/
2
,
0
,
width
/
2
,
size
.
height
);
switch
(
style
)
{
case
_HighlightPainterStyle
.
highlightTrailing
:
canvas
.
drawRect
(
textDirection
==
TextDirection
.
ltr
?
rectRight
:
rectLeft
,
paint
,
);
break
;
case
_HighlightPainterStyle
.
highlightLeading
:
canvas
.
drawRect
(
textDirection
==
TextDirection
.
ltr
?
rectLeft
:
rectRight
,
paint
,
);
break
;
case
_HighlightPainterStyle
.
highlightAll
:
canvas
.
drawRect
(
Rect
.
fromLTWH
(
0
,
0
,
width
,
size
.
height
),
paint
,
);
break
;
default
:
break
;
}
}
@override
bool
shouldRepaint
(
CustomPainter
oldDelegate
)
=>
false
;
}
packages/flutter/lib/src/material/pickers/date_picker_common.dart
View file @
4560ebcf
...
...
@@ -2,11 +2,20 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:ui'
show
hashValues
;
import
'package:flutter/foundation.dart'
;
/// Mode of the date picker dialog.
///
/// Either a calendar or text input. In [calendar] mode, a calendar view is
/// displayed and the user taps the day they wish to select. In [input] mode a
/// [TextField] is displayed and the user types in the date they wish to select.
///
/// See also:
///
/// * [showDatePicker] and [showDateRangePicker], which use this to control
/// the initial entry mode of their dialogs.
enum
DatePickerEntryMode
{
/// Tapping on a calendar.
calendar
,
...
...
@@ -34,5 +43,47 @@ enum DatePickerMode {
/// Signature for predicating dates for enabled date selections.
///
/// See [showDatePicker].
/// See [showDatePicker], which has a [SelectableDayPredicate] parameter used
/// to specify allowable days in the date picker.
typedef
SelectableDayPredicate
=
bool
Function
(
DateTime
day
);
/// Encapsulates a start and end [DateTime] that represent the range of dates
/// between them.
///
/// See also:
/// * [showDateRangePicker], which displays a dialog that allows the user to
/// select a date range.
@immutable
class
DateTimeRange
{
/// Creates a date range for the given start and end [DateTime].
///
/// [start] and [end] must be non-null.
const
DateTimeRange
({
@required
this
.
start
,
@required
this
.
end
,
})
:
assert
(
start
!=
null
),
assert
(
end
!=
null
);
/// The start of the range of dates.
final
DateTime
start
;
/// The end of the range of dates.
final
DateTime
end
;
/// Returns a [Duration] of the time between [start] and [end].
///
/// See [DateTime.difference] for more details.
Duration
get
duration
=>
end
.
difference
(
start
);
@override
bool
operator
==(
Object
other
)
{
if
(
other
.
runtimeType
!=
runtimeType
)
return
false
;
return
other
is
DateTimeRange
&&
other
.
start
==
start
&&
other
.
end
==
end
;
}
@override
int
get
hashCode
=>
hashValues
(
start
,
end
);
}
packages/flutter/lib/src/material/pickers/date_picker_dialog.dart
View file @
4560ebcf
...
...
@@ -30,6 +30,9 @@ const Size _calendarLandscapeDialogSize = Size(496.0, 346.0);
const
Size
_inputPortraitDialogSize
=
Size
(
330.0
,
270.0
);
const
Size
_inputLandscapeDialogSize
=
Size
(
496
,
160.0
);
const
Duration
_dialogSizeAnimationDuration
=
Duration
(
milliseconds:
200
);
const
double
_inputFormPortraitHeight
=
98.0
;
const
double
_inputFormLandscapeHeight
=
108.0
;
/// Shows a dialog containing a Material Design date picker.
///
...
...
@@ -60,17 +63,16 @@ const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200);
/// this can be used to only allow weekdays for selection. If provided, it must
/// return true for [initialDate].
///
/// Optional strings for the [cancelText], [confirmText], [errorFormatText],
/// [errorInvalidText], [fieldHintText], [fieldLabelText], and [helpText] allow
/// you to override the default text used for various parts of the dialog:
/// The following optional string parameters allow you to override the default
/// text used for various parts of the dialog:
///
/// * [helpText], label displayed at the top of the dialog.
/// * [cancelText], label on the cancel button.
/// * [confirmText], label on the ok button.
/// * [errorFormatText], message used when the input text isn't in a proper date format.
/// * [errorInvalidText], message used when the input text isn't a selectable date.
/// * [fieldHintText], text used to prompt the user when no text has been entered in the field.
/// * [fieldLabelText], label for the date text input field.
/// * [helpText], label on the top of the dialog.
///
/// An optional [locale] argument can be used to set the locale for the date
/// picker. It defaults to the ambient locale provided by [Localizations].
...
...
@@ -92,6 +94,14 @@ const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200);
/// calendar date picker initially appear in the [DatePickerMode.year] or
/// [DatePickerMode.day] mode. It defaults to [DatePickerMode.day], and
/// must be non-null.
///
/// See also:
///
/// * [showDateRangePicker], which shows a material design date range picker
/// used to select a range of dates.
/// * [CalendarDatePicker], which provides the calendar grid used by the date picker dialog.
/// * [InputDatePickerFormField], which provides a text input field for entering dates.
///
Future
<
DateTime
>
showDatePicker
({
@required
BuildContext
context
,
@required
DateTime
initialDate
,
...
...
@@ -304,7 +314,7 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
Navigator
.
pop
(
context
);
}
void
_hand
el
EntryModeToggle
()
{
void
_hand
le
EntryModeToggle
()
{
setState
(()
{
switch
(
_entryMode
)
{
case
DatePickerEntryMode
.
calendar
:
...
...
@@ -407,7 +417,13 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
picker
=
Form
(
key:
_formKey
,
autovalidate:
_autoValidate
,
child:
InputDatePickerFormField
(
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
24
),
height:
orientation
==
Orientation
.
portrait
?
_inputFormPortraitHeight
:
_inputFormLandscapeHeight
,
child:
Column
(
children:
<
Widget
>[
const
Spacer
(),
InputDatePickerFormField
(
initialDate:
_selectedDate
,
firstDate:
widget
.
firstDate
,
lastDate:
widget
.
lastDate
,
...
...
@@ -420,6 +436,10 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
fieldLabelText:
widget
.
fieldLabelText
,
autofocus:
true
,
),
const
Spacer
(),
],
),
),
);
entryModeIcon
=
Icons
.
calendar_today
;
// TODO(darrenaustin): localize 'Switch to calendar'
...
...
@@ -436,7 +456,7 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
isShort:
orientation
==
Orientation
.
landscape
,
icon:
entryModeIcon
,
iconTooltip:
entryModeTooltip
,
onIconPressed:
_hand
el
EntryModeToggle
,
onIconPressed:
_hand
le
EntryModeToggle
,
);
final
Size
dialogSize
=
_dialogSize
(
context
)
*
textScaleFactor
;
...
...
packages/flutter/lib/src/material/pickers/date_picker_header.dart
View file @
4560ebcf
...
...
@@ -25,6 +25,7 @@ const double _headerPaddingLandscape = 16.0;
///
/// * Single Date picker with calendar mode.
/// * Single Date picker with manual input mode.
/// * Date Range picker with manual input mode.
///
/// [helpText], [orientation], [icon], [onIconPressed] are required and must be
/// non-null.
...
...
@@ -112,7 +113,7 @@ class DatePickerHeader extends StatelessWidget {
titleText
,
semanticsLabel:
titleSemanticsLabel
??
titleText
,
style:
titleStyle
,
maxLines:
(
isShort
||
orientation
==
Orientation
.
portrait
)
?
1
:
2
,
maxLines:
orientation
==
Orientation
.
portrait
?
1
:
2
,
overflow:
TextOverflow
.
ellipsis
,
);
final
IconButton
icon
=
IconButton
(
...
...
@@ -169,13 +170,14 @@ class DatePickerHeader extends StatelessWidget {
child:
help
,
),
SizedBox
(
height:
isShort
?
16
:
56
),
Padding
(
Expanded
(
child:
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
_headerPaddingLandscape
,
),
child:
title
,
),
const
Spacer
(
),
),
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
4
,
...
...
packages/flutter/lib/src/material/pickers/date_range_picker_dialog.dart
0 → 100644
View file @
4560ebcf
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:math'
as
math
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/widgets.dart'
;
import
'../app_bar.dart'
;
import
'../back_button.dart'
;
import
'../button_bar.dart'
;
import
'../button_theme.dart'
;
import
'../color_scheme.dart'
;
import
'../debug.dart'
;
import
'../dialog.dart'
;
import
'../dialog_theme.dart'
;
import
'../flat_button.dart'
;
import
'../icon_button.dart'
;
import
'../icons.dart'
;
import
'../material_localizations.dart'
;
import
'../scaffold.dart'
;
import
'../text_theme.dart'
;
import
'../theme.dart'
;
import
'calendar_date_range_picker.dart'
;
import
'date_picker_common.dart'
;
import
'date_picker_header.dart'
;
import
'date_utils.dart'
as
utils
;
import
'input_date_range_picker.dart'
;
const
Size
_inputPortraitDialogSize
=
Size
(
330.0
,
270.0
);
const
Size
_inputLandscapeDialogSize
=
Size
(
496
,
164.0
);
const
Duration
_dialogSizeAnimationDuration
=
Duration
(
milliseconds:
200
);
const
double
_inputFormPortraitHeight
=
98.0
;
const
double
_inputFormLandscapeHeight
=
108.0
;
/// Shows a full screen modal dialog containing a Material Design date range
/// picker.
///
/// The returned [Future] resolves to the [DateTimeRange] selected by the user
/// when the user saves their selection. If the user cancels the dialog, null is
/// returned.
///
/// If [initialDateRange] is non-null, then it will be used as the initially
/// selected date range. If it is provided, [initialDateRange.start] must be
/// before or on [initialDateRange.end].
///
/// The [firstDate] is the earliest allowable date. The [lastDate] is the latest
/// allowable date. Both must be non-null.
///
/// If an initial date range is provided, [initialDateRange.start]
/// and [initialDateRange.end] must both fall between or on [firstDate] and
/// [lastDate]. For all of these [DateTime] values, only their dates are
/// considered. Their time fields are ignored.
///
/// The [currentDate] represents the current day (i.e. today). This
/// date will be highlighted in the day grid. If null, the date of
/// `DateTime.now()` will be used.
///
/// An optional [initialEntryMode] argument can be used to display the date
/// picker in the [DatePickerEntryMode.calendar] (a scrollable calendar month
/// grid) or [DatePickerEntryMode.input] (two text input fields) mode.
/// It defaults to [DatePickerEntryMode.calendar] and must be non-null.
///
/// The following optional string parameters allow you to override the default
/// text used for various parts of the dialog:
///
/// * [helpText], the label displayed at the top of the dialog.
/// * [cancelText], the label on the cancel button for the text input mode.
/// * [confirmText],the label on the ok button for the text input mode.
/// * [saveText], the label on the save button for the fullscreen calendar
/// mode.
/// * [errorFormatText], the message used when an input text isn't in a proper
/// date format.
/// * [errorInvalidText], the message used when an input text isn't a
/// selectable date.
/// * [errorInvalidRangeText], the message used when the date range is
/// invalid (e.g. start date is after end date).
/// * [fieldStartHintText], the text used to prompt the user when no text has
/// been entered in the start field.
/// * [fieldEndHintText], the text used to prompt the user when no text has
/// been entered in the end field.
/// * [fieldStartLabelText], the label for the start date text input field.
/// * [fieldEndLabelText], the label for the end date text input field.
///
/// An optional [locale] argument can be used to set the locale for the date
/// picker. It defaults to the ambient locale provided by [Localizations].
///
/// An optional [textDirection] argument can be used to set the text direction
/// ([TextDirection.ltr] or [TextDirection.rtl]) for the date picker. It
/// defaults to the ambient text direction provided by [Directionality]. If both
/// [locale] and [textDirection] are non-null, [textDirection] overrides the
/// direction chosen for the [locale].
///
/// The [context], [useRootNavigator] and [routeSettings] arguments are passed
/// to [showDialog], the documentation for which discusses how it is used.
/// [context] and [useRootNavigator] must be non-null.
///
/// The [builder] parameter can be used to wrap the dialog widget
/// to add inherited widgets like [Theme].
///
/// See also:
///
/// * [showDatePicker], which shows a material design date picker used to
/// select a single date.
/// * [DateTimeRange], which is used to describe a date range.
///
Future
<
DateTimeRange
>
showDateRangePicker
({
@required
BuildContext
context
,
DateTimeRange
initialDateRange
,
@required
DateTime
firstDate
,
@required
DateTime
lastDate
,
DateTime
currentDate
,
DatePickerEntryMode
initialEntryMode
=
DatePickerEntryMode
.
calendar
,
String
helpText
,
String
cancelText
,
String
confirmText
,
String
saveText
,
String
errorFormatText
,
String
errorInvalidText
,
String
errorInvalidRangeText
,
String
fieldStartHintText
,
String
fieldEndHintText
,
String
fieldStartLabelText
,
String
fieldEndLabelText
,
Locale
locale
,
bool
useRootNavigator
=
true
,
RouteSettings
routeSettings
,
TextDirection
textDirection
,
TransitionBuilder
builder
,
})
async
{
assert
(
context
!=
null
);
assert
(
initialDateRange
==
null
||
(
initialDateRange
.
start
!=
null
&&
initialDateRange
.
end
!=
null
),
'initialDateRange must be null or have non-null start and end dates.'
);
assert
(
initialDateRange
==
null
||
!
initialDateRange
.
start
.
isAfter
(
initialDateRange
.
end
),
'initialDateRange
\'
s start date must not be after it
\'
s end date.'
);
initialDateRange
=
initialDateRange
==
null
?
null
:
utils
.
datesOnly
(
initialDateRange
);
assert
(
firstDate
!=
null
);
firstDate
=
utils
.
dateOnly
(
firstDate
);
assert
(
lastDate
!=
null
);
lastDate
=
utils
.
dateOnly
(
lastDate
);
assert
(
!
lastDate
.
isBefore
(
firstDate
),
'lastDate
$lastDate
must be on or after firstDate
$firstDate
.'
);
assert
(
initialDateRange
==
null
||
!
initialDateRange
.
start
.
isBefore
(
firstDate
),
'initialDateRange
\'
s start date must be on or after firstDate
$firstDate
.'
);
assert
(
initialDateRange
==
null
||
!
initialDateRange
.
end
.
isBefore
(
firstDate
),
'initialDateRange
\'
s end date must be on or after firstDate
$firstDate
.'
);
assert
(
initialDateRange
==
null
||
!
initialDateRange
.
start
.
isAfter
(
lastDate
),
'initialDateRange
\'
s start date must be on or before lastDate
$lastDate
.'
);
assert
(
initialDateRange
==
null
||
!
initialDateRange
.
end
.
isAfter
(
lastDate
),
'initialDateRange
\'
s end date must be on or before lastDate
$lastDate
.'
);
currentDate
=
utils
.
dateOnly
(
currentDate
??
DateTime
.
now
());
assert
(
initialEntryMode
!=
null
);
assert
(
useRootNavigator
!=
null
);
assert
(
debugCheckHasMaterialLocalizations
(
context
));
Widget
dialog
=
_DateRangePickerDialog
(
initialDateRange:
initialDateRange
,
firstDate:
firstDate
,
lastDate:
lastDate
,
currentDate:
currentDate
,
initialEntryMode:
initialEntryMode
,
helpText:
helpText
,
cancelText:
cancelText
,
confirmText:
confirmText
,
saveText:
saveText
,
errorFormatText:
errorFormatText
,
errorInvalidText:
errorInvalidText
,
errorInvalidRangeText:
errorInvalidRangeText
,
fieldStartHintText:
fieldStartHintText
,
fieldEndHintText:
fieldEndHintText
,
fieldStartLabelText:
fieldStartLabelText
,
fieldEndLabelText:
fieldEndLabelText
,
);
if
(
textDirection
!=
null
)
{
dialog
=
Directionality
(
textDirection:
textDirection
,
child:
dialog
,
);
}
if
(
locale
!=
null
)
{
dialog
=
Localizations
.
override
(
context:
context
,
locale:
locale
,
child:
dialog
,
);
}
return
showDialog
<
DateTimeRange
>(
context:
context
,
useRootNavigator:
useRootNavigator
,
routeSettings:
routeSettings
,
useSafeArea:
false
,
builder:
(
BuildContext
context
)
{
return
builder
==
null
?
dialog
:
builder
(
context
,
dialog
);
},
);
}
class
_DateRangePickerDialog
extends
StatefulWidget
{
const
_DateRangePickerDialog
({
Key
key
,
this
.
initialDateRange
,
@required
this
.
firstDate
,
@required
this
.
lastDate
,
this
.
currentDate
,
this
.
initialEntryMode
=
DatePickerEntryMode
.
calendar
,
this
.
helpText
,
this
.
cancelText
,
this
.
confirmText
,
this
.
saveText
,
this
.
errorInvalidRangeText
,
this
.
errorFormatText
,
this
.
errorInvalidText
,
this
.
fieldStartHintText
,
this
.
fieldEndHintText
,
this
.
fieldStartLabelText
,
this
.
fieldEndLabelText
,
})
:
super
(
key:
key
);
final
DateTimeRange
initialDateRange
;
final
DateTime
firstDate
;
final
DateTime
lastDate
;
final
DateTime
currentDate
;
final
DatePickerEntryMode
initialEntryMode
;
final
String
cancelText
;
final
String
confirmText
;
final
String
saveText
;
final
String
helpText
;
final
String
errorInvalidRangeText
;
final
String
errorFormatText
;
final
String
errorInvalidText
;
final
String
fieldStartHintText
;
final
String
fieldEndHintText
;
final
String
fieldStartLabelText
;
final
String
fieldEndLabelText
;
@override
_DateRangePickerDialogState
createState
()
=>
_DateRangePickerDialogState
();
}
class
_DateRangePickerDialogState
extends
State
<
_DateRangePickerDialog
>
{
DatePickerEntryMode
_entryMode
;
DateTime
_selectedStart
;
DateTime
_selectedEnd
;
bool
_autoValidate
;
final
GlobalKey
_calendarPickerKey
=
GlobalKey
();
final
GlobalKey
<
InputDateRangePickerState
>
_inputPickerKey
=
GlobalKey
<
InputDateRangePickerState
>();
@override
void
initState
()
{
super
.
initState
();
_selectedStart
=
widget
.
initialDateRange
?.
start
;
_selectedEnd
=
widget
.
initialDateRange
?.
end
;
_entryMode
=
widget
.
initialEntryMode
;
_autoValidate
=
false
;
}
void
_handleOk
()
{
if
(
_entryMode
==
DatePickerEntryMode
.
input
)
{
final
InputDateRangePickerState
picker
=
_inputPickerKey
.
currentState
;
if
(!
picker
.
validate
())
{
setState
(()
{
_autoValidate
=
true
;
});
return
;
}
}
final
DateTimeRange
selectedRange
=
_hasSelectedDateRange
?
DateTimeRange
(
start:
_selectedStart
,
end:
_selectedEnd
)
:
null
;
Navigator
.
pop
(
context
,
selectedRange
);
}
void
_handleCancel
()
{
Navigator
.
pop
(
context
);
}
void
_handleEntryModeToggle
()
{
setState
(()
{
switch
(
_entryMode
)
{
case
DatePickerEntryMode
.
calendar
:
_autoValidate
=
false
;
_entryMode
=
DatePickerEntryMode
.
input
;
break
;
case
DatePickerEntryMode
.
input
:
// If invalid range (start after end), then just use the start date
if
(
_selectedStart
!=
null
&&
_selectedEnd
!=
null
&&
_selectedStart
.
isAfter
(
_selectedEnd
))
{
_selectedEnd
=
null
;
}
_entryMode
=
DatePickerEntryMode
.
calendar
;
break
;
}
});
}
void
_handleStartDateChanged
(
DateTime
date
)
{
setState
(()
=>
_selectedStart
=
date
);
}
void
_handleEndDateChanged
(
DateTime
date
)
{
setState
(()
=>
_selectedEnd
=
date
);
}
bool
get
_hasSelectedDateRange
=>
_selectedStart
!=
null
&&
_selectedEnd
!=
null
;
@override
Widget
build
(
BuildContext
context
)
{
final
MediaQueryData
mediaQuery
=
MediaQuery
.
of
(
context
);
final
Orientation
orientation
=
mediaQuery
.
orientation
;
final
double
textScaleFactor
=
math
.
min
(
mediaQuery
.
textScaleFactor
,
1.3
);
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
Widget
contents
;
Size
size
;
ShapeBorder
shape
;
double
elevation
;
EdgeInsets
insetPadding
;
switch
(
_entryMode
)
{
case
DatePickerEntryMode
.
calendar
:
contents
=
_CalendarRangePickerDialog
(
key:
_calendarPickerKey
,
selectedStartDate:
_selectedStart
,
selectedEndDate:
_selectedEnd
,
firstDate:
widget
.
firstDate
,
lastDate:
widget
.
lastDate
,
currentDate:
widget
.
currentDate
,
onStartDateChanged:
_handleStartDateChanged
,
onEndDateChanged:
_handleEndDateChanged
,
onConfirm:
_hasSelectedDateRange
?
_handleOk
:
null
,
onCancel:
_handleCancel
,
onToggleEntryMode:
_handleEntryModeToggle
,
// TODO(darrenaustin): localize 'SAVE'
confirmText:
widget
.
saveText
??
'SAVE'
,
// TODO(darrenaustin): localize 'SELECTED RANGE'
helpText:
widget
.
helpText
??
'SELECTED RANGE'
,
);
size
=
mediaQuery
.
size
;
insetPadding
=
const
EdgeInsets
.
all
(
0.0
);
shape
=
const
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
all
(
Radius
.
zero
)
);
elevation
=
0
;
break
;
case
DatePickerEntryMode
.
input
:
contents
=
_InputDateRangePickerDialog
(
selectedStartDate:
_selectedStart
,
selectedEndDate:
_selectedEnd
,
currentDate:
widget
.
currentDate
,
picker:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
24
),
height:
orientation
==
Orientation
.
portrait
?
_inputFormPortraitHeight
:
_inputFormLandscapeHeight
,
child:
Column
(
children:
<
Widget
>[
const
Spacer
(),
InputDateRangePicker
(
key:
_inputPickerKey
,
initialStartDate:
_selectedStart
,
initialEndDate:
_selectedEnd
,
firstDate:
widget
.
firstDate
,
lastDate:
widget
.
lastDate
,
onStartDateChanged:
_handleStartDateChanged
,
onEndDateChanged:
_handleEndDateChanged
,
autofocus:
true
,
autovalidate:
_autoValidate
,
helpText:
widget
.
helpText
,
errorInvalidRangeText:
widget
.
errorInvalidRangeText
,
errorFormatText:
widget
.
errorFormatText
,
errorInvalidText:
widget
.
errorInvalidText
,
fieldStartHintText:
widget
.
fieldStartHintText
,
fieldEndHintText:
widget
.
fieldEndHintText
,
fieldStartLabelText:
widget
.
fieldStartLabelText
,
fieldEndLabelText:
widget
.
fieldEndLabelText
,
),
const
Spacer
(),
],
),
),
onConfirm:
_handleOk
,
onCancel:
_handleCancel
,
onToggleEntryMode:
_handleEntryModeToggle
,
confirmText:
widget
.
confirmText
??
localizations
.
okButtonLabel
,
cancelText:
widget
.
cancelText
??
localizations
.
cancelButtonLabel
,
// TODO(darrenaustin): localize 'SELECTED DATE RANGE'
helpText:
widget
.
helpText
??
'SELECTED DATE RANGE'
,
);
final
DialogTheme
dialogTheme
=
Theme
.
of
(
context
).
dialogTheme
;
size
=
orientation
==
Orientation
.
portrait
?
_inputPortraitDialogSize
:
_inputLandscapeDialogSize
;
insetPadding
=
const
EdgeInsets
.
symmetric
(
horizontal:
16.0
,
vertical:
24.0
);
// The default dialog shape is radius 2 rounded rect, but the spec has
// been updated to 4, so we will use that here for the Input Date Range
// Picker, but only if there isn't one provided in the theme.
shape
=
dialogTheme
.
shape
??
const
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
all
(
Radius
.
circular
(
4.0
))
);
elevation
=
dialogTheme
.
elevation
??
24
;
break
;
}
return
Dialog
(
child:
AnimatedContainer
(
width:
size
.
width
,
height:
size
.
height
,
duration:
_dialogSizeAnimationDuration
,
curve:
Curves
.
easeIn
,
child:
MediaQuery
(
data:
MediaQuery
.
of
(
context
).
copyWith
(
textScaleFactor:
textScaleFactor
,
),
child:
Builder
(
builder:
(
BuildContext
context
)
{
return
contents
;
}),
),
),
insetPadding:
insetPadding
,
shape:
shape
,
elevation:
elevation
,
clipBehavior:
Clip
.
antiAlias
,
);
}
}
class
_CalendarRangePickerDialog
extends
StatelessWidget
{
const
_CalendarRangePickerDialog
({
Key
key
,
@required
this
.
selectedStartDate
,
@required
this
.
selectedEndDate
,
@required
this
.
firstDate
,
@required
this
.
lastDate
,
@required
this
.
currentDate
,
@required
this
.
onStartDateChanged
,
@required
this
.
onEndDateChanged
,
@required
this
.
onConfirm
,
@required
this
.
onCancel
,
@required
this
.
onToggleEntryMode
,
@required
this
.
confirmText
,
@required
this
.
helpText
,
})
:
super
(
key:
key
);
final
DateTime
selectedStartDate
;
final
DateTime
selectedEndDate
;
final
DateTime
firstDate
;
final
DateTime
lastDate
;
final
DateTime
currentDate
;
final
ValueChanged
<
DateTime
>
onStartDateChanged
;
final
ValueChanged
<
DateTime
>
onEndDateChanged
;
final
VoidCallback
onConfirm
;
final
VoidCallback
onCancel
;
final
VoidCallback
onToggleEntryMode
;
final
String
confirmText
;
final
String
helpText
;
@override
Widget
build
(
BuildContext
context
)
{
final
ThemeData
theme
=
Theme
.
of
(
context
);
final
ColorScheme
colorScheme
=
theme
.
colorScheme
;
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
final
Orientation
orientation
=
MediaQuery
.
of
(
context
).
orientation
;
final
TextTheme
textTheme
=
theme
.
textTheme
;
final
Color
headerForeground
=
colorScheme
.
brightness
==
Brightness
.
light
?
colorScheme
.
onPrimary
:
colorScheme
.
onSurface
;
final
Color
headerDisabledForeground
=
headerForeground
.
withOpacity
(
0.38
);
final
String
startDateText
=
utils
.
formatRangeStartDate
(
localizations
,
selectedStartDate
,
selectedEndDate
);
final
String
endDateText
=
utils
.
formatRangeEndDate
(
localizations
,
selectedStartDate
,
selectedEndDate
,
DateTime
.
now
());
final
TextStyle
headlineStyle
=
textTheme
.
headline5
;
final
TextStyle
startDateStyle
=
headlineStyle
?.
apply
(
color:
selectedStartDate
!=
null
?
headerForeground
:
headerDisabledForeground
);
final
TextStyle
endDateStyle
=
headlineStyle
?.
apply
(
color:
selectedEndDate
!=
null
?
headerForeground
:
headerDisabledForeground
);
final
TextStyle
saveButtonStyle
=
textTheme
.
button
.
apply
(
color:
onConfirm
!=
null
?
headerForeground
:
headerDisabledForeground
);
final
IconButton
entryModeIcon
=
IconButton
(
padding:
EdgeInsets
.
zero
,
color:
headerForeground
,
icon:
const
Icon
(
Icons
.
edit
),
tooltip:
'Switch to input'
,
onPressed:
onToggleEntryMode
,
);
return
SafeArea
(
top:
false
,
left:
false
,
right:
false
,
child:
Scaffold
(
appBar:
AppBar
(
leading:
CloseButton
(
onPressed:
onCancel
,
),
actions:
<
Widget
>[
if
(
orientation
==
Orientation
.
landscape
)
entryModeIcon
,
ButtonTheme
(
minWidth:
64
,
child:
FlatButton
(
onPressed:
onConfirm
,
child:
Text
(
confirmText
,
style:
saveButtonStyle
),
),
),
const
SizedBox
(
width:
8
),
],
bottom:
PreferredSize
(
child:
Row
(
children:
<
Widget
>[
SizedBox
(
width:
MediaQuery
.
of
(
context
).
size
.
width
<
360
?
42
:
72
),
Expanded
(
child:
Semantics
(
label:
'
$helpText
$startDateText
to
$endDateText
'
,
excludeSemantics:
true
,
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
<
Widget
>[
Text
(
helpText
,
style:
textTheme
.
overline
.
apply
(
color:
headerForeground
,
),
),
const
SizedBox
(
height:
8
),
Row
(
children:
<
Widget
>[
Text
(
startDateText
,
style:
startDateStyle
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
),
Text
(
' – '
,
style:
startDateStyle
,
),
Flexible
(
child:
Text
(
endDateText
,
style:
endDateStyle
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
),
),
],
),
const
SizedBox
(
height:
16
),
],
),
),
),
if
(
orientation
==
Orientation
.
portrait
)
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8.0
),
child:
entryModeIcon
,
),
]),
preferredSize:
const
Size
(
double
.
infinity
,
64
),
),
),
body:
CalendarDateRangePicker
(
initialStartDate:
selectedStartDate
,
initialEndDate:
selectedEndDate
,
firstDate:
firstDate
,
lastDate:
lastDate
,
currentDate:
currentDate
,
onStartDateChanged:
onStartDateChanged
,
onEndDateChanged:
onEndDateChanged
,
),
),
);
}
}
class
_InputDateRangePickerDialog
extends
StatelessWidget
{
const
_InputDateRangePickerDialog
({
Key
key
,
@required
this
.
selectedStartDate
,
@required
this
.
selectedEndDate
,
@required
this
.
currentDate
,
@required
this
.
picker
,
@required
this
.
onConfirm
,
@required
this
.
onCancel
,
@required
this
.
onToggleEntryMode
,
@required
this
.
confirmText
,
@required
this
.
cancelText
,
@required
this
.
helpText
,
})
:
super
(
key:
key
);
final
DateTime
selectedStartDate
;
final
DateTime
selectedEndDate
;
final
DateTime
currentDate
;
final
Widget
picker
;
final
VoidCallback
onConfirm
;
final
VoidCallback
onCancel
;
final
VoidCallback
onToggleEntryMode
;
final
String
confirmText
;
final
String
cancelText
;
final
String
helpText
;
String
_formatDateRange
(
BuildContext
context
,
DateTime
start
,
DateTime
end
,
DateTime
now
)
{
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
final
String
startText
=
utils
.
formatRangeStartDate
(
localizations
,
start
,
end
);
final
String
endText
=
utils
.
formatRangeEndDate
(
localizations
,
start
,
end
,
now
);
if
(
start
==
null
||
end
==
null
)
{
// TODO(darrenaustin): localize 'Date Range'
return
'Date Range'
;
}
if
(
Directionality
.
of
(
context
)
==
TextDirection
.
ltr
)
{
return
'
$startText
–
$endText
'
;
}
else
{
return
'
$endText
–
$startText
'
;
}
}
@override
Widget
build
(
BuildContext
context
)
{
final
ThemeData
theme
=
Theme
.
of
(
context
);
final
ColorScheme
colorScheme
=
theme
.
colorScheme
;
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
final
Orientation
orientation
=
MediaQuery
.
of
(
context
).
orientation
;
final
TextTheme
textTheme
=
theme
.
textTheme
;
final
Color
dateColor
=
colorScheme
.
brightness
==
Brightness
.
light
?
colorScheme
.
onPrimary
:
colorScheme
.
onSurface
;
final
TextStyle
dateStyle
=
orientation
==
Orientation
.
landscape
?
textTheme
.
headline5
?.
apply
(
color:
dateColor
)
:
textTheme
.
headline4
?.
apply
(
color:
dateColor
);
final
String
dateText
=
_formatDateRange
(
context
,
selectedStartDate
,
selectedEndDate
,
currentDate
);
final
String
semanticDateText
=
selectedStartDate
!=
null
&&
selectedEndDate
!=
null
?
'
${localizations.formatMediumDate(selectedStartDate)}
–
${localizations.formatMediumDate(selectedEndDate)}
'
:
''
;
final
Widget
header
=
DatePickerHeader
(
// TODO(darrenaustin): localize 'SELECT DATE RANGE'
helpText:
helpText
??
'SELECT DATE RANGE'
,
titleText:
dateText
,
titleSemanticsLabel:
semanticDateText
,
titleStyle:
dateStyle
,
orientation:
orientation
,
isShort:
orientation
==
Orientation
.
landscape
,
icon:
Icons
.
calendar_today
,
// TODO(darrenaustin): localize 'Switch to calendar'
iconTooltip:
'Switch to calendar'
,
onIconPressed:
onToggleEntryMode
,
);
final
Widget
actions
=
ButtonBar
(
buttonTextTheme:
ButtonTextTheme
.
primary
,
layoutBehavior:
ButtonBarLayoutBehavior
.
constrained
,
children:
<
Widget
>[
FlatButton
(
child:
Text
(
cancelText
??
localizations
.
cancelButtonLabel
),
onPressed:
onCancel
,
),
FlatButton
(
child:
Text
(
confirmText
??
localizations
.
okButtonLabel
),
onPressed:
onConfirm
,
),
],
);
switch
(
orientation
)
{
case
Orientation
.
portrait
:
return
Column
(
mainAxisSize:
MainAxisSize
.
min
,
crossAxisAlignment:
CrossAxisAlignment
.
stretch
,
children:
<
Widget
>[
header
,
Expanded
(
child:
picker
),
actions
,
],
);
case
Orientation
.
landscape
:
return
Row
(
mainAxisSize:
MainAxisSize
.
min
,
crossAxisAlignment:
CrossAxisAlignment
.
stretch
,
children:
<
Widget
>[
header
,
Flexible
(
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
crossAxisAlignment:
CrossAxisAlignment
.
stretch
,
children:
<
Widget
>[
Expanded
(
child:
picker
),
actions
,
],
),
),
],
);
}
return
null
;
}
}
packages/flutter/lib/src/material/pickers/date_utils.dart
View file @
4560ebcf
...
...
@@ -12,11 +12,18 @@
import
'../material_localizations.dart'
;
import
'date_picker_common.dart'
;
/// Returns a [DateTime] with just the date of the original, but no time set.
DateTime
dateOnly
(
DateTime
date
)
{
return
DateTime
(
date
.
year
,
date
.
month
,
date
.
day
);
}
/// Returns a [DateTimeRange] with the dates of the original without any times set.
DateTimeRange
datesOnly
(
DateTimeRange
range
)
{
return
DateTimeRange
(
start:
dateOnly
(
range
.
start
),
end:
dateOnly
(
range
.
end
));
}
/// Returns true if the two [DateTime] objects have the same day, month, and
/// year.
bool
isSameDay
(
DateTime
dateA
,
DateTime
dateB
)
{
...
...
@@ -120,3 +127,31 @@ int getDaysInMonth(int year, int month) {
const
List
<
int
>
daysInMonth
=
<
int
>[
31
,
-
1
,
31
,
30
,
31
,
30
,
31
,
31
,
30
,
31
,
30
,
31
];
return
daysInMonth
[
month
-
1
];
}
/// Returns a locale-appropriate string to describe the start of a date range.
///
/// If `startDate` is null, then it defaults to 'Start Date', otherwise if it
/// is in the same year as the `endDate` then it will use the short month
/// day format (i.e. 'Jan 21'). Otherwise it will return the short date format
/// (i.e. 'Jan 21, 2020').
String
formatRangeStartDate
(
MaterialLocalizations
localizations
,
DateTime
startDate
,
DateTime
endDate
)
{
return
startDate
==
null
?
'Start Date'
:
(
endDate
==
null
||
startDate
.
year
==
endDate
.
year
)
?
localizations
.
formatShortMonthDay
(
startDate
)
:
localizations
.
formatShortDate
(
startDate
);
}
/// Returns an locale-appropriate string to describe the end of a date range.
///
/// If `endDate` is null, then it defaults to 'End Date', otherwise if it
/// is in the same year as the `startDate` and the `currentDate` then it will
/// just use the short month day format (i.e. 'Jan 21'), otherwise it will
/// include the year (i.e. 'Jan 21, 2020').
String
formatRangeEndDate
(
MaterialLocalizations
localizations
,
DateTime
startDate
,
DateTime
endDate
,
DateTime
currentDate
)
{
return
endDate
==
null
?
'End Date'
:
(
startDate
!=
null
&&
startDate
.
year
==
endDate
.
year
&&
startDate
.
year
==
currentDate
.
year
)
?
localizations
.
formatShortMonthDay
(
endDate
)
:
localizations
.
formatShortDate
(
endDate
);
}
packages/flutter/lib/src/material/pickers/input_date_picker.dart
View file @
4560ebcf
...
...
@@ -8,15 +8,11 @@ import 'package:flutter/widgets.dart';
import
'../input_border.dart'
;
import
'../input_decorator.dart'
;
import
'../material_localizations.dart'
;
import
'../text_field.dart'
;
import
'../text_form_field.dart'
;
import
'date_picker_common.dart'
;
import
'date_utils.dart'
as
utils
;
const
double
_inputPortraitHeight
=
98.0
;
const
double
_inputLandscapeHeight
=
108.0
;
/// A [TextFormField] configured to accept and validate a date entered by the user.
///
/// The text entered into this field will be constrained to only allow digits
...
...
@@ -227,13 +223,7 @@ class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
return
OrientationBuilder
(
builder:
(
BuildContext
context
,
Orientation
orientation
)
{
assert
(
orientation
!=
null
);
return
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
24
),
height:
orientation
==
Orientation
.
portrait
?
_inputPortraitHeight
:
_inputLandscapeHeight
,
child:
Column
(
children:
<
Widget
>[
const
Spacer
(),
TextFormField
(
return
TextFormField
(
decoration:
InputDecoration
(
border:
const
UnderlineInputBorder
(),
filled:
true
,
...
...
@@ -244,37 +234,49 @@ class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
validator:
_validateDate
,
inputFormatters:
<
TextInputFormatter
>[
// TODO(darrenaustin): localize date separator '/'
_
DateTextInputFormatter
(
'/'
),
DateTextInputFormatter
(
'/'
),
],
keyboardType:
TextInputType
.
datetime
,
onSaved:
_handleSaved
,
onFieldSubmitted:
_handleSubmitted
,
autofocus:
widget
.
autofocus
,
controller:
_controller
,
),
const
Spacer
(),
],
),
);
});
}
}
class
_DateTextInputFormatter
extends
TextInputFormatter
{
_DateTextInputFormatter
(
this
.
separator
);
/// A `TextInputFormatter` set up to format dates.
///
/// Note: this is not publicly exported (see pickers.dart), as it is
/// just meant for internal use by `InputDatePickerFormField` and
/// `InputDateRangePicker`.
class
DateTextInputFormatter
extends
TextInputFormatter
{
/// Creates a date formatter with the given separator.
DateTextInputFormatter
(
this
.
separator
)
:
_filterFormatter
=
WhitelistingTextInputFormatter
(
RegExp
(
'[
\\
d
$_commonSeparators
\\
$separator
]+'
));
/// List of common separators that are used in dates. This is used to make
/// sure that if given platform's [TextInputType.datetime] keyboard doesn't
/// provide the given locale's separator character, they can still enter the
/// separator using one of these characters (slash, period, comma, dash, or
/// space).
static
const
String
_commonSeparators
=
r'\/\.,-\s'
;
/// The date separator for the current locale.
final
String
separator
;
final
WhitelistingTextInputFormatter
_filterFormatter
=
// Only allow digits and separators (slash, dot, comma, hyphen, space)
.
WhitelistingTextInputFormatter
(
RegExp
(
r'[\d\/\.,-\s]+'
))
;
// Formatter that will filter out all characters except digits and date
// separators
.
final
WhitelistingTextInputFormatter
_filterFormatter
;
@override
TextEditingValue
formatEditUpdate
(
TextEditingValue
oldValue
,
TextEditingValue
newValue
)
{
final
TextEditingValue
filteredValue
=
_filterFormatter
.
formatEditUpdate
(
oldValue
,
newValue
);
return
filteredValue
.
copyWith
(
// Replace any
separator character
with the given separator
// Replace any
non-digits
with the given separator
text:
filteredValue
.
text
.
replaceAll
(
RegExp
(
r'[\D]'
),
separator
),
);
}
...
...
packages/flutter/lib/src/material/pickers/input_date_range_picker.dart
0 → 100644
View file @
4560ebcf
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/services.dart'
;
import
'package:flutter/widgets.dart'
;
import
'../input_border.dart'
;
import
'../input_decorator.dart'
;
import
'../material_localizations.dart'
;
import
'../text_field.dart'
;
import
'date_utils.dart'
as
utils
;
import
'input_date_picker.dart'
show
DateTextInputFormatter
;
/// Provides a pair of text fields that allow the user to enter the start and
/// end dates that represent a range of dates.
///
/// Note: this is not publicly exported (see pickers.dart), as it is just an
/// internal component used by [showDateRangePicker].
class
InputDateRangePicker
extends
StatefulWidget
{
/// Creates a row with two text fields configured to accept the start and end dates
/// of a date range.
InputDateRangePicker
({
Key
key
,
DateTime
initialStartDate
,
DateTime
initialEndDate
,
@required
DateTime
firstDate
,
@required
DateTime
lastDate
,
@required
this
.
onStartDateChanged
,
@required
this
.
onEndDateChanged
,
this
.
helpText
,
this
.
errorFormatText
,
this
.
errorInvalidText
,
this
.
errorInvalidRangeText
,
this
.
fieldStartHintText
,
this
.
fieldEndHintText
,
this
.
fieldStartLabelText
,
this
.
fieldEndLabelText
,
this
.
autofocus
=
false
,
this
.
autovalidate
=
false
,
})
:
initialStartDate
=
initialStartDate
==
null
?
null
:
utils
.
dateOnly
(
initialStartDate
),
initialEndDate
=
initialEndDate
==
null
?
null
:
utils
.
dateOnly
(
initialEndDate
),
assert
(
firstDate
!=
null
),
firstDate
=
utils
.
dateOnly
(
firstDate
),
assert
(
lastDate
!=
null
),
lastDate
=
utils
.
dateOnly
(
lastDate
),
assert
(
firstDate
!=
null
),
assert
(
lastDate
!=
null
),
assert
(
autofocus
!=
null
),
assert
(
autovalidate
!=
null
),
super
(
key:
key
);
/// The [DateTime] that represents the start of the initial date range selection.
final
DateTime
initialStartDate
;
/// The [DateTime] that represents the end of the initial date range selection.
final
DateTime
initialEndDate
;
/// The earliest allowable [DateTime] that the user can select.
final
DateTime
firstDate
;
/// The latest allowable [DateTime] that the user can select.
final
DateTime
lastDate
;
/// Called when the user changes the start date of the selected range.
final
ValueChanged
<
DateTime
>
onStartDateChanged
;
/// Called when the user changes the end date of the selected range.
final
ValueChanged
<
DateTime
>
onEndDateChanged
;
/// The text that is displayed at the top of the header.
///
/// This is used to indicate to the user what they are selecting a date for.
final
String
helpText
;
/// Error text used to indicate the text in a field is not a valid date.
final
String
errorFormatText
;
/// Error text used to indicate the date in a field is not in the valid range
/// of [firstDate] - [lastDate].
final
String
errorInvalidText
;
/// Error text used to indicate the dates given don't form a valid date
/// range (i.e. the start date is after the end date).
final
String
errorInvalidRangeText
;
/// Hint text shown when the start date field is empty.
final
String
fieldStartHintText
;
/// Hint text shown when the end date field is empty.
final
String
fieldEndHintText
;
/// Label used for the start date field.
final
String
fieldStartLabelText
;
/// Label used for the end date field.
final
String
fieldEndLabelText
;
/// {@macro flutter.widgets.editableText.autofocus}
final
bool
autofocus
;
/// If true, this the date fields will validate and update their error text
/// immediately after every change. Otherwise, you must call
/// [InputDateRangePickerState.validate] to validate.
final
bool
autovalidate
;
@override
InputDateRangePickerState
createState
()
=>
InputDateRangePickerState
();
}
/// The current state of an [InputDateRangePicker]. Can be used to
/// [validate] the date field entries.
class
InputDateRangePickerState
extends
State
<
InputDateRangePicker
>
{
String
_startInputText
;
String
_endInputText
;
DateTime
_startDate
;
DateTime
_endDate
;
TextEditingController
_startController
;
TextEditingController
_endController
;
String
_startErrorText
;
String
_endErrorText
;
bool
_autoSelected
=
false
;
List
<
TextInputFormatter
>
_inputFormatters
;
@override
void
initState
()
{
super
.
initState
();
_startDate
=
widget
.
initialStartDate
;
_startController
=
TextEditingController
();
_endDate
=
widget
.
initialEndDate
;
_endController
=
TextEditingController
();
}
@override
void
dispose
()
{
_startController
.
dispose
();
_endController
.
dispose
();
super
.
dispose
();
}
@override
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
_inputFormatters
=
<
TextInputFormatter
>[
// TODO(darrenaustin): localize date separator '/'
DateTextInputFormatter
(
'/'
),
];
if
(
_startDate
!=
null
)
{
_startInputText
=
localizations
.
formatCompactDate
(
_startDate
);
final
bool
selectText
=
widget
.
autofocus
&&
!
_autoSelected
;
_updateController
(
_startController
,
_startInputText
,
selectText
);
_autoSelected
=
selectText
;
}
if
(
_endDate
!=
null
)
{
_endInputText
=
localizations
.
formatCompactDate
(
_endDate
);
_updateController
(
_endController
,
_endInputText
,
false
);
}
}
/// Validates that the text in the start and end fields represent a valid
/// date range.
///
/// Will return true if the range is valid. If not, it will
/// return false and display an appropriate error message under one of the
/// text fields.
bool
validate
()
{
String
startError
=
_validateDate
(
_startDate
);
final
String
endError
=
_validateDate
(
_endDate
);
if
(
startError
==
null
&&
endError
==
null
)
{
if
(
_startDate
.
isAfter
(
_endDate
))
{
// TODO(darrenaustin): localize 'Invalid range.'
startError
=
widget
.
errorInvalidRangeText
??
'Invalid range.'
;
}
}
setState
(()
{
_startErrorText
=
startError
;
_endErrorText
=
endError
;
});
return
startError
==
null
&&
endError
==
null
;
}
DateTime
_parseDate
(
String
text
)
{
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
return
localizations
.
parseCompactDate
(
text
);
}
String
_validateDate
(
DateTime
date
)
{
if
(
date
==
null
)
{
// TODO(darrenaustin): localize 'Invalid format.'
return
widget
.
errorFormatText
??
'Invalid format.'
;
}
else
if
(
date
.
isBefore
(
widget
.
firstDate
)
||
date
.
isAfter
(
widget
.
lastDate
))
{
// TODO(darrenaustin): localize 'Out of range.'
return
widget
.
errorInvalidText
??
'Out of range.'
;
}
return
null
;
}
void
_updateController
(
TextEditingController
controller
,
String
text
,
bool
selectText
)
{
TextEditingValue
textEditingValue
=
controller
.
value
.
copyWith
(
text:
text
);
if
(
selectText
)
{
textEditingValue
=
textEditingValue
.
copyWith
(
selection:
TextSelection
(
baseOffset:
0
,
extentOffset:
text
.
length
,
));
}
controller
.
value
=
textEditingValue
;
}
void
_handleStartChanged
(
String
text
)
{
setState
(()
{
_startInputText
=
text
;
_startDate
=
_parseDate
(
text
);
widget
.
onStartDateChanged
?.
call
(
_startDate
);
});
if
(
widget
.
autovalidate
)
{
validate
();
}
}
void
_handleEndChanged
(
String
text
)
{
setState
(()
{
_endInputText
=
text
;
_endDate
=
_parseDate
(
text
);
widget
.
onEndDateChanged
?.
call
(
_endDate
);
});
if
(
widget
.
autovalidate
)
{
validate
();
}
}
@override
Widget
build
(
BuildContext
context
)
{
return
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
<
Widget
>[
Expanded
(
child:
TextField
(
controller:
_startController
,
decoration:
InputDecoration
(
border:
const
UnderlineInputBorder
(),
filled:
true
,
// TODO(darrenaustin): localize 'mm/dd/yyyy' and 'Start Date'
hintText:
widget
.
fieldStartHintText
??
'mm/dd/yyyy'
,
labelText:
widget
.
fieldStartLabelText
??
'Start Date'
,
errorText:
_startErrorText
,
),
inputFormatters:
_inputFormatters
,
keyboardType:
TextInputType
.
datetime
,
onChanged:
_handleStartChanged
,
autofocus:
widget
.
autofocus
,
),
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
TextField
(
controller:
_endController
,
decoration:
InputDecoration
(
border:
const
UnderlineInputBorder
(),
filled:
true
,
// TODO(darrenaustin): localize 'mm/dd/yyyy' and 'End Date'
hintText:
widget
.
fieldEndHintText
??
'mm/dd/yyyy'
,
labelText:
widget
.
fieldEndLabelText
??
'End Date'
,
errorText:
_endErrorText
,
),
inputFormatters:
_inputFormatters
,
keyboardType:
TextInputType
.
datetime
,
onChanged:
_handleEndChanged
,
),
),
],
);
}
}
packages/flutter/lib/src/material/pickers/pickers.dart
View file @
4560ebcf
...
...
@@ -4,7 +4,12 @@
// Date Picker public API
export
'calendar_date_picker.dart'
show
CalendarDatePicker
;
export
'date_picker_common.dart'
show
DatePickerEntryMode
,
DatePickerMode
,
SelectableDayPredicate
;
export
'date_picker_common.dart'
show
DatePickerEntryMode
,
DatePickerMode
,
DateTimeRange
,
SelectableDayPredicate
;
export
'date_picker_deprecated.dart'
;
export
'date_picker_dialog.dart'
show
showDatePicker
;
export
'date_range_picker_dialog.dart'
show
showDateRangePicker
;
export
'input_date_picker.dart'
show
InputDatePickerFormField
;
packages/flutter/test/material/date_range_picker_test.dart
0 → 100644
View file @
4560ebcf
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/material.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'feedback_tester.dart'
;
void
main
(
)
{
DateTime
firstDate
;
DateTime
lastDate
;
DateTimeRange
initialDateRange
;
DatePickerEntryMode
initialEntryMode
=
DatePickerEntryMode
.
calendar
;
String
cancelText
;
String
confirmText
;
String
errorInvalidRangeText
;
String
errorFormatText
;
String
errorInvalidText
;
String
fieldStartHintText
;
String
fieldEndHintText
;
String
fieldStartLabelText
;
String
fieldEndLabelText
;
String
helpText
;
String
saveText
;
setUp
(()
{
firstDate
=
DateTime
(
2015
,
DateTime
.
january
,
1
);
lastDate
=
DateTime
(
2016
,
DateTime
.
december
,
31
);
initialDateRange
=
DateTimeRange
(
start:
DateTime
(
2016
,
DateTime
.
january
,
15
),
end:
DateTime
(
2016
,
DateTime
.
january
,
25
),
);
initialEntryMode
=
DatePickerEntryMode
.
calendar
;
cancelText
=
null
;
confirmText
=
null
;
errorInvalidRangeText
=
null
;
errorFormatText
=
null
;
errorInvalidText
=
null
;
fieldStartHintText
=
null
;
fieldEndHintText
=
null
;
fieldStartLabelText
=
null
;
fieldEndLabelText
=
null
;
helpText
=
null
;
saveText
=
null
;
});
Future
<
void
>
preparePicker
(
WidgetTester
tester
,
Future
<
void
>
callback
(
Future
<
DateTimeRange
>
date
))
async
{
BuildContext
buttonContext
;
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Material
(
child:
Builder
(
builder:
(
BuildContext
context
)
{
return
RaisedButton
(
onPressed:
()
{
buttonContext
=
context
;
},
child:
const
Text
(
'Go'
),
);
},
),
),
));
await
tester
.
tap
(
find
.
text
(
'Go'
));
expect
(
buttonContext
,
isNotNull
);
final
Future
<
DateTimeRange
>
range
=
showDateRangePicker
(
context:
buttonContext
,
initialDateRange:
initialDateRange
,
firstDate:
firstDate
,
lastDate:
lastDate
,
initialEntryMode:
initialEntryMode
,
cancelText:
cancelText
,
confirmText:
confirmText
,
errorInvalidRangeText:
errorInvalidRangeText
,
errorFormatText:
errorFormatText
,
errorInvalidText:
errorInvalidText
,
fieldStartHintText:
fieldStartHintText
,
fieldEndHintText:
fieldEndHintText
,
fieldStartLabelText:
fieldStartLabelText
,
fieldEndLabelText:
fieldEndLabelText
,
helpText:
helpText
,
saveText:
saveText
,
);
await
tester
.
pumpAndSettle
(
const
Duration
(
seconds:
1
));
await
callback
(
range
);
}
testWidgets
(
'Save and help text is used'
,
(
WidgetTester
tester
)
async
{
helpText
=
'help'
;
saveText
=
'make it so'
;
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
expect
(
find
.
text
(
helpText
),
findsOneWidget
);
expect
(
find
.
text
(
saveText
),
findsOneWidget
);
});
});
testWidgets
(
'Initial date is the default'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
tap
(
find
.
text
(
'SAVE'
));
expect
(
await
range
,
DateTimeRange
(
start:
DateTime
(
2016
,
DateTime
.
january
,
15
),
end:
DateTime
(
2016
,
DateTime
.
january
,
25
)
));
});
});
testWidgets
(
'Can cancel'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
close
));
expect
(
await
range
,
isNull
);
});
});
testWidgets
(
'Can select a range'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
tap
(
find
.
text
(
'12'
).
first
);
await
tester
.
tap
(
find
.
text
(
'14'
).
first
);
await
tester
.
tap
(
find
.
text
(
'SAVE'
));
expect
(
await
range
,
DateTimeRange
(
start:
DateTime
(
2016
,
DateTime
.
january
,
12
),
end:
DateTime
(
2016
,
DateTime
.
january
,
14
),
));
});
});
testWidgets
(
'Tapping earlier date resets selected range'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
tap
(
find
.
text
(
'12'
).
first
);
await
tester
.
tap
(
find
.
text
(
'11'
).
first
);
await
tester
.
tap
(
find
.
text
(
'15'
).
first
);
await
tester
.
tap
(
find
.
text
(
'SAVE'
));
expect
(
await
range
,
DateTimeRange
(
start:
DateTime
(
2016
,
DateTime
.
january
,
11
),
end:
DateTime
(
2016
,
DateTime
.
january
,
15
),
));
});
});
testWidgets
(
'Can select single day range'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
tap
(
find
.
text
(
'12'
).
first
);
await
tester
.
tap
(
find
.
text
(
'12'
).
first
);
await
tester
.
tap
(
find
.
text
(
'SAVE'
));
expect
(
await
range
,
DateTimeRange
(
start:
DateTime
(
2016
,
DateTime
.
january
,
12
),
end:
DateTime
(
2016
,
DateTime
.
january
,
12
),
));
});
});
testWidgets
(
'Cannot select a day outside bounds'
,
(
WidgetTester
tester
)
async
{
initialDateRange
=
DateTimeRange
(
start:
DateTime
(
2017
,
DateTime
.
january
,
13
),
end:
DateTime
(
2017
,
DateTime
.
january
,
15
),
);
firstDate
=
DateTime
(
2017
,
DateTime
.
january
,
12
);
lastDate
=
DateTime
(
2017
,
DateTime
.
january
,
16
);
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
// Earlier than firstDate. Should be ignored.
await
tester
.
tap
(
find
.
text
(
'10'
));
// Later than lastDate. Should be ignored.
await
tester
.
tap
(
find
.
text
(
'20'
));
await
tester
.
tap
(
find
.
text
(
'SAVE'
));
// We should still be on the initial date.
expect
(
await
range
,
initialDateRange
);
});
});
testWidgets
(
'Can toggle to input entry mode'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
expect
(
find
.
byType
(
TextField
),
findsNothing
);
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
edit
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
byType
(
TextField
),
findsNWidgets
(
2
));
});
});
testWidgets
(
'Toggle to input mode keeps selected date'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
tap
(
find
.
text
(
'12'
).
first
);
await
tester
.
tap
(
find
.
text
(
'14'
).
first
);
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
edit
));
await
tester
.
pumpAndSettle
();
await
tester
.
tap
(
find
.
text
(
'OK'
));
expect
(
await
range
,
DateTimeRange
(
start:
DateTime
(
2016
,
DateTime
.
january
,
12
),
end:
DateTime
(
2016
,
DateTime
.
january
,
14
),
));
});
});
group
(
'Haptic feedback'
,
()
{
const
Duration
hapticFeedbackInterval
=
Duration
(
milliseconds:
10
);
FeedbackTester
feedback
;
setUp
(()
{
feedback
=
FeedbackTester
();
initialDateRange
=
DateTimeRange
(
start:
DateTime
(
2017
,
DateTime
.
january
,
15
),
end:
DateTime
(
2017
,
DateTime
.
january
,
17
),
);
firstDate
=
DateTime
(
2017
,
DateTime
.
january
,
10
);
lastDate
=
DateTime
(
2018
,
DateTime
.
january
,
20
);
});
tearDown
(()
{
feedback
?.
dispose
();
});
testWidgets
(
'Selecting dates vibrates'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
tap
(
find
.
text
(
'10'
).
first
);
await
tester
.
pump
(
hapticFeedbackInterval
);
expect
(
feedback
.
hapticCount
,
1
);
await
tester
.
tap
(
find
.
text
(
'12'
).
first
);
await
tester
.
pump
(
hapticFeedbackInterval
);
expect
(
feedback
.
hapticCount
,
2
);
await
tester
.
tap
(
find
.
text
(
'14'
).
first
);
await
tester
.
pump
(
hapticFeedbackInterval
);
expect
(
feedback
.
hapticCount
,
3
);
});
});
testWidgets
(
'Tapping unselectable date does not vibrate'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
tap
(
find
.
text
(
'8'
).
first
);
await
tester
.
pump
(
hapticFeedbackInterval
);
expect
(
feedback
.
hapticCount
,
0
);
});
});
});
group
(
'Input mode'
,
()
{
setUp
(()
{
firstDate
=
DateTime
(
2015
,
DateTime
.
january
,
1
);
lastDate
=
DateTime
(
2017
,
DateTime
.
december
,
31
);
initialDateRange
=
DateTimeRange
(
start:
DateTime
(
2017
,
DateTime
.
january
,
15
),
end:
DateTime
(
2017
,
DateTime
.
january
,
17
),
);
initialEntryMode
=
DatePickerEntryMode
.
input
;
});
testWidgets
(
'Initial entry mode is used'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
expect
(
find
.
byType
(
TextField
),
findsNWidgets
(
2
));
});
});
testWidgets
(
'All custom strings are used'
,
(
WidgetTester
tester
)
async
{
initialDateRange
=
null
;
cancelText
=
'nope'
;
confirmText
=
'yep'
;
fieldStartHintText
=
'hint1'
;
fieldEndHintText
=
'hint2'
;
fieldStartLabelText
=
'label1'
;
fieldEndLabelText
=
'label2'
;
helpText
=
'help'
;
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
expect
(
find
.
text
(
cancelText
),
findsOneWidget
);
expect
(
find
.
text
(
confirmText
),
findsOneWidget
);
expect
(
find
.
text
(
fieldStartHintText
),
findsOneWidget
);
expect
(
find
.
text
(
fieldEndHintText
),
findsOneWidget
);
expect
(
find
.
text
(
fieldStartLabelText
),
findsOneWidget
);
expect
(
find
.
text
(
fieldEndLabelText
),
findsOneWidget
);
expect
(
find
.
text
(
helpText
),
findsOneWidget
);
});
});
testWidgets
(
'Initial date is the default'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
tap
(
find
.
text
(
'OK'
));
expect
(
await
range
,
DateTimeRange
(
start:
DateTime
(
2017
,
DateTime
.
january
,
15
),
end:
DateTime
(
2017
,
DateTime
.
january
,
17
),
));
});
});
testWidgets
(
'Can toggle to calendar entry mode'
,
(
WidgetTester
tester
)
async
{
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
expect
(
find
.
byType
(
TextField
),
findsNWidgets
(
2
));
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
calendar_today
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
byType
(
TextField
),
findsNothing
);
});
});
testWidgets
(
'Toggle to calendar mode keeps selected date'
,
(
WidgetTester
tester
)
async
{
initialDateRange
=
null
;
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
0
),
'12/25/2016'
);
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
1
),
'12/27/2016'
);
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
calendar_today
));
await
tester
.
pumpAndSettle
();
await
tester
.
tap
(
find
.
text
(
'SAVE'
));
expect
(
await
range
,
DateTimeRange
(
start:
DateTime
(
2016
,
DateTime
.
december
,
25
),
end:
DateTime
(
2016
,
DateTime
.
december
,
27
),
));
});
});
testWidgets
(
'Entered text returns range'
,
(
WidgetTester
tester
)
async
{
initialDateRange
=
null
;
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
0
),
'12/25/2016'
);
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
1
),
'12/27/2016'
);
await
tester
.
tap
(
find
.
text
(
'OK'
));
expect
(
await
range
,
DateTimeRange
(
start:
DateTime
(
2016
,
DateTime
.
december
,
25
),
end:
DateTime
(
2016
,
DateTime
.
december
,
27
),
));
});
});
testWidgets
(
'Too short entered text shows error'
,
(
WidgetTester
tester
)
async
{
initialDateRange
=
null
;
errorFormatText
=
'oops'
;
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
0
),
'12/25'
);
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
1
),
'12/25'
);
expect
(
find
.
text
(
errorFormatText
),
findsNothing
);
await
tester
.
tap
(
find
.
text
(
'OK'
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
errorFormatText
),
findsNWidgets
(
2
));
});
});
testWidgets
(
'Bad format entered text shows error'
,
(
WidgetTester
tester
)
async
{
initialDateRange
=
null
;
errorFormatText
=
'oops'
;
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
0
),
'20202014'
);
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
1
),
'20212014'
);
expect
(
find
.
text
(
errorFormatText
),
findsNothing
);
await
tester
.
tap
(
find
.
text
(
'OK'
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
errorFormatText
),
findsNWidgets
(
2
));
});
});
testWidgets
(
'Invalid entered text shows error'
,
(
WidgetTester
tester
)
async
{
initialDateRange
=
null
;
errorInvalidText
=
'oops'
;
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
0
),
'08/08/2014'
);
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
1
),
'08/08/2014'
);
expect
(
find
.
text
(
errorInvalidText
),
findsNothing
);
await
tester
.
tap
(
find
.
text
(
'OK'
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
errorInvalidText
),
findsNWidgets
(
2
));
});
});
testWidgets
(
'End before start date shows error'
,
(
WidgetTester
tester
)
async
{
initialDateRange
=
null
;
errorInvalidRangeText
=
'oops'
;
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
0
),
'12/27/2016'
);
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
1
),
'12/25/2016'
);
expect
(
find
.
text
(
errorInvalidRangeText
),
findsNothing
);
await
tester
.
tap
(
find
.
text
(
'OK'
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
errorInvalidRangeText
),
findsOneWidget
);
});
});
testWidgets
(
'Error text only displayed for invalid date'
,
(
WidgetTester
tester
)
async
{
initialDateRange
=
null
;
errorInvalidText
=
'oops'
;
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
0
),
'12/27/2016'
);
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
1
),
'01/01/2018'
);
expect
(
find
.
text
(
errorInvalidText
),
findsNothing
);
await
tester
.
tap
(
find
.
text
(
'OK'
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
errorInvalidText
),
findsOneWidget
);
});
});
testWidgets
(
'End before start date does not get passed to calendar mode'
,
(
WidgetTester
tester
)
async
{
initialDateRange
=
null
;
await
preparePicker
(
tester
,
(
Future
<
DateTimeRange
>
range
)
async
{
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
0
),
'12/27/2016'
);
await
tester
.
enterText
(
find
.
byType
(
TextField
).
at
(
1
),
'12/25/2016'
);
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
calendar_today
));
await
tester
.
pumpAndSettle
();
await
tester
.
tap
(
find
.
text
(
'SAVE'
));
await
tester
.
pumpAndSettle
();
// Save button should be disabled, so dialog should still be up
// with the first date selected, but no end date
expect
(
find
.
text
(
'Dec 27'
),
findsOneWidget
);
expect
(
find
.
text
(
'End Date'
),
findsOneWidget
);
});
});
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment