date_picker.dart 12.3 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 14
import 'ink_well.dart';
import 'theme.dart';
15
import 'typography.dart';
16

17
enum _DatePickerMode { day, year }
18 19 20 21 22 23 24 25 26 27 28 29 30

class DatePicker extends StatefulComponent {
  DatePicker({
    this.selectedDate,
    this.onChanged,
    this.firstDate,
    this.lastDate
  }) {
    assert(selectedDate != null);
    assert(firstDate != null);
    assert(lastDate != null);
  }

31
  final DateTime selectedDate;
Hixie's avatar
Hixie committed
32
  final ValueChanged<DateTime> onChanged;
33 34 35
  final DateTime firstDate;
  final DateTime lastDate;

36
  _DatePickerState createState() => new _DatePickerState();
37
}
38

39
class _DatePickerState extends State<DatePicker> {
40
  _DatePickerMode _mode = _DatePickerMode.day;
41

42
  void _handleModeChanged(_DatePickerMode mode) {
43
    userFeedback.performHapticFeedback(HapticFeedbackType.virtualKey);
44 45 46 47 48 49
    setState(() {
      _mode = mode;
    });
  }

  void _handleYearChanged(DateTime dateTime) {
50
    userFeedback.performHapticFeedback(HapticFeedbackType.virtualKey);
51
    setState(() {
52
      _mode = _DatePickerMode.day;
53
    });
54 55
    if (config.onChanged != null)
      config.onChanged(dateTime);
56 57
  }

58
  void _handleDayChanged(DateTime dateTime) {
59
    userFeedback.performHapticFeedback(HapticFeedbackType.virtualKey);
60 61
    if (config.onChanged != null)
      config.onChanged(dateTime);
62 63
  }

64 65
  static const double _calendarHeight = 210.0;

66
  Widget build(BuildContext context) {
67
    Widget header = new _DatePickerHeader(
68
      selectedDate: config.selectedDate,
69 70 71 72 73
      mode: _mode,
      onModeChanged: _handleModeChanged
    );
    Widget picker;
    switch (_mode) {
74
      case _DatePickerMode.day:
75
        picker = new MonthPicker(
76
          selectedDate: config.selectedDate,
77
          onChanged: _handleDayChanged,
78 79
          firstDate: config.firstDate,
          lastDate: config.lastDate,
80 81 82
          itemExtent: _calendarHeight
        );
        break;
83
      case _DatePickerMode.year:
84
        picker = new YearPicker(
85
          selectedDate: config.selectedDate,
86
          onChanged: _handleYearChanged,
87 88
          firstDate: config.firstDate,
          lastDate: config.lastDate
89 90 91
        );
        break;
    }
92 93 94 95 96 97 98 99 100 101
    return new Column(
      children: <Widget>[
        header,
        new Container(
          height: _calendarHeight,
          child: picker
        )
      ],
      alignItems: FlexAlignItems.stretch
    );
102 103 104 105 106
  }

}

// Shows the selected date in large font and toggles between year and day mode
107 108
class _DatePickerHeader extends StatelessComponent {
  _DatePickerHeader({ this.selectedDate, this.mode, this.onModeChanged }) {
109 110 111 112
    assert(selectedDate != null);
    assert(mode != null);
  }

113 114 115
  final DateTime selectedDate;
  final _DatePickerMode mode;
  final ValueChanged<_DatePickerMode> onModeChanged;
116

117
  void _handleChangeMode(_DatePickerMode value) {
Adam Barth's avatar
Adam Barth committed
118 119
    if (value != mode)
      onModeChanged(value);
120 121
  }

122 123
  Widget build(BuildContext context) {
    ThemeData theme = Theme.of(context);
Adam Barth's avatar
Adam Barth committed
124
    TextTheme headerTheme = theme.primaryTextTheme;
125 126 127 128
    Color dayColor;
    Color yearColor;
    switch(theme.primaryColorBrightness) {
      case ThemeBrightness.light:
129 130
        dayColor = mode == _DatePickerMode.day ? Colors.black87 : Colors.black54;
        yearColor = mode == _DatePickerMode.year ? Colors.black87 : Colors.black54;
131 132
        break;
      case ThemeBrightness.dark:
133 134
        dayColor = mode == _DatePickerMode.day ? Colors.white : Colors.white70;
        yearColor = mode == _DatePickerMode.year ? Colors.white : Colors.white70;
135 136 137 138 139 140 141
        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(
142 143
      padding: new EdgeDims.all(10.0),
      decoration: new BoxDecoration(backgroundColor: theme.primaryColor),
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
      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)
          )
        ]
      )
160 161 162 163 164
    );
  }
}

// Fixed height component shows a single month and allows choosing a day
165
class DayPicker extends StatelessComponent {
166 167 168 169 170 171 172 173
  DayPicker({
    this.selectedDate,
    this.currentDate,
    this.onChanged,
    this.displayedMonth
  }) {
    assert(selectedDate != null);
    assert(currentDate != null);
174
    assert(onChanged != null);
175 176 177 178 179
    assert(displayedMonth != null);
  }

  final DateTime selectedDate;
  final DateTime currentDate;
Hixie's avatar
Hixie committed
180
  final ValueChanged<DateTime> onChanged;
181 182
  final DateTime displayedMonth;

183 184
  Widget build(BuildContext context) {
    ThemeData theme = Theme.of(context);
185 186 187 188 189 190
    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;

Hixie's avatar
Hixie committed
191
    List<Text> headers = <Text>[];
192 193 194
    for (String weekDay in symbols.NARROWWEEKDAYS) {
      headers.add(new Text(weekDay, style: headerStyle));
    }
Hixie's avatar
Hixie committed
195
    List<Widget> rows = <Widget>[
196 197
      new Text(new DateFormat("MMMM y").format(displayedMonth), style: monthStyle),
      new Flex(
198
        children: headers,
199 200 201 202 203 204 205 206 207 208
        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;
Hixie's avatar
Hixie committed
209
    List<int> days = <int>[
210 211 212 213 214 215 216 217 218
      DateTime.SUNDAY,
      DateTime.MONDAY,
      DateTime.TUESDAY,
      DateTime.WEDNESDAY,
      DateTime.THURSDAY,
      DateTime.FRIDAY,
      DateTime.SATURDAY
    ];
    int daySlots = weeksShown * days.length;
Hixie's avatar
Hixie committed
219
    List<Widget> labels = <Widget>[];
220 221 222 223 224 225 226 227 228 229 230 231 232 233
    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],
234
            shape: BoxShape.circle
235 236 237 238 239 240 241 242 243
          );

        // 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);

244 245
        item = new GestureDetector(
          onTap: () {
246
            DateTime result = new DateTime(year, month, day);
247
            onChanged(result);
248 249 250 251 252 253 254 255 256 257 258 259 260 261
          },
          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;
262
      rows.add(new Row(
263
        children: labels.sublist(startIndex, startIndex + days.length)
264 265 266
      ));
    }

267
    return new Column(children: rows);
268 269 270
  }
}

271
class MonthPicker extends StatefulComponent {
272
  MonthPicker({
273
    Key key,
274 275 276 277
    this.selectedDate,
    this.onChanged,
    this.firstDate,
    this.lastDate,
278 279
    this.itemExtent
  }) : super(key: key) {
280
    assert(selectedDate != null);
281
    assert(onChanged != null);
282 283 284
    assert(lastDate.isAfter(firstDate));
  }

285
  final DateTime selectedDate;
Hixie's avatar
Hixie committed
286
  final ValueChanged<DateTime> onChanged;
287 288
  final DateTime firstDate;
  final DateTime lastDate;
289
  final double itemExtent;
290

291
  _MonthPickerState createState() => new _MonthPickerState();
292
}
293

294
class _MonthPickerState extends State<MonthPicker> {
295 296
  void initState() {
    super.initState();
297
    _updateCurrentDate();
298 299 300
  }

  DateTime _currentDate;
301 302
  Timer _timer;

303 304 305 306 307
  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
308 309 310
    if (_timer != null)
      _timer.cancel();
    _timer = new Timer(timeUntilTomorrow, () {
311 312 313 314 315 316
      setState(() {
        _updateCurrentDate();
      });
    });
  }

317
  List<Widget> buildItems(BuildContext context, int start, int count) {
318
    List<Widget> result = new List<Widget>();
319
    DateTime startDate = new DateTime(config.firstDate.year + start ~/ 12, config.firstDate.month + start % 12);
320 321 322
    for (int i = 0; i < count; ++i) {
      DateTime displayedMonth = new DateTime(startDate.year + i ~/ 12, startDate.month + i % 12);
      Widget item = new Container(
323
        height: config.itemExtent,
324 325
        key: new ObjectKey(displayedMonth),
        child: new DayPicker(
326
          selectedDate: config.selectedDate,
327
          currentDate: _currentDate,
328
          onChanged: config.onChanged,
329 330 331 332 333 334 335
          displayedMonth: displayedMonth
        )
      );
      result.add(item);
    }
    return result;
  }
336

337 338 339 340 341 342 343 344
  Widget build(BuildContext context) {
    return new ScrollableLazyList(
      itemExtent: config.itemExtent,
      itemCount: (config.lastDate.year - config.firstDate.year) * 12 + config.lastDate.month - config.firstDate.month + 1,
      itemBuilder: buildItems
    );
  }

345 346
  void dispose() {
    if (_timer != null)
347
      _timer.cancel();
348
    super.dispose();
349
  }
350 351 352
}

// Scrollable list of years to allow picking a year
353
class YearPicker extends StatefulComponent {
354
  YearPicker({
355
    Key key,
356 357 358 359
    this.selectedDate,
    this.onChanged,
    this.firstDate,
    this.lastDate
360
  }) : super(key: key) {
361
    assert(selectedDate != null);
362
    assert(onChanged != null);
363 364 365
    assert(lastDate.isAfter(firstDate));
  }

366
  final DateTime selectedDate;
Hixie's avatar
Hixie committed
367
  final ValueChanged<DateTime> onChanged;
368 369 370
  final DateTime firstDate;
  final DateTime lastDate;

371
  _YearPickerState createState() => new _YearPickerState();
372 373
}

374 375
class _YearPickerState extends State<YearPicker> {
  static const double _itemExtent = 50.0;
376

377 378
  List<Widget> buildItems(BuildContext context, int start, int count) {
    TextStyle style = Theme.of(context).text.body1.copyWith(color: Colors.black54);
379
    List<Widget> items = new List<Widget>();
Hixie's avatar
Hixie committed
380
    for (int i = start; i < start + count; i++) {
381
      int year = config.firstDate.year + i;
382
      String label = year.toString();
383
      Widget item = new InkWell(
384
        key: new Key(label),
385
        onTap: () {
386 387
          DateTime result = new DateTime(year, config.selectedDate.month, config.selectedDate.day);
          config.onChanged(result);
388
        },
389
        child: new Container(
390
          height: _itemExtent,
391 392
          decoration: year == config.selectedDate.year ? new BoxDecoration(
            backgroundColor: Theme.of(context).primarySwatch[100],
393
            shape: BoxShape.circle
394 395 396
          ) : null,
          child: new Center(
            child: new Text(label, style: style)
397 398 399 400 401 402 403
          )
        )
      );
      items.add(item);
    }
    return items;
  }
404 405 406 407 408 409 410 411

  Widget build(BuildContext context) {
    return new ScrollableLazyList(
      itemExtent: _itemExtent,
      itemCount: config.lastDate.year - config.firstDate.year + 1,
      itemBuilder: buildItems
    );
  }
412
}