switch.dart 37.2 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
/// A Material Design switch.
32 33 34 35 36 37 38 39
///
/// 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
  /// Creates a Material Design switch.
57 58 59 60 61 62
  ///
  /// 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
    super.key,
69 70
    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
        assert(activeThumbImage != null || onActiveThumbImageError == null),
93
        assert(inactiveThumbImage != null || onInactiveThumbImageError == null);
94

95 96 97
  /// 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).
98
  ///
99 100 101 102 103 104 105
  /// 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],
106
  /// [activeThumbImage], [onActiveThumbImageError], [inactiveThumbImage],
107
  /// [onInactiveThumbImageError], [materialTapTargetSize].
108 109 110
  ///
  /// The target platform is based on the current [Theme]: [ThemeData.platform].
  const Switch.adaptive({
111
    super.key,
112 113
    required this.value,
    required this.onChanged,
114 115 116 117 118
    this.activeColor,
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
    this.activeThumbImage,
119
    this.onActiveThumbImageError,
120
    this.inactiveThumbImage,
121
    this.onInactiveThumbImageError,
122
    this.materialTapTargetSize,
123 124
    this.thumbColor,
    this.trackColor,
125
    this.dragStartBehavior = DragStartBehavior.start,
126
    this.mouseCursor,
127 128
    this.focusColor,
    this.hoverColor,
129
    this.overlayColor,
130
    this.splashRadius,
131 132 133
    this.focusNode,
    this.autofocus = false,
  })  : assert(autofocus != null),
134 135
        assert(activeThumbImage != null || onActiveThumbImageError == null),
        assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
136
        _switchType = _SwitchType.adaptive;
137

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

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

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

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

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

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

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

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

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

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

223
  /// {@template flutter.material.switch.thumbColor}
224 225 226 227 228 229 230
  /// The color of this [Switch]'s thumb.
  ///
  /// Resolved in the following states:
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.disabled].
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
  ///
  /// {@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}
250 251 252 253 254 255 256 257 258 259 260 261
  /// {@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`            |
262 263
  final MaterialStateProperty<Color?>? thumbColor;

264
  /// {@template flutter.material.switch.trackColor}
265 266 267 268 269 270 271
  /// The color of this [Switch]'s track.
  ///
  /// Resolved in the following states:
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.disabled].
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
  ///
  /// {@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}
291 292 293 294 295 296 297 298 299
  /// {@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                      |
  /// |----------|---------------------------------|---------------------------------|
300
  /// | Default  | `Color(0x52000000)`             | `Colors.white30`                |
301
  /// | Selected | [activeColor] with alpha `0x80` | [activeColor] with alpha `0x80` |
302
  /// | Disabled | `Colors.black12`                | `Colors.white10`                |
303 304
  final MaterialStateProperty<Color?>? trackColor;

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

318 319
  final _SwitchType _switchType;

320
  /// {@macro flutter.cupertino.CupertinoSwitch.dragStartBehavior}
321 322
  final DragStartBehavior dragStartBehavior;

323
  /// {@template flutter.material.switch.mouseCursor}
324 325 326 327 328 329 330 331 332 333
  /// 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].
334 335 336 337 338 339
  /// {@endtemplate}
  ///
  /// If null, then the value of [SwitchThemeData.mouseCursor] is used. If that
  /// is also null, then [MaterialStateMouseCursor.clickable] is used.
  ///
  /// See also:
340
  ///
341 342 343
  ///  * [MaterialStateMouseCursor], a [MouseCursor] that implements
  ///    `MaterialStateProperty` which is used in APIs that need to accept
  ///    either a [MouseCursor] or a [MaterialStateProperty<MouseCursor>].
344
  final MouseCursor? mouseCursor;
345

346
  /// The color for the button's [Material] when it has the input focus.
347
  ///
348 349 350
  /// If [overlayColor] returns a non-null color in the [MaterialState.focused]
  /// state, it will be used instead.
  ///
351 352 353
  /// 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.
354
  final Color? focusColor;
355 356

  /// The color for the button's [Material] when a pointer is hovering over it.
357
  ///
358 359 360
  /// If [overlayColor] returns a non-null color in the [MaterialState.hovered]
  /// state, it will be used instead.
  ///
361 362 363
  /// 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.
364
  final Color? hoverColor;
365

366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
  /// {@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;

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

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

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

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

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

  Widget _buildCupertinoSwitch(BuildContext context) {
415
    final Size size = _getSwitchSize(context);
416 417 418 419 420 421 422 423 424 425 426 427
    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,
428
            trackColor: inactiveTrackColor,
429 430 431 432 433 434 435 436 437
        ),
      ),
    );
  }

  Widget _buildMaterialSwitch(BuildContext context) {
    return _MaterialSwitch(
      value: value,
      onChanged: onChanged,
438
      size: _getSwitchSize(context),
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
      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,
    );
  }

461
  @override
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
  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);
        }
      }
    }
  }
483 484

  @override
485 486
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
487 488
    properties.add(FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
    properties.add(ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled'));
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
class _MaterialSwitch extends StatefulWidget {
  const _MaterialSwitch({
    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),
518
        assert(inactiveThumbImage != null || onInactiveThumbImageError == null);
519

520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541
  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;
542

543 544 545
  @override
  State<StatefulWidget> createState() => _MaterialSwitchState();
}
546

547 548
class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderStateMixin, ToggleableStateMixin {
  final _SwitchPainter _painter = _SwitchPainter();
549

550 551 552 553 554 555 556 557 558 559 560 561
  @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();
562 563 564
    }
  }

565 566 567 568
  @override
  void dispose() {
    _painter.dispose();
    super.dispose();
569 570
  }

571 572
  @override
  ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null;
573

574 575
  @override
  bool get tristate => false;
576

577 578
  @override
  bool? get value => widget.value;
579 580 581 582 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

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

637 638 639 640 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
  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) {
686
    assert(debugCheckHasMaterial(context));
687 688 689 690 691 692

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

693
    final ThemeData theme = Theme.of(context);
694
    final SwitchThemeData switchTheme = SwitchTheme.of(context);
695

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

717
    final Set<MaterialState> focusedStates = states..add(MaterialState.focused);
718 719
    final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
      ?? widget.focusColor
720
      ?? switchTheme.overlayColor?.resolve(focusedStates)
721 722
      ?? theme.focusColor;

723
    final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
724 725
    final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
        ?? widget.hoverColor
726
        ?? switchTheme.overlayColor?.resolve(hoveredStates)
727
        ?? theme.hoverColor;
728

729 730
    final Set<MaterialState> activePressedStates = activeStates..add(MaterialState.pressed);
    final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates)
731
        ?? switchTheme.overlayColor?.resolve(activePressedStates)
732 733 734 735
        ?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha);

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

739 740
    final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
      return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
741
        ?? switchTheme.mouseCursor?.resolve(states)
742 743
        ?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, states);
    });
744

745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766
    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
767
            ..splashRadius = widget.splashRadius ?? switchTheme.splashRadius ?? kRadialReactionRadius
768 769 770 771 772 773 774 775 776 777 778 779 780 781 782
            ..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)
783
            ..surfaceColor = theme.colorScheme.surface,
784
        ),
785 786 787
      ),
    );
  }
788 789
}

790
class _SwitchPainter extends ToggleablePainter {
791 792 793
  ImageProvider? get activeThumbImage => _activeThumbImage;
  ImageProvider? _activeThumbImage;
  set activeThumbImage(ImageProvider? value) {
794
    if (value == _activeThumbImage)
795
      return;
796
    _activeThumbImage = value;
797
    notifyListeners();
798 799
  }

800 801 802
  ImageErrorListener? get onActiveThumbImageError => _onActiveThumbImageError;
  ImageErrorListener? _onActiveThumbImageError;
  set onActiveThumbImageError(ImageErrorListener? value) {
803 804 805 806
    if (value == _onActiveThumbImageError) {
      return;
    }
    _onActiveThumbImageError = value;
807
    notifyListeners();
808 809
  }

810 811 812
  ImageProvider? get inactiveThumbImage => _inactiveThumbImage;
  ImageProvider? _inactiveThumbImage;
  set inactiveThumbImage(ImageProvider? value) {
813
    if (value == _inactiveThumbImage)
814
      return;
815
    _inactiveThumbImage = value;
816
    notifyListeners();
817 818
  }

819 820 821
  ImageErrorListener? get onInactiveThumbImageError => _onInactiveThumbImageError;
  ImageErrorListener? _onInactiveThumbImageError;
  set onInactiveThumbImageError(ImageErrorListener? value) {
822 823 824 825
    if (value == _onInactiveThumbImageError) {
      return;
    }
    _onInactiveThumbImageError = value;
826
    notifyListeners();
827 828
  }

829 830
  Color get activeTrackColor => _activeTrackColor!;
  Color? _activeTrackColor;
831
  set activeTrackColor(Color value) {
832 833 834 835
    assert(value != null);
    if (value == _activeTrackColor)
      return;
    _activeTrackColor = value;
836
    notifyListeners();
837 838
  }

839 840
  Color get inactiveTrackColor => _inactiveTrackColor!;
  Color? _inactiveTrackColor;
841
  set inactiveTrackColor(Color value) {
842 843 844 845
    assert(value != null);
    if (value == _inactiveTrackColor)
      return;
    _inactiveTrackColor = value;
846
    notifyListeners();
847 848
  }

849 850
  ImageConfiguration get configuration => _configuration!;
  ImageConfiguration? _configuration;
851
  set configuration(ImageConfiguration value) {
852 853 854 855
    assert(value != null);
    if (value == _configuration)
      return;
    _configuration = value;
856
    notifyListeners();
857 858
  }

859 860
  TextDirection get textDirection => _textDirection!;
  TextDirection? _textDirection;
861 862 863 864 865
  set textDirection(TextDirection value) {
    assert(value != null);
    if (_textDirection == value)
      return;
    _textDirection = value;
866
    notifyListeners();
867 868
  }

869 870
  Color get surfaceColor => _surfaceColor!;
  Color? _surfaceColor;
871 872 873 874 875
  set surfaceColor(Color value) {
    assert(value != null);
    if (value == _surfaceColor)
      return;
    _surfaceColor = value;
876
    notifyListeners();
877 878
  }

879 880 881 882 883
  bool get isInteractive => _isInteractive!;
  bool? _isInteractive;
  set isInteractive(bool value) {
    if (value == _isInteractive) {
      return;
884
    }
885 886
    _isInteractive = value;
    notifyListeners();
887 888
  }

889 890 891 892 893
  double get trackInnerLength => _trackInnerLength!;
  double? _trackInnerLength;
  set trackInnerLength(double value) {
    if (value == _trackInnerLength) {
      return;
894
    }
895 896
    _trackInnerLength = value;
    notifyListeners();
897 898
  }

899 900 901 902
  Color? _cachedThumbColor;
  ImageProvider? _cachedThumbImage;
  ImageErrorListener? _cachedThumbErrorListener;
  BoxPainter? _cachedThumbPainter;
903

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

913 914 915 916 917 918 919 920
  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)
921
      notifyListeners();
922 923
  }

924
  @override
925 926
  void paint(Canvas canvas, Size size) {
    final bool isEnabled = isInteractive;
927 928
    final double currentValue = position.value;

929
    final double visualPosition;
930 931 932 933 934 935 936 937
    switch (textDirection) {
      case TextDirection.rtl:
        visualPosition = 1.0 - currentValue;
        break;
      case TextDirection.ltr:
        visualPosition = currentValue;
        break;
    }
938

939 940 941 942 943 944
    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);
945

946
    final ImageProvider? thumbImage = isEnabled
947 948
      ? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage)
      : inactiveThumbImage;
949

950
    final ImageErrorListener? thumbErrorListener = isEnabled
951 952 953
      ? (currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError)
      : onInactiveThumbImageError;

954
    final Paint paint = Paint()
955
      ..color = trackColor;
956 957 958 959 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

    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) {
995
    final Rect trackRect = Rect.fromLTWH(
996 997 998
      trackPaintOffset.dx,
      trackPaintOffset.dy,
      _kTrackWidth,
999
      _kTrackHeight,
1000
    );
1001 1002 1003
    final RRect trackRRect = RRect.fromRectAndRadius(
      trackRect,
      const Radius.circular(_kTrackRadius),
1004
    );
1005

1006 1007
    canvas.drawRRect(trackRRect, paint);
  }
1008

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

1028
      // The thumb contracts slightly during the animation
1029
      final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
1030
      final double radius = _kThumbRadius - inset;
1031

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

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