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
This commit is contained in:
Adam Barth 2016-05-22 23:09:11 -07:00
parent d0bac85da4
commit d5e3ea2f9c

View File

@ -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> {
@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> {
) )
) )
) )
], )
crossAxisAlignment: CrossAxisAlignment.stretch ]
); );
} }
} }
@ -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 {
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) {
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 {
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 {
-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 {
} }
} }
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> {
_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
) )
) )