segmented_control.dart 23.1 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
// 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 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
    this.padding,
91 92 93 94 95 96 97 98
  }) : 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);
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 124

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

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

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

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

188 189 190 191
  @override
  _SegmentedControlState<T> createState() => _SegmentedControlState<T>();
}

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

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

199 200 201
  ColorTween _forwardBackgroundColorTween;
  ColorTween _reverseBackgroundColorTween;
  ColorTween _textColorTween;
202

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

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

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

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

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

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

xster's avatar
xster committed
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
  @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;
      }
    }
308 309 310 311 312 313 314 315 316 317
  }

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

xster's avatar
xster committed
318

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

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

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

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

  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
352
      return _selectedColor;
353
    if (_pressedKey == currentKey)
xster's avatar
xster committed
354 355
      return _pressedColor;
    return _unselectedColor;
356 357 358 359
  }

  @override
  Widget build(BuildContext context) {
360 361
    final List<Widget> _gestureChildren = <Widget>[];
    final List<Color> _backgroundColors = <Color>[];
362 363 364 365 366 367 368 369
    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(
370
        color: getTextColor(index, currentKey),
371
      );
372
      final IconThemeData iconTheme = IconThemeData(
373
        color: getTextColor(index, currentKey),
374 375
      );

376
      Widget child = Center(
377 378 379
        child: widget.children[currentKey],
      );

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

      _backgroundColors.add(getBackgroundColor(index, currentKey));
      _gestureChildren.add(child);
404 405 406
      index += 1;
    }

407
    final Widget box = _SegmentedControlRenderWidget<T>(
408
      children: _gestureChildren,
409 410
      selectedIndex: selectedIndex,
      pressedIndex: pressedIndex,
411
      backgroundColors: _backgroundColors,
xster's avatar
xster committed
412
      borderColor: _borderColor,
413 414
    );

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

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

  final int selectedIndex;
  final int pressedIndex;
440
  final List<Color> backgroundColors;
441
  final Color borderColor;
442 443 444

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

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

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

469
typedef _NextChild = RenderBox Function(RenderBox child);
470 471 472 473 474 475 476 477 478

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,
479
    @required List<Color> backgroundColors,
480
    @required Color borderColor,
481 482 483 484 485 486
  }) : assert(textDirection != null),
       _textDirection = textDirection,
       _selectedIndex = selectedIndex,
       _pressedIndex = pressedIndex,
       _backgroundColors = backgroundColors,
       _borderColor = borderColor {
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 514 515 516 517 518 519
    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();
  }

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

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

  @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
546
      final double childWidth = child.getMinIntrinsicWidth(height);
547 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) {
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
Ian Hickson's avatar
Ian Hickson committed
559
      final double childWidth = child.getMaxIntrinsicWidth(height);
560 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) {
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
Ian Hickson's avatar
Ian Hickson committed
572
      final double childHeight = child.getMinIntrinsicHeight(width);
573 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) {
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
Ian Hickson's avatar
Ian Hickson committed
585
      final double childHeight = child.getMaxIntrinsicHeight(width);
586 587 588 589 590 591 592 593 594 595 596 597 598 599
      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) {
600
      child.parentData = _SegmentedControlContainerBoxParentData();
601 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) {
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
609
      final Offset childOffset = Offset(start, 0.0);
610
      childParentData.offset = childOffset;
611
      final Rect childRect = Rect.fromLTWH(start, 0.0, child.size.width, child.size.height);
612 613
      RRect rChildRect;
      if (child == leftChild) {
614
        rChildRect = RRect.fromRectAndCorners(childRect, topLeft: const Radius.circular(3.0),
615 616
            bottomLeft: const Radius.circular(3.0));
      } else if (child == rightChild) {
617
        rChildRect = RRect.fromRectAndCorners(childRect, topRight: const Radius.circular(3.0),
618 619
            bottomRight: const Radius.circular(3.0));
      } else {
620
        rChildRect = RRect.fromRectAndCorners(childRect);
621 622 623 624 625 626 627 628 629 630 631
      }
      childParentData.surroundingRect = rChildRect;
      start += child.size.width;
      child = nextChild(child);
    }
  }

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

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

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

    constraints.constrainHeight(maxHeight);

647
    final BoxConstraints childConstraints = BoxConstraints.tightFor(
648 649 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
      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;
    }

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

  @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),
696
      Paint()
697
        ..color = backgroundColors[childIndex]
698 699 700 701
        ..style = PaintingStyle.fill,
    );
    context.canvas.drawRRect(
      childParentData.surroundingRect.shift(offset),
702
      Paint()
703 704 705
        ..color = borderColor
        ..strokeWidth = 1.0
        ..style = PaintingStyle.stroke,
706 707 708 709 710 711
    );

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

  @override
712
  bool hitTestChildren(BoxHitTestResult result, { @required Offset position }) {
713
    assert(position != null);
714 715 716 717
    RenderBox child = lastChild;
    while (child != null) {
      final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
      if (childParentData.surroundingRect.contains(position)) {
718 719 720 721 722 723 724 725 726
        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);
          },
        );
727 728 729 730
      }
      child = childParentData.previousSibling;
    }
    return false;
731 732
  }
}