segmented_control.dart 22.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
// Copyright 2018 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.

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 15

// Minimum padding from horizontal edges of segmented control to edges of
// encompassing widget.
16
const EdgeInsets _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 64
/// See also:
///
///  * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/>
65
class CupertinoSegmentedControl<T> extends StatefulWidget {
66 67
  /// Creates an iOS-style segmented control bar.
  ///
xster's avatar
xster committed
68
  /// The [children] and [onValueChanged] arguments must not be null. The
69 70 71 72 73 74 75 76 77 78
  /// [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
79 80
  /// appear as selected. The [groupValue] must be either null or one of the keys
  /// in the [children] map.
81
  CupertinoSegmentedControl({
82 83 84 85
    Key key,
    @required this.children,
    @required this.onValueChanged,
    this.groupValue,
xster's avatar
xster committed
86 87 88 89
    this.unselectedColor,
    this.selectedColor,
    this.borderColor,
    this.pressedColor,
90 91 92 93 94 95 96 97
  }) : 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);
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123

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

159 160 161
  /// The color used to fill the backgrounds of unselected widgets and as the
  /// text color of the selected widget.
  ///
xster's avatar
xster committed
162
  /// Defaults to [CupertinoTheme]'s `primaryContrastingColor` if null.
163 164 165 166 167
  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
168
  /// Defaults to [CupertinoTheme]'s `primaryColor` if null.
169 170 171 172
  final Color selectedColor;

  /// The color used as the border around each widget.
  ///
xster's avatar
xster committed
173
  /// Defaults to [CupertinoTheme]'s `primaryColor` if null.
174 175 176 177 178
  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
179
  /// Defaults to the selectedColor at 20% opacity if null.
180 181
  final Color pressedColor;

182 183 184 185
  @override
  _SegmentedControlState<T> createState() => _SegmentedControlState<T>();
}

186 187
class _SegmentedControlState<T> extends State<CupertinoSegmentedControl<T>>
    with TickerProviderStateMixin<CupertinoSegmentedControl<T>> {
188 189
  T _pressedKey;

190 191 192
  final List<AnimationController> _selectionControllers = <AnimationController>[];
  final List<ColorTween> _childTweens = <ColorTween>[];

193 194 195
  ColorTween _forwardBackgroundColorTween;
  ColorTween _reverseBackgroundColorTween;
  ColorTween _textColorTween;
196

xster's avatar
xster committed
197 198 199 200 201 202 203 204 205 206
  Color _selectedColor;
  Color _unselectedColor;
  Color _borderColor;
  Color _pressedColor;

  AnimationController createAnimationController() {
    return AnimationController(
      duration: _kFadeDuration,
      vsync: this,
    )..addListener(() {
207 208
      setState(() {
        // State of background/text colors has changed
xster's avatar
xster committed
209
      });
210
    });
xster's avatar
xster committed
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
  }

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

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

  void _updateAnimationControllers() {
    assert(mounted, 'This should only be called after didUpdateDependencies');
    for (AnimationController controller in _selectionControllers) {
      controller.dispose();
    }
    _selectionControllers.clear();
    _childTweens.clear();
259

260 261 262
    for (T key in widget.children.keys) {
      final AnimationController animationController = createAnimationController();
      if (widget.groupValue == key) {
263
        _childTweens.add(_reverseBackgroundColorTween);
264 265
        animationController.value = 1.0;
      } else {
266
        _childTweens.add(_forwardBackgroundColorTween);
267 268 269
      }
      _selectionControllers.add(animationController);
    }
270 271
  }

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

  @override
  void dispose() {
    for (AnimationController animationController in _selectionControllers) {
      animationController.dispose();
    }
    super.dispose();
  }

xster's avatar
xster committed
312

313 314 315 316 317 318
  void _onTapDown(T currentKey) {
    if (_pressedKey == null && currentKey != widget.groupValue) {
      setState(() {
        _pressedKey = currentKey;
      });
    }
319 320 321 322 323 324 325 326 327
  }

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

  void _onTap(T currentKey) {
328
    if (currentKey != widget.groupValue && currentKey == _pressedKey) {
329
      widget.onValueChanged(currentKey);
330 331 332 333 334 335
      _pressedKey = null;
    }
  }

  Color getTextColor(int index, T currentKey) {
    if (_selectionControllers[index].isAnimating)
336
      return _textColorTween.evaluate(_selectionControllers[index]);
337
    if (widget.groupValue == currentKey)
xster's avatar
xster committed
338 339
      return _unselectedColor;
    return _selectedColor;
340 341 342 343 344 345
  }

  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
346
      return _selectedColor;
347
    if (_pressedKey == currentKey)
xster's avatar
xster committed
348 349
      return _pressedColor;
    return _unselectedColor;
350 351 352 353
  }

  @override
  Widget build(BuildContext context) {
354 355
    final List<Widget> _gestureChildren = <Widget>[];
    final List<Color> _backgroundColors = <Color>[];
356 357 358 359 360 361 362 363
    int index = 0;
    int selectedIndex;
    int pressedIndex;
    for (T currentKey in widget.children.keys) {
      selectedIndex = (widget.groupValue == currentKey) ? index : selectedIndex;
      pressedIndex = (_pressedKey == currentKey) ? index : pressedIndex;

      final TextStyle textStyle = DefaultTextStyle.of(context).style.copyWith(
364
        color: getTextColor(index, currentKey),
365
      );
366
      final IconThemeData iconTheme = IconThemeData(
367
        color: getTextColor(index, currentKey),
368 369
      );

370
      Widget child = Center(
371 372 373
        child: widget.children[currentKey],
      );

374
      child = GestureDetector(
375 376 377 378 379 380 381
        onTapDown: (TapDownDetails event) {
          _onTapDown(currentKey);
        },
        onTapCancel: _onTapCancel,
        onTap: () {
          _onTap(currentKey);
        },
382
        child: IconTheme(
383
          data: iconTheme,
384
          child: DefaultTextStyle(
385
            style: textStyle,
386
            child: Semantics(
387
              button: true,
388 389 390 391 392 393 394
              inMutuallyExclusiveGroup: true,
              selected: widget.groupValue == currentKey,
              child: child,
            ),
          ),
        ),
      );
395 396 397

      _backgroundColors.add(getBackgroundColor(index, currentKey));
      _gestureChildren.add(child);
398 399 400
      index += 1;
    }

401
    final Widget box = _SegmentedControlRenderWidget<T>(
402
      children: _gestureChildren,
403 404
      selectedIndex: selectedIndex,
      pressedIndex: pressedIndex,
405
      backgroundColors: _backgroundColors,
xster's avatar
xster committed
406
      borderColor: _borderColor,
407 408
    );

409
    return Padding(
410
      padding: _kHorizontalItemPadding.resolve(Directionality.of(context)),
411
      child: UnconstrainedBox(
412 413 414 415 416 417 418 419 420 421 422 423 424
        constrainedAxis: Axis.horizontal,
        child: box,
      ),
    );
  }
}

class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
  _SegmentedControlRenderWidget({
    Key key,
    List<Widget> children = const <Widget>[],
    @required this.selectedIndex,
    @required this.pressedIndex,
425
    @required this.backgroundColors,
426
    @required this.borderColor,
427 428 429 430 431 432 433
  }) : super(
          key: key,
          children: children,
        );

  final int selectedIndex;
  final int pressedIndex;
434
  final List<Color> backgroundColors;
435
  final Color borderColor;
436 437 438

  @override
  RenderObject createRenderObject(BuildContext context) {
439
    return _RenderSegmentedControl<T>(
440 441 442
      textDirection: Directionality.of(context),
      selectedIndex: selectedIndex,
      pressedIndex: pressedIndex,
443
      backgroundColors: backgroundColors,
444
      borderColor: borderColor,
445 446 447 448 449 450 451 452
    );
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSegmentedControl<T> renderObject) {
    renderObject
      ..textDirection = Directionality.of(context)
      ..selectedIndex = selectedIndex
453
      ..pressedIndex = pressedIndex
454 455
      ..backgroundColors = backgroundColors
      ..borderColor = borderColor;
456 457 458 459 460 461 462
  }
}

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

463
typedef _NextChild = RenderBox Function(RenderBox child);
464 465 466 467 468 469 470 471 472

class _RenderSegmentedControl<T> extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, ContainerBoxParentData<RenderBox>>,
        RenderBoxContainerDefaultsMixin<RenderBox, ContainerBoxParentData<RenderBox>> {
  _RenderSegmentedControl({
    List<RenderBox> children,
    @required int selectedIndex,
    @required int pressedIndex,
    @required TextDirection textDirection,
473
    @required List<Color> backgroundColors,
474
    @required Color borderColor,
475 476 477 478 479 480
  }) : assert(textDirection != null),
       _textDirection = textDirection,
       _selectedIndex = selectedIndex,
       _pressedIndex = pressedIndex,
       _backgroundColors = backgroundColors,
       _borderColor = borderColor {
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
    addAll(children);
  }

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

514 515 516 517 518 519 520 521 522 523
  List<Color> get backgroundColors => _backgroundColors;
  List<Color> _backgroundColors;
  set backgroundColors(List<Color> value) {
    if (_backgroundColors == value) {
      return;
    }
    _backgroundColors = value;
    markNeedsPaint();
  }

524 525 526 527 528 529 530 531 532
  Color get borderColor => _borderColor;
  Color _borderColor;
  set borderColor(Color value) {
    if (_borderColor == value) {
      return;
    }
    _borderColor = value;
    markNeedsPaint();
  }
533 534 535 536 537 538 539

  @override
  double computeMinIntrinsicWidth(double height) {
    RenderBox child = firstChild;
    double minWidth = 0.0;
    while (child != null) {
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
Ian Hickson's avatar
Ian Hickson committed
540
      final double childWidth = child.getMinIntrinsicWidth(height);
541 542 543 544 545 546 547 548 549 550 551 552
      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) {
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
Ian Hickson's avatar
Ian Hickson committed
553
      final double childWidth = child.getMaxIntrinsicWidth(height);
554 555 556 557 558 559 560 561 562 563 564 565
      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) {
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
Ian Hickson's avatar
Ian Hickson committed
566
      final double childHeight = child.getMinIntrinsicHeight(width);
567 568 569 570 571 572 573 574 575 576 577 578
      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) {
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
Ian Hickson's avatar
Ian Hickson committed
579
      final double childHeight = child.getMaxIntrinsicHeight(width);
580 581 582 583 584 585 586 587 588 589 590 591 592 593
      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) {
594
      child.parentData = _SegmentedControlContainerBoxParentData();
595 596 597 598 599 600 601 602
    }
  }

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

  @override
  void performLayout() {
    double maxHeight = _kMinSegmentedControlHeight;

626 627 628
    double childWidth = constraints.minWidth / childCount;
    for (RenderBox child in getChildrenAsList()) {
      childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
629
    }
630
    childWidth = math.min(childWidth, constraints.maxWidth / childCount);
631 632 633 634 635 636 637 638 639 640

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

    constraints.constrainHeight(maxHeight);

641
    final BoxConstraints childConstraints = BoxConstraints.tightFor(
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668
      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;
    }

669
    size = constraints.constrain(Size(childWidth * childCount, maxHeight));
670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689
  }

  @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);

    final _SegmentedControlContainerBoxParentData childParentData = child.parentData;

    context.canvas.drawRRect(
      childParentData.surroundingRect.shift(offset),
690
      Paint()
691
        ..color = backgroundColors[childIndex]
692 693 694 695
        ..style = PaintingStyle.fill,
    );
    context.canvas.drawRRect(
      childParentData.surroundingRect.shift(offset),
696
      Paint()
697 698 699
        ..color = borderColor
        ..strokeWidth = 1.0
        ..style = PaintingStyle.stroke,
700 701 702 703 704 705
    );

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

  @override
706
  bool hitTestChildren(HitTestResult result, { @required Offset position }) {
707
    assert(position != null);
708 709 710 711 712 713 714 715 716
    RenderBox child = lastChild;
    while (child != null) {
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
      if (childParentData.surroundingRect.contains(position)) {
        return child.hitTest(result, position: (Offset.zero & child.size).center);
      }
      child = childParentData.previousSibling;
    }
    return false;
717 718
  }
}