| // Copyright 2015 The Chromium 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:async'; |
| |
| import 'package:intl/date_symbols.dart'; |
| import 'package:intl/intl.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'colors.dart'; |
| import 'ink_well.dart'; |
| import 'theme.dart'; |
| import 'typography.dart'; |
| |
| typedef void DatePickerValueChanged(DateTime dateTime); |
| |
| enum DatePickerMode { day, year } |
| |
| typedef void DatePickerModeChanged(DatePickerMode value); |
| |
| class DatePicker extends StatefulComponent { |
| DatePicker({ |
| this.selectedDate, |
| this.onChanged, |
| this.firstDate, |
| this.lastDate |
| }) { |
| assert(selectedDate != null); |
| assert(firstDate != null); |
| assert(lastDate != null); |
| } |
| |
| final DateTime selectedDate; |
| final DatePickerValueChanged onChanged; |
| final DateTime firstDate; |
| final DateTime lastDate; |
| |
| _DatePickerState createState() => new _DatePickerState(); |
| } |
| |
| class _DatePickerState extends State<DatePicker> { |
| DatePickerMode _mode = DatePickerMode.day; |
| |
| void _handleModeChanged(DatePickerMode mode) { |
| userFeedback.performHapticFeedback(HapticFeedbackType.VIRTUAL_KEY); |
| setState(() { |
| _mode = mode; |
| }); |
| } |
| |
| void _handleYearChanged(DateTime dateTime) { |
| userFeedback.performHapticFeedback(HapticFeedbackType.VIRTUAL_KEY); |
| setState(() { |
| _mode = DatePickerMode.day; |
| }); |
| if (config.onChanged != null) |
| config.onChanged(dateTime); |
| } |
| |
| void _handleDayChanged(DateTime dateTime) { |
| userFeedback.performHapticFeedback(HapticFeedbackType.VIRTUAL_KEY); |
| if (config.onChanged != null) |
| config.onChanged(dateTime); |
| } |
| |
| static const double _calendarHeight = 210.0; |
| |
| Widget build(BuildContext context) { |
| Widget header = new _DatePickerHeader( |
| selectedDate: config.selectedDate, |
| mode: _mode, |
| onModeChanged: _handleModeChanged |
| ); |
| Widget picker; |
| switch (_mode) { |
| case DatePickerMode.day: |
| picker = new MonthPicker( |
| selectedDate: config.selectedDate, |
| onChanged: _handleDayChanged, |
| firstDate: config.firstDate, |
| lastDate: config.lastDate, |
| itemExtent: _calendarHeight |
| ); |
| break; |
| case DatePickerMode.year: |
| picker = new YearPicker( |
| selectedDate: config.selectedDate, |
| onChanged: _handleYearChanged, |
| firstDate: config.firstDate, |
| lastDate: config.lastDate |
| ); |
| break; |
| } |
| return new Column(<Widget>[ |
| header, |
| new Container( |
| height: _calendarHeight, |
| child: picker |
| ) |
| ], alignItems: FlexAlignItems.stretch); |
| } |
| |
| } |
| |
| // Shows the selected date in large font and toggles between year and day mode |
| class _DatePickerHeader extends StatelessComponent { |
| _DatePickerHeader({ this.selectedDate, this.mode, this.onModeChanged }) { |
| assert(selectedDate != null); |
| assert(mode != null); |
| } |
| |
| DateTime selectedDate; |
| DatePickerMode mode; |
| DatePickerModeChanged onModeChanged; |
| |
| void _handleChangeMode(DatePickerMode value) { |
| if (value != mode) |
| onModeChanged(value); |
| } |
| |
| Widget build(BuildContext context) { |
| ThemeData theme = Theme.of(context); |
| TextTheme headerTheme = theme.primaryTextTheme; |
| Color dayColor; |
| Color yearColor; |
| switch(theme.primaryColorBrightness) { |
| case ThemeBrightness.light: |
| dayColor = mode == DatePickerMode.day ? Colors.black87 : Colors.black54; |
| yearColor = mode == DatePickerMode.year ? Colors.black87 : Colors.black54; |
| break; |
| case ThemeBrightness.dark: |
| dayColor = mode == DatePickerMode.day ? Colors.white : Colors.white70; |
| yearColor = mode == DatePickerMode.year ? Colors.white : Colors.white70; |
| break; |
| } |
| TextStyle dayStyle = headerTheme.display3.copyWith(color: dayColor, height: 1.0, fontSize: 100.0); |
| TextStyle monthStyle = headerTheme.headline.copyWith(color: dayColor, height: 1.0); |
| TextStyle yearStyle = headerTheme.headline.copyWith(color: yearColor, height: 1.0); |
| |
| return new Container( |
| padding: new EdgeDims.all(10.0), |
| decoration: new BoxDecoration(backgroundColor: theme.primaryColor), |
| child: new Column(<Widget>[ |
| new GestureDetector( |
| onTap: () => _handleChangeMode(DatePickerMode.day), |
| child: new Text(new DateFormat("MMM").format(selectedDate).toUpperCase(), style: monthStyle) |
| ), |
| new GestureDetector( |
| onTap: () => _handleChangeMode(DatePickerMode.day), |
| child: new Text(new DateFormat("d").format(selectedDate), style: dayStyle) |
| ), |
| new GestureDetector( |
| onTap: () => _handleChangeMode(DatePickerMode.year), |
| child: new Text(new DateFormat("yyyy").format(selectedDate), style: yearStyle) |
| ) |
| ]) |
| ); |
| } |
| } |
| |
| // Fixed height component shows a single month and allows choosing a day |
| class DayPicker extends StatelessComponent { |
| DayPicker({ |
| this.selectedDate, |
| this.currentDate, |
| this.onChanged, |
| this.displayedMonth |
| }) { |
| assert(selectedDate != null); |
| assert(currentDate != null); |
| assert(onChanged != null); |
| assert(displayedMonth != null); |
| } |
| |
| final DateTime selectedDate; |
| final DateTime currentDate; |
| final DatePickerValueChanged onChanged; |
| final DateTime displayedMonth; |
| |
| Widget build(BuildContext context) { |
| ThemeData theme = Theme.of(context); |
| TextStyle headerStyle = theme.text.caption.copyWith(fontWeight: FontWeight.w700); |
| TextStyle monthStyle = headerStyle.copyWith(fontSize: 14.0, height: 24.0 / 14.0); |
| TextStyle dayStyle = headerStyle.copyWith(fontWeight: FontWeight.w500); |
| DateFormat dateFormat = new DateFormat(); |
| DateSymbols symbols = dateFormat.dateSymbols; |
| |
| List<Text> headers = <Text>[]; |
| for (String weekDay in symbols.NARROWWEEKDAYS) { |
| headers.add(new Text(weekDay, style: headerStyle)); |
| } |
| List<Widget> rows = <Widget>[ |
| new Text(new DateFormat("MMMM y").format(displayedMonth), style: monthStyle), |
| new Flex( |
| headers, |
| justifyContent: FlexJustifyContent.spaceAround |
| ) |
| ]; |
| int year = displayedMonth.year; |
| int month = displayedMonth.month; |
| // Dart's Date time constructor is very forgiving and will understand |
| // month 13 as January of the next year. :) |
| int daysInMonth = new DateTime(year, month + 1).difference(new DateTime(year, month)).inDays; |
| int firstDay = new DateTime(year, month).day; |
| int weeksShown = 6; |
| List<int> days = <int>[ |
| DateTime.SUNDAY, |
| DateTime.MONDAY, |
| DateTime.TUESDAY, |
| DateTime.WEDNESDAY, |
| DateTime.THURSDAY, |
| DateTime.FRIDAY, |
| DateTime.SATURDAY |
| ]; |
| int daySlots = weeksShown * days.length; |
| List<Widget> labels = <Widget>[]; |
| for (int i = 0; i < daySlots; i++) { |
| // This assumes a start day of SUNDAY, but could be changed. |
| int day = i - firstDay + 1; |
| Widget item; |
| if (day < 1 || day > daysInMonth) { |
| item = new Text(""); |
| } else { |
| // Put a light circle around the selected day |
| BoxDecoration decoration = null; |
| if (selectedDate.year == year && |
| selectedDate.month == month && |
| selectedDate.day == day) |
| decoration = new BoxDecoration( |
| backgroundColor: theme.primarySwatch[100], |
| shape: Shape.circle |
| ); |
| |
| // Use a different font color for the current day |
| TextStyle itemStyle = dayStyle; |
| if (currentDate.year == year && |
| currentDate.month == month && |
| currentDate.day == day) |
| itemStyle = itemStyle.copyWith(color: theme.primaryColor); |
| |
| item = new GestureDetector( |
| onTap: () { |
| DateTime result = new DateTime(year, month, day); |
| onChanged(result); |
| }, |
| child: new Container( |
| height: 30.0, |
| decoration: decoration, |
| child: new Center( |
| child: new Text(day.toString(), style: itemStyle) |
| ) |
| ) |
| ); |
| } |
| labels.add(new Flexible(child: item)); |
| } |
| for (int w = 0; w < weeksShown; w++) { |
| int startIndex = w * days.length; |
| rows.add(new Row( |
| labels.sublist(startIndex, startIndex + days.length) |
| )); |
| } |
| |
| return new Column(rows); |
| } |
| } |
| |
| // Scrollable list of DayPickers to allow choosing a month |
| class MonthPicker extends ScrollableWidgetList { |
| MonthPicker({ |
| this.selectedDate, |
| this.onChanged, |
| this.firstDate, |
| this.lastDate, |
| double itemExtent |
| }) : super(itemExtent: itemExtent) { |
| assert(selectedDate != null); |
| assert(onChanged != null); |
| assert(lastDate.isAfter(firstDate)); |
| } |
| |
| final DateTime selectedDate; |
| final DatePickerValueChanged onChanged; |
| final DateTime firstDate; |
| final DateTime lastDate; |
| |
| _MonthPickerState createState() => new _MonthPickerState(); |
| } |
| |
| class _MonthPickerState extends ScrollableWidgetListState<MonthPicker> { |
| void initState() { |
| super.initState(); |
| _updateCurrentDate(); |
| } |
| |
| DateTime _currentDate; |
| Timer _timer; |
| |
| void _updateCurrentDate() { |
| _currentDate = new DateTime.now(); |
| DateTime tomorrow = new DateTime(_currentDate.year, _currentDate.month, _currentDate.day + 1); |
| Duration timeUntilTomorrow = tomorrow.difference(_currentDate); |
| timeUntilTomorrow += const Duration(seconds: 1); // so we don't miss it by rounding |
| if (_timer != null) |
| _timer.cancel(); |
| _timer = new Timer(timeUntilTomorrow, () { |
| setState(() { |
| _updateCurrentDate(); |
| }); |
| }); |
| } |
| |
| int get itemCount => (config.lastDate.year - config.firstDate.year) * 12 + config.lastDate.month - config.firstDate.month + 1; |
| |
| List<Widget> buildItems(BuildContext context, int start, int count) { |
| List<Widget> result = new List<Widget>(); |
| DateTime startDate = new DateTime(config.firstDate.year + start ~/ 12, config.firstDate.month + start % 12); |
| for (int i = 0; i < count; ++i) { |
| DateTime displayedMonth = new DateTime(startDate.year + i ~/ 12, startDate.month + i % 12); |
| Widget item = new Container( |
| height: config.itemExtent, |
| key: new ObjectKey(displayedMonth), |
| child: new DayPicker( |
| selectedDate: config.selectedDate, |
| currentDate: _currentDate, |
| onChanged: config.onChanged, |
| displayedMonth: displayedMonth |
| ) |
| ); |
| result.add(item); |
| } |
| return result; |
| } |
| |
| void dispose() { |
| if (_timer != null) |
| _timer.cancel(); |
| super.dispose(); |
| } |
| } |
| |
| // Scrollable list of years to allow picking a year |
| class YearPicker extends ScrollableWidgetList { |
| YearPicker({ |
| this.selectedDate, |
| this.onChanged, |
| this.firstDate, |
| this.lastDate |
| }) : super(itemExtent: 50.0) { |
| assert(selectedDate != null); |
| assert(onChanged != null); |
| assert(lastDate.isAfter(firstDate)); |
| } |
| |
| final DateTime selectedDate; |
| final DatePickerValueChanged onChanged; |
| final DateTime firstDate; |
| final DateTime lastDate; |
| |
| _YearPickerState createState() => new _YearPickerState(); |
| } |
| |
| class _YearPickerState extends ScrollableWidgetListState<YearPicker> { |
| int get itemCount => config.lastDate.year - config.firstDate.year + 1; |
| |
| List<Widget> buildItems(BuildContext context, int start, int count) { |
| TextStyle style = Theme.of(context).text.body1.copyWith(color: Colors.black54); |
| List<Widget> items = new List<Widget>(); |
| for(int i = start; i < start + count; i++) { |
| int year = config.firstDate.year + i; |
| String label = year.toString(); |
| Widget item = new InkWell( |
| key: new Key(label), |
| onTap: () { |
| DateTime result = new DateTime(year, config.selectedDate.month, config.selectedDate.day); |
| config.onChanged(result); |
| }, |
| child: new Container( |
| height: config.itemExtent, |
| decoration: year == config.selectedDate.year ? new BoxDecoration( |
| backgroundColor: Theme.of(context).primarySwatch[100], |
| shape: Shape.circle |
| ) : null, |
| child: new Center( |
| child: new Text(label, style: style) |
| ) |
| ) |
| ); |
| items.add(item); |
| } |
| return items; |
| } |
| } |