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:
parent
d0bac85da4
commit
d5e3ea2f9c
@ -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> {
|
|||||||
|
|
||||||
@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 {
|
|||||||
|
|
||||||
@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(
|
||||||
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) {
|
|||||||
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,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);
|
|
||||||
primaryPaint.strokeWidth = 2.0;
|
|
||||||
canvas.drawLine(centerPoint, currentPoint, primaryPaint);
|
|
||||||
|
|
||||||
double labelThetaIncrement = -_kTwoPi / labels.length;
|
for (TextPainter label in labels) {
|
||||||
double labelTheta = math.PI / 2.0;
|
Offset labelOffset = new Offset(-label.width / 2.0, -label.height / 2.0);
|
||||||
|
label.paint(canvas, getOffsetForTheta(labelTheta) + labelOffset);
|
||||||
for (TextPainter label in labels) {
|
labelTheta += labelThetaIncrement;
|
||||||
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> {
|
|||||||
_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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user