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,19 +149,21 @@ class _TimePickerState extends State<TimePicker> { ...@@ -147,19 +149,21 @@ class _TimePickerState extends State<TimePicker> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget header = new _TimePickerHeader( return new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new _TimePickerHeader(
selectedTime: config.selectedTime, selectedTime: config.selectedTime,
mode: _mode, mode: _mode,
onModeChanged: _handleModeChanged, onModeChanged: _handleModeChanged,
onChanged: config.onChanged onChanged: config.onChanged
); ),
return new Column( new Center(
children: <Widget>[
header,
new AspectRatio(
aspectRatio: 1.0,
child: new Container( child: new Container(
margin: const EdgeInsets.all(12.0), margin: const EdgeInsets.all(16.0),
width: _kPreferredDialExtent,
child: new AspectRatio(
aspectRatio: 1.0,
child: new _Dial( child: new _Dial(
mode: _mode, mode: _mode,
selectedTime: config.selectedTime, selectedTime: config.selectedTime,
...@@ -167,8 +171,8 @@ class _TimePickerState extends State<TimePicker> { ...@@ -167,8 +171,8 @@ class _TimePickerState extends State<TimePicker> {
) )
) )
) )
], )
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(
child: new Align(
alignment: FractionalOffset.centerRight,
child: new GestureDetector(
onTap: () => _handleChangeMode(_TimePickerMode.hour), onTap: () => _handleChangeMode(_TimePickerMode.hour),
child: new Text(selectedTime.hourOfPeriodLabel, style: hourStyle) child: new Text(selectedTime.hourOfPeriodLabel, style: hourStyle)
)
)
), ),
new Text(':', style: inactiveStyle), new Text(':', style: inactiveStyle),
new Flexible(
child: new Align(
alignment: FractionalOffset.centerLeft,
child: new Row(
children: <Widget>[
new GestureDetector( new GestureDetector(
onTap: () => _handleChangeMode(_TimePickerMode.minute), onTap: () => _handleChangeMode(_TimePickerMode.minute),
child: new Text(selectedTime.minuteLabel, style: minuteStyle) child: new Text(selectedTime.minuteLabel, style: minuteStyle)
), ),
new Container(width: 16.0, height: 0.0), // Horizontal spacer
new GestureDetector( new GestureDetector(
onTap: _handleChangeDayPeriod, onTap: _handleChangeDayPeriod,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: new Container(
padding: const EdgeInsets.only(left: 16.0, right: 24.0),
child: new Column( child: new Column(
mainAxisAlignment: MainAxisAlignment.collapse,
children: <Widget>[ children: <Widget>[
new Text('AM', style: amStyle), new Text('AM', style: amStyle),
new Container( new Container(width: 0.0, height: 8.0), // Vertical spsacer
padding: const EdgeInsets.only(top: 4.0), new Text('PM', style: pmStyle),
child: new Text('PM', style: pmStyle) ]
), )
], )
mainAxisAlignment: MainAxisAlignment.end ]
) )
) )
) )
], ]
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,14 +349,7 @@ class _DialPainter extends CustomPainter { ...@@ -315,14 +349,7 @@ class _DialPainter extends CustomPainter {
-labelRadius * math.sin(theta)); -labelRadius * math.sin(theta));
} }
Paint primaryPaint = new Paint() void paintLabels(List<TextPainter> labels) {
..color = primaryColor;
Point currentPoint = getOffsetForTheta(theta).toPoint();
canvas.drawCircle(centerPoint, 4.0, primaryPaint);
canvas.drawCircle(currentPoint, labelPadding - 4.0, primaryPaint);
primaryPaint.strokeWidth = 2.0;
canvas.drawLine(centerPoint, currentPoint, primaryPaint);
double labelThetaIncrement = -_kTwoPi / labels.length; double labelThetaIncrement = -_kTwoPi / labels.length;
double labelTheta = math.PI / 2.0; double labelTheta = math.PI / 2.0;
...@@ -333,10 +360,33 @@ class _DialPainter extends CustomPainter { ...@@ -333,10 +360,33 @@ class _DialPainter extends CustomPainter {
} }
} }
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