switch.dart 37.3 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
import 'package:flutter/rendering.dart';
9

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

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

29 30
enum _SwitchType { material, adaptive }

31 32 33 34 35 36 37 38 39
/// 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.
///
40 41 42 43 44
/// 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].
///
45 46 47
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
48
///
49 50
///  * [SwitchListTile], which combines this widget with a [ListTile] so that
///    you can give the switch a label.
51 52 53
///  * [Checkbox], another widget with similar semantics.
///  * [Radio], for selecting among a set of explicit values.
///  * [Slider], for selecting a value in a range.
54
///  * <https://material.io/design/components/selection-controls.html#switches>
55
class Switch extends StatelessWidget {
56 57 58 59 60 61 62
  /// 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.
  ///
63 64 65 66
  /// 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.
67
  const Switch({
68 69 70
    Key? key,
    required this.value,
    required this.onChanged,
71
    this.activeColor,
72 73 74
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
75
    this.activeThumbImage,
76
    this.onActiveThumbImageError,
77
    this.inactiveThumbImage,
78
    this.onInactiveThumbImageError,
79 80
    this.thumbColor,
    this.trackColor,
81
    this.materialTapTargetSize,
82
    this.dragStartBehavior = DragStartBehavior.start,
83
    this.mouseCursor,
84 85
    this.focusColor,
    this.hoverColor,
86
    this.overlayColor,
87
    this.splashRadius,
88 89 90 91
    this.focusNode,
    this.autofocus = false,
  })  : _switchType = _SwitchType.material,
        assert(dragStartBehavior != null),
92 93
        assert(activeThumbImage != null || onActiveThumbImageError == null),
        assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
94
        super(key: key);
95

96 97 98
  /// Creates an adaptive [Switch] based on whether the target platform is iOS
  /// or macOS, following Material design's
  /// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
99
  ///
100 101 102 103 104 105 106
  /// On iOS and macOS, this constructor creates a [CupertinoSwitch], which has
  /// matching functionality and presentation as Material switches, and are the
  /// graphics expected on iOS. On other platforms, this creates a Material
  /// design [Switch].
  ///
  /// If a [CupertinoSwitch] is created, the following parameters are ignored:
  /// [activeTrackColor], [inactiveThumbColor], [inactiveTrackColor],
107
  /// [activeThumbImage], [onActiveThumbImageError], [inactiveThumbImage],
108
  /// [onInactiveThumbImageError], [materialTapTargetSize].
109 110 111
  ///
  /// The target platform is based on the current [Theme]: [ThemeData.platform].
  const Switch.adaptive({
112 113 114
    Key? key,
    required this.value,
    required this.onChanged,
115 116 117 118 119
    this.activeColor,
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
    this.activeThumbImage,
120
    this.onActiveThumbImageError,
121
    this.inactiveThumbImage,
122
    this.onInactiveThumbImageError,
123
    this.materialTapTargetSize,
124 125
    this.thumbColor,
    this.trackColor,
126
    this.dragStartBehavior = DragStartBehavior.start,
127
    this.mouseCursor,
128 129
    this.focusColor,
    this.hoverColor,
130
    this.overlayColor,
131
    this.splashRadius,
132 133 134
    this.focusNode,
    this.autofocus = false,
  })  : assert(autofocus != null),
135 136
        assert(activeThumbImage != null || onActiveThumbImageError == null),
        assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
137 138
        _switchType = _SwitchType.adaptive,
        super(key: key);
139

140
  /// Whether this switch is on or off.
141 142
  ///
  /// This property must not be null.
143
  final bool value;
144

145
  /// Called when the user toggles the switch on or off.
146 147 148 149 150 151
  ///
  /// 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.
152
  ///
153
  /// The callback provided to [onChanged] should update the state of the parent
154 155 156 157
  /// [StatefulWidget] using the [State.setState] method, so that the parent
  /// gets rebuilt; for example:
  ///
  /// ```dart
158
  /// Switch(
159 160 161 162 163 164
  ///   value: _giveVerse,
  ///   onChanged: (bool newValue) {
  ///     setState(() {
  ///       _giveVerse = newValue;
  ///     });
  ///   },
165
  /// )
166
  /// ```
167
  final ValueChanged<bool>? onChanged;
168

169 170
  /// The color to use when this switch is on.
  ///
171
  /// Defaults to [ThemeData.toggleableActiveColor].
172 173 174
  ///
  /// If [thumbColor] returns a non-null color in the [MaterialState.selected]
  /// state, it will be used instead of this color.
175
  final Color? activeColor;
176

177 178
  /// The color to use on the track when this switch is on.
  ///
179
  /// Defaults to [ThemeData.toggleableActiveColor] with the opacity set at 50%.
180 181
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
182 183 184
  ///
  /// If [trackColor] returns a non-null color in the [MaterialState.selected]
  /// state, it will be used instead of this color.
185
  final Color? activeTrackColor;
186 187 188 189

  /// The color to use on the thumb when this switch is off.
  ///
  /// Defaults to the colors described in the Material design specification.
190 191
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
192 193 194
  ///
  /// If [thumbColor] returns a non-null color in the default state, it will be
  /// used instead of this color.
195
  final Color? inactiveThumbColor;
196 197 198 199

  /// The color to use on the track when this switch is off.
  ///
  /// Defaults to the colors described in the Material design specification.
200 201
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
202 203 204
  ///
  /// If [trackColor] returns a non-null color in the default state, it will be
  /// used instead of this color.
205
  final Color? inactiveTrackColor;
206

207
  /// An image to use on the thumb of this switch when the switch is on.
208 209
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
210
  final ImageProvider? activeThumbImage;
211

212 213
  /// An optional error callback for errors emitted when loading
  /// [activeThumbImage].
214
  final ImageErrorListener? onActiveThumbImageError;
215

216
  /// An image to use on the thumb of this switch when the switch is off.
217 218
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
219
  final ImageProvider? inactiveThumbImage;
220

221 222
  /// An optional error callback for errors emitted when loading
  /// [inactiveThumbImage].
223
  final ImageErrorListener? onInactiveThumbImageError;
224

225
  /// {@template flutter.material.switch.thumbColor}
226 227 228 229 230 231 232
  /// The color of this [Switch]'s thumb.
  ///
  /// Resolved in the following states:
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.disabled].
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
  ///
  /// {@tool snippet}
  /// This example resolves the [thumbColor] based on the current
  /// [MaterialState] of the [Switch], providing a different [Color] when it is
  /// [MaterialState.disabled].
  ///
  /// ```dart
  /// Switch(
  ///   value: true,
  ///   onChanged: (_) => true,
  ///   thumbColor: MaterialStateProperty.resolveWith<Color>((states) {
  ///     if (states.contains(MaterialState.disabled)) {
  ///       return Colors.orange.withOpacity(.48);
  ///     }
  ///     return Colors.orange;
  ///   }),
  /// )
  /// ```
  /// {@end-tool}
252 253 254 255 256 257 258 259 260 261 262 263
  /// {@endtemplate}
  ///
  /// If null, then the value of [activeColor] is used in the selected
  /// state and [inactiveThumbColor] in the default state. If that is also null,
  /// then the value of [SwitchThemeData.thumbColor] is used. If that is also
  /// null, then the following colors are used:
  ///
  /// | State    | Light theme                       | Dark theme                        |
  /// |----------|-----------------------------------|-----------------------------------|
  /// | Default  | `Colors.grey.shade50`             | `Colors.grey.shade400`            |
  /// | Selected | [ThemeData.toggleableActiveColor] | [ThemeData.toggleableActiveColor] |
  /// | Disabled | `Colors.grey.shade400`            | `Colors.grey.shade800`            |
264 265
  final MaterialStateProperty<Color?>? thumbColor;

266
  /// {@template flutter.material.switch.trackColor}
267 268 269 270 271 272 273
  /// The color of this [Switch]'s track.
  ///
  /// Resolved in the following states:
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.disabled].
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
  ///
  /// {@tool snippet}
  /// This example resolves the [trackColor] based on the current
  /// [MaterialState] of the [Switch], providing a different [Color] when it is
  /// [MaterialState.disabled].
  ///
  /// ```dart
  /// Switch(
  ///   value: true,
  ///   onChanged: (_) => true,
  ///   thumbColor: MaterialStateProperty.resolveWith<Color>((states) {
  ///     if (states.contains(MaterialState.disabled)) {
  ///       return Colors.orange.withOpacity(.48);
  ///     }
  ///     return Colors.orange;
  ///   }),
  /// )
  /// ```
  /// {@end-tool}
293 294 295 296 297 298 299 300 301
  /// {@endtemplate}
  ///
  /// If null, then the value of [activeTrackColor] is used in the selected
  /// state and [inactiveTrackColor] in the default state. If that is also null,
  /// then the value of [SwitchThemeData.trackColor] is used. If that is also
  /// null, then the following colors are used:
  ///
  /// | State    | Light theme                     | Dark theme                      |
  /// |----------|---------------------------------|---------------------------------|
302
  /// | Default  | `Color(0x52000000)`             | `Colors.white30`                |
303
  /// | Selected | [activeColor] with alpha `0x80` | [activeColor] with alpha `0x80` |
304
  /// | Disabled | `Colors.black12`                | `Colors.white10`                |
305 306
  final MaterialStateProperty<Color?>? trackColor;

307
  /// {@template flutter.material.switch.materialTapTargetSize}
308
  /// Configures the minimum size of the tap target.
309
  /// {@endtemplate}
310
  ///
311 312 313
  /// If null, then the value of [SwitchThemeData.materialTapTargetSize] is
  /// used. If that is also null, then the value of
  /// [ThemeData.materialTapTargetSize] is used.
314 315 316
  ///
  /// See also:
  ///
317
  ///  * [MaterialTapTargetSize], for a description of how this affects tap targets.
318
  final MaterialTapTargetSize? materialTapTargetSize;
319

320 321
  final _SwitchType _switchType;

322
  /// {@macro flutter.cupertino.CupertinoSwitch.dragStartBehavior}
323 324
  final DragStartBehavior dragStartBehavior;

325
  /// {@template flutter.material.switch.mouseCursor}
326 327 328 329 330 331 332 333 334 335
  /// The cursor for a mouse pointer when it enters or is hovering over the
  /// widget.
  ///
  /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
  /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
  ///
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.disabled].
336 337 338 339 340 341
  /// {@endtemplate}
  ///
  /// If null, then the value of [SwitchThemeData.mouseCursor] is used. If that
  /// is also null, then [MaterialStateMouseCursor.clickable] is used.
  ///
  /// See also:
342
  ///
343 344 345
  ///  * [MaterialStateMouseCursor], a [MouseCursor] that implements
  ///    `MaterialStateProperty` which is used in APIs that need to accept
  ///    either a [MouseCursor] or a [MaterialStateProperty<MouseCursor>].
346
  final MouseCursor? mouseCursor;
347

348
  /// The color for the button's [Material] when it has the input focus.
349
  ///
350 351 352
  /// If [overlayColor] returns a non-null color in the [MaterialState.focused]
  /// state, it will be used instead.
  ///
353 354 355
  /// If null, then the value of [SwitchThemeData.overlayColor] is used in the
  /// focused state. If that is also null, then the value of
  /// [ThemeData.focusColor] is used.
356
  final Color? focusColor;
357 358

  /// The color for the button's [Material] when a pointer is hovering over it.
359
  ///
360 361 362
  /// If [overlayColor] returns a non-null color in the [MaterialState.hovered]
  /// state, it will be used instead.
  ///
363 364 365
  /// If null, then the value of [SwitchThemeData.overlayColor] is used in the
  /// hovered state. If that is also null, then the value of
  /// [ThemeData.hoverColor] is used.
366
  final Color? hoverColor;
367

368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
  /// {@template flutter.material.switch.overlayColor}
  /// The color for the switch's [Material].
  ///
  /// Resolves in the following states:
  ///  * [MaterialState.pressed].
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  /// {@endtemplate}
  ///
  /// If null, then the value of [activeColor] with alpha
  /// [kRadialReactionAlpha], [focusColor] and [hoverColor] is used in the
  /// pressed, focused and hovered state. If that is also null,
  /// the value of [SwitchThemeData.overlayColor] is used. If that is
  /// also null, then the value of [ThemeData.toggleableActiveColor] with alpha
  /// [kRadialReactionAlpha], [ThemeData.focusColor] and [ThemeData.hoverColor]
  /// is used in the pressed, focused and hovered state.
  final MaterialStateProperty<Color?>? overlayColor;

387
  /// {@template flutter.material.switch.splashRadius}
388
  /// The splash radius of the circular [Material] ink response.
389
  /// {@endtemplate}
390
  ///
391 392
  /// If null, then the value of [SwitchThemeData.splashRadius] is used. If that
  /// is also null, then [kRadialReactionRadius] is used.
393 394
  final double? splashRadius;

395
  /// {@macro flutter.widgets.Focus.focusNode}
396
  final FocusNode? focusNode;
397 398 399 400

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

401 402 403 404
  Size _getSwitchSize(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final SwitchThemeData switchTheme = SwitchTheme.of(context);

405
    final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize
406
      ?? switchTheme.materialTapTargetSize
407 408 409 410 411 412 413 414 415 416
      ?? theme.materialTapTargetSize;
    switch (effectiveMaterialTapTargetSize) {
      case MaterialTapTargetSize.padded:
        return const Size(_kSwitchWidth, _kSwitchHeight);
      case MaterialTapTargetSize.shrinkWrap:
        return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
    }
  }

  Widget _buildCupertinoSwitch(BuildContext context) {
417
    final Size size = _getSwitchSize(context);
418 419 420 421 422 423 424 425 426 427 428 429
    return Focus(
      focusNode: focusNode,
      autofocus: autofocus,
      child: Container(
        width: size.width, // Same size as the Material switch.
        height: size.height,
        alignment: Alignment.center,
        child: CupertinoSwitch(
            dragStartBehavior: dragStartBehavior,
            value: value,
            onChanged: onChanged,
            activeColor: activeColor,
430
            trackColor: inactiveTrackColor,
431 432 433 434 435 436 437 438 439
        ),
      ),
    );
  }

  Widget _buildMaterialSwitch(BuildContext context) {
    return _MaterialSwitch(
      value: value,
      onChanged: onChanged,
440
      size: _getSwitchSize(context),
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
      activeColor: activeColor,
      activeTrackColor: activeTrackColor,
      inactiveThumbColor: inactiveThumbColor,
      inactiveTrackColor: inactiveTrackColor,
      activeThumbImage: activeThumbImage,
      onActiveThumbImageError: onActiveThumbImageError,
      inactiveThumbImage: inactiveThumbImage,
      onInactiveThumbImageError: onInactiveThumbImageError,
      thumbColor: thumbColor,
      trackColor: trackColor,
      materialTapTargetSize: materialTapTargetSize,
      dragStartBehavior: dragStartBehavior,
      mouseCursor: mouseCursor,
      focusColor: focusColor,
      hoverColor: hoverColor,
      overlayColor: overlayColor,
      splashRadius: splashRadius,
      focusNode: focusNode,
      autofocus: autofocus,
    );
  }

463
  @override
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
  Widget build(BuildContext context) {
    switch (_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:
          case TargetPlatform.linux:
          case TargetPlatform.windows:
            return _buildMaterialSwitch(context);
          case TargetPlatform.iOS:
          case TargetPlatform.macOS:
            return _buildCupertinoSwitch(context);
        }
      }
    }
  }
485 486

  @override
487 488
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
489 490
    properties.add(FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
    properties.add(ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled'));
491 492 493
  }
}

494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522
class _MaterialSwitch extends StatefulWidget {
  const _MaterialSwitch({
    Key? key,
    required this.value,
    required this.onChanged,
    required this.size,
    this.activeColor,
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
    this.activeThumbImage,
    this.onActiveThumbImageError,
    this.inactiveThumbImage,
    this.onInactiveThumbImageError,
    this.thumbColor,
    this.trackColor,
    this.materialTapTargetSize,
    this.dragStartBehavior = DragStartBehavior.start,
    this.mouseCursor,
    this.focusColor,
    this.hoverColor,
    this.overlayColor,
    this.splashRadius,
    this.focusNode,
    this.autofocus = false,
  })  : assert(dragStartBehavior != null),
        assert(activeThumbImage != null || onActiveThumbImageError == null),
        assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
        super(key: key);
523

524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
  final bool value;
  final ValueChanged<bool>? onChanged;
  final Color? activeColor;
  final Color? activeTrackColor;
  final Color? inactiveThumbColor;
  final Color? inactiveTrackColor;
  final ImageProvider? activeThumbImage;
  final ImageErrorListener? onActiveThumbImageError;
  final ImageProvider? inactiveThumbImage;
  final ImageErrorListener? onInactiveThumbImageError;
  final MaterialStateProperty<Color?>? thumbColor;
  final MaterialStateProperty<Color?>? trackColor;
  final MaterialTapTargetSize? materialTapTargetSize;
  final DragStartBehavior dragStartBehavior;
  final MouseCursor? mouseCursor;
  final Color? focusColor;
  final Color? hoverColor;
  final MaterialStateProperty<Color?>? overlayColor;
  final double? splashRadius;
  final FocusNode? focusNode;
  final bool autofocus;
  final Size size;
546

547 548 549
  @override
  State<StatefulWidget> createState() => _MaterialSwitchState();
}
550

551 552
class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderStateMixin, ToggleableStateMixin {
  final _SwitchPainter _painter = _SwitchPainter();
553

554 555 556 557 558 559 560 561 562 563 564 565
  @override
  void didUpdateWidget(_MaterialSwitch oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.value != widget.value) {
      // During a drag we may have modified the curve, reset it if its possible
      // to do without visual discontinuation.
      if (position.value == 0.0 || position.value == 1.0) {
        position
          ..curve = Curves.easeIn
          ..reverseCurve = Curves.easeOut;
      }
      animateToValue();
566 567 568
    }
  }

569 570 571 572
  @override
  void dispose() {
    _painter.dispose();
    super.dispose();
573 574
  }

575 576
  @override
  ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null;
577

578 579
  @override
  bool get tristate => false;
580

581 582
  @override
  bool? get value => widget.value;
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640

  MaterialStateProperty<Color?> get _widgetThumbColor {
    return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        return widget.inactiveThumbColor;
      }
      if (states.contains(MaterialState.selected)) {
        return widget.activeColor;
      }
      return widget.inactiveThumbColor;
    });
  }

  MaterialStateProperty<Color> get _defaultThumbColor {
    final ThemeData theme = Theme.of(context);
    final bool isDark = theme.brightness == Brightness.dark;

    return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        return isDark ? Colors.grey.shade800 : Colors.grey.shade400;
      }
      if (states.contains(MaterialState.selected)) {
        return theme.toggleableActiveColor;
      }
      return isDark ? Colors.grey.shade400 : Colors.grey.shade50;
    });
  }

  MaterialStateProperty<Color?> get _widgetTrackColor {
    return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        return widget.inactiveTrackColor;
      }
      if (states.contains(MaterialState.selected)) {
        return widget.activeTrackColor;
      }
      return widget.inactiveTrackColor;
    });
  }

  MaterialStateProperty<Color> get _defaultTrackColor {
    final ThemeData theme = Theme.of(context);
    final bool isDark = theme.brightness == Brightness.dark;
    const Color black32 = Color(0x52000000); // Black with 32% opacity

    return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        return isDark ? Colors.white10 : Colors.black12;
      }
      if (states.contains(MaterialState.selected)) {
        final Set<MaterialState> activeState = states..add(MaterialState.selected);
        final Color activeColor = _widgetThumbColor.resolve(activeState) ?? _defaultThumbColor.resolve(activeState);
        return activeColor.withAlpha(0x80);
      }
      return isDark ? Colors.white30 : black32;
    });
  }

641 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 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689
  double get _trackInnerLength => widget.size.width - _kSwitchMinSize;

  void _handleDragStart(DragStartDetails details) {
    if (isInteractive)
      reactionController.forward();
  }

  void _handleDragUpdate(DragUpdateDetails details) {
    if (isInteractive) {
      position
        ..curve = Curves.linear
        ..reverseCurve = null;
      final double delta = details.primaryDelta! / _trackInnerLength;
      switch (Directionality.of(context)) {
        case TextDirection.rtl:
          positionController.value -= delta;
          break;
        case TextDirection.ltr:
          positionController.value += delta;
          break;
      }
    }
  }

  bool _needsPositionAnimation = false;

  void _handleDragEnd(DragEndDetails details) {
    if (position.value >= 0.5 != widget.value) {
      widget.onChanged!(!widget.value);
      // Wait with finishing the animation until widget.value has changed to
      // !widget.value as part of the widget.onChanged call above.
      setState(() {
        _needsPositionAnimation = true;
      });
    } else {
      animateToValue();
    }
    reactionController.reverse();

  }

  void _handleChanged(bool? value) {
    assert(value != null);
    assert(widget.onChanged != null);
    widget.onChanged!(value!);
  }

  @override
  Widget build(BuildContext context) {
690
    assert(debugCheckHasMaterial(context));
691 692 693 694 695 696

    if (_needsPositionAnimation) {
      _needsPositionAnimation = false;
      animateToValue();
    }

697
    final ThemeData theme = Theme.of(context);
698
    final SwitchThemeData switchTheme = SwitchTheme.of(context);
699

700 701
    // Colors need to be resolved in selected and non selected states separately
    // so that they can be lerped between.
702 703
    final Set<MaterialState> activeStates = states..add(MaterialState.selected);
    final Set<MaterialState> inactiveStates = states..remove(MaterialState.selected);
704 705
    final Color effectiveActiveThumbColor = widget.thumbColor?.resolve(activeStates)
      ?? _widgetThumbColor.resolve(activeStates)
706
      ?? switchTheme.thumbColor?.resolve(activeStates)
707 708 709
      ?? _defaultThumbColor.resolve(activeStates);
    final Color effectiveInactiveThumbColor = widget.thumbColor?.resolve(inactiveStates)
      ?? _widgetThumbColor.resolve(inactiveStates)
710
      ?? switchTheme.thumbColor?.resolve(inactiveStates)
711 712 713
      ?? _defaultThumbColor.resolve(inactiveStates);
    final Color effectiveActiveTrackColor = widget.trackColor?.resolve(activeStates)
      ?? _widgetTrackColor.resolve(activeStates)
714
      ?? switchTheme.trackColor?.resolve(activeStates)
715 716 717
      ?? _defaultTrackColor.resolve(activeStates);
    final Color effectiveInactiveTrackColor = widget.trackColor?.resolve(inactiveStates)
      ?? _widgetTrackColor.resolve(inactiveStates)
718
      ?? switchTheme.trackColor?.resolve(inactiveStates)
719 720
      ?? _defaultTrackColor.resolve(inactiveStates);

721
    final Set<MaterialState> focusedStates = states..add(MaterialState.focused);
722 723
    final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
      ?? widget.focusColor
724
      ?? switchTheme.overlayColor?.resolve(focusedStates)
725 726
      ?? theme.focusColor;

727
    final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
728 729
    final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
        ?? widget.hoverColor
730
        ?? switchTheme.overlayColor?.resolve(hoveredStates)
731
        ?? theme.hoverColor;
732

733 734
    final Set<MaterialState> activePressedStates = activeStates..add(MaterialState.pressed);
    final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates)
735
        ?? switchTheme.overlayColor?.resolve(activePressedStates)
736 737 738 739
        ?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha);

    final Set<MaterialState> inactivePressedStates = inactiveStates..add(MaterialState.pressed);
    final Color effectiveInactivePressedOverlayColor = widget.overlayColor?.resolve(inactivePressedStates)
740
        ?? switchTheme.overlayColor?.resolve(inactivePressedStates)
741 742
        ?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha);

743 744
    final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
      return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
745
        ?? switchTheme.mouseCursor?.resolve(states)
746 747
        ?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, states);
    });
748

749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770
    return Semantics(
      toggled: widget.value,
      child: GestureDetector(
        excludeFromSemantics: true,
        onHorizontalDragStart: _handleDragStart,
        onHorizontalDragUpdate: _handleDragUpdate,
        onHorizontalDragEnd: _handleDragEnd,
        dragStartBehavior: widget.dragStartBehavior,
        child: buildToggleable(
          mouseCursor: effectiveMouseCursor,
          focusNode: widget.focusNode,
          autofocus: widget.autofocus,
          size: widget.size,
          painter: _painter
            ..position = position
            ..reaction = reaction
            ..reactionFocusFade = reactionFocusFade
            ..reactionHoverFade = reactionHoverFade
            ..inactiveReactionColor = effectiveInactivePressedOverlayColor
            ..reactionColor = effectiveActivePressedOverlayColor
            ..hoverColor = effectiveHoverOverlayColor
            ..focusColor = effectiveFocusOverlayColor
771
            ..splashRadius = widget.splashRadius ?? switchTheme.splashRadius ?? kRadialReactionRadius
772 773 774 775 776 777 778 779 780 781 782 783 784 785 786
            ..downPosition = downPosition
            ..isFocused = states.contains(MaterialState.focused)
            ..isHovered = states.contains(MaterialState.hovered)
            ..activeColor = effectiveActiveThumbColor
            ..inactiveColor = effectiveInactiveThumbColor
            ..activeThumbImage = widget.activeThumbImage
            ..onActiveThumbImageError = widget.onActiveThumbImageError
            ..inactiveThumbImage = widget.inactiveThumbImage
            ..onInactiveThumbImageError = widget.onInactiveThumbImageError
            ..activeTrackColor = effectiveActiveTrackColor
            ..inactiveTrackColor = effectiveInactiveTrackColor
            ..configuration = createLocalImageConfiguration(context)
            ..isInteractive = isInteractive
            ..trackInnerLength = _trackInnerLength
            ..textDirection = Directionality.of(context)
787
            ..surfaceColor = theme.colorScheme.surface,
788
        ),
789 790 791
      ),
    );
  }
792 793
}

794
class _SwitchPainter extends ToggleablePainter {
795 796 797
  ImageProvider? get activeThumbImage => _activeThumbImage;
  ImageProvider? _activeThumbImage;
  set activeThumbImage(ImageProvider? value) {
798
    if (value == _activeThumbImage)
799
      return;
800
    _activeThumbImage = value;
801
    notifyListeners();
802 803
  }

804 805 806
  ImageErrorListener? get onActiveThumbImageError => _onActiveThumbImageError;
  ImageErrorListener? _onActiveThumbImageError;
  set onActiveThumbImageError(ImageErrorListener? value) {
807 808 809 810
    if (value == _onActiveThumbImageError) {
      return;
    }
    _onActiveThumbImageError = value;
811
    notifyListeners();
812 813
  }

814 815 816
  ImageProvider? get inactiveThumbImage => _inactiveThumbImage;
  ImageProvider? _inactiveThumbImage;
  set inactiveThumbImage(ImageProvider? value) {
817
    if (value == _inactiveThumbImage)
818
      return;
819
    _inactiveThumbImage = value;
820
    notifyListeners();
821 822
  }

823 824 825
  ImageErrorListener? get onInactiveThumbImageError => _onInactiveThumbImageError;
  ImageErrorListener? _onInactiveThumbImageError;
  set onInactiveThumbImageError(ImageErrorListener? value) {
826 827 828 829
    if (value == _onInactiveThumbImageError) {
      return;
    }
    _onInactiveThumbImageError = value;
830
    notifyListeners();
831 832
  }

833 834
  Color get activeTrackColor => _activeTrackColor!;
  Color? _activeTrackColor;
835
  set activeTrackColor(Color value) {
836 837 838 839
    assert(value != null);
    if (value == _activeTrackColor)
      return;
    _activeTrackColor = value;
840
    notifyListeners();
841 842
  }

843 844
  Color get inactiveTrackColor => _inactiveTrackColor!;
  Color? _inactiveTrackColor;
845
  set inactiveTrackColor(Color value) {
846 847 848 849
    assert(value != null);
    if (value == _inactiveTrackColor)
      return;
    _inactiveTrackColor = value;
850
    notifyListeners();
851 852
  }

853 854
  ImageConfiguration get configuration => _configuration!;
  ImageConfiguration? _configuration;
855
  set configuration(ImageConfiguration value) {
856 857 858 859
    assert(value != null);
    if (value == _configuration)
      return;
    _configuration = value;
860
    notifyListeners();
861 862
  }

863 864
  TextDirection get textDirection => _textDirection!;
  TextDirection? _textDirection;
865 866 867 868 869
  set textDirection(TextDirection value) {
    assert(value != null);
    if (_textDirection == value)
      return;
    _textDirection = value;
870
    notifyListeners();
871 872
  }

873 874
  Color get surfaceColor => _surfaceColor!;
  Color? _surfaceColor;
875 876 877 878 879
  set surfaceColor(Color value) {
    assert(value != null);
    if (value == _surfaceColor)
      return;
    _surfaceColor = value;
880
    notifyListeners();
881 882
  }

883 884 885 886 887
  bool get isInteractive => _isInteractive!;
  bool? _isInteractive;
  set isInteractive(bool value) {
    if (value == _isInteractive) {
      return;
888
    }
889 890
    _isInteractive = value;
    notifyListeners();
891 892
  }

893 894 895 896 897
  double get trackInnerLength => _trackInnerLength!;
  double? _trackInnerLength;
  set trackInnerLength(double value) {
    if (value == _trackInnerLength) {
      return;
898
    }
899 900
    _trackInnerLength = value;
    notifyListeners();
901 902
  }

903 904 905 906
  Color? _cachedThumbColor;
  ImageProvider? _cachedThumbImage;
  ImageErrorListener? _cachedThumbErrorListener;
  BoxPainter? _cachedThumbPainter;
907

908
  BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider? image, ImageErrorListener? errorListener) {
909
    return BoxDecoration(
910
      color: color,
911
      image: image == null ? null : DecorationImage(image: image, onError: errorListener),
912
      shape: BoxShape.circle,
913
      boxShadow: kElevationToShadow[1],
914 915
    );
  }
916

917 918 919 920 921 922 923 924
  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)
925
      notifyListeners();
926 927
  }

928
  @override
929 930
  void paint(Canvas canvas, Size size) {
    final bool isEnabled = isInteractive;
931 932
    final double currentValue = position.value;

933
    final double visualPosition;
934 935 936 937 938 939 940 941
    switch (textDirection) {
      case TextDirection.rtl:
        visualPosition = 1.0 - currentValue;
        break;
      case TextDirection.ltr:
        visualPosition = currentValue;
        break;
    }
942

943 944 945 946 947 948
    final Color trackColor = Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)!;
    final Color lerpedThumbColor = Color.lerp(inactiveColor, activeColor, currentValue)!;
    // Blend the thumb color against a `surfaceColor` background in case the
    // thumbColor is not opaque. This way we do not see through the thumb to the
    // track underneath.
    final Color thumbColor = Color.alphaBlend(lerpedThumbColor, surfaceColor);
949

950
    final ImageProvider? thumbImage = isEnabled
951 952
      ? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage)
      : inactiveThumbImage;
953

954
    final ImageErrorListener? thumbErrorListener = isEnabled
955 956 957
      ? (currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError)
      : onInactiveThumbImageError;

958
    final Paint paint = Paint()
959
      ..color = trackColor;
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998

    final Offset trackPaintOffset = _computeTrackPaintOffset(size, _kTrackWidth, _kTrackHeight);
    final Offset thumbPaintOffset = _computeThumbPaintOffset(trackPaintOffset, visualPosition);
    final Offset radialReactionOrigin = Offset(thumbPaintOffset.dx + _kThumbRadius, size.height / 2);

    _paintTrackWith(canvas, paint, trackPaintOffset);
    paintRadialReaction(canvas: canvas, origin: radialReactionOrigin);
    _paintThumbWith(
      thumbPaintOffset,
      canvas,
      currentValue,
      thumbColor,
      thumbImage,
      thumbErrorListener,
    );
  }

  /// Computes canvas offset for track's upper left corner
  Offset _computeTrackPaintOffset(Size canvasSize, double trackWidth, double trackHeight) {
    final double horizontalOffset = (canvasSize.width - _kTrackWidth) / 2.0;
    final double verticalOffset = (canvasSize.height - _kTrackHeight) / 2.0;

    return Offset(horizontalOffset, verticalOffset);
  }

  /// Computes canvas offset for thumb's upper left corner as if it were a
  /// square
  Offset _computeThumbPaintOffset(Offset trackPaintOffset, double visualPosition) {
    // How much thumb radius extends beyond the track
    const double additionalThumbRadius = _kThumbRadius - _kTrackRadius;

    final double horizontalProgress = visualPosition * trackInnerLength;
    final double thumbHorizontalOffset = trackPaintOffset.dx - additionalThumbRadius + horizontalProgress;
    final double thumbVerticalOffset = trackPaintOffset.dy - additionalThumbRadius;

    return Offset(thumbHorizontalOffset, thumbVerticalOffset);
  }

  void _paintTrackWith(Canvas canvas, Paint paint, Offset trackPaintOffset) {
999
    final Rect trackRect = Rect.fromLTWH(
1000 1001 1002
      trackPaintOffset.dx,
      trackPaintOffset.dy,
      _kTrackWidth,
1003
      _kTrackHeight,
1004
    );
1005 1006 1007
    final RRect trackRRect = RRect.fromRectAndRadius(
      trackRect,
      const Radius.circular(_kTrackRadius),
1008
    );
1009

1010 1011
    canvas.drawRRect(trackRRect, paint);
  }
1012

1013 1014 1015 1016 1017 1018 1019 1020
  void _paintThumbWith(
    Offset thumbPaintOffset,
    Canvas canvas,
    double currentValue,
    Color thumbColor,
    ImageProvider? thumbImage,
    ImageErrorListener? thumbErrorListener,
  ) {
1021 1022
    try {
      _isPainting = true;
1023
      if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage || thumbErrorListener != _cachedThumbErrorListener) {
1024
        _cachedThumbColor = thumbColor;
1025
        _cachedThumbImage = thumbImage;
1026
        _cachedThumbErrorListener = thumbErrorListener;
1027
        _cachedThumbPainter?.dispose();
1028
        _cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage, thumbErrorListener).createBoxPainter(_handleDecorationChanged);
1029
      }
1030
      final BoxPainter thumbPainter = _cachedThumbPainter!;
1031

1032
      // The thumb contracts slightly during the animation
1033
      final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
1034
      final double radius = _kThumbRadius - inset;
1035

1036 1037
      thumbPainter.paint(
        canvas,
1038
        thumbPaintOffset + Offset(0, inset),
1039
        configuration.copyWith(size: Size.fromRadius(radius)),
1040 1041 1042 1043
      );
    } finally {
      _isPainting = false;
    }
1044
  }
1045 1046 1047 1048 1049 1050 1051 1052 1053 1054

  @override
  void dispose() {
    _cachedThumbPainter?.dispose();
    _cachedThumbPainter = null;
    _cachedThumbColor = null;
    _cachedThumbImage = null;
    _cachedThumbErrorListener = null;
    super.dispose();
  }
1055
}