Commit 717b9211 authored by Adam Barth's avatar Adam Barth

Teach the TimerPicker how to pick a time

The TimePicker can now actually pick a time. However, it doesn't understand AM
or PM and there's lots of visual polish missing.
parent 12cbd659
...@@ -107,9 +107,9 @@ class _DatePickerHeader extends StatelessComponent { ...@@ -107,9 +107,9 @@ class _DatePickerHeader extends StatelessComponent {
assert(mode != null); assert(mode != null);
} }
DateTime selectedDate; final DateTime selectedDate;
_DatePickerMode mode; final _DatePickerMode mode;
ValueChanged<_DatePickerMode> onModeChanged; final ValueChanged<_DatePickerMode> onModeChanged;
void _handleChangeMode(_DatePickerMode value) { void _handleChangeMode(_DatePickerMode value) {
if (value != mode) if (value != mode)
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -12,11 +16,32 @@ import 'typography.dart'; ...@@ -12,11 +16,32 @@ import 'typography.dart';
class TimeOfDay { class TimeOfDay {
const TimeOfDay({ this.hour, this.minute }); const TimeOfDay({ this.hour, this.minute });
TimeOfDay replacing({ int hour, int minute }) {
return new TimeOfDay(hour: hour ?? this.hour, minute: minute ?? this.minute);
}
/// The selected hour, in 24 hour time from 0..23 /// The selected hour, in 24 hour time from 0..23
final int hour; final int hour;
/// The selected minute. /// The selected minute.
final int minute; final int minute;
bool operator ==(dynamic other) {
if (other is! TimeOfDay)
return false;
final TimeOfDay typedOther = other;
return typedOther.hour == hour
&& typedOther.minute == minute;
}
int get hashCode {
int value = 373;
value = 37 * value + hour.hashCode;
value = 37 * value + minute.hashCode;
return value;
}
String toString() => 'TimeOfDay(hour: $hour, minute: $minute)';
} }
enum _TimePickerMode { hour, minute } enum _TimePickerMode { hour, minute }
...@@ -57,15 +82,15 @@ class _TimePickerState extends State<TimePicker> { ...@@ -57,15 +82,15 @@ class _TimePickerState extends State<TimePicker> {
aspectRatio: 1.0, aspectRatio: 1.0,
child: new Container( child: new Container(
margin: const EdgeDims.all(12.0), margin: const EdgeDims.all(12.0),
decoration: new BoxDecoration( child: new _Dial(
backgroundColor: Colors.grey[300], mode: _mode,
shape: Shape.circle selectedTime: config.selectedTime,
onChanged: config.onChanged
) )
) )
) )
], alignItems: FlexAlignItems.stretch); ], alignItems: FlexAlignItems.stretch);
} }
} }
// Shows the selected date in large font and toggles between year and day mode // Shows the selected date in large font and toggles between year and day mode
...@@ -75,9 +100,9 @@ class _TimePickerHeader extends StatelessComponent { ...@@ -75,9 +100,9 @@ class _TimePickerHeader extends StatelessComponent {
assert(mode != null); assert(mode != null);
} }
TimeOfDay selectedTime; final TimeOfDay selectedTime;
_TimePickerMode mode; final _TimePickerMode mode;
ValueChanged<_TimePickerMode> onModeChanged; final ValueChanged<_TimePickerMode> onModeChanged;
void _handleChangeMode(_TimePickerMode value) { void _handleChangeMode(_TimePickerMode value) {
if (value != mode) if (value != mode)
...@@ -100,8 +125,8 @@ class _TimePickerHeader extends StatelessComponent { ...@@ -100,8 +125,8 @@ class _TimePickerHeader extends StatelessComponent {
inactiveColor = Colors.white70; inactiveColor = Colors.white70;
break; break;
} }
TextStyle activeStyle = headerTheme.display3.copyWith(color: activeColor, height: 1.0); TextStyle activeStyle = headerTheme.display3.copyWith(color: activeColor);
TextStyle inactiveStyle = headerTheme.display3.copyWith(color: inactiveColor, height: 1.0); TextStyle inactiveStyle = headerTheme.display3.copyWith(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;
...@@ -123,3 +148,192 @@ class _TimePickerHeader extends StatelessComponent { ...@@ -123,3 +148,192 @@ class _TimePickerHeader extends StatelessComponent {
); );
} }
} }
final List<TextPainter> _kHours = _initHours();
final List<TextPainter> _kMinutes = _initMinutes();
List<TextPainter> _initPainters(List<String> labels) {
TextStyle style = Typography.black.subhead.copyWith(height: 1.0);
List<TextPainter> painters = new List<TextPainter>(labels.length);
for (int i = 0; i < painters.length; ++i) {
String label = labels[i];
TextPainter painter = new TextPainter(
new StyledTextSpan(style, [
new PlainTextSpan(label)
])
);
painter
..maxWidth = double.INFINITY
..maxHeight = double.INFINITY
..layout()
..maxWidth = painter.maxIntrinsicWidth
..layout();
painters[i] = painter;
}
return painters;
}
List<TextPainter> _initHours() {
return _initPainters(['12', '1', '2', '3', '4', '5',
'6', '7', '8', '9', '10', '11']);
}
List<TextPainter> _initMinutes() {
return _initPainters(['00', '05', '10', '15', '20', '25',
'30', '35', '40', '45', '50', '55']);
}
class _DialPainter extends CustomPainter {
const _DialPainter({
this.labels,
this.primaryColor,
this.theta
});
final List<TextPainter> labels;
final Color primaryColor;
final double theta;
void paint(Canvas canvas, Size size) {
double radius = size.shortestSide / 2.0;
Offset center = new Offset(size.width / 2.0, size.height / 2.0);
Point centerPoint = center.toPoint();
canvas.drawCircle(centerPoint, radius, new Paint()..color = Colors.grey[200]);
const double labelPadding = 24.0;
double labelRadius = radius - labelPadding;
Offset getOffsetForTheta(double theta) {
return center + new Offset(labelRadius * math.cos(theta),
-labelRadius * math.sin(theta));
}
Paint primaryPaint = new Paint()
..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 = -2 * math.PI / _kHours.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;
}
}
bool shouldRepaint(_DialPainter oldPainter) {
return oldPainter.labels != labels
|| oldPainter.primaryColor != primaryColor
|| oldPainter.theta != theta;
}
}
class _Dial extends StatefulComponent {
_Dial({
this.selectedTime,
this.mode,
this.onChanged
}) {
assert(selectedTime != null);
}
final TimeOfDay selectedTime;
final _TimePickerMode mode;
final ValueChanged<TimeOfDay> onChanged;
_DialState createState() => new _DialState();
}
class _DialState extends State<_Dial> {
double _theta;
void initState() {
super.initState();
_theta = _getThetaForTime(config.selectedTime);
}
void didUpdateConfig(_Dial oldConfig) {
if (config.mode != oldConfig.mode)
_theta = _getThetaForTime(config.selectedTime);
}
double _getThetaForTime(TimeOfDay time) {
double fraction = (config.mode == _TimePickerMode.hour) ?
(time.hour / 12) % 12 : (time.minute / 60) % 60;
return math.PI / 2.0 - fraction * 2 * math.PI;
}
TimeOfDay _getTimeForTheta(double theta) {
double fraction = (0.25 - (theta % (2 * math.PI)) / (2 * math.PI)) % 1.0;
if (config.mode == _TimePickerMode.hour) {
return config.selectedTime.replacing(
hour: (fraction * 12).round()
);
} else {
return config.selectedTime.replacing(
minute: (fraction * 60).round()
);
}
}
void _notifyOnChangedIfNeeded() {
if (config.onChanged == null)
return;
TimeOfDay current = _getTimeForTheta(_theta);
if (current != config.selectedTime)
config.onChanged(current);
}
void _updateThetaForPan() {
setState(() {
Offset offset = _position - _center;
_theta = (math.atan2(offset.dx, offset.dy) - math.PI / 2.0) % (2 * math.PI);
});
}
Point _position;
Point _center;
void _handlePanStart(Point globalPosition) {
RenderBox box = context.findRenderObject();
_position = box.globalToLocal(globalPosition);
double radius = box.size.shortestSide / 2.0;
_center = new Point(radius, radius);
_updateThetaForPan();
_notifyOnChangedIfNeeded();
}
void _handlePanUpdate(Offset delta) {
_position += delta;
_updateThetaForPan();
_notifyOnChangedIfNeeded();
}
void _handlePanEnd(Offset velocity) {
_position = null;
_center = null;
setState(() {
// TODO(abarth): Animate to the final value.
_theta = _getThetaForTime(config.selectedTime);
});
}
Widget build(BuildContext context) {
return new GestureDetector(
onPanStart: _handlePanStart,
onPanUpdate: _handlePanUpdate,
onPanEnd: _handlePanEnd,
child: new CustomPaint(
painter: new _DialPainter(
labels: config.mode == _TimePickerMode.hour ? _kHours : _kMinutes,
primaryColor: Theme.of(context).primaryColor,
theta: _theta
)
)
);
}
}
...@@ -29,6 +29,12 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -29,6 +29,12 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
TimeOfDay _selectedTime; TimeOfDay _selectedTime;
void _handleTimeChanged(TimeOfDay value) {
setState(() {
_selectedTime = value;
});
}
void _handleCancel() { void _handleCancel() {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
...@@ -40,7 +46,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -40,7 +46,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Dialog( return new Dialog(
content: new TimePicker( content: new TimePicker(
selectedTime: _selectedTime selectedTime: _selectedTime,
onChanged: _handleTimeChanged
), ),
contentPadding: EdgeDims.zero, contentPadding: EdgeDims.zero,
actions: <Widget>[ actions: <Widget>[
......
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