time_picker.dart 58 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6 7
import 'dart:math' as math;

8
import 'package:flutter/rendering.dart';
9
import 'package:flutter/services.dart';
10 11
import 'package:flutter/widgets.dart';

12
import 'button_bar.dart';
13
import 'colors.dart';
14
import 'debug.dart';
15
import 'dialog.dart';
16
import 'feedback.dart';
17
import 'flat_button.dart';
18 19
import 'ink_well.dart';
import 'material.dart';
20
import 'material_localizations.dart';
21
import 'text_theme.dart';
22
import 'theme.dart';
23
import 'theme_data.dart';
24
import 'time.dart';
25

26 27 28
// Examples can assume:
// BuildContext context;

29
const Duration _kDialAnimateDuration = Duration(milliseconds: 200);
30
const double _kTwoPi = 2 * math.pi;
31
const Duration _kVibrateCommitDelay = Duration(milliseconds: 100);
Adam Barth's avatar
Adam Barth committed
32

33 34
enum _TimePickerMode { hour, minute }

35 36
const double _kTimePickerHeaderPortraitHeight = 96.0;
const double _kTimePickerHeaderLandscapeWidth = 168.0;
37

38

39
const double _kTimePickerWidthPortrait = 328.0;
40
const double _kTimePickerWidthLandscape = 512.0;
41

42 43 44 45 46
const double _kTimePickerHeightPortrait = 496.0;
const double _kTimePickerHeightLandscape = 316.0;

const double _kTimePickerHeightPortraitCollapsed = 484.0;
const double _kTimePickerHeightLandscapeCollapsed = 304.0;
47

48
const BoxConstraints _kMinTappableRegion = BoxConstraints(minWidth: 48, minHeight: 48);
Yegor's avatar
Yegor committed
49

50 51 52 53 54
enum _TimePickerHeaderId {
  hour,
  colon,
  minute,
  period, // AM/PM picker
Yegor's avatar
Yegor committed
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
  dot,
  hString, // French Canadian "h" literal
}

/// Provides properties for rendering time picker header fragments.
@immutable
class _TimePickerFragmentContext {
  const _TimePickerFragmentContext({
    @required this.headerTextTheme,
    @required this.textDirection,
    @required this.selectedTime,
    @required this.mode,
    @required this.activeColor,
    @required this.activeStyle,
    @required this.inactiveColor,
    @required this.inactiveStyle,
    @required this.onTimeChange,
    @required this.onModeChange,
73
    @required this.targetPlatform,
74
    @required this.use24HourDials,
Yegor's avatar
Yegor committed
75 76 77 78 79 80 81 82 83
  }) : assert(headerTextTheme != null),
       assert(textDirection != null),
       assert(selectedTime != null),
       assert(mode != null),
       assert(activeColor != null),
       assert(activeStyle != null),
       assert(inactiveColor != null),
       assert(inactiveStyle != null),
       assert(onTimeChange != null),
84
       assert(onModeChange != null),
85 86
       assert(targetPlatform != null),
       assert(use24HourDials != null);
Yegor's avatar
Yegor committed
87 88 89 90 91 92 93 94 95 96 97

  final TextTheme headerTextTheme;
  final TextDirection textDirection;
  final TimeOfDay selectedTime;
  final _TimePickerMode mode;
  final Color activeColor;
  final TextStyle activeStyle;
  final Color inactiveColor;
  final TextStyle inactiveStyle;
  final ValueChanged<TimeOfDay> onTimeChange;
  final ValueChanged<_TimePickerMode> onModeChange;
98
  final TargetPlatform targetPlatform;
99
  final bool use24HourDials;
Yegor's avatar
Yegor committed
100 101 102 103 104 105 106 107 108
}

/// Contains the [widget] and layout properties of an atom of time information,
/// such as am/pm indicator, hour, minute and string literals appearing in the
/// formatted time string.
class _TimePickerHeaderFragment {
  const _TimePickerHeaderFragment({
    @required this.layoutId,
    @required this.widget,
109
    this.startMargin = 0.0,
Yegor's avatar
Yegor committed
110
  }) : assert(layoutId != null),
111 112
       assert(widget != null),
       assert(startMargin != null);
Yegor's avatar
Yegor committed
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137

  /// Identifier used by the custom layout to refer to the widget.
  final _TimePickerHeaderId layoutId;

  /// The widget that renders a piece of time information.
  final Widget widget;

  /// Horizontal distance from the fragment appearing at the start of this
  /// fragment.
  ///
  /// This value contributes to the total horizontal width of all fragments
  /// appearing on the same line, unless it is the first fragment on the line,
  /// in which case this value is ignored.
  final double startMargin;
}

/// An unbreakable part of the time picker header.
///
/// When the picker is laid out vertically, [fragments] of the piece are laid
/// out on the same line, with each piece getting its own line.
class _TimePickerHeaderPiece {
  /// Creates a time picker header piece.
  ///
  /// All arguments must be non-null. If the piece does not contain a pivot
  /// fragment, use the value -1 as a convention.
138
  const _TimePickerHeaderPiece(this.pivotIndex, this.fragments, { this.bottomMargin = 0.0 })
139 140 141
    : assert(pivotIndex != null),
      assert(fragments != null),
      assert(bottomMargin != null);
Yegor's avatar
Yegor committed
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168

  /// Index into the [fragments] list, pointing at the fragment that's centered
  /// horizontally.
  final int pivotIndex;

  /// Fragments this piece is made of.
  final List<_TimePickerHeaderFragment> fragments;

  /// Vertical distance between this piece and the next piece.
  ///
  /// This property applies only when the header is laid out vertically.
  final double bottomMargin;
}

/// Describes how the time picker header must be formatted.
///
/// A [_TimePickerHeaderFormat] is made of multiple [_TimePickerHeaderPiece]s.
/// A piece is made of multiple [_TimePickerHeaderFragment]s. A fragment has a
/// widget used to render some time information and contains some layout
/// properties.
///
/// ## Layout rules
///
/// Pieces are laid out such that all fragments inside the same piece are laid
/// out horizontally. Pieces are laid out horizontally if portrait orientation,
/// and vertically in landscape orientation.
///
169
/// One of the pieces is identified as a _centerpiece_. It is a piece that is
Yegor's avatar
Yegor committed
170 171 172
/// positioned in the center of the header, with all other pieces positioned
/// to the left or right of it.
class _TimePickerHeaderFormat {
173 174
  const _TimePickerHeaderFormat(this.centerpieceIndex, this.pieces)
    : assert(centerpieceIndex != null),
175
      assert(pieces != null);
Yegor's avatar
Yegor committed
176 177 178

  /// Index into the [pieces] list pointing at the piece that contains the
  /// pivot fragment.
179
  final int centerpieceIndex;
Yegor's avatar
Yegor committed
180 181 182 183 184 185 186 187 188 189

  /// Pieces that constitute a time picker header.
  final List<_TimePickerHeaderPiece> pieces;
}

/// Displays the am/pm fragment and provides controls for switching between am
/// and pm.
class _DayPeriodControl extends StatelessWidget {
  const _DayPeriodControl({
    @required this.fragmentContext,
190
    @required this.orientation,
Yegor's avatar
Yegor committed
191 192 193
  });

  final _TimePickerFragmentContext fragmentContext;
194
  final Orientation orientation;
Yegor's avatar
Yegor committed
195

196
  void _togglePeriod() {
197
    final int newHour = (fragmentContext.selectedTime.hour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
198 199 200 201 202 203 204 205
    final TimeOfDay newTime = fragmentContext.selectedTime.replacing(hour: newHour);
    fragmentContext.onTimeChange(newTime);
  }

  void _setAm(BuildContext context) {
    if (fragmentContext.selectedTime.period == DayPeriod.am) {
      return;
    }
206 207 208 209 210 211 212
    switch (fragmentContext.targetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        _announceToAccessibility(context, MaterialLocalizations.of(context).anteMeridiemAbbreviation);
        break;
      case TargetPlatform.iOS:
        break;
213 214 215 216 217 218 219 220
    }
    _togglePeriod();
  }

  void _setPm(BuildContext context) {
    if (fragmentContext.selectedTime.period == DayPeriod.pm) {
      return;
    }
221 222 223 224 225 226 227
    switch (fragmentContext.targetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        _announceToAccessibility(context, MaterialLocalizations.of(context).postMeridiemAbbreviation);
        break;
      case TargetPlatform.iOS:
        break;
228 229
    }
    _togglePeriod();
Yegor's avatar
Yegor committed
230 231 232 233 234 235 236 237 238
  }

  @override
  Widget build(BuildContext context) {
    final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(context);
    final TextTheme headerTextTheme = fragmentContext.headerTextTheme;
    final TimeOfDay selectedTime = fragmentContext.selectedTime;
    final Color activeColor = fragmentContext.activeColor;
    final Color inactiveColor = fragmentContext.inactiveColor;
239
    final bool amSelected = selectedTime.period == DayPeriod.am;
Yegor's avatar
Yegor committed
240
    final TextStyle amStyle = headerTextTheme.subhead.copyWith(
241
      color: amSelected ? activeColor: inactiveColor
Yegor's avatar
Yegor committed
242 243
    );
    final TextStyle pmStyle = headerTextTheme.subhead.copyWith(
244
      color: !amSelected ? activeColor: inactiveColor
Yegor's avatar
Yegor committed
245
    );
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
    final bool layoutPortrait = orientation == Orientation.portrait;

    final Widget amButton = ConstrainedBox(
      constraints: _kMinTappableRegion,
      child: Material(
        type: MaterialType.transparency,
        child: InkWell(
          onTap: Feedback.wrapForTap(() => _setAm(context), context),
          child: Padding(
            padding: layoutPortrait ? const EdgeInsets.only(bottom: 2.0) : const EdgeInsets.only(right: 4.0),
            child: Align(
              alignment: layoutPortrait ? Alignment.bottomCenter : Alignment.centerRight,
              widthFactor: 1,
              heightFactor: 1,
              child: Semantics(
                selected: amSelected,
                child: Text(materialLocalizations.anteMeridiemAbbreviation, style: amStyle)
              ),
            ),
265 266
          ),
        ),
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
      ),
    );

    final Widget pmButton = ConstrainedBox(
      constraints: _kMinTappableRegion,
      child: Material(
        type: MaterialType.transparency,
        textStyle: pmStyle,
        child: InkWell(
          onTap: Feedback.wrapForTap(() => _setPm(context), context),
          child: Padding(
            padding: layoutPortrait ? const EdgeInsets.only(top: 2.0) : const EdgeInsets.only(left: 4.0),
            child: Align(
              alignment: orientation == Orientation.portrait ? Alignment.topCenter : Alignment.centerLeft,
              widthFactor: 1,
              heightFactor: 1,
              child: Semantics(
                selected: !amSelected,
                child: Text(materialLocalizations.postMeridiemAbbreviation, style: pmStyle),
              ),
            ),
288 289
          ),
        ),
290
      ),
Yegor's avatar
Yegor committed
291
    );
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312

    switch (orientation) {
      case Orientation.portrait:
        return Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            amButton,
            pmButton,
          ],
        );

      case Orientation.landscape:
        return Row(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            amButton,
            pmButton,
          ],
        );
    }
    return null;
Yegor's avatar
Yegor committed
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
  }
}

/// Displays the hour fragment.
///
/// When tapped changes time picker dial mode to [_TimePickerMode.hour].
class _HourControl extends StatelessWidget {
  const _HourControl({
    @required this.fragmentContext,
  });

  final _TimePickerFragmentContext fragmentContext;

  @override
  Widget build(BuildContext context) {
328
    assert(debugCheckHasMediaQuery(context));
329
    final bool alwaysUse24HourFormat = MediaQuery.of(context).alwaysUse24HourFormat;
330
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
Yegor's avatar
Yegor committed
331 332 333
    final TextStyle hourStyle = fragmentContext.mode == _TimePickerMode.hour
        ? fragmentContext.activeStyle
        : fragmentContext.inactiveStyle;
334 335
    final String formattedHour = localizations.formatHour(
      fragmentContext.selectedTime,
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
      alwaysUse24HourFormat: alwaysUse24HourFormat,
    );

    TimeOfDay hoursFromSelected(int hoursToAdd) {
      if (fragmentContext.use24HourDials) {
        final int selectedHour = fragmentContext.selectedTime.hour;
        return fragmentContext.selectedTime.replacing(
          hour: (selectedHour + hoursToAdd) % TimeOfDay.hoursPerDay,
        );
      } else {
        // Cycle 1 through 12 without changing day period.
        final int periodOffset = fragmentContext.selectedTime.periodOffset;
        final int hours = fragmentContext.selectedTime.hourOfPeriod;
        return fragmentContext.selectedTime.replacing(
          hour: periodOffset + (hours + hoursToAdd) % TimeOfDay.hoursPerPeriod,
        );
      }
    }

    final TimeOfDay nextHour = hoursFromSelected(1);
    final String formattedNextHour = localizations.formatHour(
      nextHour,
      alwaysUse24HourFormat: alwaysUse24HourFormat,
    );
    final TimeOfDay previousHour = hoursFromSelected(-1);
    final String formattedPreviousHour = localizations.formatHour(
      previousHour,
      alwaysUse24HourFormat: alwaysUse24HourFormat,
364
    );
Yegor's avatar
Yegor committed
365

366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
    return Semantics(
      hint: localizations.timePickerHourModeAnnouncement,
      value: formattedHour,
      excludeSemantics: true,
      increasedValue: formattedNextHour,
      onIncrease: () {
        fragmentContext.onTimeChange(nextHour);
      },
      decreasedValue: formattedPreviousHour,
      onDecrease: () {
        fragmentContext.onTimeChange(previousHour);
      },
      child: ConstrainedBox(
        constraints: _kMinTappableRegion,
        child: Material(
          type: MaterialType.transparency,
          child: InkWell(
            onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context),
            child: Text(formattedHour, style: hourStyle, textAlign: TextAlign.end),
          ),
386
        ),
387
      ),
Yegor's avatar
Yegor committed
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
    );
  }
}

/// A passive fragment showing a string value.
class _StringFragment extends StatelessWidget {
  const _StringFragment({
    @required this.fragmentContext,
    @required this.value,
  });

  final _TimePickerFragmentContext fragmentContext;
  final String value;

  @override
  Widget build(BuildContext context) {
404 405
    return ExcludeSemantics(
      child: Text(value, style: fragmentContext.inactiveStyle),
406
    );
Yegor's avatar
Yegor committed
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
  }
}

/// Displays the minute fragment.
///
/// When tapped changes time picker dial mode to [_TimePickerMode.minute].
class _MinuteControl extends StatelessWidget {
  const _MinuteControl({
    @required this.fragmentContext,
  });

  final _TimePickerFragmentContext fragmentContext;

  @override
  Widget build(BuildContext context) {
422
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
Yegor's avatar
Yegor committed
423 424 425
    final TextStyle minuteStyle = fragmentContext.mode == _TimePickerMode.minute
        ? fragmentContext.activeStyle
        : fragmentContext.inactiveStyle;
426 427 428 429 430 431 432 433 434
    final String formattedMinute = localizations.formatMinute(fragmentContext.selectedTime);
    final TimeOfDay nextMinute = fragmentContext.selectedTime.replacing(
      minute: (fragmentContext.selectedTime.minute + 1) % TimeOfDay.minutesPerHour,
    );
    final String formattedNextMinute = localizations.formatMinute(nextMinute);
    final TimeOfDay previousMinute = fragmentContext.selectedTime.replacing(
      minute: (fragmentContext.selectedTime.minute - 1) % TimeOfDay.minutesPerHour,
    );
    final String formattedPreviousMinute = localizations.formatMinute(previousMinute);
Yegor's avatar
Yegor committed
435

436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
    return Semantics(
      excludeSemantics: true,
      hint: localizations.timePickerMinuteModeAnnouncement,
      value: formattedMinute,
      increasedValue: formattedNextMinute,
      onIncrease: () {
        fragmentContext.onTimeChange(nextMinute);
      },
      decreasedValue: formattedPreviousMinute,
      onDecrease: () {
        fragmentContext.onTimeChange(previousMinute);
      },
      child: ConstrainedBox(
        constraints: _kMinTappableRegion,
        child: Material(
          type: MaterialType.transparency,
          child: InkWell(
            onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context),
            child: Text(formattedMinute, style: minuteStyle, textAlign: TextAlign.start),
          ),
456
        ),
457
      ),
Yegor's avatar
Yegor committed
458 459 460 461 462
    );
  }
}

/// Provides time picker header layout configuration for the given
463 464
/// [timeOfDayFormat] passing [context] to each widget in the
/// configuration.
Yegor's avatar
Yegor committed
465
///
466
/// The [timeOfDayFormat] and [context] arguments must not be null.
467 468 469 470 471 472
_TimePickerHeaderFormat _buildHeaderFormat(
  TimeOfDayFormat timeOfDayFormat,
  _TimePickerFragmentContext context,
  Orientation orientation
) {

Yegor's avatar
Yegor committed
473
  // Creates an hour fragment.
474
  _TimePickerHeaderFragment hour() {
475
    return _TimePickerHeaderFragment(
Yegor's avatar
Yegor committed
476
      layoutId: _TimePickerHeaderId.hour,
477
      widget: _HourControl(fragmentContext: context),
Yegor's avatar
Yegor committed
478 479 480 481 482
    );
  }

  // Creates a minute fragment.
  _TimePickerHeaderFragment minute() {
483
    return _TimePickerHeaderFragment(
Yegor's avatar
Yegor committed
484
      layoutId: _TimePickerHeaderId.minute,
485
      widget: _MinuteControl(fragmentContext: context),
Yegor's avatar
Yegor committed
486 487 488 489 490
    );
  }

  // Creates a string fragment.
  _TimePickerHeaderFragment string(_TimePickerHeaderId layoutId, String value) {
491
    return _TimePickerHeaderFragment(
Yegor's avatar
Yegor committed
492
      layoutId: layoutId,
493
      widget: _StringFragment(
Yegor's avatar
Yegor committed
494 495 496 497 498 499 500 501
        fragmentContext: context,
        value: value,
      ),
    );
  }

  // Creates an am/pm fragment.
  _TimePickerHeaderFragment dayPeriod() {
502
    return _TimePickerHeaderFragment(
Yegor's avatar
Yegor committed
503
      layoutId: _TimePickerHeaderId.period,
504
      widget: _DayPeriodControl(fragmentContext: context, orientation: orientation),
Yegor's avatar
Yegor committed
505 506 507 508
    );
  }

  // Convenience function for creating a time header format with up to two pieces.
509 510 511 512
  _TimePickerHeaderFormat format(
    _TimePickerHeaderPiece piece1, [
    _TimePickerHeaderPiece piece2,
  ]) {
Yegor's avatar
Yegor committed
513 514 515 516 517 518 519 520 521 522 523 524 525
    final List<_TimePickerHeaderPiece> pieces = <_TimePickerHeaderPiece>[];
    switch (context.textDirection) {
      case TextDirection.ltr:
        pieces.add(piece1);
        if (piece2 != null)
          pieces.add(piece2);
        break;
      case TextDirection.rtl:
        if (piece2 != null)
          pieces.add(piece2);
        pieces.add(piece1);
        break;
    }
526
    int centerpieceIndex;
527 528
    for (int i = 0; i < pieces.length; i += 1) {
      if (pieces[i].pivotIndex >= 0) {
529
        centerpieceIndex = i;
530 531
      }
    }
532 533
    assert(centerpieceIndex != null);
    return _TimePickerHeaderFormat(centerpieceIndex, pieces);
Yegor's avatar
Yegor committed
534 535 536
  }

  // Convenience function for creating a time header piece with up to three fragments.
537 538 539 540 541 542 543
  _TimePickerHeaderPiece piece({
    int pivotIndex = -1,
    double bottomMargin = 0.0,
    _TimePickerHeaderFragment fragment1,
    _TimePickerHeaderFragment fragment2,
    _TimePickerHeaderFragment fragment3,
  }) {
Yegor's avatar
Yegor committed
544 545 546 547 548 549
    final List<_TimePickerHeaderFragment> fragments = <_TimePickerHeaderFragment>[fragment1];
    if (fragment2 != null) {
      fragments.add(fragment2);
      if (fragment3 != null)
        fragments.add(fragment3);
    }
550
    return _TimePickerHeaderPiece(pivotIndex, fragments, bottomMargin: bottomMargin);
Yegor's avatar
Yegor committed
551 552 553 554 555 556 557
  }

  switch (timeOfDayFormat) {
    case TimeOfDayFormat.h_colon_mm_space_a:
      return format(
        piece(
          pivotIndex: 1,
558
          fragment1: hour(),
Yegor's avatar
Yegor committed
559 560 561 562 563 564 565 566
          fragment2: string(_TimePickerHeaderId.colon, ':'),
          fragment3: minute(),
        ),
        piece(
          fragment1: dayPeriod(),
        ),
      );
    case TimeOfDayFormat.H_colon_mm:
567
      return format(piece(
Yegor's avatar
Yegor committed
568
        pivotIndex: 1,
569
        fragment1: hour(),
Yegor's avatar
Yegor committed
570 571 572 573
        fragment2: string(_TimePickerHeaderId.colon, ':'),
        fragment3: minute(),
      ));
    case TimeOfDayFormat.HH_dot_mm:
574
      return format(piece(
Yegor's avatar
Yegor committed
575
        pivotIndex: 1,
576
        fragment1: hour(),
Yegor's avatar
Yegor committed
577 578 579 580 581 582 583 584 585 586
        fragment2: string(_TimePickerHeaderId.dot, '.'),
        fragment3: minute(),
      ));
    case TimeOfDayFormat.a_space_h_colon_mm:
      return format(
        piece(
          fragment1: dayPeriod(),
        ),
        piece(
          pivotIndex: 1,
587
          fragment1: hour(),
Yegor's avatar
Yegor committed
588 589 590 591 592
          fragment2: string(_TimePickerHeaderId.colon, ':'),
          fragment3: minute(),
        ),
      );
    case TimeOfDayFormat.frenchCanadian:
593
      return format(piece(
Yegor's avatar
Yegor committed
594
        pivotIndex: 1,
595
        fragment1: hour(),
Yegor's avatar
Yegor committed
596 597 598 599
        fragment2: string(_TimePickerHeaderId.hString, 'h'),
        fragment3: minute(),
      ));
    case TimeOfDayFormat.HH_colon_mm:
600
      return format(piece(
Yegor's avatar
Yegor committed
601
        pivotIndex: 1,
602
        fragment1: hour(),
Yegor's avatar
Yegor committed
603 604 605 606 607 608
        fragment2: string(_TimePickerHeaderId.colon, ':'),
        fragment3: minute(),
      ));
  }

  return null;
609 610 611
}

class _TimePickerHeaderLayout extends MultiChildLayoutDelegate {
Yegor's avatar
Yegor committed
612 613 614
  _TimePickerHeaderLayout(this.orientation, this.format)
    : assert(orientation != null),
      assert(format != null);
615 616

  final Orientation orientation;
Yegor's avatar
Yegor committed
617
  final _TimePickerHeaderFormat format;
618 619 620

  @override
  void performLayout(Size size) {
621
    final BoxConstraints constraints = BoxConstraints.loose(size);
622 623 624

    switch (orientation) {
      case Orientation.portrait:
Yegor's avatar
Yegor committed
625 626 627 628 629 630 631
        _layoutHorizontally(size, constraints);
        break;
      case Orientation.landscape:
        _layoutVertically(size, constraints);
        break;
    }
  }
632

Yegor's avatar
Yegor committed
633 634 635 636 637 638 639 640 641 642
  void _layoutHorizontally(Size size, BoxConstraints constraints) {
    final List<_TimePickerHeaderFragment> fragmentsFlattened = <_TimePickerHeaderFragment>[];
    final Map<_TimePickerHeaderId, Size> childSizes = <_TimePickerHeaderId, Size>{};
    int pivotIndex = 0;
    for (int pieceIndex = 0; pieceIndex < format.pieces.length; pieceIndex += 1) {
      final _TimePickerHeaderPiece piece = format.pieces[pieceIndex];
      for (final _TimePickerHeaderFragment fragment in piece.fragments) {
        childSizes[fragment.layoutId] = layoutChild(fragment.layoutId, constraints);
        fragmentsFlattened.add(fragment);
      }
643

644 645 646
      if (pieceIndex == format.centerpieceIndex)
        pivotIndex += format.pieces[format.centerpieceIndex].pivotIndex;
      else if (pieceIndex < format.centerpieceIndex)
Yegor's avatar
Yegor committed
647 648
        pivotIndex += piece.fragments.length;
    }
649

Yegor's avatar
Yegor committed
650 651
    _positionPivoted(size.width, size.height / 2.0, childSizes, fragmentsFlattened, pivotIndex);
  }
652

Yegor's avatar
Yegor committed
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669
  void _layoutVertically(Size size, BoxConstraints constraints) {
    final Map<_TimePickerHeaderId, Size> childSizes = <_TimePickerHeaderId, Size>{};
    final List<double> pieceHeights = <double>[];
    double height = 0.0;
    double margin = 0.0;
    for (final _TimePickerHeaderPiece piece in format.pieces) {
      double pieceHeight = 0.0;
      for (final _TimePickerHeaderFragment fragment in piece.fragments) {
        final Size childSize = childSizes[fragment.layoutId] = layoutChild(fragment.layoutId, constraints);
        pieceHeight = math.max(pieceHeight, childSize.height);
      }
      pieceHeights.add(pieceHeight);
      height += pieceHeight + margin;
      // Delay application of margin until next piece because margin of the
      // bottom-most piece should not contribute to the size.
      margin = piece.bottomMargin;
    }
670

671
    final _TimePickerHeaderPiece centerpiece = format.pieces[format.centerpieceIndex];
Yegor's avatar
Yegor committed
672 673
    double y = (size.height - height) / 2.0;
    for (int pieceIndex = 0; pieceIndex < format.pieces.length; pieceIndex += 1) {
674
      final double pieceVerticalCenter = y + pieceHeights[pieceIndex] / 2.0;
675
      if (pieceIndex != format.centerpieceIndex)
676
        _positionPiece(size.width, pieceVerticalCenter, childSizes, format.pieces[pieceIndex].fragments);
Yegor's avatar
Yegor committed
677
      else
678
        _positionPivoted(size.width, pieceVerticalCenter, childSizes, centerpiece.fragments, centerpiece.pivotIndex);
679

Yegor's avatar
Yegor committed
680 681 682
      y += pieceHeights[pieceIndex] + format.pieces[pieceIndex].bottomMargin;
    }
  }
683

Yegor's avatar
Yegor committed
684 685 686 687 688
  void _positionPivoted(double width, double y, Map<_TimePickerHeaderId, Size> childSizes, List<_TimePickerHeaderFragment> fragments, int pivotIndex) {
    double tailWidth = childSizes[fragments[pivotIndex].layoutId].width / 2.0;
    for (_TimePickerHeaderFragment fragment in fragments.skip(pivotIndex + 1)) {
      tailWidth += childSizes[fragment.layoutId].width + fragment.startMargin;
    }
689

Yegor's avatar
Yegor committed
690 691 692 693 694 695
    double x = width / 2.0 + tailWidth;
    x = math.min(x, width);
    for (int i = fragments.length - 1; i >= 0; i -= 1) {
      final _TimePickerHeaderFragment fragment = fragments[i];
      final Size childSize = childSizes[fragment.layoutId];
      x -= childSize.width;
696
      positionChild(fragment.layoutId, Offset(x, y - childSize.height / 2.0));
Yegor's avatar
Yegor committed
697 698 699
      x -= fragment.startMargin;
    }
  }
700

Yegor's avatar
Yegor committed
701 702 703 704 705 706 707 708 709 710 711 712 713 714 715
  void _positionPiece(double width, double centeredAroundY, Map<_TimePickerHeaderId, Size> childSizes, List<_TimePickerHeaderFragment> fragments) {
    double pieceWidth = 0.0;
    double nextMargin = 0.0;
    for (_TimePickerHeaderFragment fragment in fragments) {
      final Size childSize = childSizes[fragment.layoutId];
      pieceWidth += childSize.width + nextMargin;
      // Delay application of margin until next element because margin of the
      // left-most fragment should not contribute to the size.
      nextMargin = fragment.startMargin;
    }
    double x = (width + pieceWidth) / 2.0;
    for (int i = fragments.length - 1; i >= 0; i -= 1) {
      final _TimePickerHeaderFragment fragment = fragments[i];
      final Size childSize = childSizes[fragment.layoutId];
      x -= childSize.width;
716
      positionChild(fragment.layoutId, Offset(x, centeredAroundY - childSize.height / 2.0));
Yegor's avatar
Yegor committed
717
      x -= fragment.startMargin;
718 719 720 721
    }
  }

  @override
Yegor's avatar
Yegor committed
722
  bool shouldRelayout(_TimePickerHeaderLayout oldDelegate) => orientation != oldDelegate.orientation || format != oldDelegate.format;
723 724
}

725
class _TimePickerHeader extends StatelessWidget {
726
  const _TimePickerHeader({
727 728
    @required this.selectedTime,
    @required this.mode,
729
    @required this.orientation,
730
    @required this.onModeChanged,
731
    @required this.onChanged,
732
    @required this.use24HourDials,
733 734
  }) : assert(selectedTime != null),
       assert(mode != null),
735 736
       assert(orientation != null),
       assert(use24HourDials != null);
737

738 739
  final TimeOfDay selectedTime;
  final _TimePickerMode mode;
740
  final Orientation orientation;
741
  final ValueChanged<_TimePickerMode> onModeChanged;
Adam Barth's avatar
Adam Barth committed
742
  final ValueChanged<TimeOfDay> onChanged;
743
  final bool use24HourDials;
744 745 746 747 748 749

  void _handleChangeMode(_TimePickerMode value) {
    if (value != mode)
      onModeChanged(value);
  }

750 751 752 753 754 755 756 757 758 759 760 761 762 763
  TextStyle _getBaseHeaderStyle(TextTheme headerTextTheme) {
    // These font sizes aren't listed in the spec explicitly. I worked them out
    // by measuring the text using a screen ruler and comparing them to the
    // screen shots of the time picker in the spec.
    assert(orientation != null);
    switch (orientation) {
      case Orientation.portrait:
        return headerTextTheme.display3.copyWith(fontSize: 60.0);
      case Orientation.landscape:
        return headerTextTheme.display2.copyWith(fontSize: 50.0);
    }
    return null;
  }

764
  @override
765
  Widget build(BuildContext context) {
766
    assert(debugCheckHasMediaQuery(context));
767
    final ThemeData themeData = Theme.of(context);
768 769 770
    final MediaQueryData media = MediaQuery.of(context);
    final TimeOfDayFormat timeOfDayFormat = MaterialLocalizations.of(context)
        .timeOfDayFormat(alwaysUse24HourFormat: media.alwaysUse24HourFormat);
Yegor's avatar
Yegor committed
771 772 773 774 775 776 777 778 779 780

    EdgeInsets padding;
    double height;
    double width;

    assert(orientation != null);
    switch (orientation) {
      case Orientation.portrait:
        height = _kTimePickerHeaderPortraitHeight;
        padding = const EdgeInsets.symmetric(horizontal: 24.0);
781
        break;
Yegor's avatar
Yegor committed
782 783 784
      case Orientation.landscape:
        width = _kTimePickerHeaderLandscapeWidth;
        padding = const EdgeInsets.symmetric(horizontal: 16.0);
785 786
        break;
    }
787 788 789

    Color backgroundColor;
    switch (themeData.brightness) {
790
      case Brightness.light:
791 792
        backgroundColor = themeData.primaryColor;
        break;
793
      case Brightness.dark:
794 795 796 797
        backgroundColor = themeData.backgroundColor;
        break;
    }

Yegor's avatar
Yegor committed
798 799 800 801 802 803
    Color activeColor;
    Color inactiveColor;
    switch (themeData.primaryColorBrightness) {
      case Brightness.light:
        activeColor = Colors.black87;
        inactiveColor = Colors.black54;
804
        break;
Yegor's avatar
Yegor committed
805 806 807
      case Brightness.dark:
        activeColor = Colors.white;
        inactiveColor = Colors.white70;
808
        break;
809
    }
810

Yegor's avatar
Yegor committed
811 812
    final TextTheme headerTextTheme = themeData.primaryTextTheme;
    final TextStyle baseHeaderStyle = _getBaseHeaderStyle(headerTextTheme);
813
    final _TimePickerFragmentContext fragmentContext = _TimePickerFragmentContext(
Yegor's avatar
Yegor committed
814 815 816 817 818 819 820 821 822 823
      headerTextTheme: headerTextTheme,
      textDirection: Directionality.of(context),
      selectedTime: selectedTime,
      mode: mode,
      activeColor: activeColor,
      activeStyle: baseHeaderStyle.copyWith(color: activeColor),
      inactiveColor: inactiveColor,
      inactiveStyle: baseHeaderStyle.copyWith(color: inactiveColor),
      onTimeChange: onChanged,
      onModeChange: _handleChangeMode,
824
      targetPlatform: themeData.platform,
825
      use24HourDials: use24HourDials,
Yegor's avatar
Yegor committed
826 827
    );

828
    final _TimePickerHeaderFormat format = _buildHeaderFormat(timeOfDayFormat, fragmentContext, orientation);
Yegor's avatar
Yegor committed
829

830
    return Container(
831 832 833
      width: width,
      height: height,
      padding: padding,
834
      color: backgroundColor,
835 836
      child: CustomMultiChildLayout(
        delegate: _TimePickerHeaderLayout(orientation, format),
Yegor's avatar
Yegor committed
837 838 839
        children: format.pieces
          .expand<_TimePickerHeaderFragment>((_TimePickerHeaderPiece piece) => piece.fragments)
          .map<Widget>((_TimePickerHeaderFragment fragment) {
840
            return LayoutId(
Yegor's avatar
Yegor committed
841 842 843 844 845
              id: fragment.layoutId,
              child: fragment.widget,
            );
          })
          .toList(),
846
      ),
847
    );
848 849
  }
}
850

Yegor's avatar
Yegor committed
851 852 853 854 855
enum _DialRing {
  outer,
  inner,
}

856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872
class _TappableLabel {
  _TappableLabel({
    @required this.value,
    @required this.painter,
    @required this.onTap,
  });

  /// The value this label is displaying.
  final int value;

  /// Paints the text of the label.
  final TextPainter painter;

  /// Called when a tap gesture is detected on the label.
  final VoidCallback onTap;
}

873 874
class _DialPainter extends CustomPainter {
  const _DialPainter({
Yegor's avatar
Yegor committed
875 876 877 878 879 880 881 882
    @required this.primaryOuterLabels,
    @required this.primaryInnerLabels,
    @required this.secondaryOuterLabels,
    @required this.secondaryInnerLabels,
    @required this.backgroundColor,
    @required this.accentColor,
    @required this.theta,
    @required this.activeRing,
883 884
    @required this.textDirection,
    @required this.selectedValue,
885 886
  });

887 888 889 890
  final List<_TappableLabel> primaryOuterLabels;
  final List<_TappableLabel> primaryInnerLabels;
  final List<_TappableLabel> secondaryOuterLabels;
  final List<_TappableLabel> secondaryInnerLabels;
891 892
  final Color backgroundColor;
  final Color accentColor;
893
  final double theta;
Yegor's avatar
Yegor committed
894
  final _DialRing activeRing;
895 896
  final TextDirection textDirection;
  final int selectedValue;
897

898
  @override
899
  void paint(Canvas canvas, Size size) {
900
    final double radius = size.shortestSide / 2.0;
901
    final Offset center = Offset(size.width / 2.0, size.height / 2.0);
902
    final Offset centerPoint = center;
903
    canvas.drawCircle(centerPoint, radius, Paint()..color = backgroundColor);
904 905

    const double labelPadding = 24.0;
Yegor's avatar
Yegor committed
906 907 908 909 910 911 912 913 914 915 916 917
    final double outerLabelRadius = radius - labelPadding;
    final double innerLabelRadius = radius - labelPadding * 2.5;
    Offset getOffsetForTheta(double theta, _DialRing ring) {
      double labelRadius;
      switch (ring) {
        case _DialRing.outer:
          labelRadius = outerLabelRadius;
          break;
        case _DialRing.inner:
          labelRadius = innerLabelRadius;
          break;
      }
918
      return center + Offset(labelRadius * math.cos(theta),
919 920 921
                                 -labelRadius * math.sin(theta));
    }

922
    void paintLabels(List<_TappableLabel> labels, _DialRing ring) {
Yegor's avatar
Yegor committed
923 924
      if (labels == null)
        return;
925
      final double labelThetaIncrement = -_kTwoPi / labels.length;
926
      double labelTheta = math.pi / 2.0;
927

928 929
      for (_TappableLabel label in labels) {
        final TextPainter labelPainter = label.painter;
930
        final Offset labelOffset = Offset(-labelPainter.width / 2.0, -labelPainter.height / 2.0);
931
        labelPainter.paint(canvas, getOffsetForTheta(labelTheta, ring) + labelOffset);
932 933
        labelTheta += labelThetaIncrement;
      }
934
    }
935

Yegor's avatar
Yegor committed
936 937
    paintLabels(primaryOuterLabels, _DialRing.outer);
    paintLabels(primaryInnerLabels, _DialRing.inner);
938

939
    final Paint selectorPaint = Paint()
940
      ..color = accentColor;
Yegor's avatar
Yegor committed
941
    final Offset focusedPoint = getOffsetForTheta(theta, activeRing);
942
    const double focusedRadius = labelPadding - 4.0;
943 944 945 946 947
    canvas.drawCircle(centerPoint, 4.0, selectorPaint);
    canvas.drawCircle(focusedPoint, focusedRadius, selectorPaint);
    selectorPaint.strokeWidth = 2.0;
    canvas.drawLine(centerPoint, focusedPoint, selectorPaint);

948
    final Rect focusedRect = Rect.fromCircle(
949
      center: focusedPoint, radius: focusedRadius,
950 951
    );
    canvas
952
      ..save()
953
      ..clipPath(Path()..addOval(focusedRect));
Yegor's avatar
Yegor committed
954 955
    paintLabels(secondaryOuterLabels, _DialRing.outer);
    paintLabels(secondaryInnerLabels, _DialRing.inner);
956
    canvas.restore();
957 958
  }

959
  static const double _semanticNodeSizeScale = 1.5;
960 961 962 963 964 965 966

  @override
  SemanticsBuilderCallback get semanticsBuilder => _buildSemantics;

  /// Creates semantics nodes for the hour/minute labels painted on the dial.
  ///
  /// The nodes are positioned on top of the text and their size is
967
  /// [_semanticNodeSizeScale] bigger than those of the text boxes to provide
968 969 970
  /// bigger tap area.
  List<CustomPainterSemantics> _buildSemantics(Size size) {
    final double radius = size.shortestSide / 2.0;
971
    final Offset center = Offset(size.width / 2.0, size.height / 2.0);
972 973 974 975 976 977 978 979 980 981 982 983 984 985
    const double labelPadding = 24.0;
    final double outerLabelRadius = radius - labelPadding;
    final double innerLabelRadius = radius - labelPadding * 2.5;

    Offset getOffsetForTheta(double theta, _DialRing ring) {
      double labelRadius;
      switch (ring) {
        case _DialRing.outer:
          labelRadius = outerLabelRadius;
          break;
        case _DialRing.inner:
          labelRadius = innerLabelRadius;
          break;
      }
986
      return center + Offset(labelRadius * math.cos(theta),
987 988 989 990 991 992 993 994 995
          -labelRadius * math.sin(theta));
    }

    final List<CustomPainterSemantics> nodes = <CustomPainterSemantics>[];

    void paintLabels(List<_TappableLabel> labels, _DialRing ring) {
      if (labels == null)
        return;
      final double labelThetaIncrement = -_kTwoPi / labels.length;
996
      final double ordinalOffset = ring == _DialRing.inner ? 12.0 : 0.0;
997
      double labelTheta = math.pi / 2.0;
998

999 1000
      for (int i = 0; i < labels.length; i++) {
        final _TappableLabel label = labels[i];
1001
        final TextPainter labelPainter = label.painter;
1002 1003
        final double width = labelPainter.width * _semanticNodeSizeScale;
        final double height = labelPainter.height * _semanticNodeSizeScale;
1004
        final Offset nodeOffset = getOffsetForTheta(labelTheta, ring) + Offset(-width / 2.0, -height / 2.0);
1005
        final TextSpan textSpan = labelPainter.text;
1006 1007
        final CustomPainterSemantics node = CustomPainterSemantics(
          rect: Rect.fromLTRB(
1008 1009 1010 1011
            nodeOffset.dx - 24.0 + width / 2,
            nodeOffset.dy - 24.0 + height / 2,
            nodeOffset.dx + 24.0 + width / 2,
            nodeOffset.dy + 24.0 + height / 2,
1012
          ),
1013 1014
          properties: SemanticsProperties(
            sortKey: OrdinalSortKey(i.toDouble() + ordinalOffset),
1015
            selected: label.value == selectedValue,
1016
            value: textSpan?.text,
1017 1018 1019
            textDirection: textDirection,
            onTap: label.onTap,
          ),
1020
          tags: const <SemanticsTag>{
1021
            // Used by tests to find this node.
1022
            SemanticsTag('dial-label'),
1023
          },
1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035
        );
        nodes.add(node);
        labelTheta += labelThetaIncrement;
      }
    }

    paintLabels(primaryOuterLabels, _DialRing.outer);
    paintLabels(primaryInnerLabels, _DialRing.inner);

    return nodes;
  }

1036
  @override
1037
  bool shouldRepaint(_DialPainter oldPainter) {
Yegor's avatar
Yegor committed
1038 1039 1040 1041
    return oldPainter.primaryOuterLabels != primaryOuterLabels
        || oldPainter.primaryInnerLabels != primaryInnerLabels
        || oldPainter.secondaryOuterLabels != secondaryOuterLabels
        || oldPainter.secondaryInnerLabels != secondaryInnerLabels
1042 1043
        || oldPainter.backgroundColor != backgroundColor
        || oldPainter.accentColor != accentColor
Yegor's avatar
Yegor committed
1044 1045
        || oldPainter.theta != theta
        || oldPainter.activeRing != activeRing;
1046 1047 1048
  }
}

1049
class _Dial extends StatefulWidget {
1050
  const _Dial({
1051 1052
    @required this.selectedTime,
    @required this.mode,
1053
    @required this.use24HourDials,
1054
    @required this.onChanged,
1055 1056 1057 1058
    @required this.onHourSelected,
  }) : assert(selectedTime != null),
       assert(mode != null),
       assert(use24HourDials != null);
1059 1060 1061

  final TimeOfDay selectedTime;
  final _TimePickerMode mode;
1062
  final bool use24HourDials;
1063
  final ValueChanged<TimeOfDay> onChanged;
1064
  final VoidCallback onHourSelected;
1065

1066
  @override
1067
  _DialState createState() => _DialState();
1068 1069
}

1070
class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
1071
  @override
1072 1073
  void initState() {
    super.initState();
1074
    _updateDialRingFromWidget();
1075
    _thetaController = AnimationController(
1076 1077 1078
      duration: _kDialAnimateDuration,
      vsync: this,
    );
1079
    _thetaTween = Tween<double>(begin: _getThetaForTime(widget.selectedTime));
1080 1081 1082 1083
    _theta = _thetaController
      .drive(CurveTween(curve: Curves.fastOutSlowIn))
      .drive(_thetaTween)
      ..addListener(() => setState(() { /* _theta.value has changed */ }));
1084 1085
  }

1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098
  ThemeData themeData;
  MaterialLocalizations localizations;
  MediaQueryData media;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    assert(debugCheckHasMediaQuery(context));
    themeData = Theme.of(context);
    localizations = MaterialLocalizations.of(context);
    media = MediaQuery.of(context);
  }

1099
  @override
1100
  void didUpdateWidget(_Dial oldWidget) {
1101
    super.didUpdateWidget(oldWidget);
1102 1103
    _updateDialRingFromWidget();
    if (widget.mode != oldWidget.mode || widget.selectedTime != oldWidget.selectedTime) {
Yegor's avatar
Yegor committed
1104 1105 1106
      if (!_dragging)
        _animateTo(_getThetaForTime(widget.selectedTime));
    }
1107 1108 1109 1110 1111 1112 1113
  }

  void _updateDialRingFromWidget() {
    if (widget.mode == _TimePickerMode.hour && widget.use24HourDials) {
      _activeRing = widget.selectedTime.hour >= 1 && widget.selectedTime.hour <= 12
          ? _DialRing.inner
          : _DialRing.outer;
Yegor's avatar
Yegor committed
1114 1115 1116
    } else {
      _activeRing = _DialRing.outer;
    }
Adam Barth's avatar
Adam Barth committed
1117 1118
  }

1119 1120 1121 1122 1123 1124
  @override
  void dispose() {
    _thetaController.dispose();
    super.dispose();
  }

1125
  Tween<double> _thetaTween;
1126
  Animation<double> _theta;
1127
  AnimationController _thetaController;
Adam Barth's avatar
Adam Barth committed
1128 1129 1130 1131 1132 1133 1134
  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) {
1135
    final double currentTheta = _theta.value;
Adam Barth's avatar
Adam Barth committed
1136 1137
    double beginTheta = _nearest(targetTheta, currentTheta, currentTheta + _kTwoPi);
    beginTheta = _nearest(targetTheta, beginTheta, currentTheta - _kTwoPi);
1138 1139 1140 1141 1142 1143
    _thetaTween
      ..begin = beginTheta
      ..end = targetTheta;
    _thetaController
      ..value = 0.0
      ..forward();
1144 1145 1146
  }

  double _getThetaForTime(TimeOfDay time) {
1147 1148 1149
    final double fraction = widget.mode == _TimePickerMode.hour
      ? (time.hour / TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerPeriod
      : (time.minute / TimeOfDay.minutesPerHour) % TimeOfDay.minutesPerHour;
1150
    return (math.pi / 2.0 - fraction * _kTwoPi) % _kTwoPi;
1151 1152 1153
  }

  TimeOfDay _getTimeForTheta(double theta) {
1154
    final double fraction = (0.25 - (theta % _kTwoPi) / _kTwoPi) % 1.0;
1155
    if (widget.mode == _TimePickerMode.hour) {
1156
      int newHour = (fraction * TimeOfDay.hoursPerPeriod).round() % TimeOfDay.hoursPerPeriod;
1157
      if (widget.use24HourDials) {
Yegor's avatar
Yegor committed
1158 1159
        if (_activeRing == _DialRing.outer) {
          if (newHour != 0)
1160
            newHour = (newHour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
Yegor's avatar
Yegor committed
1161
        } else if (newHour == 0) {
1162
          newHour = TimeOfDay.hoursPerPeriod;
Yegor's avatar
Yegor committed
1163 1164 1165 1166 1167
        }
      } else {
        newHour = newHour + widget.selectedTime.periodOffset;
      }
      return widget.selectedTime.replacing(hour: newHour);
1168
    } else {
1169
      return widget.selectedTime.replacing(
1170
        minute: (fraction * TimeOfDay.minutesPerHour).round() % TimeOfDay.minutesPerHour
1171 1172 1173 1174
      );
    }
  }

1175
  TimeOfDay _notifyOnChangedIfNeeded() {
1176
    final TimeOfDay current = _getTimeForTheta(_theta.value);
1177 1178
    if (widget.onChanged == null)
      return current;
1179 1180
    if (current != widget.selectedTime)
      widget.onChanged(current);
1181
    return current;
1182 1183 1184 1185
  }

  void _updateThetaForPan() {
    setState(() {
Hans Muller's avatar
Hans Muller committed
1186
      final Offset offset = _position - _center;
1187
      final double angle = (math.atan2(offset.dx, offset.dy) - math.pi / 2.0) % _kTwoPi;
1188
      _thetaTween
Hans Muller's avatar
Hans Muller committed
1189 1190
        ..begin = angle
        ..end = angle; // The controller doesn't animate during the pan gesture.
Yegor's avatar
Yegor committed
1191 1192
      final RenderBox box = context.findRenderObject();
      final double radius = box.size.shortestSide / 2.0;
1193
      if (widget.mode == _TimePickerMode.hour && widget.use24HourDials) {
Yegor's avatar
Yegor committed
1194 1195 1196 1197 1198
        if (offset.distance * 1.5 < radius)
          _activeRing = _DialRing.inner;
        else
          _activeRing = _DialRing.outer;
      }
1199 1200 1201
    });
  }

1202 1203
  Offset _position;
  Offset _center;
Yegor's avatar
Yegor committed
1204
  _DialRing _activeRing = _DialRing.outer;
1205

1206
  void _handlePanStart(DragStartDetails details) {
Adam Barth's avatar
Adam Barth committed
1207 1208
    assert(!_dragging);
    _dragging = true;
1209
    final RenderBox box = context.findRenderObject();
1210
    _position = box.globalToLocal(details.globalPosition);
1211
    _center = box.size.center(Offset.zero);
1212 1213 1214 1215
    _updateThetaForPan();
    _notifyOnChangedIfNeeded();
  }

1216 1217
  void _handlePanUpdate(DragUpdateDetails details) {
    _position += details.delta;
1218 1219 1220 1221
    _updateThetaForPan();
    _notifyOnChangedIfNeeded();
  }

1222
  void _handlePanEnd(DragEndDetails details) {
Adam Barth's avatar
Adam Barth committed
1223 1224
    assert(_dragging);
    _dragging = false;
1225 1226
    _position = null;
    _center = null;
1227
    _animateTo(_getThetaForTime(widget.selectedTime));
1228 1229 1230 1231 1232
    if (widget.mode == _TimePickerMode.hour) {
      if (widget.onHourSelected != null) {
        widget.onHourSelected();
      }
    }
1233 1234
  }

1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246
  void _handleTapUp(TapUpDetails details) {
    final RenderBox box = context.findRenderObject();
    _position = box.globalToLocal(details.globalPosition);
    _center = box.size.center(Offset.zero);
    _updateThetaForPan();
    final TimeOfDay newTime = _notifyOnChangedIfNeeded();
    if (widget.mode == _TimePickerMode.hour) {
      if (widget.use24HourDials) {
        _announceToAccessibility(context, localizations.formatDecimal(newTime.hour));
      } else {
        _announceToAccessibility(context, localizations.formatDecimal(newTime.hourOfPeriod));
      }
1247 1248 1249
      if (widget.onHourSelected != null) {
        widget.onHourSelected();
      }
1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265
    } else {
      _announceToAccessibility(context, localizations.formatDecimal(newTime.minute));
    }
    _animateTo(_getThetaForTime(_getTimeForTheta(_theta.value)));
    _dragging = false;
    _position = null;
    _center = null;
  }

  void _selectHour(int hour) {
    _announceToAccessibility(context, localizations.formatDecimal(hour));
    TimeOfDay time;
    if (widget.mode == _TimePickerMode.hour && widget.use24HourDials) {
      _activeRing = hour >= 1 && hour <= 12
          ? _DialRing.inner
          : _DialRing.outer;
1266
      time = TimeOfDay(hour: hour, minute: widget.selectedTime.minute);
1267 1268 1269
    } else {
      _activeRing = _DialRing.outer;
      if (widget.selectedTime.period == DayPeriod.am) {
1270
        time = TimeOfDay(hour: hour, minute: widget.selectedTime.minute);
1271
      } else {
1272
        time = TimeOfDay(hour: hour + TimeOfDay.hoursPerPeriod, minute: widget.selectedTime.minute);
1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283
      }
    }
    final double angle = _getThetaForTime(time);
    _thetaTween
      ..begin = angle
      ..end = angle;
    _notifyOnChangedIfNeeded();
  }

  void _selectMinute(int minute) {
    _announceToAccessibility(context, localizations.formatDecimal(minute));
1284
    final TimeOfDay time = TimeOfDay(
1285 1286 1287 1288 1289 1290 1291 1292 1293 1294
      hour: widget.selectedTime.hour,
      minute: minute,
    );
    final double angle = _getThetaForTime(time);
    _thetaTween
      ..begin = angle
      ..end = angle;
    _notifyOnChangedIfNeeded();
  }

1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307
  static const List<TimeOfDay> _amHours = <TimeOfDay>[
    TimeOfDay(hour: 12, minute: 0),
    TimeOfDay(hour: 1, minute: 0),
    TimeOfDay(hour: 2, minute: 0),
    TimeOfDay(hour: 3, minute: 0),
    TimeOfDay(hour: 4, minute: 0),
    TimeOfDay(hour: 5, minute: 0),
    TimeOfDay(hour: 6, minute: 0),
    TimeOfDay(hour: 7, minute: 0),
    TimeOfDay(hour: 8, minute: 0),
    TimeOfDay(hour: 9, minute: 0),
    TimeOfDay(hour: 10, minute: 0),
    TimeOfDay(hour: 11, minute: 0),
1308 1309
  ];

1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322
  static const List<TimeOfDay> _pmHours = <TimeOfDay>[
    TimeOfDay(hour: 0, minute: 0),
    TimeOfDay(hour: 13, minute: 0),
    TimeOfDay(hour: 14, minute: 0),
    TimeOfDay(hour: 15, minute: 0),
    TimeOfDay(hour: 16, minute: 0),
    TimeOfDay(hour: 17, minute: 0),
    TimeOfDay(hour: 18, minute: 0),
    TimeOfDay(hour: 19, minute: 0),
    TimeOfDay(hour: 20, minute: 0),
    TimeOfDay(hour: 21, minute: 0),
    TimeOfDay(hour: 22, minute: 0),
    TimeOfDay(hour: 23, minute: 0),
1323 1324
  ];

1325 1326 1327 1328
  _TappableLabel _buildTappableLabel(TextTheme textTheme, int value, String label, VoidCallback onTap) {
    final TextStyle style = textTheme.subhead;
    // TODO(abarth): Handle textScaleFactor.
    // https://github.com/flutter/flutter/issues/5939
1329
    return _TappableLabel(
1330
      value: value,
1331 1332
      painter: TextPainter(
        text: TextSpan(style: style, text: label),
1333 1334 1335 1336
        textDirection: TextDirection.ltr,
      )..layout(),
      onTap: onTap,
    );
1337 1338
  }

1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351
  List<_TappableLabel> _build24HourInnerRing(TextTheme textTheme) {
    final List<_TappableLabel> labels = <_TappableLabel>[];
    for (TimeOfDay timeOfDay in _amHours) {
      labels.add(_buildTappableLabel(
        textTheme,
        timeOfDay.hour,
        localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
        () {
          _selectHour(timeOfDay.hour);
        },
      ));
    }
    return labels;
1352 1353
  }

1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381
  List<_TappableLabel> _build24HourOuterRing(TextTheme textTheme) {
    final List<_TappableLabel> labels = <_TappableLabel>[];
    for (TimeOfDay timeOfDay in _pmHours) {
      labels.add(_buildTappableLabel(
        textTheme,
        timeOfDay.hour,
        localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
        () {
          _selectHour(timeOfDay.hour);
        },
      ));
    }
    return labels;
  }

  List<_TappableLabel> _build12HourOuterRing(TextTheme textTheme) {
    final List<_TappableLabel> labels = <_TappableLabel>[];
    for (TimeOfDay timeOfDay in _amHours) {
      labels.add(_buildTappableLabel(
        textTheme,
        timeOfDay.hour,
        localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
        () {
          _selectHour(timeOfDay.hour);
        },
      ));
    }
    return labels;
1382 1383
  }

1384
  List<_TappableLabel> _buildMinutes(TextTheme textTheme) {
1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397
    const List<TimeOfDay> _minuteMarkerValues = <TimeOfDay>[
      TimeOfDay(hour: 0, minute: 0),
      TimeOfDay(hour: 0, minute: 5),
      TimeOfDay(hour: 0, minute: 10),
      TimeOfDay(hour: 0, minute: 15),
      TimeOfDay(hour: 0, minute: 20),
      TimeOfDay(hour: 0, minute: 25),
      TimeOfDay(hour: 0, minute: 30),
      TimeOfDay(hour: 0, minute: 35),
      TimeOfDay(hour: 0, minute: 40),
      TimeOfDay(hour: 0, minute: 45),
      TimeOfDay(hour: 0, minute: 50),
      TimeOfDay(hour: 0, minute: 55),
1398 1399
    ];

1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411
    final List<_TappableLabel> labels = <_TappableLabel>[];
    for (TimeOfDay timeOfDay in _minuteMarkerValues) {
      labels.add(_buildTappableLabel(
        textTheme,
        timeOfDay.minute,
        localizations.formatMinute(timeOfDay),
        () {
          _selectMinute(timeOfDay.minute);
        },
      ));
    }
    return labels;
1412 1413
  }

1414
  @override
1415
  Widget build(BuildContext context) {
1416 1417
    Color backgroundColor;
    switch (themeData.brightness) {
1418
      case Brightness.light:
1419 1420
        backgroundColor = Colors.grey[200];
        break;
1421
      case Brightness.dark:
1422 1423 1424 1425
        backgroundColor = themeData.backgroundColor;
        break;
    }

1426
    final ThemeData theme = Theme.of(context);
1427 1428 1429 1430 1431
    List<_TappableLabel> primaryOuterLabels;
    List<_TappableLabel> primaryInnerLabels;
    List<_TappableLabel> secondaryOuterLabels;
    List<_TappableLabel> secondaryInnerLabels;
    int selectedDialValue;
1432
    switch (widget.mode) {
1433
      case _TimePickerMode.hour:
1434
        if (widget.use24HourDials) {
1435
          selectedDialValue = widget.selectedTime.hour;
1436 1437 1438 1439
          primaryOuterLabels = _build24HourOuterRing(theme.textTheme);
          secondaryOuterLabels = _build24HourOuterRing(theme.accentTextTheme);
          primaryInnerLabels = _build24HourInnerRing(theme.textTheme);
          secondaryInnerLabels = _build24HourInnerRing(theme.accentTextTheme);
1440
        } else {
1441
          selectedDialValue = widget.selectedTime.hourOfPeriod;
1442 1443
          primaryOuterLabels = _build12HourOuterRing(theme.textTheme);
          secondaryOuterLabels = _build12HourOuterRing(theme.accentTextTheme);
1444
        }
1445 1446
        break;
      case _TimePickerMode.minute:
1447
        selectedDialValue = widget.selectedTime.minute;
1448
        primaryOuterLabels = _buildMinutes(theme.textTheme);
Yegor's avatar
Yegor committed
1449
        primaryInnerLabels = null;
1450
        secondaryOuterLabels = _buildMinutes(theme.accentTextTheme);
Yegor's avatar
Yegor committed
1451
        secondaryInnerLabels = null;
1452 1453 1454
        break;
    }

1455
    return GestureDetector(
1456
      excludeFromSemantics: true,
1457 1458 1459
      onPanStart: _handlePanStart,
      onPanUpdate: _handlePanUpdate,
      onPanEnd: _handlePanEnd,
1460
      onTapUp: _handleTapUp,
1461
      child: CustomPaint(
1462
        key: const ValueKey<String>('time-picker-dial'),
1463
        painter: _DialPainter(
1464
          selectedValue: selectedDialValue,
Yegor's avatar
Yegor committed
1465 1466 1467 1468
          primaryOuterLabels: primaryOuterLabels,
          primaryInnerLabels: primaryInnerLabels,
          secondaryOuterLabels: secondaryOuterLabels,
          secondaryInnerLabels: secondaryInnerLabels,
1469 1470
          backgroundColor: backgroundColor,
          accentColor: themeData.accentColor,
Yegor's avatar
Yegor committed
1471 1472
          theta: _theta.value,
          activeRing: _activeRing,
1473 1474
          textDirection: Directionality.of(context),
        ),
1475
      ),
1476 1477 1478
    );
  }
}
1479

1480 1481 1482 1483 1484 1485
/// A material design time picker designed to appear inside a popup dialog.
///
/// Pass this widget to [showDialog]. The value returned by [showDialog] is the
/// selected [TimeOfDay] if the user taps the "OK" button, or null if the user
/// taps the "CANCEL" button. The selected time is reported by calling
/// [Navigator.pop].
1486
class _TimePickerDialog extends StatefulWidget {
1487 1488 1489
  /// Creates a material time picker.
  ///
  /// [initialTime] must not be null.
1490
  const _TimePickerDialog({
1491
    Key key,
1492
    @required this.initialTime,
1493 1494
  }) : assert(initialTime != null),
       super(key: key);
1495

1496
  /// The time initially selected when the dialog is shown.
1497 1498 1499
  final TimeOfDay initialTime;

  @override
1500
  _TimePickerDialogState createState() => _TimePickerDialogState();
1501 1502 1503 1504 1505 1506
}

class _TimePickerDialogState extends State<_TimePickerDialog> {
  @override
  void initState() {
    super.initState();
1507
    _selectedTime = widget.initialTime;
1508 1509
  }

1510 1511 1512 1513 1514 1515 1516 1517
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    localizations = MaterialLocalizations.of(context);
    _announceInitialTimeOnce();
    _announceModeOnce();
  }

1518
  _TimePickerMode _mode = _TimePickerMode.hour;
1519
  _TimePickerMode _lastModeAnnounced;
1520 1521

  TimeOfDay get selectedTime => _selectedTime;
1522
  TimeOfDay _selectedTime;
1523

1524
  Timer _vibrateTimer;
1525
  MaterialLocalizations localizations;
1526

1527 1528 1529 1530
  void _vibrate() {
    switch (Theme.of(context).platform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
1531
        _vibrateTimer?.cancel();
1532
        _vibrateTimer = Timer(_kVibrateCommitDelay, () {
1533 1534 1535
          HapticFeedback.vibrate();
          _vibrateTimer = null;
        });
1536 1537 1538 1539 1540 1541
        break;
      case TargetPlatform.iOS:
        break;
    }
  }

1542
  void _handleModeChanged(_TimePickerMode mode) {
1543
    _vibrate();
1544 1545
    setState(() {
      _mode = mode;
1546
      _announceModeOnce();
1547 1548 1549
    });
  }

1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581
  void _announceModeOnce() {
    if (_lastModeAnnounced == _mode) {
      // Already announced it.
      return;
    }

    switch (_mode) {
      case _TimePickerMode.hour:
        _announceToAccessibility(context, localizations.timePickerHourModeAnnouncement);
        break;
      case _TimePickerMode.minute:
        _announceToAccessibility(context, localizations.timePickerMinuteModeAnnouncement);
        break;
    }
    _lastModeAnnounced = _mode;
  }

  bool _announcedInitialTime = false;

  void _announceInitialTimeOnce() {
    if (_announcedInitialTime)
      return;

    final MediaQueryData media = MediaQuery.of(context);
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
    _announceToAccessibility(
      context,
      localizations.formatTimeOfDay(widget.initialTime, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
    );
    _announcedInitialTime = true;
  }

1582
  void _handleTimeChanged(TimeOfDay value) {
1583
    _vibrate();
1584 1585 1586 1587 1588
    setState(() {
      _selectedTime = value;
    });
  }

1589 1590 1591 1592 1593 1594
  void _handleHourSelected() {
    setState(() {
      _mode = _TimePickerMode.minute;
    });
  }

1595 1596 1597 1598 1599 1600 1601 1602 1603 1604
  void _handleCancel() {
    Navigator.pop(context);
  }

  void _handleOk() {
    Navigator.pop(context, _selectedTime);
  }

  @override
  Widget build(BuildContext context) {
1605 1606 1607
    assert(debugCheckHasMediaQuery(context));
    final MediaQueryData media = MediaQuery.of(context);
    final TimeOfDayFormat timeOfDayFormat = localizations.timeOfDayFormat(alwaysUse24HourFormat: media.alwaysUse24HourFormat);
1608
    final bool use24HourDials = hourFormat(of: timeOfDayFormat) != HourFormat.h;
1609
    final ThemeData theme = Theme.of(context);
Yegor's avatar
Yegor committed
1610

1611
    final Widget picker = Padding(
1612
      padding: const EdgeInsets.all(16.0),
1613
      child: AspectRatio(
1614
        aspectRatio: 1.0,
1615
        child: _Dial(
1616
          mode: _mode,
1617
          use24HourDials: use24HourDials,
1618 1619
          selectedTime: _selectedTime,
          onChanged: _handleTimeChanged,
1620
          onHourSelected: _handleHourSelected,
1621 1622
        ),
      ),
1623 1624
    );

1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635
    final Widget actions = ButtonBar(
      children: <Widget>[
        FlatButton(
          child: Text(localizations.cancelButtonLabel),
          onPressed: _handleCancel,
        ),
        FlatButton(
          child: Text(localizations.okButtonLabel),
          onPressed: _handleOk,
        ),
      ],
1636 1637
    );

1638 1639
    final Dialog dialog = Dialog(
      child: OrientationBuilder(
1640
        builder: (BuildContext context, Orientation orientation) {
1641
          final Widget header = _TimePickerHeader(
1642 1643 1644 1645 1646
            selectedTime: _selectedTime,
            mode: _mode,
            orientation: orientation,
            onModeChanged: _handleModeChanged,
            onChanged: _handleTimeChanged,
1647
            use24HourDials: use24HourDials,
1648 1649
          );

1650
          final Widget pickerAndActions = Container(
1651
            color: theme.dialogBackgroundColor,
1652
            child: Column(
1653 1654
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
1655
                Expanded(child: picker), // picker grows and shrinks with the available space
1656 1657 1658 1659 1660
                actions,
              ],
            ),
          );

1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673
          double timePickerHeightPortrait;
          double timePickerHeightLandscape;
          switch (theme.materialTapTargetSize) {
            case MaterialTapTargetSize.padded:
              timePickerHeightPortrait = _kTimePickerHeightPortrait;
              timePickerHeightLandscape = _kTimePickerHeightLandscape;
              break;
            case MaterialTapTargetSize.shrinkWrap:
              timePickerHeightPortrait = _kTimePickerHeightPortraitCollapsed;
              timePickerHeightLandscape = _kTimePickerHeightLandscapeCollapsed;
              break;
          }

1674 1675 1676
          assert(orientation != null);
          switch (orientation) {
            case Orientation.portrait:
1677
              return SizedBox(
1678
                width: _kTimePickerWidthPortrait,
1679
                height: timePickerHeightPortrait,
1680
                child: Column(
1681 1682 1683 1684
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: <Widget>[
                    header,
1685
                    Expanded(
1686 1687
                      child: pickerAndActions,
                    ),
1688 1689
                  ],
                ),
1690 1691
              );
            case Orientation.landscape:
1692
              return SizedBox(
1693
                width: _kTimePickerWidthLandscape,
1694
                height: timePickerHeightLandscape,
1695
                child: Row(
1696 1697 1698 1699
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: <Widget>[
                    header,
1700
                    Flexible(
1701
                      child: pickerAndActions,
1702
                    ),
1703 1704
                  ],
                ),
1705 1706 1707 1708
              );
          }
          return null;
        }
1709
      ),
1710
    );
1711

1712
    return Theme(
1713 1714 1715 1716 1717
      data: theme.copyWith(
        dialogBackgroundColor: Colors.transparent,
      ),
      child: dialog,
    );
1718
  }
1719 1720 1721 1722 1723 1724 1725

  @override
  void dispose() {
    _vibrateTimer?.cancel();
    _vibrateTimer = null;
    super.dispose();
  }
1726 1727 1728 1729 1730
}

/// Shows a dialog containing a material design time picker.
///
/// The returned Future resolves to the time selected by the user when the user
1731
/// closes the dialog. If the user cancels the dialog, null is returned.
1732
///
1733 1734
/// {@tool sample}
/// Show a dialog with [initialTime] equal to the current time.
Ian Hickson's avatar
Ian Hickson committed
1735
///
1736
/// ```dart
1737
/// Future<TimeOfDay> selectedTime = showTimePicker(
1738
///   initialTime: TimeOfDay.now(),
Ian Hickson's avatar
Ian Hickson committed
1739
///   context: context,
1740 1741
/// );
/// ```
1742
/// {@end-tool}
1743
///
1744
/// The [context] argument is passed to [showDialog], the documentation for
Ian Hickson's avatar
Ian Hickson committed
1745 1746
/// which discusses how it is used.
///
1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784
/// The [builder] parameter can be used to wrap the dialog widget
/// to add inherited widgets like [Localizations.override],
/// [Directionality], or [MediaQuery].
///
/// {@tool sample}
/// Show a dialog with the text direction overridden to be [TextDirection.rtl].
///
/// ```dart
/// Future<TimeOfDay> selectedTimeRTL = showTimePicker(
///   context: context,
///   initialTime: TimeOfDay.now(),
///   builder: (BuildContext context, Widget child) {
///     return Directionality(
///       textDirection: TextDirection.rtl,
///       child: child,
///     );
///   },
/// );
/// ```
/// {@end-tool}
///
/// {@tool sample}
/// Show a dialog with time unconditionally displayed in 24 hour format.
///
/// ```dart
/// Future<TimeOfDay> selectedTime24Hour = showTimePicker(
///   context: context,
///   initialTime: TimeOfDay(hour: 10, minute: 47),
///   builder: (BuildContext context, Widget child) {
///     return MediaQuery(
///       data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true),
///       child: child,
///     );
///   },
/// );
/// ```
/// {@end-tool}
///
1785 1786
/// See also:
///
1787 1788
///  * [showDatePicker], which shows a dialog that contains a material design
///    date picker.
1789
Future<TimeOfDay> showTimePicker({
1790
  @required BuildContext context,
1791 1792
  @required TimeOfDay initialTime,
  TransitionBuilder builder,
1793
}) async {
1794
  assert(context != null);
1795
  assert(initialTime != null);
1796
  assert(debugCheckHasMaterialLocalizations(context));
1797

1798
  final Widget dialog = _TimePickerDialog(initialTime: initialTime);
1799
  return await showDialog<TimeOfDay>(
1800
    context: context,
1801 1802 1803
    builder: (BuildContext context) {
      return builder == null ? dialog : builder(context, dialog);
    },
1804
  );
1805
}
1806 1807 1808 1809

void _announceToAccessibility(BuildContext context, String message) {
  SemanticsService.announce(message, Directionality.of(context));
}