Commit 72d2706e authored by Adam Barth's avatar Adam Barth

Finish TimePicker

After this patch, TimePicker should work correctly.

Fixes #559
parent 134b11e3
...@@ -29,7 +29,7 @@ class _TimePickerDemoState extends State<TimePickerDemo> { ...@@ -29,7 +29,7 @@ class _TimePickerDemoState extends State<TimePickerDemo> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Column([ return new Column([
new Text('${_selectedTime.hour}:${_selectedTime.minute}'), new Text('$_selectedTime'),
new RaisedButton( new RaisedButton(
onPressed: _handleSelectTime, onPressed: _handleSelectTime,
child: new Text('SELECT TIME') child: new Text('SELECT TIME')
......
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
// 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.
// Modeled after Android's ViewConfiguration: import 'package:flutter/widgets.dart';
// https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/ViewConfiguration.java
// TODO(ianh): Figure out actual specced height for status bar // TODO(ianh): Figure out actual specced height for status bar
const double kStatusBarHeight = 50.0; const double kStatusBarHeight = 50.0;
...@@ -32,3 +31,5 @@ const Duration kScrollbarFadeDelay = const Duration(milliseconds: 300); ...@@ -32,3 +31,5 @@ const Duration kScrollbarFadeDelay = const Duration(milliseconds: 300);
const double kFadingEdgeLength = 12.0; const double kFadingEdgeLength = 12.0;
const double kPressedStateDuration = 64.0; // units? const double kPressedStateDuration = 64.0; // units?
const Duration kThemeChangeDuration = const Duration(milliseconds: 200); const Duration kThemeChangeDuration = const Duration(milliseconds: 200);
const EdgeDims kDialogHeadingPadding = const EdgeDims.TRBL(24.0, 24.0, 20.0, 24.0);
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -12,11 +13,27 @@ import 'package:flutter/widgets.dart'; ...@@ -12,11 +13,27 @@ import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'theme.dart'; import 'theme.dart';
import 'typography.dart'; import 'typography.dart';
import 'constants.dart';
const Duration _kDialAnimateDuration = const Duration(milliseconds: 200);
const double _kTwoPi = 2 * math.PI;
const int _kHoursPerDay = 24;
const int _kHoursPerPeriod = 12;
const int _kMinutesPerHour = 60;
enum DayPeriod {
am,
pm,
}
/// A value representing a time during the day
class TimeOfDay { class TimeOfDay {
const TimeOfDay({ this.hour, this.minute }); const TimeOfDay({ this.hour, this.minute });
/// Returns a new TimeOfDay with the hour and/or minute replaced.
TimeOfDay replacing({ int hour, int minute }) { TimeOfDay replacing({ int hour, int minute }) {
assert(hour == null || (hour >= 0 && hour < _kHoursPerDay));
assert(minute == null || (minute >= 0 && minute < _kMinutesPerHour));
return new TimeOfDay(hour: hour ?? this.hour, minute: minute ?? this.minute); return new TimeOfDay(hour: hour ?? this.hour, minute: minute ?? this.minute);
} }
...@@ -26,6 +43,39 @@ class TimeOfDay { ...@@ -26,6 +43,39 @@ class TimeOfDay {
/// The selected minute. /// The selected minute.
final int minute; final int minute;
/// Whether this time of day is before or after noon.
DayPeriod get period => hour < _kHoursPerPeriod ? DayPeriod.am : DayPeriod.pm;
/// Which hour of the current period (e.g., am or pm) this time is.
int get hourOfPeriod => hour - periodOffset;
String _addLeadingZeroIfNeeded(int value) {
if (value < 10)
return '0$value';
return value.toString();
}
/// A string representing the hour, in 24 hour time (e.g., '04' or '18').
String get hourLabel => _addLeadingZeroIfNeeded(hour);
/// A string representing the minute (e.g., '07').
String get minuteLabel => _addLeadingZeroIfNeeded(minute);
/// A string representing the hour of the current period (e.g., '4' or '6').
String get hourOfPeriodLabel {
// TODO(ianh): Localize.
final int hourOfPeriod = this.hourOfPeriod;
if (hourOfPeriod == 0)
return '12';
return hourOfPeriod.toString();
}
/// A string representing the current period (e.g., 'a.m.').
String get periodLabel => period == DayPeriod.am ? 'a.m.' : 'p.m.'; // TODO(ianh): Localize.
/// The hour at which the current period starts.
int get periodOffset => period == DayPeriod.am ? 0 : _kHoursPerPeriod;
bool operator ==(dynamic other) { bool operator ==(dynamic other) {
if (other is! TimeOfDay) if (other is! TimeOfDay)
return false; return false;
...@@ -41,7 +91,8 @@ class TimeOfDay { ...@@ -41,7 +91,8 @@ class TimeOfDay {
return value; return value;
} }
String toString() => 'TimeOfDay(hour: $hour, minute: $minute)'; // TODO(ianh): Localize.
String toString() => '$hourOfPeriodLabel:$minuteLabel $periodLabel';
} }
enum _TimePickerMode { hour, minute } enum _TimePickerMode { hour, minute }
...@@ -74,7 +125,8 @@ class _TimePickerState extends State<TimePicker> { ...@@ -74,7 +125,8 @@ class _TimePickerState extends State<TimePicker> {
Widget header = new _TimePickerHeader( Widget header = new _TimePickerHeader(
selectedTime: config.selectedTime, selectedTime: config.selectedTime,
mode: _mode, mode: _mode,
onModeChanged: _handleModeChanged onModeChanged: _handleModeChanged,
onChanged: config.onChanged
); );
return new Column(<Widget>[ return new Column(<Widget>[
header, header,
...@@ -93,9 +145,14 @@ class _TimePickerState extends State<TimePicker> { ...@@ -93,9 +145,14 @@ class _TimePickerState extends State<TimePicker> {
} }
} }
// Shows the selected date in large font and toggles between year and day mode // TODO(ianh): Localize!
class _TimePickerHeader extends StatelessComponent { class _TimePickerHeader extends StatelessComponent {
_TimePickerHeader({ this.selectedTime, this.mode, this.onModeChanged }) { _TimePickerHeader({
this.selectedTime,
this.mode,
this.onModeChanged,
this.onChanged
}) {
assert(selectedTime != null); assert(selectedTime != null);
assert(mode != null); assert(mode != null);
} }
...@@ -103,12 +160,18 @@ class _TimePickerHeader extends StatelessComponent { ...@@ -103,12 +160,18 @@ class _TimePickerHeader extends StatelessComponent {
final TimeOfDay selectedTime; final TimeOfDay selectedTime;
final _TimePickerMode mode; final _TimePickerMode mode;
final ValueChanged<_TimePickerMode> onModeChanged; final ValueChanged<_TimePickerMode> onModeChanged;
final ValueChanged<TimeOfDay> onChanged;
void _handleChangeMode(_TimePickerMode value) { void _handleChangeMode(_TimePickerMode value) {
if (value != mode) if (value != mode)
onModeChanged(value); onModeChanged(value);
} }
void _handleChangeDayPeriod() {
int newHour = (selectedTime.hour + _kHoursPerPeriod) % _kHoursPerDay;
onChanged(selectedTime.replacing(hour: newHour));
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
ThemeData theme = Theme.of(context); ThemeData theme = Theme.of(context);
TextTheme headerTheme = theme.primaryTextTheme; TextTheme headerTheme = theme.primaryTextTheme;
...@@ -131,27 +194,45 @@ class _TimePickerHeader extends StatelessComponent { ...@@ -131,27 +194,45 @@ class _TimePickerHeader extends StatelessComponent {
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(
color: selectedTime.period == DayPeriod.am ? activeColor: inactiveColor
);
TextStyle pmStyle = headerTheme.subhead.copyWith(
color: selectedTime.period == DayPeriod.pm ? activeColor: inactiveColor
);
return new Container( return new Container(
padding: new EdgeDims.all(10.0), padding: kDialogHeadingPadding,
decoration: new BoxDecoration(backgroundColor: theme.primaryColor), decoration: new BoxDecoration(backgroundColor: theme.primaryColor),
child: new Row(<Widget>[ child: new Row(<Widget>[
new GestureDetector( new GestureDetector(
onTap: () => _handleChangeMode(_TimePickerMode.hour), onTap: () => _handleChangeMode(_TimePickerMode.hour),
child: new Text(selectedTime.hour.toString(), style: hourStyle) child: new Text(selectedTime.hourOfPeriodLabel, style: hourStyle)
), ),
new Text(':', style: inactiveStyle), new Text(':', style: inactiveStyle),
new GestureDetector( new GestureDetector(
onTap: () => _handleChangeMode(_TimePickerMode.minute), onTap: () => _handleChangeMode(_TimePickerMode.minute),
child: new Text(selectedTime.minute.toString(), style: minuteStyle) child: new Text(selectedTime.minuteLabel, style: minuteStyle)
), ),
new GestureDetector(
onTap: _handleChangeDayPeriod,
behavior: HitTestBehavior.opaque,
child: new Container(
padding: const EdgeDims.only(left: 16.0, right: 24.0),
child: new Column([
new Text('AM', style: amStyle),
new Container(
padding: const EdgeDims.only(top: 4.0),
child: new Text('PM', style: pmStyle)
),
], justifyContent: FlexJustifyContent.end)
)
)
], justifyContent: FlexJustifyContent.end) ], justifyContent: FlexJustifyContent.end)
); );
} }
} }
final List<TextPainter> _kHours = _initHours();
final List<TextPainter> _kMinutes = _initMinutes();
List<TextPainter> _initPainters(List<String> labels) { List<TextPainter> _initPainters(List<String> labels) {
TextStyle style = Typography.black.subhead.copyWith(height: 1.0); TextStyle style = Typography.black.subhead.copyWith(height: 1.0);
List<TextPainter> painters = new List<TextPainter>(labels.length); List<TextPainter> painters = new List<TextPainter>(labels.length);
...@@ -215,7 +296,7 @@ class _DialPainter extends CustomPainter { ...@@ -215,7 +296,7 @@ class _DialPainter extends CustomPainter {
primaryPaint.strokeWidth = 2.0; primaryPaint.strokeWidth = 2.0;
canvas.drawLine(centerPoint, currentPoint, primaryPaint); canvas.drawLine(centerPoint, currentPoint, primaryPaint);
double labelThetaIncrement = -2 * math.PI / _kHours.length; double labelThetaIncrement = -_kTwoPi / labels.length;
double labelTheta = math.PI / 2.0; double labelTheta = math.PI / 2.0;
for (TextPainter label in labels) { for (TextPainter label in labels) {
...@@ -249,33 +330,54 @@ class _Dial extends StatefulComponent { ...@@ -249,33 +330,54 @@ class _Dial extends StatefulComponent {
} }
class _DialState extends State<_Dial> { class _DialState extends State<_Dial> {
double _theta;
void initState() { void initState() {
super.initState(); super.initState();
_theta = _getThetaForTime(config.selectedTime); _theta = new ValuePerformance(
variable: new AnimatedValue<double>(_getThetaForTime(config.selectedTime), curve: Curves.ease),
duration: _kDialAnimateDuration
)..addListener(() => setState(() { }));
} }
void didUpdateConfig(_Dial oldConfig) { void didUpdateConfig(_Dial oldConfig) {
if (config.mode != oldConfig.mode) if (config.mode != oldConfig.mode && !_dragging)
_theta = _getThetaForTime(config.selectedTime); _animateTo(_getThetaForTime(config.selectedTime));
}
ValuePerformance<double> _theta;
bool _dragging = false;
static double _nearest(double target, double a, double b) {
return ((target - a).abs() < (target - b).abs()) ? a : b;
}
void _animateTo(double targetTheta) {
double currentTheta = _theta.value;
double beginTheta = _nearest(targetTheta, currentTheta, currentTheta + _kTwoPi);
beginTheta = _nearest(targetTheta, beginTheta, currentTheta - _kTwoPi);
_theta
..variable.begin = beginTheta
..variable.end = targetTheta
..progress = 0.0
..play();
} }
double _getThetaForTime(TimeOfDay time) { double _getThetaForTime(TimeOfDay time) {
double fraction = (config.mode == _TimePickerMode.hour) ? double fraction = (config.mode == _TimePickerMode.hour) ?
(time.hour / 12) % 12 : (time.minute / 60) % 60; (time.hour / _kHoursPerPeriod) % _kHoursPerPeriod :
return math.PI / 2.0 - fraction * 2 * math.PI; (time.minute / _kMinutesPerHour) % _kMinutesPerHour;
return (math.PI / 2.0 - fraction * _kTwoPi) % _kTwoPi;
} }
TimeOfDay _getTimeForTheta(double theta) { TimeOfDay _getTimeForTheta(double theta) {
double fraction = (0.25 - (theta % (2 * math.PI)) / (2 * math.PI)) % 1.0; double fraction = (0.25 - (theta % _kTwoPi) / _kTwoPi) % 1.0;
if (config.mode == _TimePickerMode.hour) { if (config.mode == _TimePickerMode.hour) {
int hourOfPeriod = (fraction * _kHoursPerPeriod).round() % _kHoursPerPeriod;
return config.selectedTime.replacing( return config.selectedTime.replacing(
hour: (fraction * 12).round() hour: hourOfPeriod + config.selectedTime.periodOffset
); );
} else { } else {
return config.selectedTime.replacing( return config.selectedTime.replacing(
minute: (fraction * 60).round() minute: (fraction * _kMinutesPerHour).round() % _kMinutesPerHour
); );
} }
} }
...@@ -283,7 +385,7 @@ class _DialState extends State<_Dial> { ...@@ -283,7 +385,7 @@ class _DialState extends State<_Dial> {
void _notifyOnChangedIfNeeded() { void _notifyOnChangedIfNeeded() {
if (config.onChanged == null) if (config.onChanged == null)
return; return;
TimeOfDay current = _getTimeForTheta(_theta); TimeOfDay current = _getTimeForTheta(_theta.value);
if (current != config.selectedTime) if (current != config.selectedTime)
config.onChanged(current); config.onChanged(current);
} }
...@@ -291,7 +393,7 @@ class _DialState extends State<_Dial> { ...@@ -291,7 +393,7 @@ class _DialState extends State<_Dial> {
void _updateThetaForPan() { void _updateThetaForPan() {
setState(() { setState(() {
Offset offset = _position - _center; Offset offset = _position - _center;
_theta = (math.atan2(offset.dx, offset.dy) - math.PI / 2.0) % (2 * math.PI); _theta.variable.value = (math.atan2(offset.dx, offset.dy) - math.PI / 2.0) % _kTwoPi;
}); });
} }
...@@ -299,6 +401,8 @@ class _DialState extends State<_Dial> { ...@@ -299,6 +401,8 @@ class _DialState extends State<_Dial> {
Point _center; Point _center;
void _handlePanStart(Point globalPosition) { void _handlePanStart(Point globalPosition) {
assert(!_dragging);
_dragging = true;
RenderBox box = context.findRenderObject(); RenderBox box = context.findRenderObject();
_position = box.globalToLocal(globalPosition); _position = box.globalToLocal(globalPosition);
double radius = box.size.shortestSide / 2.0; double radius = box.size.shortestSide / 2.0;
...@@ -314,14 +418,16 @@ class _DialState extends State<_Dial> { ...@@ -314,14 +418,16 @@ class _DialState extends State<_Dial> {
} }
void _handlePanEnd(Offset velocity) { void _handlePanEnd(Offset velocity) {
assert(_dragging);
_dragging = false;
_position = null; _position = null;
_center = null; _center = null;
setState(() { _animateTo(_getThetaForTime(config.selectedTime));
// TODO(abarth): Animate to the final value.
_theta = _getThetaForTime(config.selectedTime);
});
} }
final List<TextPainter> _hours = _initHours();
final List<TextPainter> _minutes = _initMinutes();
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new GestureDetector( return new GestureDetector(
onPanStart: _handlePanStart, onPanStart: _handlePanStart,
...@@ -329,9 +435,9 @@ class _DialState extends State<_Dial> { ...@@ -329,9 +435,9 @@ class _DialState extends State<_Dial> {
onPanEnd: _handlePanEnd, onPanEnd: _handlePanEnd,
child: new CustomPaint( child: new CustomPaint(
painter: new _DialPainter( painter: new _DialPainter(
labels: config.mode == _TimePickerMode.hour ? _kHours : _kMinutes, labels: config.mode == _TimePickerMode.hour ? _hours : _minutes,
primaryColor: Theme.of(context).primaryColor, primaryColor: Theme.of(context).primaryColor,
theta: _theta 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