switch.dart 28 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';
14
import 'material_state.dart';
Adam Barth's avatar
Adam Barth committed
15
import 'shadows.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 StatefulWidget {
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
    this.materialTapTargetSize,
80
    this.dragStartBehavior = DragStartBehavior.start,
81
    this.mouseCursor,
82 83
    this.focusColor,
    this.hoverColor,
84
    this.splashRadius,
85 86 87 88
    this.focusNode,
    this.autofocus = false,
  })  : _switchType = _SwitchType.material,
        assert(dragStartBehavior != null),
89 90
        assert(activeThumbImage != null || onActiveThumbImageError == null),
        assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
91
        super(key: key);
92

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

134
  /// Whether this switch is on or off.
135 136
  ///
  /// This property must not be null.
137
  final bool value;
138

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

163 164
  /// The color to use when this switch is on.
  ///
165
  /// Defaults to [ThemeData.toggleableActiveColor].
166
  final Color? activeColor;
167

168 169
  /// The color to use on the track when this switch is on.
  ///
170
  /// Defaults to [ThemeData.toggleableActiveColor] with the opacity set at 50%.
171 172
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
173
  final Color? activeTrackColor;
174 175 176 177

  /// The color to use on the thumb when this switch is off.
  ///
  /// Defaults to the colors described in the Material design specification.
178 179
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
180
  final Color? inactiveThumbColor;
181 182 183 184

  /// The color to use on the track when this switch is off.
  ///
  /// Defaults to the colors described in the Material design specification.
185 186
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
187
  final Color? inactiveTrackColor;
188

189
  /// An image to use on the thumb of this switch when the switch is on.
190 191
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
192
  final ImageProvider? activeThumbImage;
193

194 195
  /// An optional error callback for errors emitted when loading
  /// [activeThumbImage].
196
  final ImageErrorListener? onActiveThumbImageError;
197

198
  /// An image to use on the thumb of this switch when the switch is off.
199 200
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
201
  final ImageProvider? inactiveThumbImage;
202

203 204
  /// An optional error callback for errors emitted when loading
  /// [inactiveThumbImage].
205
  final ImageErrorListener? onInactiveThumbImageError;
206

207 208 209 210 211 212
  /// Configures the minimum size of the tap target.
  ///
  /// Defaults to [ThemeData.materialTapTargetSize].
  ///
  /// See also:
  ///
213
  ///  * [MaterialTapTargetSize], for a description of how this affects tap targets.
214
  final MaterialTapTargetSize? materialTapTargetSize;
215

216 217
  final _SwitchType _switchType;

218
  /// {@macro flutter.cupertino.CupertinoSwitch.dragStartBehavior}
219 220
  final DragStartBehavior dragStartBehavior;

221 222 223 224 225 226 227 228 229 230 231 232
  /// 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].
  ///
  /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
233
  final MouseCursor? mouseCursor;
234

235
  /// The color for the button's [Material] when it has the input focus.
236
  final Color? focusColor;
237 238

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

241 242 243 244 245
  /// The splash radius of the circular [Material] ink response.
  ///
  /// If null, then [kRadialReactionRadius] is used.
  final double? splashRadius;

246
  /// {@macro flutter.widgets.Focus.focusNode}
247
  final FocusNode? focusNode;
248 249 250 251

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

252
  @override
253
  _SwitchState createState() => _SwitchState();
254 255

  @override
256 257
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
258 259
    properties.add(FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
    properties.add(ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled'));
260 261 262 263
  }
}

class _SwitchState extends State<Switch> with TickerProviderStateMixin {
264
  late Map<Type, Action<Intent>> _actionMap;
265 266 267 268

  @override
  void initState() {
    super.initState();
269 270
    _actionMap = <Type, Action<Intent>>{
      ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _actionHandler),
271 272 273
    };
  }

274
  void _actionHandler(ActivateIntent intent) {
275
    if (widget.onChanged != null) {
276
      widget.onChanged!(!widget.value);
277
    }
278
    final RenderObject renderObject = context.findRenderObject()!;
279 280 281
    renderObject.sendSemanticsEvent(const TapSemanticEvent());
  }

282 283 284 285
  bool _focused = false;
  void _handleFocusHighlightChanged(bool focused) {
    if (focused != _focused) {
      setState(() { _focused = focused; });
286 287 288
    }
  }

289 290 291 292
  bool _hovering = false;
  void _handleHoverChanged(bool hovering) {
    if (hovering != _hovering) {
      setState(() { _hovering = hovering; });
293 294 295
    }
  }

296 297 298 299 300 301 302 303 304
  Size getSwitchSize(ThemeData theme) {
    switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
      case MaterialTapTargetSize.padded:
        return const Size(_kSwitchWidth, _kSwitchHeight);
      case MaterialTapTargetSize.shrinkWrap:
        return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
    }
  }

305 306
  bool get enabled => widget.onChanged != null;

307 308 309 310 311 312
  void _didFinishDragging() {
    // The user has finished dragging the thumb of this switch. Rebuild the switch
    // to update the animation.
    setState(() {});
  }

313
  Widget buildMaterialSwitch(BuildContext context) {
314
    assert(debugCheckHasMaterial(context));
315
    final ThemeData theme = Theme.of(context);
316
    final bool isDark = theme.brightness == Brightness.dark;
317

318
    final Color activeThumbColor = widget.activeColor ?? theme.toggleableActiveColor;
319
    final Color activeTrackColor = widget.activeTrackColor ?? activeThumbColor.withAlpha(0x80);
320 321
    final Color hoverColor = widget.hoverColor ?? theme.hoverColor;
    final Color focusColor = widget.focusColor ?? theme.focusColor;
322

323 324
    final Color inactiveThumbColor;
    final Color inactiveTrackColor;
325
    if (enabled) {
326
      const Color black32 = Color(0x52000000); // Black with 32% opacity
327
      inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade400 : Colors.grey.shade50);
328
      inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white30 : black32);
329
    } else {
330 331
      inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400);
      inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
332
    }
333 334 335 336 337 338 339 340 341
    final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
      widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
      <MaterialState>{
        if (!enabled) MaterialState.disabled,
        if (_hovering) MaterialState.hovered,
        if (_focused) MaterialState.focused,
        if (widget.value) MaterialState.selected,
      },
    );
342

343 344 345 346 347 348 349
    return FocusableActionDetector(
      actions: _actionMap,
      focusNode: widget.focusNode,
      autofocus: widget.autofocus,
      enabled: enabled,
      onShowFocusHighlight: _handleFocusHighlightChanged,
      onShowHoverHighlight: _handleHoverChanged,
350
      mouseCursor: effectiveMouseCursor,
351 352 353 354 355 356 357 358 359
      child: Builder(
        builder: (BuildContext context) {
          return _SwitchRenderObjectWidget(
            dragStartBehavior: widget.dragStartBehavior,
            value: widget.value,
            activeColor: activeThumbColor,
            inactiveColor: inactiveThumbColor,
            hoverColor: hoverColor,
            focusColor: focusColor,
360
            splashRadius: widget.splashRadius ?? kRadialReactionRadius,
361
            activeThumbImage: widget.activeThumbImage,
362
            onActiveThumbImageError: widget.onActiveThumbImageError,
363
            inactiveThumbImage: widget.inactiveThumbImage,
364
            onInactiveThumbImageError: widget.onInactiveThumbImageError,
365 366 367 368 369 370 371
            activeTrackColor: activeTrackColor,
            inactiveTrackColor: inactiveTrackColor,
            configuration: createLocalImageConfiguration(context),
            onChanged: widget.onChanged,
            additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
            hasFocus: _focused,
            hovering: _hovering,
372
            state: this,
373 374
          );
        },
375
      ),
376 377
    );
  }
378 379

  Widget buildCupertinoSwitch(BuildContext context) {
380
    final Size size = getSwitchSize(Theme.of(context));
381 382 383 384 385 386 387 388 389 390 391 392
    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,
393
          trackColor: widget.inactiveTrackColor
394
        ),
395 396 397 398 399 400 401 402 403 404 405
      ),
    );
  }

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

      case _SwitchType.adaptive: {
406
        final ThemeData theme = Theme.of(context);
407 408 409 410
        assert(theme.platform != null);
        switch (theme.platform) {
          case TargetPlatform.android:
          case TargetPlatform.fuchsia:
411 412
          case TargetPlatform.linux:
          case TargetPlatform.windows:
413 414
            return buildMaterialSwitch(context);
          case TargetPlatform.iOS:
415
          case TargetPlatform.macOS:
416 417 418 419 420
            return buildCupertinoSwitch(context);
        }
      }
    }
  }
421 422
}

423
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
424
  const _SwitchRenderObjectWidget({
425 426 427 428 429 430
    Key? key,
    required this.value,
    required this.activeColor,
    required this.inactiveColor,
    required this.hoverColor,
    required this.focusColor,
431
    required this.splashRadius,
432 433 434 435 436 437 438 439 440 441 442 443 444
    required this.activeThumbImage,
    required this.onActiveThumbImageError,
    required this.inactiveThumbImage,
    required this.onInactiveThumbImageError,
    required this.activeTrackColor,
    required this.inactiveTrackColor,
    required this.configuration,
    required this.onChanged,
    required this.additionalConstraints,
    required this.dragStartBehavior,
    required this.hasFocus,
    required this.hovering,
    required this.state,
445
  }) : super(key: key);
446 447

  final bool value;
448 449
  final Color activeColor;
  final Color inactiveColor;
450 451
  final Color hoverColor;
  final Color focusColor;
452
  final double splashRadius;
453 454 455 456
  final ImageProvider? activeThumbImage;
  final ImageErrorListener? onActiveThumbImageError;
  final ImageProvider? inactiveThumbImage;
  final ImageErrorListener? onInactiveThumbImageError;
457 458
  final Color activeTrackColor;
  final Color inactiveTrackColor;
459
  final ImageConfiguration configuration;
460
  final ValueChanged<bool>? onChanged;
461
  final BoxConstraints additionalConstraints;
462
  final DragStartBehavior dragStartBehavior;
463 464
  final bool hasFocus;
  final bool hovering;
465
  final _SwitchState state;
466

467
  @override
468
  _RenderSwitch createRenderObject(BuildContext context) {
469
    return _RenderSwitch(
470
      dragStartBehavior: dragStartBehavior,
471 472 473
      value: value,
      activeColor: activeColor,
      inactiveColor: inactiveColor,
474 475
      hoverColor: hoverColor,
      focusColor: focusColor,
476
      splashRadius: splashRadius,
477
      activeThumbImage: activeThumbImage,
478
      onActiveThumbImageError: onActiveThumbImageError,
479
      inactiveThumbImage: inactiveThumbImage,
480
      onInactiveThumbImageError: onInactiveThumbImageError,
481 482 483
      activeTrackColor: activeTrackColor,
      inactiveTrackColor: inactiveTrackColor,
      configuration: configuration,
484
      onChanged: onChanged != null ? _handleValueChanged : null,
485
      textDirection: Directionality.of(context),
486
      additionalConstraints: additionalConstraints,
487 488
      hasFocus: hasFocus,
      hovering: hovering,
489
      state: state,
490 491
    );
  }
492

493
  @override
494
  void updateRenderObject(BuildContext context, _RenderSwitch renderObject) {
495 496 497 498
    renderObject
      ..value = value
      ..activeColor = activeColor
      ..inactiveColor = inactiveColor
499 500
      ..hoverColor = hoverColor
      ..focusColor = focusColor
501
      ..splashRadius = splashRadius
502
      ..activeThumbImage = activeThumbImage
503
      ..onActiveThumbImageError = onActiveThumbImageError
504
      ..inactiveThumbImage = inactiveThumbImage
505
      ..onInactiveThumbImageError = onInactiveThumbImageError
506 507
      ..activeTrackColor = activeTrackColor
      ..inactiveTrackColor = inactiveTrackColor
508
      ..configuration = configuration
509
      ..onChanged = onChanged != null ? _handleValueChanged : null
510
      ..textDirection = Directionality.of(context)
511
      ..additionalConstraints = additionalConstraints
512
      ..dragStartBehavior = dragStartBehavior
513 514
      ..hasFocus = hasFocus
      ..hovering = hovering
515
      ..vsync = state;
516
  }
517 518 519 520 521 522 523 524 525 526 527

  void _handleValueChanged(bool? value) {
    // Wrap the onChanged callback because the RenderToggleable supports tri-state
    // values (i.e. value can be null), but the Switch doesn't. We pass false
    // for the tristate param to RenderToggleable, so value should never
    // be null.
    assert(value != null);
    if (onChanged != null) {
      onChanged!(value!);
    }
  }
528 529
}

530
class _RenderSwitch extends RenderToggleable {
531
  _RenderSwitch({
532 533 534 535 536
    required bool value,
    required Color activeColor,
    required Color inactiveColor,
    required Color hoverColor,
    required Color focusColor,
537
    required double splashRadius,
538 539 540 541 542 543 544 545 546 547 548 549 550 551
    required ImageProvider? activeThumbImage,
    required ImageErrorListener? onActiveThumbImageError,
    required ImageProvider? inactiveThumbImage,
    required ImageErrorListener? onInactiveThumbImageError,
    required Color activeTrackColor,
    required Color inactiveTrackColor,
    required ImageConfiguration configuration,
    required BoxConstraints additionalConstraints,
    required TextDirection textDirection,
    required ValueChanged<bool?>? onChanged,
    required DragStartBehavior dragStartBehavior,
    required bool hasFocus,
    required bool hovering,
    required this.state,
552 553
  }) : assert(textDirection != null),
       _activeThumbImage = activeThumbImage,
554
       _onActiveThumbImageError = onActiveThumbImageError,
555
       _inactiveThumbImage = inactiveThumbImage,
556
       _onInactiveThumbImageError = onInactiveThumbImageError,
557 558
       _activeTrackColor = activeTrackColor,
       _inactiveTrackColor = inactiveTrackColor,
559
       _configuration = configuration,
560
       _textDirection = textDirection,
561 562
       super(
         value: value,
563
         tristate: false,
564 565
         activeColor: activeColor,
         inactiveColor: inactiveColor,
566 567
         hoverColor: hoverColor,
         focusColor: focusColor,
568
         splashRadius: splashRadius,
569
         onChanged: onChanged,
570
         additionalConstraints: additionalConstraints,
571 572
         hasFocus: hasFocus,
         hovering: hovering,
573
         vsync: state,
574
       ) {
575
    _drag = HorizontalDragGestureRecognizer()
576 577
      ..onStart = _handleDragStart
      ..onUpdate = _handleDragUpdate
578 579
      ..onEnd = _handleDragEnd
      ..dragStartBehavior = dragStartBehavior;
580 581
  }

582 583 584
  ImageProvider? get activeThumbImage => _activeThumbImage;
  ImageProvider? _activeThumbImage;
  set activeThumbImage(ImageProvider? value) {
585
    if (value == _activeThumbImage)
586
      return;
587
    _activeThumbImage = value;
588 589 590
    markNeedsPaint();
  }

591 592 593
  ImageErrorListener? get onActiveThumbImageError => _onActiveThumbImageError;
  ImageErrorListener? _onActiveThumbImageError;
  set onActiveThumbImageError(ImageErrorListener? value) {
594 595 596 597 598 599 600
    if (value == _onActiveThumbImageError) {
      return;
    }
    _onActiveThumbImageError = value;
    markNeedsPaint();
  }

601 602 603
  ImageProvider? get inactiveThumbImage => _inactiveThumbImage;
  ImageProvider? _inactiveThumbImage;
  set inactiveThumbImage(ImageProvider? value) {
604
    if (value == _inactiveThumbImage)
605
      return;
606
    _inactiveThumbImage = value;
607 608 609
    markNeedsPaint();
  }

610 611 612
  ImageErrorListener? get onInactiveThumbImageError => _onInactiveThumbImageError;
  ImageErrorListener? _onInactiveThumbImageError;
  set onInactiveThumbImageError(ImageErrorListener? value) {
613 614 615 616 617 618 619
    if (value == _onInactiveThumbImageError) {
      return;
    }
    _onInactiveThumbImageError = value;
    markNeedsPaint();
  }

620 621
  Color get activeTrackColor => _activeTrackColor;
  Color _activeTrackColor;
622
  set activeTrackColor(Color value) {
623 624 625 626 627 628 629 630 631
    assert(value != null);
    if (value == _activeTrackColor)
      return;
    _activeTrackColor = value;
    markNeedsPaint();
  }

  Color get inactiveTrackColor => _inactiveTrackColor;
  Color _inactiveTrackColor;
632
  set inactiveTrackColor(Color value) {
633 634 635 636 637 638 639
    assert(value != null);
    if (value == _inactiveTrackColor)
      return;
    _inactiveTrackColor = value;
    markNeedsPaint();
  }

640 641
  ImageConfiguration get configuration => _configuration;
  ImageConfiguration _configuration;
642
  set configuration(ImageConfiguration value) {
643 644 645 646 647
    assert(value != null);
    if (value == _configuration)
      return;
    _configuration = value;
    markNeedsPaint();
648 649
  }

650 651 652 653 654 655 656 657 658 659
  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
    assert(value != null);
    if (_textDirection == value)
      return;
    _textDirection = value;
    markNeedsPaint();
  }

660 661 662
  DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior;
  set dragStartBehavior(DragStartBehavior value) {
    assert(value != null);
663
    if (_drag.dragStartBehavior == value)
664 665 666 667
      return;
    _drag.dragStartBehavior = value;
  }

668 669 670
  _SwitchState state;

  @override
671
  set value(bool? newValue) {
672 673 674 675 676
    assert(value != null);
    super.value = newValue;
    // The widget is rebuilt and we have pending position animation to play.
    if (_needsPositionAnimation) {
      _needsPositionAnimation = false;
677 678
      position.reverseCurve = null;
      if (newValue!)
679 680 681 682 683 684
        positionController.forward();
      else
        positionController.reverse();
    }
  }

685 686
  @override
  void detach() {
687 688
    _cachedThumbPainter?.dispose();
    _cachedThumbPainter = null;
689 690 691
    super.detach();
  }

692
  double get _trackInnerLength => size.width - _kSwitchMinSize;
693

694
  late HorizontalDragGestureRecognizer _drag;
695

696 697
  bool _needsPositionAnimation = false;

698
  void _handleDragStart(DragStartDetails details) {
699
    if (isInteractive)
700
      reactionController.forward();
701 702
  }

703
  void _handleDragUpdate(DragUpdateDetails details) {
704
    if (isInteractive) {
705 706
      position.reverseCurve = null;
      final double delta = details.primaryDelta! / _trackInnerLength;
707 708 709 710 711 712 713 714
      switch (textDirection) {
        case TextDirection.rtl:
          positionController.value -= delta;
          break;
        case TextDirection.ltr:
          positionController.value += delta;
          break;
      }
715 716 717
    }
  }

718
  void _handleDragEnd(DragEndDetails details) {
719 720 721
    _needsPositionAnimation = true;

    if (position.value >= 0.5 != value)
722
      onChanged!(!value!);
723
    reactionController.reverse();
724
    state._didFinishDragging();
725 726
  }

727
  @override
Ian Hickson's avatar
Ian Hickson committed
728
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
729
    assert(debugHandleEvent(event, entry));
Ian Hickson's avatar
Ian Hickson committed
730
    if (event is PointerDownEvent && onChanged != null)
731 732
      _drag.addPointer(event);
    super.handleEvent(event, entry);
733 734
  }

735 736 737 738
  Color? _cachedThumbColor;
  ImageProvider? _cachedThumbImage;
  ImageErrorListener? _cachedThumbErrorListener;
  BoxPainter? _cachedThumbPainter;
739

740
  BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider? image, ImageErrorListener? errorListener) {
741
    return BoxDecoration(
742
      color: color,
743
      image: image == null ? null : DecorationImage(image: image, onError: errorListener),
744
      shape: BoxShape.circle,
745
      boxShadow: kElevationToShadow[1],
746 747
    );
  }
748

749 750 751 752 753 754 755 756 757 758 759
  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();
  }

760 761 762 763 764 765
  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config.isToggled = value == true;
  }

766
  @override
767
  void paint(PaintingContext context, Offset offset) {
Adam Barth's avatar
Adam Barth committed
768
    final Canvas canvas = context.canvas;
769
    final bool isEnabled = onChanged != null;
770 771
    final double currentValue = position.value;

772
    final double visualPosition;
773 774 775 776 777 778 779 780
    switch (textDirection) {
      case TextDirection.rtl:
        visualPosition = 1.0 - currentValue;
        break;
      case TextDirection.ltr:
        visualPosition = currentValue;
        break;
    }
781

782
    final Color trackColor = isEnabled
783
      ? Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)!
784 785 786
      : inactiveTrackColor;

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

790
    final ImageProvider? thumbImage = isEnabled
791 792
      ? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage)
      : inactiveThumbImage;
793

794
    final ImageErrorListener? thumbErrorListener = isEnabled
795 796 797
      ? (currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError)
      : onInactiveThumbImageError;

798
    // Paint the track
799
    final Paint paint = Paint()
800
      ..color = trackColor;
801
    const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
802
    final Rect trackRect = Rect.fromLTWH(
803 804 805
      offset.dx + trackHorizontalPadding,
      offset.dy + (size.height - _kTrackHeight) / 2.0,
      size.width - 2.0 * trackHorizontalPadding,
806
      _kTrackHeight,
807
    );
808
    final RRect trackRRect = RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius));
809 810
    canvas.drawRRect(trackRRect, paint);

811
    final Offset thumbPosition = Offset(
812
      kRadialReactionRadius + visualPosition * _trackInnerLength,
813
      size.height / 2.0,
814
    );
815

Adam Barth's avatar
Adam Barth committed
816
    paintRadialReaction(canvas, offset, thumbPosition);
817

818 819
    try {
      _isPainting = true;
820
      if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage || thumbErrorListener != _cachedThumbErrorListener) {
821
        _cachedThumbColor = thumbColor;
822
        _cachedThumbImage = thumbImage;
823 824
        _cachedThumbErrorListener = thumbErrorListener;
        _cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage, thumbErrorListener).createBoxPainter(_handleDecorationChanged);
825
      }
826
      final BoxPainter thumbPainter = _cachedThumbPainter!;
827

828
      // The thumb contracts slightly during the animation
829
      final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
830 831 832
      final double radius = _kThumbRadius - inset;
      thumbPainter.paint(
        canvas,
833
        thumbPosition + offset - Offset(radius, radius),
834
        configuration.copyWith(size: Size.fromRadius(radius)),
835 836 837 838
      );
    } finally {
      _isPainting = false;
    }
839 840
  }
}