Merge pull request #569 from abarth/timer_picker2
Teach the TimerPicker how to pick a time
This commit is contained in:
@@ -107,9 +107,9 @@ class _DatePickerHeader extends StatelessComponent {
|
||||
assert(mode != null);
|
||||
}
|
||||
|
||||
DateTime selectedDate;
|
||||
_DatePickerMode mode;
|
||||
ValueChanged<_DatePickerMode> onModeChanged;
|
||||
final DateTime selectedDate;
|
||||
final _DatePickerMode mode;
|
||||
final ValueChanged<_DatePickerMode> onModeChanged;
|
||||
|
||||
void _handleChangeMode(_DatePickerMode value) {
|
||||
if (value != mode)
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// 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/widgets.dart';
|
||||
|
||||
@@ -12,11 +16,32 @@ import 'typography.dart';
|
||||
class TimeOfDay {
|
||||
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
|
||||
final int hour;
|
||||
|
||||
/// The selected 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 }
|
||||
@@ -57,15 +82,15 @@ class _TimePickerState extends State<TimePicker> {
|
||||
aspectRatio: 1.0,
|
||||
child: new Container(
|
||||
margin: const EdgeDims.all(12.0),
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: Colors.grey[300],
|
||||
shape: Shape.circle
|
||||
child: new _Dial(
|
||||
mode: _mode,
|
||||
selectedTime: config.selectedTime,
|
||||
onChanged: config.onChanged
|
||||
)
|
||||
)
|
||||
)
|
||||
], alignItems: FlexAlignItems.stretch);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Shows the selected date in large font and toggles between year and day mode
|
||||
@@ -75,9 +100,9 @@ class _TimePickerHeader extends StatelessComponent {
|
||||
assert(mode != null);
|
||||
}
|
||||
|
||||
TimeOfDay selectedTime;
|
||||
_TimePickerMode mode;
|
||||
ValueChanged<_TimePickerMode> onModeChanged;
|
||||
final TimeOfDay selectedTime;
|
||||
final _TimePickerMode mode;
|
||||
final ValueChanged<_TimePickerMode> onModeChanged;
|
||||
|
||||
void _handleChangeMode(_TimePickerMode value) {
|
||||
if (value != mode)
|
||||
@@ -100,8 +125,8 @@ class _TimePickerHeader extends StatelessComponent {
|
||||
inactiveColor = Colors.white70;
|
||||
break;
|
||||
}
|
||||
TextStyle activeStyle = headerTheme.display3.copyWith(color: activeColor, height: 1.0);
|
||||
TextStyle inactiveStyle = headerTheme.display3.copyWith(color: inactiveColor, height: 1.0);
|
||||
TextStyle activeStyle = headerTheme.display3.copyWith(color: activeColor);
|
||||
TextStyle inactiveStyle = headerTheme.display3.copyWith(color: inactiveColor);
|
||||
|
||||
TextStyle hourStyle = mode == _TimePickerMode.hour ? activeStyle : inactiveStyle;
|
||||
TextStyle minuteStyle = mode == _TimePickerMode.minute ? activeStyle : inactiveStyle;
|
||||
@@ -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> {
|
||||
|
||||
TimeOfDay _selectedTime;
|
||||
|
||||
void _handleTimeChanged(TimeOfDay value) {
|
||||
setState(() {
|
||||
_selectedTime = value;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleCancel() {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
@@ -40,7 +46,8 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
return new Dialog(
|
||||
content: new TimePicker(
|
||||
selectedTime: _selectedTime
|
||||
selectedTime: _selectedTime,
|
||||
onChanged: _handleTimeChanged
|
||||
),
|
||||
contentPadding: EdgeDims.zero,
|
||||
actions: <Widget>[
|
||||
|
||||
Reference in New Issue
Block a user