Commit d5e3ea2f authored by Adam Barth's avatar Adam Barth

Improve time picker fidelity

We now match the spec much better, including handling dark theme.

The main thing we're still missing is the landscape layout.

Fixes #989
parent d0bac85d
...@@ -98,6 +98,8 @@ class TimeOfDay { ...@@ -98,6 +98,8 @@ class TimeOfDay {
} }
enum _TimePickerMode { hour, minute } enum _TimePickerMode { hour, minute }
const double _kHeaderFontSize = 65.0;
const double _kPreferredDialExtent = 300.0;
/// A material design time picker. /// A material design time picker.
/// ///
...@@ -147,28 +149,30 @@ class _TimePickerState extends State<TimePicker> { ...@@ -147,28 +149,30 @@ class _TimePickerState extends State<TimePicker> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget header = new _TimePickerHeader(
selectedTime: config.selectedTime,
mode: _mode,
onModeChanged: _handleModeChanged,
onChanged: config.onChanged
);
return new Column( return new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
header, new _TimePickerHeader(
new AspectRatio( selectedTime: config.selectedTime,
aspectRatio: 1.0, mode: _mode,
onModeChanged: _handleModeChanged,
onChanged: config.onChanged
),
new Center(
child: new Container( child: new Container(
margin: const EdgeInsets.all(12.0), margin: const EdgeInsets.all(16.0),
child: new _Dial( width: _kPreferredDialExtent,
mode: _mode, child: new AspectRatio(
selectedTime: config.selectedTime, aspectRatio: 1.0,
onChanged: config.onChanged child: new _Dial(
mode: _mode,
selectedTime: config.selectedTime,
onChanged: config.onChanged
)
) )
) )
) )
], ]
crossAxisAlignment: CrossAxisAlignment.stretch
); );
} }
} }
...@@ -202,12 +206,11 @@ class _TimePickerHeader extends StatelessWidget { ...@@ -202,12 +206,11 @@ class _TimePickerHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ThemeData theme = Theme.of(context); ThemeData themeData = Theme.of(context);
TextTheme headerTheme = theme.primaryTextTheme; TextTheme headerTextTheme = themeData.primaryTextTheme;
Color activeColor; Color activeColor;
Color inactiveColor; Color inactiveColor;
switch(theme.primaryColorBrightness) { switch(themeData.primaryColorBrightness) {
case ThemeBrightness.light: case ThemeBrightness.light:
activeColor = Colors.black87; activeColor = Colors.black87;
inactiveColor = Colors.black54; inactiveColor = Colors.black54;
...@@ -217,59 +220,84 @@ class _TimePickerHeader extends StatelessWidget { ...@@ -217,59 +220,84 @@ class _TimePickerHeader extends StatelessWidget {
inactiveColor = Colors.white70; inactiveColor = Colors.white70;
break; break;
} }
TextStyle activeStyle = headerTheme.display3.copyWith(color: activeColor);
TextStyle inactiveStyle = headerTheme.display3.copyWith(color: inactiveColor); Color backgroundColor;
switch (themeData.brightness) {
case ThemeBrightness.light:
backgroundColor = themeData.primaryColor;
break;
case ThemeBrightness.dark:
backgroundColor = themeData.backgroundColor;
break;
}
TextStyle activeStyle = headerTextTheme.display3.copyWith(
fontSize: _kHeaderFontSize, color: activeColor
);
TextStyle inactiveStyle = headerTextTheme.display3.copyWith(
fontSize: _kHeaderFontSize, color: inactiveColor
);
TextStyle hourStyle = mode == _TimePickerMode.hour ? activeStyle : inactiveStyle; TextStyle hourStyle = mode == _TimePickerMode.hour ? activeStyle : inactiveStyle;
TextStyle minuteStyle = mode == _TimePickerMode.minute ? activeStyle : inactiveStyle; TextStyle minuteStyle = mode == _TimePickerMode.minute ? activeStyle : inactiveStyle;
TextStyle amStyle = headerTheme.subhead.copyWith( TextStyle amStyle = headerTextTheme.subhead.copyWith(
color: selectedTime.period == DayPeriod.am ? activeColor: inactiveColor color: selectedTime.period == DayPeriod.am ? activeColor: inactiveColor
); );
TextStyle pmStyle = headerTheme.subhead.copyWith( TextStyle pmStyle = headerTextTheme.subhead.copyWith(
color: selectedTime.period == DayPeriod.pm ? activeColor: inactiveColor color: selectedTime.period == DayPeriod.pm ? activeColor: inactiveColor
); );
return new Container( return new Container(
padding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 20.0), height: 100.0,
decoration: new BoxDecoration(backgroundColor: theme.primaryColor), padding: const EdgeInsets.symmetric(horizontal: 24.0),
decoration: new BoxDecoration(backgroundColor: backgroundColor),
child: new Row( child: new Row(
children: <Widget>[ children: <Widget>[
new GestureDetector( new Flexible(
onTap: () => _handleChangeMode(_TimePickerMode.hour), child: new Align(
child: new Text(selectedTime.hourOfPeriodLabel, style: hourStyle) alignment: FractionalOffset.centerRight,
child: new GestureDetector(
onTap: () => _handleChangeMode(_TimePickerMode.hour),
child: new Text(selectedTime.hourOfPeriodLabel, style: hourStyle)
)
)
), ),
new Text(':', style: inactiveStyle), new Text(':', style: inactiveStyle),
new GestureDetector( new Flexible(
onTap: () => _handleChangeMode(_TimePickerMode.minute), child: new Align(
child: new Text(selectedTime.minuteLabel, style: minuteStyle) alignment: FractionalOffset.centerLeft,
), child: new Row(
new GestureDetector(
onTap: _handleChangeDayPeriod,
behavior: HitTestBehavior.opaque,
child: new Container(
padding: const EdgeInsets.only(left: 16.0, right: 24.0),
child: new Column(
children: <Widget>[ children: <Widget>[
new Text('AM', style: amStyle), new GestureDetector(
new Container( onTap: () => _handleChangeMode(_TimePickerMode.minute),
padding: const EdgeInsets.only(top: 4.0), child: new Text(selectedTime.minuteLabel, style: minuteStyle)
child: new Text('PM', style: pmStyle)
), ),
], new Container(width: 16.0, height: 0.0), // Horizontal spacer
mainAxisAlignment: MainAxisAlignment.end new GestureDetector(
onTap: _handleChangeDayPeriod,
behavior: HitTestBehavior.opaque,
child: new Column(
mainAxisAlignment: MainAxisAlignment.collapse,
children: <Widget>[
new Text('AM', style: amStyle),
new Container(width: 0.0, height: 8.0), // Vertical spsacer
new Text('PM', style: pmStyle),
]
)
)
]
) )
) )
) )
], ]
mainAxisAlignment: MainAxisAlignment.end
) )
); );
} }
} }
List<TextPainter> _initPainters(List<String> labels) { List<TextPainter> _initPainters(TextTheme textTheme, List<String> labels) {
TextStyle style = Typography.black.subhead.copyWith(height: 1.0); TextStyle style = textTheme.subhead;
List<TextPainter> painters = new List<TextPainter>(labels.length); List<TextPainter> painters = new List<TextPainter>(labels.length);
for (int i = 0; i < painters.length; ++i) { for (int i = 0; i < painters.length; ++i) {
String label = labels[i]; String label = labels[i];
...@@ -280,25 +308,31 @@ List<TextPainter> _initPainters(List<String> labels) { ...@@ -280,25 +308,31 @@ List<TextPainter> _initPainters(List<String> labels) {
return painters; return painters;
} }
List<TextPainter> _initHours() { List<TextPainter> _initHours(TextTheme textTheme) {
return _initPainters(<String>['12', '1', '2', '3', '4', '5', return _initPainters(textTheme, <String>[
'6', '7', '8', '9', '10', '11']); '12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'
]);
} }
List<TextPainter> _initMinutes() { List<TextPainter> _initMinutes(TextTheme textTheme) {
return _initPainters(<String>['00', '05', '10', '15', '20', '25', return _initPainters(textTheme, <String>[
'30', '35', '40', '45', '50', '55']); '00', '05', '10', '15', '20', '25', '30', '35', '40', '45', '50', '55'
]);
} }
class _DialPainter extends CustomPainter { class _DialPainter extends CustomPainter {
const _DialPainter({ const _DialPainter({
this.labels, this.primaryLabels,
this.primaryColor, this.secondaryLabels,
this.backgroundColor,
this.accentColor,
this.theta this.theta
}); });
final List<TextPainter> labels; final List<TextPainter> primaryLabels;
final Color primaryColor; final List<TextPainter> secondaryLabels;
final Color backgroundColor;
final Color accentColor;
final double theta; final double theta;
@override @override
...@@ -306,7 +340,7 @@ class _DialPainter extends CustomPainter { ...@@ -306,7 +340,7 @@ class _DialPainter extends CustomPainter {
double radius = size.shortestSide / 2.0; double radius = size.shortestSide / 2.0;
Offset center = new Offset(size.width / 2.0, size.height / 2.0); Offset center = new Offset(size.width / 2.0, size.height / 2.0);
Point centerPoint = center.toPoint(); Point centerPoint = center.toPoint();
canvas.drawCircle(centerPoint, radius, new Paint()..color = Colors.grey[200]); canvas.drawCircle(centerPoint, radius, new Paint()..color = backgroundColor);
const double labelPadding = 24.0; const double labelPadding = 24.0;
double labelRadius = radius - labelPadding; double labelRadius = radius - labelPadding;
...@@ -315,28 +349,44 @@ class _DialPainter extends CustomPainter { ...@@ -315,28 +349,44 @@ class _DialPainter extends CustomPainter {
-labelRadius * math.sin(theta)); -labelRadius * math.sin(theta));
} }
Paint primaryPaint = new Paint() void paintLabels(List<TextPainter> labels) {
..color = primaryColor; double labelThetaIncrement = -_kTwoPi / labels.length;
Point currentPoint = getOffsetForTheta(theta).toPoint(); double labelTheta = math.PI / 2.0;
canvas.drawCircle(centerPoint, 4.0, primaryPaint);
canvas.drawCircle(currentPoint, labelPadding - 4.0, primaryPaint); for (TextPainter label in labels) {
primaryPaint.strokeWidth = 2.0; Offset labelOffset = new Offset(-label.width / 2.0, -label.height / 2.0);
canvas.drawLine(centerPoint, currentPoint, primaryPaint); label.paint(canvas, getOffsetForTheta(labelTheta) + labelOffset);
labelTheta += labelThetaIncrement;
double labelThetaIncrement = -_kTwoPi / labels.length; }
double labelTheta = math.PI / 2.0;
for (TextPainter label in labels) {
Offset labelOffset = new Offset(-label.width / 2.0, -label.height / 2.0);
label.paint(canvas, getOffsetForTheta(labelTheta) + labelOffset);
labelTheta += labelThetaIncrement;
} }
paintLabels(primaryLabels);
final Paint selectorPaint = new Paint()
..color = accentColor;
final Point focusedPoint = getOffsetForTheta(theta).toPoint();
final double focusedRadius = labelPadding - 4.0;
canvas.drawCircle(centerPoint, 4.0, selectorPaint);
canvas.drawCircle(focusedPoint, focusedRadius, selectorPaint);
selectorPaint.strokeWidth = 2.0;
canvas.drawLine(centerPoint, focusedPoint, selectorPaint);
final Rect focusedRect = new Rect.fromCircle(
center: focusedPoint, radius: focusedRadius
);
canvas
..saveLayer(focusedRect, new Paint())
..clipPath(new Path()..addOval(focusedRect));
paintLabels(secondaryLabels);
canvas.restore();
} }
@override @override
bool shouldRepaint(_DialPainter oldPainter) { bool shouldRepaint(_DialPainter oldPainter) {
return oldPainter.labels != labels return oldPainter.primaryLabels != primaryLabels
|| oldPainter.primaryColor != primaryColor || oldPainter.secondaryLabels != secondaryLabels
|| oldPainter.backgroundColor != backgroundColor
|| oldPainter.accentColor != accentColor
|| oldPainter.theta != theta; || oldPainter.theta != theta;
} }
} }
...@@ -464,19 +514,64 @@ class _DialState extends State<_Dial> { ...@@ -464,19 +514,64 @@ class _DialState extends State<_Dial> {
_animateTo(_getThetaForTime(config.selectedTime)); _animateTo(_getThetaForTime(config.selectedTime));
} }
final List<TextPainter> _hours = _initHours(); final List<TextPainter> _hoursWhite = _initHours(Typography.white);
final List<TextPainter> _minutes = _initMinutes(); final List<TextPainter> _hoursBlack = _initHours(Typography.black);
final List<TextPainter> _minutesWhite = _initMinutes(Typography.white);
final List<TextPainter> _minutesBlack = _initMinutes(Typography.black);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ThemeData themeData = Theme.of(context);
Color backgroundColor;
switch (themeData.brightness) {
case ThemeBrightness.light:
backgroundColor = Colors.grey[200];
break;
case ThemeBrightness.dark:
backgroundColor = themeData.backgroundColor;
break;
}
List<TextPainter> primaryLabels;
List<TextPainter> secondaryLabels;
switch (config.mode) {
case _TimePickerMode.hour:
switch (themeData.brightness) {
case ThemeBrightness.light:
primaryLabels = _hoursBlack;
secondaryLabels = _hoursWhite;
break;
case ThemeBrightness.dark:
primaryLabels = _hoursWhite;
secondaryLabels = _hoursBlack;
break;
}
break;
case _TimePickerMode.minute:
switch (themeData.brightness) {
case ThemeBrightness.light:
primaryLabels = _minutesBlack;
secondaryLabels = _minutesWhite;
break;
case ThemeBrightness.dark:
primaryLabels = _minutesWhite;
secondaryLabels = _minutesBlack;
break;
}
break;
}
return new GestureDetector( return new GestureDetector(
onPanStart: _handlePanStart, onPanStart: _handlePanStart,
onPanUpdate: _handlePanUpdate, onPanUpdate: _handlePanUpdate,
onPanEnd: _handlePanEnd, onPanEnd: _handlePanEnd,
child: new CustomPaint( child: new CustomPaint(
painter: new _DialPainter( painter: new _DialPainter(
labels: config.mode == _TimePickerMode.hour ? _hours : _minutes, primaryLabels: primaryLabels,
primaryColor: Theme.of(context).primaryColor, secondaryLabels: secondaryLabels,
backgroundColor: backgroundColor,
accentColor: themeData.accentColor,
theta: _theta.value theta: _theta.value
) )
) )
......
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