segmented_control.dart 23.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:collection';
import 'dart:math' as math;

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

xster's avatar
xster committed
12
import 'theme.dart';
13

14
// Minimum padding from edges of the segmented control to edges of
15
// encompassing widget.
16
const EdgeInsetsGeometry _kHorizontalItemPadding = EdgeInsets.symmetric(horizontal: 16.0);
17 18 19 20

// Minimum height of the segmented control.
const double _kMinSegmentedControlHeight = 28.0;

21 22
// The duration of the fade animation used to transition when a new widget
// is selected.
23
const Duration _kFadeDuration = Duration(milliseconds: 165);
24

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
/// An iOS-style segmented control.
///
/// Displays the widgets provided in the [Map] of [children] in a
/// horizontal list. Used to select between a number of mutually exclusive
/// options. When one option in the segmented control is selected, the other
/// options in the segmented control cease to be selected.
///
/// A segmented control can feature any [Widget] as one of the values in its
/// [Map] of [children]. The type T is the type of the keys used
/// to identify each widget and determine which widget is selected. As
/// required by the [Map] class, keys must be of consistent types
/// and must be comparable. The ordering of the keys will determine the order
/// of the widgets in the segmented control.
///
/// When the state of the segmented control changes, the widget calls the
/// [onValueChanged] callback. The map key associated with the newly selected
/// widget is returned in the [onValueChanged] callback. Typically, widgets
/// that use a segmented control will listen for the [onValueChanged] callback
/// and rebuild the segmented control with a new [groupValue] to update which
/// option is currently selected.
///
/// The [children] will be displayed in the order of the keys in the [Map].
/// The height of the segmented control is determined by the height of the
/// tallest widget provided as a value in the [Map] of [children].
49 50 51 52 53
/// The width of each child in the segmented control will be equal to the width
/// of widest child, unless the combined width of the children is wider than
/// the available horizontal space. In this case, the available horizontal space
/// is divided by the number of provided [children] to determine the width of
/// each widget. The selection area for each of the widgets in the [Map] of
54 55 56
/// [children] will then be expanded to fill the calculated space, so each
/// widget will appear to have the same dimensions.
///
57 58
/// A segmented control may optionally be created with custom colors. The
/// [unselectedColor], [selectedColor], [borderColor], and [pressedColor]
xster's avatar
xster committed
59 60
/// arguments can be used to override the segmented control's colors from
/// [CupertinoTheme] defaults.
61
///
62 63
/// See also:
///
Dan Field's avatar
Dan Field committed
64 65
///  * [CupertinoSegmentedControl], a segmented control widget in the style used
///    up until iOS 13.
66
///  * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/>
67
class CupertinoSegmentedControl<T> extends StatefulWidget {
68 69
  /// Creates an iOS-style segmented control bar.
  ///
xster's avatar
xster committed
70
  /// The [children] and [onValueChanged] arguments must not be null. The
71 72 73 74 75 76 77 78 79 80
  /// [children] argument must be an ordered [Map] such as a [LinkedHashMap].
  /// Further, the length of the [children] list must be greater than one.
  ///
  /// Each widget value in the map of [children] must have an associated key
  /// that uniquely identifies this widget. This key is what will be returned
  /// in the [onValueChanged] callback when a new value from the [children] map
  /// is selected.
  ///
  /// The [groupValue] is the currently selected value for the segmented control.
  /// If no [groupValue] is provided, or the [groupValue] is null, no widget will
81 82
  /// appear as selected. The [groupValue] must be either null or one of the keys
  /// in the [children] map.
83
  CupertinoSegmentedControl({
84 85 86 87
    Key key,
    @required this.children,
    @required this.onValueChanged,
    this.groupValue,
xster's avatar
xster committed
88 89 90 91
    this.unselectedColor,
    this.selectedColor,
    this.borderColor,
    this.pressedColor,
92
    this.padding,
93 94 95 96 97 98 99 100
  }) : assert(children != null),
       assert(children.length >= 2),
       assert(onValueChanged != null),
       assert(
         groupValue == null || children.keys.any((T child) => child == groupValue),
         'The groupValue must be either null or one of the keys in the children map.',
       ),
       super(key: key);
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126

  /// The identifying keys and corresponding widget values in the
  /// segmented control.
  ///
  /// The map must have more than one entry.
  /// This attribute must be an ordered [Map] such as a [LinkedHashMap].
  final Map<T, Widget> children;

  /// The identifier of the widget that is currently selected.
  ///
  /// This must be one of the keys in the [Map] of [children].
  /// If this attribute is null, no widget will be initially selected.
  final T groupValue;

  /// The callback that is called when a new option is tapped.
  ///
  /// This attribute must not be null.
  ///
  /// The segmented control passes the newly selected widget's associated key
  /// to the callback but does not actually change state until the parent
  /// widget rebuilds the segmented control with the new [groupValue].
  ///
  /// The callback provided to [onValueChanged] should update the state of
  /// the parent [StatefulWidget] using the [State.setState] method, so that
  /// the parent gets rebuilt; for example:
  ///
127
  /// {@tool snippet}
128 129 130 131
  ///
  /// ```dart
  /// class SegmentedControlExample extends StatefulWidget {
  ///   @override
132
  ///   State createState() => SegmentedControlExampleState();
133 134 135 136
  /// }
  ///
  /// class SegmentedControlExampleState extends State<SegmentedControlExample> {
  ///   final Map<int, Widget> children = const {
137 138
  ///     0: Text('Child 1'),
  ///     1: Text('Child 2'),
139 140 141 142 143 144
  ///   };
  ///
  ///   int currentValue;
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
145 146
  ///     return Container(
  ///       child: CupertinoSegmentedControl<int>(
147 148 149 150 151 152 153 154 155 156 157 158
  ///         children: children,
  ///         onValueChanged: (int newValue) {
  ///           setState(() {
  ///             currentValue = newValue;
  ///           });
  ///         },
  ///         groupValue: currentValue,
  ///       ),
  ///     );
  ///   }
  /// }
  /// ```
159
  /// {@end-tool}
160 161
  final ValueChanged<T> onValueChanged;

162 163 164
  /// The color used to fill the backgrounds of unselected widgets and as the
  /// text color of the selected widget.
  ///
xster's avatar
xster committed
165
  /// Defaults to [CupertinoTheme]'s `primaryContrastingColor` if null.
166 167 168 169 170
  final Color unselectedColor;

  /// The color used to fill the background of the selected widget and as the text
  /// color of unselected widgets.
  ///
xster's avatar
xster committed
171
  /// Defaults to [CupertinoTheme]'s `primaryColor` if null.
172 173 174 175
  final Color selectedColor;

  /// The color used as the border around each widget.
  ///
xster's avatar
xster committed
176
  /// Defaults to [CupertinoTheme]'s `primaryColor` if null.
177 178 179 180 181
  final Color borderColor;

  /// The color used to fill the background of the widget the user is
  /// temporarily interacting with through a long press or drag.
  ///
xster's avatar
xster committed
182
  /// Defaults to the selectedColor at 20% opacity if null.
183 184
  final Color pressedColor;

185 186 187 188 189
  /// The CupertinoSegmentedControl will be placed inside this padding
  ///
  /// Defaults to EdgeInsets.symmetric(horizontal: 16.0)
  final EdgeInsetsGeometry padding;

190 191 192 193
  @override
  _SegmentedControlState<T> createState() => _SegmentedControlState<T>();
}

194 195
class _SegmentedControlState<T> extends State<CupertinoSegmentedControl<T>>
    with TickerProviderStateMixin<CupertinoSegmentedControl<T>> {
196 197
  T _pressedKey;

198 199 200
  final List<AnimationController> _selectionControllers = <AnimationController>[];
  final List<ColorTween> _childTweens = <ColorTween>[];

201 202 203
  ColorTween _forwardBackgroundColorTween;
  ColorTween _reverseBackgroundColorTween;
  ColorTween _textColorTween;
204

xster's avatar
xster committed
205 206 207 208 209 210 211 212 213 214
  Color _selectedColor;
  Color _unselectedColor;
  Color _borderColor;
  Color _pressedColor;

  AnimationController createAnimationController() {
    return AnimationController(
      duration: _kFadeDuration,
      vsync: this,
    )..addListener(() {
215 216
      setState(() {
        // State of background/text colors has changed
xster's avatar
xster committed
217
      });
218
    });
xster's avatar
xster committed
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
  }

  bool _updateColors() {
    assert(mounted, 'This should only be called after didUpdateDependencies');
    bool changed = false;
    final Color selectedColor = widget.selectedColor ?? CupertinoTheme.of(context).primaryColor;
    if (_selectedColor != selectedColor) {
      changed = true;
      _selectedColor = selectedColor;
    }
    final Color unselectedColor = widget.unselectedColor ?? CupertinoTheme.of(context).primaryContrastingColor;
    if (_unselectedColor != unselectedColor) {
      changed = true;
      _unselectedColor = unselectedColor;
    }
    final Color borderColor = widget.borderColor ?? CupertinoTheme.of(context).primaryColor;
    if (_borderColor != borderColor) {
      changed = true;
      _borderColor = borderColor;
    }
    final Color pressedColor = widget.pressedColor ?? CupertinoTheme.of(context).primaryColor.withOpacity(0.2);
    if (_pressedColor != pressedColor) {
      changed = true;
      _pressedColor = pressedColor;
    }

245
    _forwardBackgroundColorTween = ColorTween(
xster's avatar
xster committed
246 247
      begin: _pressedColor,
      end: _selectedColor,
248
    );
249
    _reverseBackgroundColorTween = ColorTween(
xster's avatar
xster committed
250 251
      begin: _unselectedColor,
      end: _selectedColor,
252
    );
253
    _textColorTween = ColorTween(
xster's avatar
xster committed
254 255
      begin: _selectedColor,
      end: _unselectedColor,
256
    );
xster's avatar
xster committed
257 258 259 260 261
    return changed;
  }

  void _updateAnimationControllers() {
    assert(mounted, 'This should only be called after didUpdateDependencies');
262
    for (final AnimationController controller in _selectionControllers) {
xster's avatar
xster committed
263 264 265 266
      controller.dispose();
    }
    _selectionControllers.clear();
    _childTweens.clear();
267

268
    for (final T key in widget.children.keys) {
269 270
      final AnimationController animationController = createAnimationController();
      if (widget.groupValue == key) {
271
        _childTweens.add(_reverseBackgroundColorTween);
272 273
        animationController.value = 1.0;
      } else {
274
        _childTweens.add(_forwardBackgroundColorTween);
275 276 277
      }
      _selectionControllers.add(animationController);
    }
278 279
  }

xster's avatar
xster committed
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    if (_updateColors()) {
      _updateAnimationControllers();
    }
  }

  @override
  void didUpdateWidget(CupertinoSegmentedControl<T> oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (_updateColors() || oldWidget.children.length != widget.children.length) {
      _updateAnimationControllers();
    }

    if (oldWidget.groupValue != widget.groupValue) {
      int index = 0;
299
      for (final T key in widget.children.keys) {
xster's avatar
xster committed
300 301 302 303 304 305 306 307 308 309
        if (widget.groupValue == key) {
          _childTweens[index] = _forwardBackgroundColorTween;
          _selectionControllers[index].forward();
        } else {
          _childTweens[index] = _reverseBackgroundColorTween;
          _selectionControllers[index].reverse();
        }
        index += 1;
      }
    }
310 311 312 313
  }

  @override
  void dispose() {
314
    for (final AnimationController animationController in _selectionControllers) {
315 316 317 318 319
      animationController.dispose();
    }
    super.dispose();
  }

xster's avatar
xster committed
320

321 322 323 324 325 326
  void _onTapDown(T currentKey) {
    if (_pressedKey == null && currentKey != widget.groupValue) {
      setState(() {
        _pressedKey = currentKey;
      });
    }
327 328 329 330 331 332 333 334 335
  }

  void _onTapCancel() {
    setState(() {
      _pressedKey = null;
    });
  }

  void _onTap(T currentKey) {
336 337 338
    if (currentKey != _pressedKey)
      return;
    if (currentKey != widget.groupValue) {
339
      widget.onValueChanged(currentKey);
340
    }
341
    _pressedKey = null;
342 343 344 345
  }

  Color getTextColor(int index, T currentKey) {
    if (_selectionControllers[index].isAnimating)
346
      return _textColorTween.evaluate(_selectionControllers[index]);
347
    if (widget.groupValue == currentKey)
xster's avatar
xster committed
348 349
      return _unselectedColor;
    return _selectedColor;
350 351 352 353 354 355
  }

  Color getBackgroundColor(int index, T currentKey) {
    if (_selectionControllers[index].isAnimating)
      return _childTweens[index].evaluate(_selectionControllers[index]);
    if (widget.groupValue == currentKey)
xster's avatar
xster committed
356
      return _selectedColor;
357
    if (_pressedKey == currentKey)
xster's avatar
xster committed
358 359
      return _pressedColor;
    return _unselectedColor;
360 361 362 363
  }

  @override
  Widget build(BuildContext context) {
364 365
    final List<Widget> _gestureChildren = <Widget>[];
    final List<Color> _backgroundColors = <Color>[];
366 367 368
    int index = 0;
    int selectedIndex;
    int pressedIndex;
369
    for (final T currentKey in widget.children.keys) {
370 371 372 373
      selectedIndex = (widget.groupValue == currentKey) ? index : selectedIndex;
      pressedIndex = (_pressedKey == currentKey) ? index : pressedIndex;

      final TextStyle textStyle = DefaultTextStyle.of(context).style.copyWith(
374
        color: getTextColor(index, currentKey),
375
      );
376
      final IconThemeData iconTheme = IconThemeData(
377
        color: getTextColor(index, currentKey),
378 379
      );

380
      Widget child = Center(
381 382 383
        child: widget.children[currentKey],
      );

384
      child = GestureDetector(
385 386 387 388 389 390 391
        onTapDown: (TapDownDetails event) {
          _onTapDown(currentKey);
        },
        onTapCancel: _onTapCancel,
        onTap: () {
          _onTap(currentKey);
        },
392
        child: IconTheme(
393
          data: iconTheme,
394
          child: DefaultTextStyle(
395
            style: textStyle,
396
            child: Semantics(
397
              button: true,
398 399 400 401 402 403 404
              inMutuallyExclusiveGroup: true,
              selected: widget.groupValue == currentKey,
              child: child,
            ),
          ),
        ),
      );
405 406 407

      _backgroundColors.add(getBackgroundColor(index, currentKey));
      _gestureChildren.add(child);
408 409 410
      index += 1;
    }

411
    final Widget box = _SegmentedControlRenderWidget<T>(
412
      children: _gestureChildren,
413 414
      selectedIndex: selectedIndex,
      pressedIndex: pressedIndex,
415
      backgroundColors: _backgroundColors,
xster's avatar
xster committed
416
      borderColor: _borderColor,
417 418
    );

419
    return Padding(
420
      padding: widget.padding ?? _kHorizontalItemPadding,
421
      child: UnconstrainedBox(
422 423 424 425 426 427 428 429 430 431 432 433 434
        constrainedAxis: Axis.horizontal,
        child: box,
      ),
    );
  }
}

class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
  _SegmentedControlRenderWidget({
    Key key,
    List<Widget> children = const <Widget>[],
    @required this.selectedIndex,
    @required this.pressedIndex,
435
    @required this.backgroundColors,
436
    @required this.borderColor,
437 438 439 440 441 442 443
  }) : super(
          key: key,
          children: children,
        );

  final int selectedIndex;
  final int pressedIndex;
444
  final List<Color> backgroundColors;
445
  final Color borderColor;
446 447 448

  @override
  RenderObject createRenderObject(BuildContext context) {
449
    return _RenderSegmentedControl<T>(
450 451 452
      textDirection: Directionality.of(context),
      selectedIndex: selectedIndex,
      pressedIndex: pressedIndex,
453
      backgroundColors: backgroundColors,
454
      borderColor: borderColor,
455 456 457 458 459 460 461 462
    );
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSegmentedControl<T> renderObject) {
    renderObject
      ..textDirection = Directionality.of(context)
      ..selectedIndex = selectedIndex
463
      ..pressedIndex = pressedIndex
464 465
      ..backgroundColors = backgroundColors
      ..borderColor = borderColor;
466 467 468 469 470 471 472
  }
}

class _SegmentedControlContainerBoxParentData extends ContainerBoxParentData<RenderBox> {
  RRect surroundingRect;
}

473
typedef _NextChild = RenderBox Function(RenderBox child);
474 475 476 477 478 479 480 481

class _RenderSegmentedControl<T> extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, ContainerBoxParentData<RenderBox>>,
        RenderBoxContainerDefaultsMixin<RenderBox, ContainerBoxParentData<RenderBox>> {
  _RenderSegmentedControl({
    @required int selectedIndex,
    @required int pressedIndex,
    @required TextDirection textDirection,
482
    @required List<Color> backgroundColors,
483
    @required Color borderColor,
484 485 486 487 488
  }) : assert(textDirection != null),
       _textDirection = textDirection,
       _selectedIndex = selectedIndex,
       _pressedIndex = pressedIndex,
       _backgroundColors = backgroundColors,
489
       _borderColor = borderColor;
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520

  int get selectedIndex => _selectedIndex;
  int _selectedIndex;
  set selectedIndex(int value) {
    if (_selectedIndex == value) {
      return;
    }
    _selectedIndex = value;
    markNeedsPaint();
  }

  int get pressedIndex => _pressedIndex;
  int _pressedIndex;
  set pressedIndex(int value) {
    if (_pressedIndex == value) {
      return;
    }
    _pressedIndex = value;
    markNeedsPaint();
  }

  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
    if (_textDirection == value) {
      return;
    }
    _textDirection = value;
    markNeedsLayout();
  }

521 522 523 524 525 526 527 528 529 530
  List<Color> get backgroundColors => _backgroundColors;
  List<Color> _backgroundColors;
  set backgroundColors(List<Color> value) {
    if (_backgroundColors == value) {
      return;
    }
    _backgroundColors = value;
    markNeedsPaint();
  }

531 532 533 534 535 536 537 538 539
  Color get borderColor => _borderColor;
  Color _borderColor;
  set borderColor(Color value) {
    if (_borderColor == value) {
      return;
    }
    _borderColor = value;
    markNeedsPaint();
  }
540 541 542 543 544 545

  @override
  double computeMinIntrinsicWidth(double height) {
    RenderBox child = firstChild;
    double minWidth = 0.0;
    while (child != null) {
546
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData;
Ian Hickson's avatar
Ian Hickson committed
547
      final double childWidth = child.getMinIntrinsicWidth(height);
548 549 550 551 552 553 554 555 556 557 558
      minWidth = math.max(minWidth, childWidth);
      child = childParentData.nextSibling;
    }
    return minWidth * childCount;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    RenderBox child = firstChild;
    double maxWidth = 0.0;
    while (child != null) {
559
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData;
Ian Hickson's avatar
Ian Hickson committed
560
      final double childWidth = child.getMaxIntrinsicWidth(height);
561 562 563 564 565 566 567 568 569 570 571
      maxWidth = math.max(maxWidth, childWidth);
      child = childParentData.nextSibling;
    }
    return maxWidth * childCount;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    RenderBox child = firstChild;
    double minHeight = 0.0;
    while (child != null) {
572
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData;
Ian Hickson's avatar
Ian Hickson committed
573
      final double childHeight = child.getMinIntrinsicHeight(width);
574 575 576 577 578 579 580 581 582 583 584
      minHeight = math.max(minHeight, childHeight);
      child = childParentData.nextSibling;
    }
    return minHeight;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    RenderBox child = firstChild;
    double maxHeight = 0.0;
    while (child != null) {
585
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData;
Ian Hickson's avatar
Ian Hickson committed
586
      final double childHeight = child.getMaxIntrinsicHeight(width);
587 588 589 590 591 592 593 594 595 596 597 598 599 600
      maxHeight = math.max(maxHeight, childHeight);
      child = childParentData.nextSibling;
    }
    return maxHeight;
  }

  @override
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    return defaultComputeDistanceToHighestActualBaseline(baseline);
  }

  @override
  void setupParentData(RenderBox child) {
    if (child.parentData is! _SegmentedControlContainerBoxParentData) {
601
      child.parentData = _SegmentedControlContainerBoxParentData();
602 603 604 605 606 607 608
    }
  }

  void _layoutRects(_NextChild nextChild, RenderBox leftChild, RenderBox rightChild) {
    RenderBox child = leftChild;
    double start = 0.0;
    while (child != null) {
609
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData;
610
      final Offset childOffset = Offset(start, 0.0);
611
      childParentData.offset = childOffset;
612
      final Rect childRect = Rect.fromLTWH(start, 0.0, child.size.width, child.size.height);
613 614
      RRect rChildRect;
      if (child == leftChild) {
615
        rChildRect = RRect.fromRectAndCorners(childRect, topLeft: const Radius.circular(3.0),
616 617
            bottomLeft: const Radius.circular(3.0));
      } else if (child == rightChild) {
618
        rChildRect = RRect.fromRectAndCorners(childRect, topRight: const Radius.circular(3.0),
619 620
            bottomRight: const Radius.circular(3.0));
      } else {
621
        rChildRect = RRect.fromRectAndCorners(childRect);
622 623 624 625 626 627 628 629 630
      }
      childParentData.surroundingRect = rChildRect;
      start += child.size.width;
      child = nextChild(child);
    }
  }

  @override
  void performLayout() {
631
    final BoxConstraints constraints = this.constraints;
632 633
    double maxHeight = _kMinSegmentedControlHeight;

634
    double childWidth = constraints.minWidth / childCount;
635
    for (final RenderBox child in getChildrenAsList()) {
636
      childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
637
    }
638
    childWidth = math.min(childWidth, constraints.maxWidth / childCount);
639 640 641 642 643 644 645 646 647 648

    RenderBox child = firstChild;
    while (child != null) {
      final double boxHeight = child.getMaxIntrinsicHeight(childWidth);
      maxHeight = math.max(maxHeight, boxHeight);
      child = childAfter(child);
    }

    constraints.constrainHeight(maxHeight);

649
    final BoxConstraints childConstraints = BoxConstraints.tightFor(
650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676
      width: childWidth,
      height: maxHeight,
    );

    child = firstChild;
    while (child != null) {
      child.layout(childConstraints, parentUsesSize: true);
      child = childAfter(child);
    }

    switch (textDirection) {
      case TextDirection.rtl:
        _layoutRects(
          childBefore,
          lastChild,
          firstChild,
        );
        break;
      case TextDirection.ltr:
        _layoutRects(
          childAfter,
          firstChild,
          lastChild,
        );
        break;
    }

677
    size = constraints.constrain(Size(childWidth * childCount, maxHeight));
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    RenderBox child = firstChild;
    int index = 0;
    while (child != null) {
      _paintChild(context, offset, child, index);
      child = childAfter(child);
      index += 1;
    }
  }

  void _paintChild(PaintingContext context, Offset offset, RenderBox child, int childIndex) {
    assert(child != null);

694
    final _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData;
695 696 697

    context.canvas.drawRRect(
      childParentData.surroundingRect.shift(offset),
698
      Paint()
699
        ..color = backgroundColors[childIndex]
700 701 702 703
        ..style = PaintingStyle.fill,
    );
    context.canvas.drawRRect(
      childParentData.surroundingRect.shift(offset),
704
      Paint()
705 706 707
        ..color = borderColor
        ..strokeWidth = 1.0
        ..style = PaintingStyle.stroke,
708 709 710 711 712 713
    );

    context.paintChild(child, childParentData.offset + offset);
  }

  @override
714
  bool hitTestChildren(BoxHitTestResult result, { @required Offset position }) {
715
    assert(position != null);
716 717
    RenderBox child = lastChild;
    while (child != null) {
718
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData;
719
      if (childParentData.surroundingRect.contains(position)) {
720 721 722 723 724 725 726 727 728
        final Offset center = (Offset.zero & child.size).center;
        return result.addWithRawTransform(
          transform: MatrixUtils.forceToPoint(center),
          position: center,
          hitTest: (BoxHitTestResult result, Offset position) {
            assert(position == center);
            return child.hitTest(result, position: center);
          },
        );
729 730 731 732
      }
      child = childParentData.previousSibling;
    }
    return false;
733 734
  }
}