date_picker.dart 15.7 KB
Newer Older
1 2 3 4 5 6
// 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';

7 8
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
Hixie's avatar
Hixie committed
9 10
import 'package:intl/date_symbols.dart';
import 'package:intl/intl.dart';
11

12
import 'colors.dart';
13
import 'debug.dart';
14 15
import 'ink_well.dart';
import 'theme.dart';
16
import 'typography.dart';
17

18
enum _DatePickerMode { day, year }
19

20 21 22 23 24 25 26 27
/// A material design date picker.
///
/// The date picker widget is rarely used directly. Instead, consider using
/// [showDatePicker], which creates a date picker dialog.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
28
///
29 30
///  * [showDatePicker]
///  * <https://www.google.com/design/spec/components/pickers.html#pickers-date-pickers>
31
class DatePicker extends StatefulWidget {
32 33 34 35
  /// Creates a date picker.
  ///
  /// Rather than creating a date picker directly, consider using
  /// [showDatePicker] to show a date picker in a dialog.
36 37 38 39 40 41 42 43 44 45 46
  DatePicker({
    this.selectedDate,
    this.onChanged,
    this.firstDate,
    this.lastDate
  }) {
    assert(selectedDate != null);
    assert(firstDate != null);
    assert(lastDate != null);
  }

47 48 49
  /// The currently selected date.
  ///
  /// This date is highlighted in the picker.
50
  final DateTime selectedDate;
51 52

  /// Called when the user picks a date.
Hixie's avatar
Hixie committed
53
  final ValueChanged<DateTime> onChanged;
54 55

  /// The earliest date the user is permitted to pick.
56
  final DateTime firstDate;
57 58

  /// The latest date the user is permitted to pick.
59 60
  final DateTime lastDate;

61
  @override
62
  _DatePickerState createState() => new _DatePickerState();
63
}
64

65
class _DatePickerState extends State<DatePicker> {
66
  _DatePickerMode _mode = _DatePickerMode.day;
67

68
  void _handleModeChanged(_DatePickerMode mode) {
69
    userFeedback.performHapticFeedback(HapticFeedbackType.virtualKey);
70 71 72 73 74 75
    setState(() {
      _mode = mode;
    });
  }

  void _handleYearChanged(DateTime dateTime) {
76
    userFeedback.performHapticFeedback(HapticFeedbackType.virtualKey);
77
    setState(() {
78
      _mode = _DatePickerMode.day;
79
    });
80 81
    if (config.onChanged != null)
      config.onChanged(dateTime);
82 83
  }

84
  void _handleDayChanged(DateTime dateTime) {
85
    userFeedback.performHapticFeedback(HapticFeedbackType.virtualKey);
86 87
    if (config.onChanged != null)
      config.onChanged(dateTime);
88 89
  }

90 91
  static const double _calendarHeight = 210.0;

92
  @override
93
  Widget build(BuildContext context) {
94
    Widget header = new _DatePickerHeader(
95
      selectedDate: config.selectedDate,
96 97 98 99 100
      mode: _mode,
      onModeChanged: _handleModeChanged
    );
    Widget picker;
    switch (_mode) {
101
      case _DatePickerMode.day:
102
        picker = new MonthPicker(
103
          selectedDate: config.selectedDate,
104
          onChanged: _handleDayChanged,
105 106
          firstDate: config.firstDate,
          lastDate: config.lastDate,
107 108 109
          itemExtent: _calendarHeight
        );
        break;
110
      case _DatePickerMode.year:
111
        picker = new YearPicker(
112
          selectedDate: config.selectedDate,
113
          onChanged: _handleYearChanged,
114 115
          firstDate: config.firstDate,
          lastDate: config.lastDate
116 117 118
        );
        break;
    }
119 120 121 122 123 124 125 126
    return new Column(
      children: <Widget>[
        header,
        new Container(
          height: _calendarHeight,
          child: picker
        )
      ],
127
      crossAxisAlignment: CrossAxisAlignment.stretch
128
    );
129 130 131 132 133
  }

}

// Shows the selected date in large font and toggles between year and day mode
134
class _DatePickerHeader extends StatelessWidget {
135
  _DatePickerHeader({ this.selectedDate, this.mode, this.onModeChanged }) {
136 137 138 139
    assert(selectedDate != null);
    assert(mode != null);
  }

140 141 142
  final DateTime selectedDate;
  final _DatePickerMode mode;
  final ValueChanged<_DatePickerMode> onModeChanged;
143

144
  void _handleChangeMode(_DatePickerMode value) {
Adam Barth's avatar
Adam Barth committed
145 146
    if (value != mode)
      onModeChanged(value);
147 148
  }

149
  @override
150 151
  Widget build(BuildContext context) {
    ThemeData theme = Theme.of(context);
Adam Barth's avatar
Adam Barth committed
152
    TextTheme headerTheme = theme.primaryTextTheme;
153 154 155 156
    Color dayColor;
    Color yearColor;
    switch(theme.primaryColorBrightness) {
      case ThemeBrightness.light:
157 158
        dayColor = mode == _DatePickerMode.day ? Colors.black87 : Colors.black54;
        yearColor = mode == _DatePickerMode.year ? Colors.black87 : Colors.black54;
159 160
        break;
      case ThemeBrightness.dark:
161 162
        dayColor = mode == _DatePickerMode.day ? Colors.white : Colors.white70;
        yearColor = mode == _DatePickerMode.year ? Colors.white : Colors.white70;
163 164 165 166 167 168 169
        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(
170
      padding: new EdgeInsets.all(10.0),
171
      decoration: new BoxDecoration(backgroundColor: theme.primaryColor),
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
      child: new Column(
        children: <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)
          )
        ]
      )
188 189 190 191
    );
  }
}

192 193 194 195 196 197 198 199
/// Displays the days of a given month and allows choosing a day.
///
/// The days are arranged in a rectangular grid with one column for each day of
/// the week.
///
/// Part of the material design [DatePicker].
///
/// See also:
200
///
201 202
///  * [DatePicker].
///  * <https://www.google.com/design/spec/components/pickers.html#pickers-date-pickers>
203
class DayPicker extends StatelessWidget {
204 205 206
  /// Creates a day picker.
  ///
  /// Rarely used directly. Instead, typically used as part of a [DatePicker].
207 208 209 210 211 212 213 214
  DayPicker({
    this.selectedDate,
    this.currentDate,
    this.onChanged,
    this.displayedMonth
  }) {
    assert(selectedDate != null);
    assert(currentDate != null);
215
    assert(onChanged != null);
216 217 218
    assert(displayedMonth != null);
  }

219 220 221
  /// The currently selected date.
  ///
  /// This date is highlighted in the picker.
222
  final DateTime selectedDate;
223 224

  /// The current date at the time the picker is displayed.
225
  final DateTime currentDate;
226 227

  /// Called when the user picks a day.
Hixie's avatar
Hixie committed
228
  final ValueChanged<DateTime> onChanged;
229 230

  /// The month whose days are displayed by this picker.
231 232
  final DateTime displayedMonth;

233
  @override
234
  Widget build(BuildContext context) {
235
    ThemeData themeData = Theme.of(context);
236
    TextStyle headerStyle = themeData.textTheme.caption.copyWith(fontWeight: FontWeight.w700);
237 238 239 240 241
    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;

Hixie's avatar
Hixie committed
242
    List<Text> headers = <Text>[];
243 244 245
    for (String weekDay in symbols.NARROWWEEKDAYS) {
      headers.add(new Text(weekDay, style: headerStyle));
    }
Hixie's avatar
Hixie committed
246
    List<Widget> rows = <Widget>[
247 248
      new Text(new DateFormat("MMMM y").format(displayedMonth), style: monthStyle),
      new Flex(
249
        children: headers,
250
        mainAxisAlignment: MainAxisAlignment.spaceAround
251 252 253 254 255 256 257 258 259
      )
    ];
    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;
Hixie's avatar
Hixie committed
260
    List<int> days = <int>[
261 262 263 264 265 266 267 268 269
      DateTime.SUNDAY,
      DateTime.MONDAY,
      DateTime.TUESDAY,
      DateTime.WEDNESDAY,
      DateTime.THURSDAY,
      DateTime.FRIDAY,
      DateTime.SATURDAY
    ];
    int daySlots = weeksShown * days.length;
Hixie's avatar
Hixie committed
270
    List<Widget> labels = <Widget>[];
271 272 273 274 275 276 277
    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 {
Ian Hickson's avatar
Ian Hickson committed
278
        BoxDecoration decoration;
Hans Muller's avatar
Hans Muller committed
279 280 281 282 283 284 285 286
        TextStyle itemStyle = dayStyle;

        if (selectedDate.year == year && selectedDate.month == month && selectedDate.day == day) {
          // The selected day gets a circle background highlight, and a contrasting text color.
          final ThemeData theme = Theme.of(context);
          itemStyle = itemStyle.copyWith(
            color: (theme.brightness == ThemeBrightness.light) ? Colors.white : Colors.black87
          );
287
          decoration = new BoxDecoration(
Hans Muller's avatar
Hans Muller committed
288
            backgroundColor: themeData.accentColor,
289
            shape: BoxShape.circle
290
          );
Hans Muller's avatar
Hans Muller committed
291 292 293 294
        } else if (currentDate.year == year && currentDate.month == month && currentDate.day == day) {
          // The current day gets a different text color.
          itemStyle = itemStyle.copyWith(color: themeData.accentColor);
        }
295

296
        item = new GestureDetector(
Hans Muller's avatar
Hans Muller committed
297
          behavior: HitTestBehavior.translucent,
298
          onTap: () {
299
            DateTime result = new DateTime(year, month, day);
300
            onChanged(result);
301 302 303 304 305 306 307 308 309 310 311 312 313 314
          },
          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;
315
      rows.add(new Row(
316
        children: labels.sublist(startIndex, startIndex + days.length)
317 318 319
      ));
    }

320
    return new Column(children: rows);
321 322 323
  }
}

324 325 326 327 328 329 330 331
/// A scrollable list of months to allow picking a month.
///
/// Shows the days of each month in a rectangular grid with one column for each
/// day of the week.
///
/// Part of the material design [DatePicker].
///
/// See also:
332
///
333 334
///  * [DatePicker]
///  * <https://www.google.com/design/spec/components/pickers.html#pickers-date-pickers>
335
class MonthPicker extends StatefulWidget {
336 337 338
  /// Creates a month picker.
  ///
  /// Rarely used directly. Instead, typically used as part of a [DatePicker].
339
  MonthPicker({
340
    Key key,
341 342 343 344
    this.selectedDate,
    this.onChanged,
    this.firstDate,
    this.lastDate,
345 346
    this.itemExtent
  }) : super(key: key) {
347
    assert(selectedDate != null);
348
    assert(onChanged != null);
349
    assert(lastDate.isAfter(firstDate));
Hans Muller's avatar
Hans Muller committed
350
    assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate));
351 352
  }

353 354 355
  /// The currently selected date.
  ///
  /// This date is highlighted in the picker.
356
  final DateTime selectedDate;
357 358

  /// Called when the user picks a month.
Hixie's avatar
Hixie committed
359
  final ValueChanged<DateTime> onChanged;
360 361

  /// The earliest date the user is permitted to pick.
362
  final DateTime firstDate;
363 364

  /// The latest date the user is permitted to pick.
365
  final DateTime lastDate;
366 367

  /// The amount of vertical space to use for each month in the picker.
368
  final double itemExtent;
369

370
  @override
371
  _MonthPickerState createState() => new _MonthPickerState();
372
}
373

374
class _MonthPickerState extends State<MonthPicker> {
375
  @override
376 377
  void initState() {
    super.initState();
378
    _updateCurrentDate();
379 380 381
  }

  DateTime _currentDate;
382 383
  Timer _timer;

384 385 386 387 388
  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
389 390 391
    if (_timer != null)
      _timer.cancel();
    _timer = new Timer(timeUntilTomorrow, () {
392 393 394 395 396 397
      setState(() {
        _updateCurrentDate();
      });
    });
  }

Hans Muller's avatar
Hans Muller committed
398 399 400 401
  int _monthDelta(DateTime startDate, DateTime endDate) {
    return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month;
  }

402
  List<Widget> buildItems(BuildContext context, int start, int count) {
403
    List<Widget> result = new List<Widget>();
404
    DateTime startDate = new DateTime(config.firstDate.year + start ~/ 12, config.firstDate.month + start % 12);
405 406 407
    for (int i = 0; i < count; ++i) {
      DateTime displayedMonth = new DateTime(startDate.year + i ~/ 12, startDate.month + i % 12);
      Widget item = new Container(
408
        height: config.itemExtent,
409 410
        key: new ObjectKey(displayedMonth),
        child: new DayPicker(
411
          selectedDate: config.selectedDate,
412
          currentDate: _currentDate,
413
          onChanged: config.onChanged,
414 415 416 417 418 419 420
          displayedMonth: displayedMonth
        )
      );
      result.add(item);
    }
    return result;
  }
421

422
  @override
423 424
  Widget build(BuildContext context) {
    return new ScrollableLazyList(
Hans Muller's avatar
Hans Muller committed
425 426
      key: new ValueKey<DateTime>(config.selectedDate),
      initialScrollOffset: config.itemExtent * _monthDelta(config.firstDate, config.selectedDate),
427
      itemExtent: config.itemExtent,
Hans Muller's avatar
Hans Muller committed
428
      itemCount: _monthDelta(config.firstDate, config.lastDate) + 1,
429 430 431 432
      itemBuilder: buildItems
    );
  }

433
  @override
434 435
  void dispose() {
    if (_timer != null)
436
      _timer.cancel();
437
    super.dispose();
438
  }
439 440
}

441 442 443 444 445 446 447
/// A scrollable list of years to allow picking a year.
///
/// Part of the material design [DatePicker].
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
448
///
449 450
///  * [DatePicker]
///  * <https://www.google.com/design/spec/components/pickers.html#pickers-date-pickers>
451
class YearPicker extends StatefulWidget {
452
  YearPicker({
453
    Key key,
454 455 456 457
    this.selectedDate,
    this.onChanged,
    this.firstDate,
    this.lastDate
458
  }) : super(key: key) {
459
    assert(selectedDate != null);
460
    assert(onChanged != null);
461 462 463
    assert(lastDate.isAfter(firstDate));
  }

464 465 466
  /// The currently selected date.
  ///
  /// This date is highlighted in the picker.
467
  final DateTime selectedDate;
468 469

  /// Called when the user picks a year.
Hixie's avatar
Hixie committed
470
  final ValueChanged<DateTime> onChanged;
471 472

  /// The earliest date the user is permitted to pick.
473
  final DateTime firstDate;
474 475

  /// The latest date the user is permitted to pick.
476 477
  final DateTime lastDate;

478
  @override
479
  _YearPickerState createState() => new _YearPickerState();
480 481
}

482 483
class _YearPickerState extends State<YearPicker> {
  static const double _itemExtent = 50.0;
484

485
  List<Widget> buildItems(BuildContext context, int start, int count) {
486
    TextStyle style = Theme.of(context).textTheme.body1.copyWith(color: Colors.black54);
487
    List<Widget> items = new List<Widget>();
Hixie's avatar
Hixie committed
488
    for (int i = start; i < start + count; i++) {
489
      int year = config.firstDate.year + i;
490
      String label = year.toString();
491
      Widget item = new InkWell(
492
        key: new Key(label),
493
        onTap: () {
494 495
          DateTime result = new DateTime(year, config.selectedDate.month, config.selectedDate.day);
          config.onChanged(result);
496
        },
497
        child: new Container(
498
          height: _itemExtent,
499
          decoration: year == config.selectedDate.year ? new BoxDecoration(
500
            backgroundColor: Theme.of(context).backgroundColor,
501
            shape: BoxShape.circle
502 503 504
          ) : null,
          child: new Center(
            child: new Text(label, style: style)
505 506 507 508 509 510 511
          )
        )
      );
      items.add(item);
    }
    return items;
  }
512

513
  @override
514
  Widget build(BuildContext context) {
515
    assert(debugCheckHasMaterial(context));
516 517 518 519 520 521
    return new ScrollableLazyList(
      itemExtent: _itemExtent,
      itemCount: config.lastDate.year - config.firstDate.year + 1,
      itemBuilder: buildItems
    );
  }
522
}