switch.dart 26.5 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 6
// @dart = 2.8

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

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

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

30 31
enum _SwitchType { material, adaptive }

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

  /// Creates a [CupertinoSwitch] if the target platform is iOS, creates a
  /// material design switch otherwise.
  ///
  /// If a [CupertinoSwitch] is created, the following parameters are
  /// ignored: [activeTrackColor], [inactiveThumbColor], [inactiveTrackColor],
98 99
  /// [activeThumbImage], [onActiveThumbImageError], [inactiveThumbImage],
  /// [onInactiveImageThumbError], [materialTapTargetSize].
100 101 102 103 104 105 106 107 108 109 110
  ///
  /// The target platform is based on the current [Theme]: [ThemeData.platform].
  const Switch.adaptive({
    Key key,
    @required this.value,
    @required this.onChanged,
    this.activeColor,
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
    this.activeThumbImage,
111
    this.onActiveThumbImageError,
112
    this.inactiveThumbImage,
113
    this.onInactiveThumbImageError,
114
    this.materialTapTargetSize,
115
    this.dragStartBehavior = DragStartBehavior.start,
116
    this.mouseCursor,
117 118 119 120 121
    this.focusColor,
    this.hoverColor,
    this.focusNode,
    this.autofocus = false,
  })  : assert(autofocus != null),
122 123
        assert(activeThumbImage != null || onActiveThumbImageError == null),
        assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
124 125
        _switchType = _SwitchType.adaptive,
        super(key: key);
126

127
  /// Whether this switch is on or off.
128 129
  ///
  /// This property must not be null.
130
  final bool value;
131

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

156 157
  /// The color to use when this switch is on.
  ///
158
  /// Defaults to [ThemeData.toggleableActiveColor].
159
  final Color activeColor;
160

161 162
  /// The color to use on the track when this switch is on.
  ///
163
  /// Defaults to [ThemeData.toggleableActiveColor] with the opacity set at 50%.
164 165
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
166 167 168 169 170
  final Color activeTrackColor;

  /// The color to use on the thumb when this switch is off.
  ///
  /// Defaults to the colors described in the Material design specification.
171 172
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
173 174 175 176 177
  final Color inactiveThumbColor;

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

182
  /// An image to use on the thumb of this switch when the switch is on.
183 184
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
185
  final ImageProvider activeThumbImage;
186

187 188 189 190
  /// An optional error callback for errors emitted when loading
  /// [activeThumbImage].
  final ImageErrorListener onActiveThumbImageError;

191
  /// An image to use on the thumb of this switch when the switch is off.
192 193
  ///
  /// Ignored if this switch is created with [Switch.adaptive].
194
  final ImageProvider inactiveThumbImage;
195

196 197 198 199
  /// An optional error callback for errors emitted when loading
  /// [inactiveThumbImage].
  final ImageErrorListener onInactiveThumbImageError;

200 201 202 203 204 205
  /// Configures the minimum size of the tap target.
  ///
  /// Defaults to [ThemeData.materialTapTargetSize].
  ///
  /// See also:
  ///
206
  ///  * [MaterialTapTargetSize], for a description of how this affects tap targets.
207 208
  final MaterialTapTargetSize materialTapTargetSize;

209 210
  final _SwitchType _switchType;

211 212 213
  /// {@macro flutter.cupertino.switch.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

214 215 216 217 218 219 220 221 222 223 224 225 226 227
  /// 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.
  final MouseCursor mouseCursor;

228 229 230 231 232 233 234 235 236 237 238 239
  /// The color for the button's [Material] when it has the input focus.
  final Color focusColor;

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

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

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

240
  @override
241
  _SwitchState createState() => _SwitchState();
242 243

  @override
244 245
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
246 247
    properties.add(FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
    properties.add(ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled'));
248 249 250 251
  }
}

class _SwitchState extends State<Switch> with TickerProviderStateMixin {
252
  Map<Type, Action<Intent>> _actionMap;
253 254 255 256

  @override
  void initState() {
    super.initState();
257 258
    _actionMap = <Type, Action<Intent>>{
      ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _actionHandler),
259 260 261
    };
  }

262
  void _actionHandler(ActivateIntent intent) {
263 264 265
    if (widget.onChanged != null) {
      widget.onChanged(!widget.value);
    }
266
    final RenderObject renderObject = context.findRenderObject();
267 268 269
    renderObject.sendSemanticsEvent(const TapSemanticEvent());
  }

270 271 272 273
  bool _focused = false;
  void _handleFocusHighlightChanged(bool focused) {
    if (focused != _focused) {
      setState(() { _focused = focused; });
274 275 276
    }
  }

277 278 279 280
  bool _hovering = false;
  void _handleHoverChanged(bool hovering) {
    if (hovering != _hovering) {
      setState(() { _hovering = hovering; });
281 282 283
    }
  }

284 285 286 287 288 289 290 291 292 293 294 295 296
  Size getSwitchSize(ThemeData theme) {
    switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
      case MaterialTapTargetSize.padded:
        return const Size(_kSwitchWidth, _kSwitchHeight);
        break;
      case MaterialTapTargetSize.shrinkWrap:
        return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
        break;
    }
    assert(false);
    return null;
  }

297 298
  bool get enabled => widget.onChanged != null;

299 300 301 302 303 304
  void _didFinishDragging() {
    // The user has finished dragging the thumb of this switch. Rebuild the switch
    // to update the animation.
    setState(() {});
  }

305
  Widget buildMaterialSwitch(BuildContext context) {
306
    assert(debugCheckHasMaterial(context));
307 308
    final ThemeData theme = Theme.of(context);
    final bool isDark = theme.brightness == Brightness.dark;
309

310
    final Color activeThumbColor = widget.activeColor ?? theme.toggleableActiveColor;
311
    final Color activeTrackColor = widget.activeTrackColor ?? activeThumbColor.withAlpha(0x80);
312 313
    final Color hoverColor = widget.hoverColor ?? theme.hoverColor;
    final Color focusColor = widget.focusColor ?? theme.focusColor;
314 315 316

    Color inactiveThumbColor;
    Color inactiveTrackColor;
317
    if (enabled) {
318
      const Color black32 = Color(0x52000000); // Black with 32% opacity
319
      inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade400 : Colors.grey.shade50);
320
      inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white30 : black32);
321
    } else {
322 323
      inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400);
      inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
324
    }
325 326 327 328 329 330 331 332 333
    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,
      },
    );
334

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

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

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

      case _SwitchType.adaptive: {
        final ThemeData theme = Theme.of(context);
        assert(theme.platform != null);
        switch (theme.platform) {
          case TargetPlatform.android:
          case TargetPlatform.fuchsia:
402 403
          case TargetPlatform.linux:
          case TargetPlatform.windows:
404 405
            return buildMaterialSwitch(context);
          case TargetPlatform.iOS:
406
          case TargetPlatform.macOS:
407 408 409 410 411 412 413
            return buildCupertinoSwitch(context);
        }
      }
    }
    assert(false);
    return null;
  }
414 415
}

416
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
417
  const _SwitchRenderObjectWidget({
418 419
    Key key,
    this.value,
420 421
    this.activeColor,
    this.inactiveColor,
422 423
    this.hoverColor,
    this.focusColor,
424
    this.activeThumbImage,
425
    this.onActiveThumbImageError,
426
    this.inactiveThumbImage,
427
    this.onInactiveThumbImageError,
428 429
    this.activeTrackColor,
    this.inactiveTrackColor,
430
    this.configuration,
431
    this.onChanged,
432
    this.additionalConstraints,
433
    this.dragStartBehavior,
434 435
    this.hasFocus,
    this.hovering,
436
    this.state,
437
  }) : super(key: key);
438 439

  final bool value;
440 441
  final Color activeColor;
  final Color inactiveColor;
442 443
  final Color hoverColor;
  final Color focusColor;
444
  final ImageProvider activeThumbImage;
445
  final ImageErrorListener onActiveThumbImageError;
446
  final ImageProvider inactiveThumbImage;
447
  final ImageErrorListener onInactiveThumbImageError;
448 449
  final Color activeTrackColor;
  final Color inactiveTrackColor;
450
  final ImageConfiguration configuration;
Hixie's avatar
Hixie committed
451
  final ValueChanged<bool> onChanged;
452
  final BoxConstraints additionalConstraints;
453
  final DragStartBehavior dragStartBehavior;
454 455
  final bool hasFocus;
  final bool hovering;
456
  final _SwitchState state;
457

458
  @override
459
  _RenderSwitch createRenderObject(BuildContext context) {
460
    return _RenderSwitch(
461
      dragStartBehavior: dragStartBehavior,
462 463 464
      value: value,
      activeColor: activeColor,
      inactiveColor: inactiveColor,
465 466
      hoverColor: hoverColor,
      focusColor: focusColor,
467
      activeThumbImage: activeThumbImage,
468
      onActiveThumbImageError: onActiveThumbImageError,
469
      inactiveThumbImage: inactiveThumbImage,
470
      onInactiveThumbImageError: onInactiveThumbImageError,
471 472 473 474 475
      activeTrackColor: activeTrackColor,
      inactiveTrackColor: inactiveTrackColor,
      configuration: configuration,
      onChanged: onChanged,
      textDirection: Directionality.of(context),
476
      additionalConstraints: additionalConstraints,
477 478
      hasFocus: hasFocus,
      hovering: hovering,
479
      state: state,
480 481
    );
  }
482

483
  @override
484
  void updateRenderObject(BuildContext context, _RenderSwitch renderObject) {
485 486 487 488
    renderObject
      ..value = value
      ..activeColor = activeColor
      ..inactiveColor = inactiveColor
489 490
      ..hoverColor = hoverColor
      ..focusColor = focusColor
491
      ..activeThumbImage = activeThumbImage
492
      ..onActiveThumbImageError = onActiveThumbImageError
493
      ..inactiveThumbImage = inactiveThumbImage
494
      ..onInactiveThumbImageError = onInactiveThumbImageError
495 496
      ..activeTrackColor = activeTrackColor
      ..inactiveTrackColor = inactiveTrackColor
497
      ..configuration = configuration
498
      ..onChanged = onChanged
499
      ..textDirection = Directionality.of(context)
500
      ..additionalConstraints = additionalConstraints
501
      ..dragStartBehavior = dragStartBehavior
502 503
      ..hasFocus = hasFocus
      ..hovering = hovering
504
      ..vsync = state;
505 506 507
  }
}

508
class _RenderSwitch extends RenderToggleable {
509 510
  _RenderSwitch({
    bool value,
511 512
    Color activeColor,
    Color inactiveColor,
513 514
    Color hoverColor,
    Color focusColor,
515
    ImageProvider activeThumbImage,
516
    ImageErrorListener onActiveThumbImageError,
517
    ImageProvider inactiveThumbImage,
518
    ImageErrorListener onInactiveThumbImageError,
519 520
    Color activeTrackColor,
    Color inactiveTrackColor,
521
    ImageConfiguration configuration,
522
    BoxConstraints additionalConstraints,
523
    @required TextDirection textDirection,
524
    ValueChanged<bool> onChanged,
525
    DragStartBehavior dragStartBehavior,
526 527
    bool hasFocus,
    bool hovering,
528
    @required this.state,
529 530
  }) : assert(textDirection != null),
       _activeThumbImage = activeThumbImage,
531
       _onActiveThumbImageError = onActiveThumbImageError,
532
       _inactiveThumbImage = inactiveThumbImage,
533
       _onInactiveThumbImageError = onInactiveThumbImageError,
534 535
       _activeTrackColor = activeTrackColor,
       _inactiveTrackColor = inactiveTrackColor,
536
       _configuration = configuration,
537
       _textDirection = textDirection,
538 539
       super(
         value: value,
540
         tristate: false,
541 542
         activeColor: activeColor,
         inactiveColor: inactiveColor,
543 544
         hoverColor: hoverColor,
         focusColor: focusColor,
545
         onChanged: onChanged,
546
         additionalConstraints: additionalConstraints,
547 548
         hasFocus: hasFocus,
         hovering: hovering,
549
         vsync: state,
550
       ) {
551
    _drag = HorizontalDragGestureRecognizer()
552 553
      ..onStart = _handleDragStart
      ..onUpdate = _handleDragUpdate
554 555
      ..onEnd = _handleDragEnd
      ..dragStartBehavior = dragStartBehavior;
556 557
  }

558 559 560 561
  ImageProvider get activeThumbImage => _activeThumbImage;
  ImageProvider _activeThumbImage;
  set activeThumbImage(ImageProvider value) {
    if (value == _activeThumbImage)
562
      return;
563
    _activeThumbImage = value;
564 565 566
    markNeedsPaint();
  }

567 568 569 570 571 572 573 574 575 576
  ImageErrorListener get onActiveThumbImageError => _onActiveThumbImageError;
  ImageErrorListener _onActiveThumbImageError;
  set onActiveThumbImageError(ImageErrorListener value) {
    if (value == _onActiveThumbImageError) {
      return;
    }
    _onActiveThumbImageError = value;
    markNeedsPaint();
  }

577 578 579 580
  ImageProvider get inactiveThumbImage => _inactiveThumbImage;
  ImageProvider _inactiveThumbImage;
  set inactiveThumbImage(ImageProvider value) {
    if (value == _inactiveThumbImage)
581
      return;
582
    _inactiveThumbImage = value;
583 584 585
    markNeedsPaint();
  }

586 587 588 589 590 591 592 593 594 595
  ImageErrorListener get onInactiveThumbImageError => _onInactiveThumbImageError;
  ImageErrorListener _onInactiveThumbImageError;
  set onInactiveThumbImageError(ImageErrorListener value) {
    if (value == _onInactiveThumbImageError) {
      return;
    }
    _onInactiveThumbImageError = value;
    markNeedsPaint();
  }

596 597
  Color get activeTrackColor => _activeTrackColor;
  Color _activeTrackColor;
598
  set activeTrackColor(Color value) {
599 600 601 602 603 604 605 606 607
    assert(value != null);
    if (value == _activeTrackColor)
      return;
    _activeTrackColor = value;
    markNeedsPaint();
  }

  Color get inactiveTrackColor => _inactiveTrackColor;
  Color _inactiveTrackColor;
608
  set inactiveTrackColor(Color value) {
609 610 611 612 613 614 615
    assert(value != null);
    if (value == _inactiveTrackColor)
      return;
    _inactiveTrackColor = value;
    markNeedsPaint();
  }

616 617
  ImageConfiguration get configuration => _configuration;
  ImageConfiguration _configuration;
618
  set configuration(ImageConfiguration value) {
619 620 621 622 623
    assert(value != null);
    if (value == _configuration)
      return;
    _configuration = value;
    markNeedsPaint();
624 625
  }

626 627 628 629 630 631 632 633 634 635
  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
    assert(value != null);
    if (_textDirection == value)
      return;
    _textDirection = value;
    markNeedsPaint();
  }

636 637 638
  DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior;
  set dragStartBehavior(DragStartBehavior value) {
    assert(value != null);
639
    if (_drag.dragStartBehavior == value)
640 641 642 643
      return;
    _drag.dragStartBehavior = value;
  }

644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
  _SwitchState state;

  @override
  set value(bool newValue) {
    assert(value != null);
    super.value = newValue;
    // The widget is rebuilt and we have pending position animation to play.
    if (_needsPositionAnimation) {
      _needsPositionAnimation = false;
      position
        ..curve = null
        ..reverseCurve = null;
      if (newValue)
        positionController.forward();
      else
        positionController.reverse();
    }
  }


664 665
  @override
  void detach() {
666 667
    _cachedThumbPainter?.dispose();
    _cachedThumbPainter = null;
668 669 670
    super.detach();
  }

671
  double get _trackInnerLength => size.width - 2.0 * kRadialReactionRadius;
672

673 674
  HorizontalDragGestureRecognizer _drag;

675 676
  bool _needsPositionAnimation = false;

677
  void _handleDragStart(DragStartDetails details) {
678
    if (isInteractive)
679
      reactionController.forward();
680 681
  }

682
  void _handleDragUpdate(DragUpdateDetails details) {
683
    if (isInteractive) {
684
      position
685 686
        ..curve = null
        ..reverseCurve = null;
687 688 689 690 691 692 693 694 695
      final double delta = details.primaryDelta / _trackInnerLength;
      switch (textDirection) {
        case TextDirection.rtl:
          positionController.value -= delta;
          break;
        case TextDirection.ltr:
          positionController.value += delta;
          break;
      }
696 697 698
    }
  }

699
  void _handleDragEnd(DragEndDetails details) {
700 701 702 703
    _needsPositionAnimation = true;

    if (position.value >= 0.5 != value)
      onChanged(!value);
704
    reactionController.reverse();
705
    state._didFinishDragging();
706 707
  }

708
  @override
Ian Hickson's avatar
Ian Hickson committed
709
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
710
    assert(debugHandleEvent(event, entry));
Ian Hickson's avatar
Ian Hickson committed
711
    if (event is PointerDownEvent && onChanged != null)
712 713
      _drag.addPointer(event);
    super.handleEvent(event, entry);
714 715
  }

716
  Color _cachedThumbColor;
717
  ImageProvider _cachedThumbImage;
718
  ImageErrorListener _cachedThumbErrorListener;
719 720
  BoxPainter _cachedThumbPainter;

721
  BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider image, ImageErrorListener errorListener) {
722
    return BoxDecoration(
723
      color: color,
724
      image: image == null ? null : DecorationImage(image: image, onError: errorListener),
725
      shape: BoxShape.circle,
726
      boxShadow: kElevationToShadow[1],
727 728
    );
  }
729

730 731 732 733 734 735 736 737 738 739 740
  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();
  }

741 742 743 744 745 746
  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config.isToggled = value == true;
  }

747
  @override
748
  void paint(PaintingContext context, Offset offset) {
Adam Barth's avatar
Adam Barth committed
749
    final Canvas canvas = context.canvas;
750
    final bool isEnabled = onChanged != null;
751 752 753 754 755 756 757 758 759 760 761
    final double currentValue = position.value;

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

763 764 765 766 767 768 769 770 771 772 773
    final Color trackColor = isEnabled
      ? Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)
      : inactiveTrackColor;

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

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

775 776 777 778
    final ImageErrorListener thumbErrorListener = isEnabled
      ? (currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError)
      : onInactiveThumbImageError;

779
    // Paint the track
780
    final Paint paint = Paint()
781
      ..color = trackColor;
782
    const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
783
    final Rect trackRect = Rect.fromLTWH(
784 785 786
      offset.dx + trackHorizontalPadding,
      offset.dy + (size.height - _kTrackHeight) / 2.0,
      size.width - 2.0 * trackHorizontalPadding,
787
      _kTrackHeight,
788
    );
789
    final RRect trackRRect = RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius));
790 791
    canvas.drawRRect(trackRRect, paint);

792
    final Offset thumbPosition = Offset(
793
      kRadialReactionRadius + visualPosition * _trackInnerLength,
794
      size.height / 2.0,
795
    );
796

Adam Barth's avatar
Adam Barth committed
797
    paintRadialReaction(canvas, offset, thumbPosition);
798

799 800 801
    try {
      _isPainting = true;
      BoxPainter thumbPainter;
802
      if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage || thumbErrorListener != _cachedThumbErrorListener) {
803
        _cachedThumbColor = thumbColor;
804
        _cachedThumbImage = thumbImage;
805 806
        _cachedThumbErrorListener = thumbErrorListener;
        _cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage, thumbErrorListener).createBoxPainter(_handleDecorationChanged);
807 808
      }
      thumbPainter = _cachedThumbPainter;
809

810
      // The thumb contracts slightly during the animation
811
      final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
812 813 814
      final double radius = _kThumbRadius - inset;
      thumbPainter.paint(
        canvas,
815
        thumbPosition + offset - Offset(radius, radius),
816
        configuration.copyWith(size: Size.fromRadius(radius)),
817 818 819 820
      );
    } finally {
      _isPainting = false;
    }
821 822
  }
}