segmented_control.dart 24.2 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
// 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/rendering.dart';
import 'package:flutter/widgets.dart';

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

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

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

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

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
/// 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].
48 49 50 51 52
/// 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
53 54 55
/// [children] will then be expanded to fill the calculated space, so each
/// widget will appear to have the same dimensions.
///
56 57
/// A segmented control may optionally be created with custom colors. The
/// [unselectedColor], [selectedColor], [borderColor], and [pressedColor]
xster's avatar
xster committed
58 59
/// arguments can be used to override the segmented control's colors from
/// [CupertinoTheme] defaults.
60
///
61 62
/// See also:
///
Dan Field's avatar
Dan Field committed
63 64
///  * [CupertinoSegmentedControl], a segmented control widget in the style used
///    up until iOS 13.
65
///  * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/>
66
class CupertinoSegmentedControl<T extends Object> extends StatefulWidget {
67 68
  /// Creates an iOS-style segmented control bar.
  ///
xster's avatar
xster committed
69
  /// The [children] and [onValueChanged] arguments must not be null. The
70 71 72 73 74 75 76 77 78 79
  /// [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
80 81
  /// appear as selected. The [groupValue] must be either null or one of the keys
  /// in the [children] map.
82
  CupertinoSegmentedControl({
83 84 85
    Key? key,
    required this.children,
    required this.onValueChanged,
86
    this.groupValue,
xster's avatar
xster committed
87 88 89 90
    this.unselectedColor,
    this.selectedColor,
    this.borderColor,
    this.pressedColor,
91
    this.padding,
92 93 94 95 96 97 98 99
  }) : 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);
100 101 102 103 104 105 106 107 108 109 110 111

  /// 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.
112
  final T? groupValue;
113 114 115 116 117 118 119 120 121 122 123 124 125

  /// 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:
  ///
126
  /// {@tool snippet}
127 128 129
  ///
  /// ```dart
  /// class SegmentedControlExample extends StatefulWidget {
130 131
  ///   const SegmentedControlExample({Key? key}) : super(key: key);
  ///
132
  ///   @override
133
  ///   State createState() => SegmentedControlExampleState();
134 135 136
  /// }
  ///
  /// class SegmentedControlExampleState extends State<SegmentedControlExample> {
137
  ///   final Map<int, Widget> children = const <int, Widget>{
138 139
  ///     0: Text('Child 1'),
  ///     1: Text('Child 2'),
140 141
  ///   };
  ///
142
  ///   late int currentValue;
143 144 145
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
146 147 148 149 150 151 152 153
  ///     return CupertinoSegmentedControl<int>(
  ///       children: children,
  ///       onValueChanged: (int newValue) {
  ///         setState(() {
  ///           currentValue = newValue;
  ///         });
  ///       },
  ///       groupValue: currentValue,
154 155 156 157
  ///     );
  ///   }
  /// }
  /// ```
158
  /// {@end-tool}
159 160
  final ValueChanged<T> onValueChanged;

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

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

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

  /// 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
181
  /// Defaults to the selectedColor at 20% opacity if null.
182
  final Color? pressedColor;
183

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

189
  @override
190
  State<CupertinoSegmentedControl<T>> createState() => _SegmentedControlState<T>();
191 192
}

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

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

200 201 202
  late ColorTween _forwardBackgroundColorTween;
  late ColorTween _reverseBackgroundColorTween;
  late ColorTween _textColorTween;
203

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

  AnimationController createAnimationController() {
    return AnimationController(
      duration: _kFadeDuration,
      vsync: this,
    )..addListener(() {
214 215
      setState(() {
        // State of background/text colors has changed
xster's avatar
xster committed
216
      });
217
    });
xster's avatar
xster committed
218 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
  }

  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;
    }

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

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

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

xster's avatar
xster committed
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
  @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;
298
      for (final T key in widget.children.keys) {
xster's avatar
xster committed
299 300 301 302 303 304 305 306 307 308
        if (widget.groupValue == key) {
          _childTweens[index] = _forwardBackgroundColorTween;
          _selectionControllers[index].forward();
        } else {
          _childTweens[index] = _reverseBackgroundColorTween;
          _selectionControllers[index].reverse();
        }
        index += 1;
      }
    }
309 310 311 312
  }

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

xster's avatar
xster committed
319

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

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

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

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

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

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

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

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

383
      child = GestureDetector(
384
        behavior: HitTestBehavior.opaque,
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
      _backgroundColors.add(getBackgroundColor(index, currentKey)!);
407
      _gestureChildren.add(child);
408 409 410
      index += 1;
    }

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

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

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

442 443
  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
      textDirection: Directionality.of(context),
451 452
      selectedIndex: selectedIndex,
      pressedIndex: pressedIndex,
453
      backgroundColors: backgroundColors,
454
      borderColor: borderColor,
455 456 457 458 459 460
    );
  }

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

class _SegmentedControlContainerBoxParentData extends ContainerBoxParentData<RenderBox> {
470
  RRect? surroundingRect;
471 472
}

473
typedef _NextChild = RenderBox? Function(RenderBox child);
474 475 476 477 478

class _RenderSegmentedControl<T> extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, ContainerBoxParentData<RenderBox>>,
        RenderBoxContainerDefaultsMixin<RenderBox, ContainerBoxParentData<RenderBox>> {
  _RenderSegmentedControl({
479 480 481 482 483
    required int? selectedIndex,
    required int? pressedIndex,
    required TextDirection textDirection,
    required List<Color> backgroundColors,
    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
  int? get selectedIndex => _selectedIndex;
  int? _selectedIndex;
  set selectedIndex(int? value) {
494 495 496 497 498 499 500
    if (_selectedIndex == value) {
      return;
    }
    _selectedIndex = value;
    markNeedsPaint();
  }

501 502 503
  int? get pressedIndex => _pressedIndex;
  int? _pressedIndex;
  set pressedIndex(int? value) {
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
    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

  @override
  double computeMinIntrinsicWidth(double height) {
543
    RenderBox? child = firstChild;
544 545
    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
      minWidth = math.max(minWidth, childWidth);
      child = childParentData.nextSibling;
    }
    return minWidth * childCount;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
556
    RenderBox? child = firstChild;
557 558
    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
      maxWidth = math.max(maxWidth, childWidth);
      child = childParentData.nextSibling;
    }
    return maxWidth * childCount;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
569
    RenderBox? child = firstChild;
570 571
    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
      minHeight = math.max(minHeight, childHeight);
      child = childParentData.nextSibling;
    }
    return minHeight;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
582
    RenderBox? child = firstChild;
583 584
    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
      maxHeight = math.max(maxHeight, childHeight);
      child = childParentData.nextSibling;
    }
    return maxHeight;
  }

  @override
594
  double? computeDistanceToActualBaseline(TextBaseline baseline) {
595 596 597 598 599 600
    return defaultComputeDistanceToHighestActualBaseline(baseline);
  }

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

605 606
  void _layoutRects(_NextChild nextChild, RenderBox? leftChild, RenderBox? rightChild) {
    RenderBox? child = leftChild;
607 608
    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
      final RRect rChildRect;
614
      if (child == leftChild) {
615 616 617 618 619
        rChildRect = RRect.fromRectAndCorners(
          childRect,
          topLeft: const Radius.circular(3.0),
          bottomLeft: const Radius.circular(3.0),
        );
620
      } else if (child == rightChild) {
621 622 623 624 625
        rChildRect = RRect.fromRectAndCorners(
          childRect,
          topRight: const Radius.circular(3.0),
          bottomRight: const Radius.circular(3.0),
        );
626
      } else {
627
        rChildRect = RRect.fromRectAndCorners(childRect);
628 629 630 631 632 633 634
      }
      childParentData.surroundingRect = rChildRect;
      start += child.size.width;
      child = nextChild(child);
    }
  }

635
  Size _calculateChildSize(BoxConstraints constraints) {
636
    double maxHeight = _kMinSegmentedControlHeight;
637
    double childWidth = constraints.minWidth / childCount;
638 639
    RenderBox? child = firstChild;
    while (child != null) {
640
      childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
641
      child = childAfter(child);
642
    }
643
    childWidth = math.min(childWidth, constraints.maxWidth / childCount);
644
    child = firstChild;
645 646 647 648 649
    while (child != null) {
      final double boxHeight = child.getMaxIntrinsicHeight(childWidth);
      maxHeight = math.max(maxHeight, boxHeight);
      child = childAfter(child);
    }
650 651
    return Size(childWidth, maxHeight);
  }
652

653 654 655 656 657 658 659 660 661 662 663 664 665 666
  Size _computeOverallSizeFromChildSize(Size childSize) {
    return constraints.constrain(Size(childSize.width * childCount, childSize.height));
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    final Size childSize = _calculateChildSize(constraints);
    return _computeOverallSizeFromChildSize(childSize);
  }

  @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    final Size childSize = _calculateChildSize(constraints);
667

668
    final BoxConstraints childConstraints = BoxConstraints.tightFor(
669 670
      width: childSize.width,
      height: childSize.height,
671 672
    );

673
    RenderBox? child = firstChild;
674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695
    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;
    }

696
    size = _computeOverallSizeFromChildSize(childSize);
697 698 699 700
  }

  @override
  void paint(PaintingContext context, Offset offset) {
701
    RenderBox? child = firstChild;
702 703 704 705 706 707 708 709 710 711 712
    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);

713
    final _SegmentedControlContainerBoxParentData childParentData = child.parentData! as _SegmentedControlContainerBoxParentData;
714 715

    context.canvas.drawRRect(
716
      childParentData.surroundingRect!.shift(offset),
717
      Paint()
718
        ..color = backgroundColors[childIndex]
719 720 721
        ..style = PaintingStyle.fill,
    );
    context.canvas.drawRRect(
722
      childParentData.surroundingRect!.shift(offset),
723
      Paint()
724 725 726
        ..color = borderColor
        ..strokeWidth = 1.0
        ..style = PaintingStyle.stroke,
727 728 729 730 731 732
    );

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

  @override
733
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
734
    assert(position != null);
735
    RenderBox? child = lastChild;
736
    while (child != null) {
737
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData! as _SegmentedControlContainerBoxParentData;
738
      if (childParentData.surroundingRect!.contains(position)) {
739 740 741 742 743
        return result.addWithPaintOffset(
          offset: childParentData.offset,
          position: position,
          hitTest: (BoxHitTestResult result, Offset localOffset) {
            assert(localOffset == position - childParentData.offset);
744
            return child!.hitTest(result, position: localOffset);
745 746
          },
        );
747 748 749 750
      }
      child = childParentData.previousSibling;
    }
    return false;
751 752
  }
}