Commit f164e560 authored by Collin Jackson's avatar Collin Jackson

Add date picker to widgets library and teach fitness app to use it

Also, add an example for the date picker
parent ee5f1146
......@@ -13,6 +13,12 @@ class DateUtils {
static const MS_IN_WEEK =
DateTime.DAYS_PER_WEEK * Duration.MILLISECONDS_PER_DAY;
// TODO(jmesserly): locale specific date format
static String _twoDigits(int n) {
if (n >= 10) return "${n}";
return "0${n}";
}
/** Formats a time in H:MM A format */
static String toHourMinutesString(Duration duration) {
assert(duration.inDays == 0);
......@@ -68,14 +74,16 @@ class DateUtils {
} else if (delta.inMilliseconds < MS_IN_WEEK) {
return WEEKDAYS[then.weekday];
} else {
// TODO(jmesserly): locale specific date format
String twoDigits(int n) {
if (n >= 10) return "${n}";
return "0${n}";
}
String twoDigitMonth = twoDigits(then.month);
String twoDigitDay = twoDigits(then.day);
String twoDigitMonth = _twoDigits(then.month);
String twoDigitDay = _twoDigits(then.day);
return "${then.year}-${twoDigitMonth}-${twoDigitDay}";
}
}
static String toDateString(DateTime then) {
// TODO(jmesserly): locale specific date format
String twoDigitMonth = _twoDigits(then.month);
String twoDigitDay = _twoDigits(then.day);
return "${then.year}-${twoDigitMonth}-${twoDigitDay}";
}
}
......@@ -21,7 +21,7 @@ abstract class FitnessItem {
Map toJson() => { 'when' : when.toIso8601String() };
// TODO(jackson): Internationalize
String get displayDate => DateUtils.toRecentTimeString(when);
String get displayDate => DateUtils.toDateString(when);
FitnessItemRow toRow({ FitnessItemHandler onDismissed });
}
......
......@@ -17,6 +17,7 @@ class Measurement extends FitnessItem {
Map toJson() {
Map json = super.toJson();
json['weight'] = weight;
json['type'] = runtimeType.toString();
return json;
}
......@@ -53,6 +54,55 @@ class MeasurementRow extends FitnessItemRow {
}
}
class MeasurementDateDialog extends StatefulComponent {
MeasurementDateDialog({ this.navigator, this.previousDate });
Navigator navigator;
DateTime previousDate;
@override
void initState() {
_selectedDate = previousDate;
}
void syncConstructorArguments(MeasurementDateDialog source) {
navigator = source.navigator;
previousDate = source.previousDate;
}
DateTime _selectedDate;
void _handleDateChanged(DateTime value) {
setState(() {
_selectedDate = value;
});
}
Widget build() {
return new Dialog(
content: new DatePicker(
selectedDate: _selectedDate,
firstDate: new DateTime(2015, 8),
lastDate: new DateTime(2101),
onChanged: _handleDateChanged
),
contentPadding: EdgeDims.zero,
actions: [
new FlatButton(
child: new Text('CANCEL'),
onPressed: navigator.pop
),
new FlatButton(
child: new Text('OK'),
onPressed: () {
navigator.pop(_selectedDate);
}
),
]
);
}
}
class MeasurementFragment extends StatefulComponent {
MeasurementFragment({ this.navigator, this.onCreated });
......@@ -66,6 +116,7 @@ class MeasurementFragment extends StatefulComponent {
}
String _weight = "";
DateTime _when = new DateTime.now();
String _errorMessage = null;
EventDisposition _handleSave() {
......@@ -79,7 +130,7 @@ class MeasurementFragment extends StatefulComponent {
});
return EventDisposition.processed;
}
onCreated(new Measurement(when: new DateTime.now(), weight: parsedWeight));
onCreated(new Measurement(when: _when, weight: parsedWeight));
navigator.pop();
return EventDisposition.processed;
}
......@@ -107,23 +158,44 @@ class MeasurementFragment extends StatefulComponent {
static final GlobalKey weightKey = new GlobalKey();
EventDisposition _handleDatePressed(_) {
showDialog(navigator, (navigator) {
return new MeasurementDateDialog(navigator: navigator, previousDate: _when);
}).then((DateTime value) {
if (value == null)
return;
setState(() {
_when = value;
});
});
return EventDisposition.processed;
}
Widget buildBody() {
Measurement measurement = new Measurement(when: new DateTime.now());
Measurement measurement = new Measurement(when: _when);
// TODO(jackson): Revisit the layout of this pane to be more maintainable
return new Material(
type: MaterialType.canvas,
child: new ScrollableViewport(
child: new Container(
padding: const EdgeDims.all(20.0),
child: new BlockBody([
new Text(measurement.displayDate),
new Input(
key: weightKey,
placeholder: 'Enter weight',
keyboardType: KeyboardType_NUMBER,
onChanged: _handleWeightChanged
),
])
)
child: new Container(
padding: const EdgeDims.all(20.0),
child: new Column([
new Listener(
onGestureTap: _handleDatePressed,
child: new Container(
height: 50.0,
child: new Column([
new Text('Measurement Date'),
new Text(measurement.displayDate, style: Theme.of(this).text.caption),
], alignItems: FlexAlignItems.start)
)
),
new Input(
key: weightKey,
placeholder: 'Enter weight',
keyboardType: KeyboardType_NUMBER,
onChanged: _handleWeightChanged
),
], alignItems: FlexAlignItems.stretch)
)
);
}
......
// 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 'package:sky/widgets.dart';
import 'package:sky/theme/colors.dart' as colors;
void main() => runApp(new DatePickerDemo());
class DatePickerDemo extends App {
DateTime _dateTime;
void initState() {
DateTime now = new DateTime.now();
_dateTime = new DateTime(now.year, now.month, now.day);
}
void _handleDateChanged(DateTime dateTime) {
setState(() {
_dateTime = dateTime;
});
}
Widget build() {
return new Theme(
data: new ThemeData(
brightness: ThemeBrightness.light,
primarySwatch: colors.Teal
),
child: new Stack([
new Scaffold(
toolbar: new ToolBar(center: new Text("Date Picker")),
body: new Material(
child: new Row(
[new Text(_dateTime.toString())],
alignItems: FlexAlignItems.end,
justifyContent: FlexJustifyContent.center
)
)
),
new Dialog(
content: new DatePicker(
selectedDate: _dateTime,
firstDate: new DateTime(2015, 8),
lastDate: new DateTime(2101),
onChanged: _handleDateChanged
),
contentPadding: EdgeDims.zero,
actions: [
new FlatButton(
child: new Text('CANCEL')
),
new FlatButton(
child: new Text('OK')
),
]
)
])
);
}
}
......@@ -10,6 +10,7 @@ export 'widgets/basic.dart';
export 'widgets/button_base.dart';
export 'widgets/card.dart';
export 'widgets/checkbox.dart';
export 'widgets/date_picker.dart';
export 'widgets/default_text_style.dart';
export 'widgets/dialog.dart';
export 'widgets/dismissable.dart';
......
// 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:sky/theme/colors.dart' as colors;
import 'package:sky/theme/typography.dart' as typography;
import 'package:sky/widgets.dart';
import 'package:intl/intl.dart';
import 'package:intl/date_symbols.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);
}
DateTime selectedDate;
DatePickerValueChanged onChanged;
DateTime firstDate;
DateTime lastDate;
void syncConstructorArguments(DatePicker source) {
selectedDate = source.selectedDate;
onChanged = source.onChanged;
firstDate = source.firstDate;
lastDate = source.lastDate;
}
DatePickerMode _mode = DatePickerMode.day;
void _handleModeChanged(DatePickerMode mode) {
setState(() {
_mode = mode;
});
}
void _handleYearChanged(DateTime dateTime) {
setState(() {
_mode = DatePickerMode.day;
});
if (onChanged != null)
onChanged(dateTime);
}
static const double _calendarHeight = 210.0;
Widget build() {
Widget header = new DatePickerHeader(
selectedDate: selectedDate,
mode: _mode,
onModeChanged: _handleModeChanged
);
Widget picker;
switch (_mode) {
case DatePickerMode.day:
picker = new MonthPicker(
selectedDate: selectedDate,
onChanged: onChanged,
firstDate: firstDate,
lastDate: lastDate,
itemExtent: _calendarHeight
);
break;
case DatePickerMode.year:
picker = new YearPicker(
selectedDate: selectedDate,
onChanged: _handleYearChanged,
firstDate: firstDate,
lastDate: lastDate
);
break;
}
return new BlockBody([header, new Container(height: _calendarHeight, child: picker)]);
}
}
// Shows the selected date in large font and toggles between year and day mode
class DatePickerHeader extends Component {
DatePickerHeader({ this.selectedDate, this.mode, this.onModeChanged }) {
assert(selectedDate != null);
assert(mode != null);
}
DateTime selectedDate;
DatePickerMode mode;
DatePickerModeChanged onModeChanged;
EventDisposition _handleChangeMode(DatePickerMode value) {
if (value == mode)
return EventDisposition.ignored;
onModeChanged(value);
return EventDisposition.processed;
}
Widget build() {
ThemeData theme = Theme.of(this);
typography.TextTheme headerTheme;
Color dayColor;
Color yearColor;
switch(theme.primaryColorBrightness) {
case ThemeBrightness.light:
headerTheme = typography.black;
dayColor = mode == DatePickerMode.day ? colors.black87 : colors.black54;
yearColor = mode == DatePickerMode.year ? colors.black87 : colors.black54;
break;
case ThemeBrightness.dark:
headerTheme = typography.white;
dayColor = mode == DatePickerMode.day ? colors.white87 : colors.white54;
yearColor = mode == DatePickerMode.year ? colors.white87 : colors.white54;
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);
DateTime firstDate = new DateTime(1900);
DateTime lastDate = new DateTime(2101);
return new Container(
child: new BlockBody([
new Center(
child: new Listener(
child: new Text(new DateFormat("MMM").format(selectedDate).toUpperCase(), style: monthStyle),
onGestureTap: (_) => _handleChangeMode(DatePickerMode.day)
)
),
new Center(
child: new Listener(
child: new Text(new DateFormat("d").format(selectedDate), style: dayStyle),
onGestureTap: (_) => _handleChangeMode(DatePickerMode.day)
)
),
new Center(
child: new Listener(
child: new Text(new DateFormat("yyyy").format(selectedDate), style: yearStyle),
onGestureTap: (_) => _handleChangeMode(DatePickerMode.year)
)
)
]),
padding: new EdgeDims.all(10.0),
decoration: new BoxDecoration(backgroundColor: theme.primaryColor)
);
}
}
// Fixed height component shows a single month and allows choosing a day
class DayPicker extends Component {
DayPicker({
this.selectedDate,
this.currentDate,
this.onChanged,
this.displayedMonth
}) {
assert(selectedDate != null);
assert(currentDate != null);
assert(displayedMonth != null);
}
final DateTime selectedDate;
final DateTime currentDate;
final DatePickerValueChanged onChanged;
final DateTime displayedMonth;
Widget build() {
ThemeData theme = Theme.of(this);
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 = [];
for (String weekDay in symbols.NARROWWEEKDAYS) {
headers.add(new Text(weekDay, style: headerStyle));
}
List<Widget> rows = [
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 = [
DateTime.SUNDAY,
DateTime.MONDAY,
DateTime.TUESDAY,
DateTime.WEDNESDAY,
DateTime.THURSDAY,
DateTime.FRIDAY,
DateTime.SATURDAY
];
int daySlots = weeksShown * days.length;
List<Widget> labels = [];
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 Listener(
onGestureTap: (_) {
DateTime result = new DateTime(year, month, day);
if (onChanged != null)
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 Container(
child: new Flex(
labels.sublist(startIndex, startIndex + days.length),
justifyContent: FlexJustifyContent.spaceAround
)
));
}
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(lastDate.isAfter(firstDate));
}
DateTime selectedDate;
DatePickerValueChanged onChanged;
DateTime firstDate;
DateTime lastDate;
void syncConstructorArguments(MonthPicker source) {
selectedDate = source.selectedDate;
onChanged = source.onChanged;
firstDate = source.firstDate;
lastDate = source.lastDate;
super.syncConstructorArguments(source);
}
void initState() {
_updateCurrentDate();
super.initState();
}
DateTime _currentDate;
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
new Timer(timeUntilTomorrow, () {
setState(() {
_updateCurrentDate();
});
});
}
int get itemCount => (lastDate.year - firstDate.year) * 12 + lastDate.month - firstDate.month + 1;
List<Widget> buildItems(int start, int count) {
List<Widget> result = new List<Widget>();
DateTime startDate = new DateTime(firstDate.year + start ~/ 12, 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: itemExtent,
key: new ObjectKey(displayedMonth),
child: new DayPicker(
selectedDate: selectedDate,
currentDate: _currentDate,
onChanged: onChanged,
displayedMonth: displayedMonth
)
);
result.add(item);
}
return result;
}
}
// 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(lastDate.isAfter(firstDate));
}
DateTime selectedDate;
DatePickerValueChanged onChanged;
DateTime firstDate;
DateTime lastDate;
void syncConstructorArguments(YearPicker source) {
selectedDate = source.selectedDate;
onChanged = source.onChanged;
firstDate = source.firstDate;
lastDate = source.lastDate;
super.syncConstructorArguments(source);
}
int get itemCount => lastDate.year - firstDate.year + 1;
List<Widget> buildItems(int start, int count) {
TextStyle style = Theme.of(this).text.body1.copyWith(color: colors.black54);
List<Widget> items = new List<Widget>();
for(int i = start; i < start + count; i++) {
int year = firstDate.year + i;
String label = year.toString();
Widget item = new Listener(
key: new Key(label),
onGestureTap: (_) {
DateTime result = new DateTime(year, selectedDate.month, selectedDate.day);
if (onChanged != null)
onChanged(result);
},
child: new InkWell(
child: new Container(
height: itemExtent,
decoration: year == selectedDate.year ? new BoxDecoration(
backgroundColor: Theme.of(this).primarySwatch[100],
shape: Shape.circle
) : null,
child: new Center(
child: new Text(label, style: style)
)
)
)
);
items.add(item);
}
return items;
}
}
......@@ -25,7 +25,9 @@ class Dialog extends Component {
Dialog({
Key key,
this.title,
this.titlePadding,
this.content,
this.contentPadding,
this.actions,
this.onDismiss
}): super(key: key);
......@@ -34,10 +36,17 @@ class Dialog extends Component {
/// of the dialog.
final Widget title;
// Padding around the title; uses material design default if none is supplied
// If there is no title, no padding will be provided
final EdgeDims titlePadding;
/// The (optional) content of the dialog is displayed in the center of the
/// dialog in a lighter font.
final Widget content;
// Padding around the content; uses material design default if none is supplied
final EdgeDims contentPadding;
/// The (optional) set of actions that are displayed at the bottom of the
/// dialog.
final List<Widget> actions;
......@@ -59,8 +68,11 @@ class Dialog extends Component {
List<Widget> dialogBody = new List<Widget>();
if (title != null) {
EdgeDims padding = titlePadding;
if (padding == null)
padding = new EdgeDims(24.0, 24.0, content == null ? 20.0 : 0.0, 24.0);
dialogBody.add(new Padding(
padding: new EdgeDims(24.0, 24.0, content == null ? 20.0 : 0.0, 24.0),
padding: padding,
child: new DefaultTextStyle(
style: Theme.of(this).text.title,
child: title
......@@ -69,8 +81,11 @@ class Dialog extends Component {
}
if (content != null) {
EdgeDims padding = contentPadding;
if (padding == null)
padding = const EdgeDims(20.0, 24.0, 24.0, 24.0);
dialogBody.add(new Padding(
padding: const EdgeDims(20.0, 24.0, 24.0, 24.0),
padding: padding,
child: new DefaultTextStyle(
style: Theme.of(this).text.subhead,
child: content
......
......@@ -13,5 +13,6 @@ dependencies:
sky_services: ^0.0.14
sky_tools: ^0.0.10
vector_math: ^1.4.3
intl: ^0.12.4+2
environment:
sdk: '>=1.8.0 <2.0.0'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment