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

5
import 'package:flutter/cupertino.dart';
6
import 'package:flutter/foundation.dart';
7
import 'package:flutter/gestures.dart';
8 9
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
10

11
import 'colors.dart';
12
import 'constants.dart';
13
import 'debug.dart';
Adam Barth's avatar
Adam Barth committed
14
import 'shadows.dart';
15
import 'theme.dart';
16
import 'theme_data.dart';
17
import 'toggleable.dart';
18

19 20 21 22 23 24 25 26
const double _kTrackHeight = 14.0;
const double _kTrackWidth = 33.0;
const double _kTrackRadius = _kTrackHeight / 2.0;
const double _kThumbRadius = 10.0;
const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReactionRadius;
const double _kSwitchHeight = 2 * kRadialReactionRadius + 8.0;
const double _kSwitchHeightCollapsed = 2 * kRadialReactionRadius;

27 28
enum _SwitchType { material, adaptive }

29 30 31 32 33 34 35 36 37
/// A material design switch.
///
/// Used to toggle the on/off state of a single setting.
///
/// The switch itself does not maintain any state. Instead, when the state of
/// the switch changes, the widget calls the [onChanged] callback. Most widgets
/// that use a switch will listen for the [onChanged] callback and rebuild the
/// switch with a new [value] to update the visual appearance of the switch.
///
38 39 40 41 42
/// If the [onChanged] callback is null, then the switch will be disabled (it
/// will not respond to input). A disabled switch's thumb and track are rendered
/// in shades of grey by default. The default appearance of a disabled switch
/// can be overridden with [inactiveThumbColor] and [inactiveTrackColor].
///
43 44 45
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
46
///
47 48
///  * [SwitchListTile], which combines this widget with a [ListTile] so that
///    you can give the switch a label.
49 50 51
///  * [Checkbox], another widget with similar semantics.
///  * [Radio], for selecting among a set of explicit values.
///  * [Slider], for selecting a value in a range.
52
///  * <https://material.io/design/components/selection-controls.html#switches>
53
class Switch extends StatefulWidget {
54 55 56 57 58 59 60
  /// Creates a material design switch.
  ///
  /// The switch itself does not maintain any state. Instead, when the state of
  /// the switch changes, the widget calls the [onChanged] callback. Most widgets
  /// that use a switch will listen for the [onChanged] callback and rebuild the
  /// switch with a new [value] to update the visual appearance of the switch.
  ///
61 62 63 64
  /// The following arguments are required:
  ///
  /// * [value] determines whether this switch is on or off.
  /// * [onChanged] is called when the user toggles the switch on or off.
65
  const Switch({
66
    Key key,
67 68
    @required this.value,
    @required this.onChanged,
69
    this.activeColor,
70 71 72
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
73
    this.activeThumbImage,
74 75
    this.inactiveThumbImage,
    this.materialTapTargetSize,
76
    this.dragStartBehavior = DragStartBehavior.start,
77 78 79 80 81 82 83
    this.focusColor,
    this.hoverColor,
    this.focusNode,
    this.autofocus = false,
  })  : _switchType = _SwitchType.material,
        assert(dragStartBehavior != null),
        super(key: key);
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103

  /// Creates a [CupertinoSwitch] if the target platform is iOS, creates a
  /// material design switch otherwise.
  ///
  /// If a [CupertinoSwitch] is created, the following parameters are
  /// ignored: [activeTrackColor], [inactiveThumbColor], [inactiveTrackColor],
  /// [activeThumbImage], [inactiveThumbImage], [materialTapTargetSize].
  ///
  /// The target platform is based on the current [Theme]: [ThemeData.platform].
  const Switch.adaptive({
    Key key,
    @required this.value,
    @required this.onChanged,
    this.activeColor,
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
    this.activeThumbImage,
    this.inactiveThumbImage,
    this.materialTapTargetSize,
104
    this.dragStartBehavior = DragStartBehavior.start,
105 106 107 108 109 110 111
    this.focusColor,
    this.hoverColor,
    this.focusNode,
    this.autofocus = false,
  })  : assert(autofocus != null),
        _switchType = _SwitchType.adaptive,
        super(key: key);
112

113
  /// Whether this switch is on or off.
114 115
  ///
  /// This property must not be null.
116
  final bool value;
117

118
  /// Called when the user toggles the switch on or off.
119 120 121 122 123 124
  ///
  /// The switch passes the new value to the callback but does not actually
  /// change state until the parent widget rebuilds the switch with the new
  /// value.
  ///
  /// If null, the switch will be displayed as disabled.
125
  ///
126
  /// The callback provided to [onChanged] should update the state of the parent
127 128 129 130
  /// [StatefulWidget] using the [State.setState] method, so that the parent
  /// gets rebuilt; for example:
  ///
  /// ```dart
131
  /// Switch(
132 133 134 135 136 137
  ///   value: _giveVerse,
  ///   onChanged: (bool newValue) {
  ///     setState(() {
  ///       _giveVerse = newValue;
  ///     });
  ///   },
138
  /// )
139
  /// ```
140 141
  final ValueChanged<bool> onChanged;

142 143
  /// The color to use when this switch is on.
  ///
144
  /// Defaults to [ThemeData.toggleableActiveColor].
145
  final Color activeColor;
146

147 148
  /// The color to use on the track when this switch is on.
  ///
149
  /// Defaults to [ThemeData.toggleableActiveColor] with the opacity set at 50%.
150 151
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
152 153 154 155 156
  final Color activeTrackColor;

  /// The color to use on the thumb when this switch is off.
  ///
  /// Defaults to the colors described in the Material design specification.
157 158
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
159 160 161 162 163
  final Color inactiveThumbColor;

  /// The color to use on the track when this switch is off.
  ///
  /// Defaults to the colors described in the Material design specification.
164 165
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
166 167
  final Color inactiveTrackColor;

168
  /// An image to use on the thumb of this switch when the switch is on.
169 170
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
171
  final ImageProvider activeThumbImage;
172

173
  /// An image to use on the thumb of this switch when the switch is off.
174 175
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
176
  final ImageProvider inactiveThumbImage;
177

178 179 180 181 182 183
  /// Configures the minimum size of the tap target.
  ///
  /// Defaults to [ThemeData.materialTapTargetSize].
  ///
  /// See also:
  ///
184
  ///  * [MaterialTapTargetSize], for a description of how this affects tap targets.
185 186
  final MaterialTapTargetSize materialTapTargetSize;

187 188
  final _SwitchType _switchType;

189 190 191
  /// {@macro flutter.cupertino.switch.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

192 193 194 195 196 197 198 199 200 201 202 203
  /// The color for the button's [Material] when it has the input focus.
  final Color focusColor;

  /// The color for the button's [Material] when a pointer is hovering over it.
  final Color hoverColor;

  /// {@macro flutter.widgets.Focus.focusNode}
  final FocusNode focusNode;

  /// {@macro flutter.widgets.Focus.autofocus}
  final bool autofocus;

204
  @override
205
  _SwitchState createState() => _SwitchState();
206 207

  @override
208 209
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
210 211
    properties.add(FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
    properties.add(ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled'));
212 213 214 215
  }
}

class _SwitchState extends State<Switch> with TickerProviderStateMixin {
216 217 218 219 220 221
  Map<LocalKey, ActionFactory> _actionMap;

  @override
  void initState() {
    super.initState();
    _actionMap = <LocalKey, ActionFactory>{
222
      ActivateAction.key: _createAction,
223 224 225 226 227 228 229 230 231 232 233 234 235
    };
  }

  void _actionHandler(FocusNode node, Intent intent){
    if (widget.onChanged != null) {
      widget.onChanged(!widget.value);
    }
    final RenderObject renderObject = node.context.findRenderObject();
    renderObject.sendSemanticsEvent(const TapSemanticEvent());
  }

  Action _createAction() {
    return CallbackAction(
236
      ActivateAction.key,
237 238 239 240
      onInvoke: _actionHandler,
    );
  }

241 242 243 244
  bool _focused = false;
  void _handleFocusHighlightChanged(bool focused) {
    if (focused != _focused) {
      setState(() { _focused = focused; });
245 246 247
    }
  }

248 249 250 251
  bool _hovering = false;
  void _handleHoverChanged(bool hovering) {
    if (hovering != _hovering) {
      setState(() { _hovering = hovering; });
252 253 254
    }
  }

255 256 257 258 259 260 261 262 263 264 265 266 267
  Size getSwitchSize(ThemeData theme) {
    switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
      case MaterialTapTargetSize.padded:
        return const Size(_kSwitchWidth, _kSwitchHeight);
        break;
      case MaterialTapTargetSize.shrinkWrap:
        return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
        break;
    }
    assert(false);
    return null;
  }

268 269
  bool get enabled => widget.onChanged != null;

270
  Widget buildMaterialSwitch(BuildContext context) {
271
    assert(debugCheckHasMaterial(context));
272 273
    final ThemeData theme = Theme.of(context);
    final bool isDark = theme.brightness == Brightness.dark;
274

275
    final Color activeThumbColor = widget.activeColor ?? theme.toggleableActiveColor;
276
    final Color activeTrackColor = widget.activeTrackColor ?? activeThumbColor.withAlpha(0x80);
277 278
    final Color hoverColor = widget.hoverColor ?? theme.hoverColor;
    final Color focusColor = widget.focusColor ?? theme.focusColor;
279 280 281

    Color inactiveThumbColor;
    Color inactiveTrackColor;
282
    if (enabled) {
283
      const Color black32 = Color(0x52000000); // Black with 32% opacity
284
      inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade400 : Colors.grey.shade50);
285
      inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white30 : black32);
286
    } else {
287 288
      inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400);
      inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
289 290
    }

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
    return FocusableActionDetector(
      actions: _actionMap,
      focusNode: widget.focusNode,
      autofocus: widget.autofocus,
      enabled: enabled,
      onShowFocusHighlight: _handleFocusHighlightChanged,
      onShowHoverHighlight: _handleHoverChanged,
      child: Builder(
        builder: (BuildContext context) {
          return _SwitchRenderObjectWidget(
            dragStartBehavior: widget.dragStartBehavior,
            value: widget.value,
            activeColor: activeThumbColor,
            inactiveColor: inactiveThumbColor,
            hoverColor: hoverColor,
            focusColor: focusColor,
            activeThumbImage: widget.activeThumbImage,
            inactiveThumbImage: widget.inactiveThumbImage,
            activeTrackColor: activeTrackColor,
            inactiveTrackColor: inactiveTrackColor,
            configuration: createLocalImageConfiguration(context),
            onChanged: widget.onChanged,
            additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
            hasFocus: _focused,
            hovering: _hovering,
            vsync: this,
          );
        },
319
      ),
320 321
    );
  }
322 323 324

  Widget buildCupertinoSwitch(BuildContext context) {
    final Size size = getSwitchSize(Theme.of(context));
325 326 327 328 329 330 331 332 333 334 335 336
    return Focus(
      focusNode: widget.focusNode,
      autofocus: widget.autofocus,
      child: Container(
        width: size.width, // Same size as the Material switch.
        height: size.height,
        alignment: Alignment.center,
        child: CupertinoSwitch(
          dragStartBehavior: widget.dragStartBehavior,
          value: widget.value,
          onChanged: widget.onChanged,
          activeColor: widget.activeColor,
337
          trackColor: widget.inactiveTrackColor
338
        ),
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    switch (widget._switchType) {
      case _SwitchType.material:
        return buildMaterialSwitch(context);

      case _SwitchType.adaptive: {
        final ThemeData theme = Theme.of(context);
        assert(theme.platform != null);
        switch (theme.platform) {
          case TargetPlatform.android:
          case TargetPlatform.fuchsia:
355 356
          case TargetPlatform.linux:
          case TargetPlatform.windows:
357 358
            return buildMaterialSwitch(context);
          case TargetPlatform.iOS:
359
          case TargetPlatform.macOS:
360 361 362 363 364 365 366
            return buildCupertinoSwitch(context);
        }
      }
    }
    assert(false);
    return null;
  }
367 368
}

369
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
370
  const _SwitchRenderObjectWidget({
371 372
    Key key,
    this.value,
373 374
    this.activeColor,
    this.inactiveColor,
375 376
    this.hoverColor,
    this.focusColor,
377 378
    this.activeThumbImage,
    this.inactiveThumbImage,
379 380
    this.activeTrackColor,
    this.inactiveTrackColor,
381
    this.configuration,
382 383
    this.onChanged,
    this.vsync,
384
    this.additionalConstraints,
385
    this.dragStartBehavior,
386 387
    this.hasFocus,
    this.hovering,
388
  }) : super(key: key);
389 390

  final bool value;
391 392
  final Color activeColor;
  final Color inactiveColor;
393 394
  final Color hoverColor;
  final Color focusColor;
395 396
  final ImageProvider activeThumbImage;
  final ImageProvider inactiveThumbImage;
397 398
  final Color activeTrackColor;
  final Color inactiveTrackColor;
399
  final ImageConfiguration configuration;
Hixie's avatar
Hixie committed
400
  final ValueChanged<bool> onChanged;
401
  final TickerProvider vsync;
402
  final BoxConstraints additionalConstraints;
403
  final DragStartBehavior dragStartBehavior;
404 405
  final bool hasFocus;
  final bool hovering;
406

407
  @override
408
  _RenderSwitch createRenderObject(BuildContext context) {
409
    return _RenderSwitch(
410
      dragStartBehavior: dragStartBehavior,
411 412 413
      value: value,
      activeColor: activeColor,
      inactiveColor: inactiveColor,
414 415
      hoverColor: hoverColor,
      focusColor: focusColor,
416 417 418 419 420 421 422
      activeThumbImage: activeThumbImage,
      inactiveThumbImage: inactiveThumbImage,
      activeTrackColor: activeTrackColor,
      inactiveTrackColor: inactiveTrackColor,
      configuration: configuration,
      onChanged: onChanged,
      textDirection: Directionality.of(context),
423
      additionalConstraints: additionalConstraints,
424 425
      hasFocus: hasFocus,
      hovering: hovering,
426 427 428
      vsync: vsync,
    );
  }
429

430
  @override
431
  void updateRenderObject(BuildContext context, _RenderSwitch renderObject) {
432 433 434 435
    renderObject
      ..value = value
      ..activeColor = activeColor
      ..inactiveColor = inactiveColor
436 437
      ..hoverColor = hoverColor
      ..focusColor = focusColor
438 439
      ..activeThumbImage = activeThumbImage
      ..inactiveThumbImage = inactiveThumbImage
440 441
      ..activeTrackColor = activeTrackColor
      ..inactiveTrackColor = inactiveTrackColor
442
      ..configuration = configuration
443
      ..onChanged = onChanged
444
      ..textDirection = Directionality.of(context)
445
      ..additionalConstraints = additionalConstraints
446
      ..dragStartBehavior = dragStartBehavior
447 448
      ..hasFocus = hasFocus
      ..hovering = hovering
449
      ..vsync = vsync;
450 451 452
  }
}

453
class _RenderSwitch extends RenderToggleable {
454 455
  _RenderSwitch({
    bool value,
456 457
    Color activeColor,
    Color inactiveColor,
458 459
    Color hoverColor,
    Color focusColor,
460 461
    ImageProvider activeThumbImage,
    ImageProvider inactiveThumbImage,
462 463
    Color activeTrackColor,
    Color inactiveTrackColor,
464
    ImageConfiguration configuration,
465
    BoxConstraints additionalConstraints,
466
    @required TextDirection textDirection,
467
    ValueChanged<bool> onChanged,
468
    DragStartBehavior dragStartBehavior,
469 470 471
    bool hasFocus,
    bool hovering,
    @required TickerProvider vsync,
472 473
  }) : assert(textDirection != null),
       _activeThumbImage = activeThumbImage,
474
       _inactiveThumbImage = inactiveThumbImage,
475 476
       _activeTrackColor = activeTrackColor,
       _inactiveTrackColor = inactiveTrackColor,
477
       _configuration = configuration,
478
       _textDirection = textDirection,
479 480
       super(
         value: value,
481
         tristate: false,
482 483
         activeColor: activeColor,
         inactiveColor: inactiveColor,
484 485
         hoverColor: hoverColor,
         focusColor: focusColor,
486
         onChanged: onChanged,
487
         additionalConstraints: additionalConstraints,
488 489
         hasFocus: hasFocus,
         hovering: hovering,
490
         vsync: vsync,
491
       ) {
492
    _drag = HorizontalDragGestureRecognizer()
493 494
      ..onStart = _handleDragStart
      ..onUpdate = _handleDragUpdate
495 496
      ..onEnd = _handleDragEnd
      ..dragStartBehavior = dragStartBehavior;
497 498
  }

499 500 501 502
  ImageProvider get activeThumbImage => _activeThumbImage;
  ImageProvider _activeThumbImage;
  set activeThumbImage(ImageProvider value) {
    if (value == _activeThumbImage)
503
      return;
504
    _activeThumbImage = value;
505 506 507
    markNeedsPaint();
  }

508 509 510 511
  ImageProvider get inactiveThumbImage => _inactiveThumbImage;
  ImageProvider _inactiveThumbImage;
  set inactiveThumbImage(ImageProvider value) {
    if (value == _inactiveThumbImage)
512
      return;
513
    _inactiveThumbImage = value;
514 515 516
    markNeedsPaint();
  }

517 518
  Color get activeTrackColor => _activeTrackColor;
  Color _activeTrackColor;
519
  set activeTrackColor(Color value) {
520 521 522 523 524 525 526 527 528
    assert(value != null);
    if (value == _activeTrackColor)
      return;
    _activeTrackColor = value;
    markNeedsPaint();
  }

  Color get inactiveTrackColor => _inactiveTrackColor;
  Color _inactiveTrackColor;
529
  set inactiveTrackColor(Color value) {
530 531 532 533 534 535 536
    assert(value != null);
    if (value == _inactiveTrackColor)
      return;
    _inactiveTrackColor = value;
    markNeedsPaint();
  }

537 538
  ImageConfiguration get configuration => _configuration;
  ImageConfiguration _configuration;
539
  set configuration(ImageConfiguration value) {
540 541 542 543 544
    assert(value != null);
    if (value == _configuration)
      return;
    _configuration = value;
    markNeedsPaint();
545 546
  }

547 548 549 550 551 552 553 554 555 556
  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
    assert(value != null);
    if (_textDirection == value)
      return;
    _textDirection = value;
    markNeedsPaint();
  }

557 558 559
  DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior;
  set dragStartBehavior(DragStartBehavior value) {
    assert(value != null);
560
    if (_drag.dragStartBehavior == value)
561 562 563 564
      return;
    _drag.dragStartBehavior = value;
  }

565 566
  @override
  void detach() {
567 568
    _cachedThumbPainter?.dispose();
    _cachedThumbPainter = null;
569 570 571
    super.detach();
  }

572
  double get _trackInnerLength => size.width - 2.0 * kRadialReactionRadius;
573

574 575
  HorizontalDragGestureRecognizer _drag;

576
  void _handleDragStart(DragStartDetails details) {
577
    if (isInteractive)
578
      reactionController.forward();
579 580
  }

581
  void _handleDragUpdate(DragUpdateDetails details) {
582
    if (isInteractive) {
583
      position
584 585
        ..curve = null
        ..reverseCurve = null;
586 587 588 589 590 591 592 593 594
      final double delta = details.primaryDelta / _trackInnerLength;
      switch (textDirection) {
        case TextDirection.rtl:
          positionController.value -= delta;
          break;
        case TextDirection.ltr:
          positionController.value += delta;
          break;
      }
595 596 597
    }
  }

598
  void _handleDragEnd(DragEndDetails details) {
599 600
    if (position.value >= 0.5)
      positionController.forward();
601
    else
602 603
      positionController.reverse();
    reactionController.reverse();
604 605
  }

606
  @override
Ian Hickson's avatar
Ian Hickson committed
607
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
608
    assert(debugHandleEvent(event, entry));
Ian Hickson's avatar
Ian Hickson committed
609
    if (event is PointerDownEvent && onChanged != null)
610 611
      _drag.addPointer(event);
    super.handleEvent(event, entry);
612 613
  }

614
  Color _cachedThumbColor;
615
  ImageProvider _cachedThumbImage;
616 617
  BoxPainter _cachedThumbPainter;

618
  BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider image) {
619
    return BoxDecoration(
620
      color: color,
621
      image: image == null ? null : DecorationImage(image: image),
622
      shape: BoxShape.circle,
623
      boxShadow: kElevationToShadow[1],
624 625
    );
  }
626

627 628 629 630 631 632 633 634 635 636 637
  bool _isPainting = false;

  void _handleDecorationChanged() {
    // If the image decoration is available synchronously, we'll get called here
    // during paint. There's no reason to mark ourselves as needing paint if we
    // are already in the middle of painting. (In fact, doing so would trigger
    // an assert).
    if (!_isPainting)
      markNeedsPaint();
  }

638 639 640 641 642 643
  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config.isToggled = value == true;
  }

644
  @override
645
  void paint(PaintingContext context, Offset offset) {
Adam Barth's avatar
Adam Barth committed
646
    final Canvas canvas = context.canvas;
647
    final bool isEnabled = onChanged != null;
648 649 650 651 652 653 654 655 656 657 658
    final double currentValue = position.value;

    double visualPosition;
    switch (textDirection) {
      case TextDirection.rtl:
        visualPosition = 1.0 - currentValue;
        break;
      case TextDirection.ltr:
        visualPosition = currentValue;
        break;
    }
659

660 661 662 663 664 665 666 667 668 669 670
    final Color trackColor = isEnabled
      ? Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)
      : inactiveTrackColor;

    final Color thumbColor = isEnabled
      ? Color.lerp(inactiveColor, activeColor, currentValue)
      : inactiveColor;

    final ImageProvider thumbImage = isEnabled
      ? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage)
      : inactiveThumbImage;
671

672
    // Paint the track
673
    final Paint paint = Paint()
674
      ..color = trackColor;
675
    const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
676
    final Rect trackRect = Rect.fromLTWH(
677 678 679
      offset.dx + trackHorizontalPadding,
      offset.dy + (size.height - _kTrackHeight) / 2.0,
      size.width - 2.0 * trackHorizontalPadding,
680
      _kTrackHeight,
681
    );
682
    final RRect trackRRect = RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius));
683 684
    canvas.drawRRect(trackRRect, paint);

685
    final Offset thumbPosition = Offset(
686
      kRadialReactionRadius + visualPosition * _trackInnerLength,
687
      size.height / 2.0,
688
    );
689

Adam Barth's avatar
Adam Barth committed
690
    paintRadialReaction(canvas, offset, thumbPosition);
691

692 693 694 695
    try {
      _isPainting = true;
      BoxPainter thumbPainter;
      if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage) {
696
        _cachedThumbColor = thumbColor;
697 698
        _cachedThumbImage = thumbImage;
        _cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage).createBoxPainter(_handleDecorationChanged);
699 700
      }
      thumbPainter = _cachedThumbPainter;
701

702
      // The thumb contracts slightly during the animation
703
      final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
704 705 706
      final double radius = _kThumbRadius - inset;
      thumbPainter.paint(
        canvas,
707
        thumbPosition + offset - Offset(radius, radius),
708
        configuration.copyWith(size: Size.fromRadius(radius)),
709 710 711 712
      );
    } finally {
      _isPainting = false;
    }
713 714
  }
}