// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'colors.dart'; import 'constants.dart'; import 'debug.dart'; import 'material_state.dart'; import 'shadows.dart'; import 'switch_theme.dart'; import 'theme.dart'; import 'theme_data.dart'; import 'toggleable.dart'; const double _kTrackHeight = 14.0; const double _kTrackWidth = 33.0; const double _kTrackRadius = _kTrackHeight / 2.0; const double _kThumbRadius = 10.0; const double _kSwitchMinSize = kMinInteractiveDimension - 8.0; const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + _kSwitchMinSize; const double _kSwitchHeight = _kSwitchMinSize + 8.0; const double _kSwitchHeightCollapsed = _kSwitchMinSize; enum _SwitchType { material, adaptive } /// 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. /// /// 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]. /// /// Requires one of its ancestors to be a [Material] widget. /// /// See also: /// /// * [SwitchListTile], which combines this widget with a [ListTile] so that /// you can give the switch a label. /// * [Checkbox], another widget with similar semantics. /// * [Radio], for selecting among a set of explicit values. /// * [Slider], for selecting a value in a range. /// * <https://material.io/design/components/selection-controls.html#switches> class Switch extends StatelessWidget { /// 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. /// /// 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. const Switch({ super.key, required this.value, required this.onChanged, 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, }) : _switchType = _SwitchType.material, assert(dragStartBehavior != null), assert(activeThumbImage != null || onActiveThumbImageError == null), assert(inactiveThumbImage != null || onInactiveThumbImageError == null); /// 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). /// /// 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], /// [activeThumbImage], [onActiveThumbImageError], [inactiveThumbImage], /// [onInactiveThumbImageError], [materialTapTargetSize]. /// /// The target platform is based on the current [Theme]: [ThemeData.platform]. const Switch.adaptive({ super.key, required this.value, required this.onChanged, this.activeColor, this.activeTrackColor, this.inactiveThumbColor, this.inactiveTrackColor, this.activeThumbImage, this.onActiveThumbImageError, this.inactiveThumbImage, this.onInactiveThumbImageError, this.materialTapTargetSize, this.thumbColor, this.trackColor, this.dragStartBehavior = DragStartBehavior.start, this.mouseCursor, this.focusColor, this.hoverColor, this.overlayColor, this.splashRadius, this.focusNode, this.autofocus = false, }) : assert(autofocus != null), assert(activeThumbImage != null || onActiveThumbImageError == null), assert(inactiveThumbImage != null || onInactiveThumbImageError == null), _switchType = _SwitchType.adaptive; /// Whether this switch is on or off. /// /// This property must not be null. final bool value; /// Called when the user toggles the switch on or off. /// /// 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. /// /// The callback provided to [onChanged] should update the state of the parent /// [StatefulWidget] using the [State.setState] method, so that the parent /// gets rebuilt; for example: /// /// ```dart /// Switch( /// value: _giveVerse, /// onChanged: (bool newValue) { /// setState(() { /// _giveVerse = newValue; /// }); /// }, /// ) /// ``` final ValueChanged<bool>? onChanged; /// The color to use when this switch is on. /// /// Defaults to [ThemeData.toggleableActiveColor]. /// /// If [thumbColor] returns a non-null color in the [MaterialState.selected] /// state, it will be used instead of this color. final Color? activeColor; /// The color to use on the track when this switch is on. /// /// Defaults to [ThemeData.toggleableActiveColor] with the opacity set at 50%. /// /// Ignored if this switch is created with [Switch.adaptive]. /// /// If [trackColor] returns a non-null color in the [MaterialState.selected] /// state, it will be used instead of this color. 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. /// /// Ignored if this switch is created with [Switch.adaptive]. /// /// If [thumbColor] returns a non-null color in the default state, it will be /// used instead of this color. 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. /// /// Ignored if this switch is created with [Switch.adaptive]. /// /// If [trackColor] returns a non-null color in the default state, it will be /// used instead of this color. final Color? inactiveTrackColor; /// An image to use on the thumb of this switch when the switch is on. /// /// Ignored if this switch is created with [Switch.adaptive]. final ImageProvider? activeThumbImage; /// An optional error callback for errors emitted when loading /// [activeThumbImage]. final ImageErrorListener? onActiveThumbImageError; /// An image to use on the thumb of this switch when the switch is off. /// /// Ignored if this switch is created with [Switch.adaptive]. final ImageProvider? inactiveThumbImage; /// An optional error callback for errors emitted when loading /// [inactiveThumbImage]. final ImageErrorListener? onInactiveThumbImageError; /// {@template flutter.material.switch.thumbColor} /// The color of this [Switch]'s thumb. /// /// Resolved in the following states: /// * [MaterialState.selected]. /// * [MaterialState.hovered]. /// * [MaterialState.focused]. /// * [MaterialState.disabled]. /// /// {@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>((Set<MaterialState> states) { /// if (states.contains(MaterialState.disabled)) { /// return Colors.orange.withOpacity(.48); /// } /// return Colors.orange; /// }), /// ) /// ``` /// {@end-tool} /// {@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` | final MaterialStateProperty<Color?>? thumbColor; /// {@template flutter.material.switch.trackColor} /// The color of this [Switch]'s track. /// /// Resolved in the following states: /// * [MaterialState.selected]. /// * [MaterialState.hovered]. /// * [MaterialState.focused]. /// * [MaterialState.disabled]. /// /// {@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>((Set<MaterialState> states) { /// if (states.contains(MaterialState.disabled)) { /// return Colors.orange.withOpacity(.48); /// } /// return Colors.orange; /// }), /// ) /// ``` /// {@end-tool} /// {@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 | /// |----------|---------------------------------|---------------------------------| /// | Default | `Color(0x52000000)` | `Colors.white30` | /// | Selected | [activeColor] with alpha `0x80` | [activeColor] with alpha `0x80` | /// | Disabled | `Colors.black12` | `Colors.white10` | final MaterialStateProperty<Color?>? trackColor; /// {@template flutter.material.switch.materialTapTargetSize} /// Configures the minimum size of the tap target. /// {@endtemplate} /// /// If null, then the value of [SwitchThemeData.materialTapTargetSize] is /// used. If that is also null, then the value of /// [ThemeData.materialTapTargetSize] is used. /// /// See also: /// /// * [MaterialTapTargetSize], for a description of how this affects tap targets. final MaterialTapTargetSize? materialTapTargetSize; final _SwitchType _switchType; /// {@macro flutter.cupertino.CupertinoSwitch.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// {@template flutter.material.switch.mouseCursor} /// 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]. /// {@endtemplate} /// /// If null, then the value of [SwitchThemeData.mouseCursor] is used. If that /// is also null, then [MaterialStateMouseCursor.clickable] is used. /// /// See also: /// /// * [MaterialStateMouseCursor], a [MouseCursor] that implements /// `MaterialStateProperty` which is used in APIs that need to accept /// either a [MouseCursor] or a [MaterialStateProperty<MouseCursor>]. final MouseCursor? mouseCursor; /// The color for the button's [Material] when it has the input focus. /// /// If [overlayColor] returns a non-null color in the [MaterialState.focused] /// state, it will be used instead. /// /// 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. final Color? focusColor; /// The color for the button's [Material] when a pointer is hovering over it. /// /// If [overlayColor] returns a non-null color in the [MaterialState.hovered] /// state, it will be used instead. /// /// 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. final Color? hoverColor; /// {@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; /// {@template flutter.material.switch.splashRadius} /// The splash radius of the circular [Material] ink response. /// {@endtemplate} /// /// If null, then the value of [SwitchThemeData.splashRadius] is used. If that /// is also null, then [kRadialReactionRadius] is used. final double? splashRadius; /// {@macro flutter.widgets.Focus.focusNode} final FocusNode? focusNode; /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; Size _getSwitchSize(BuildContext context) { final ThemeData theme = Theme.of(context); final SwitchThemeData switchTheme = SwitchTheme.of(context); final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize ?? switchTheme.materialTapTargetSize ?? theme.materialTapTargetSize; switch (effectiveMaterialTapTargetSize) { case MaterialTapTargetSize.padded: return const Size(_kSwitchWidth, _kSwitchHeight); case MaterialTapTargetSize.shrinkWrap: return const Size(_kSwitchWidth, _kSwitchHeightCollapsed); } } Widget _buildCupertinoSwitch(BuildContext context) { final Size size = _getSwitchSize(context); 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, trackColor: inactiveTrackColor, ), ), ); } Widget _buildMaterialSwitch(BuildContext context) { return _MaterialSwitch( value: value, onChanged: onChanged, size: _getSwitchSize(context), 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, ); } @override 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); } } } } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true)); properties.add(ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled')); } } 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), assert(inactiveThumbImage != null || onInactiveThumbImageError == null); 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; @override State<StatefulWidget> createState() => _MaterialSwitchState(); } class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderStateMixin, ToggleableStateMixin { final _SwitchPainter _painter = _SwitchPainter(); @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(); } } @override void dispose() { _painter.dispose(); super.dispose(); } @override ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null; @override bool get tristate => false; @override bool? get value => widget.value; 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; }); } 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) { assert(debugCheckHasMaterial(context)); if (_needsPositionAnimation) { _needsPositionAnimation = false; animateToValue(); } final ThemeData theme = Theme.of(context); final SwitchThemeData switchTheme = SwitchTheme.of(context); // Colors need to be resolved in selected and non selected states separately // so that they can be lerped between. final Set<MaterialState> activeStates = states..add(MaterialState.selected); final Set<MaterialState> inactiveStates = states..remove(MaterialState.selected); final Color effectiveActiveThumbColor = widget.thumbColor?.resolve(activeStates) ?? _widgetThumbColor.resolve(activeStates) ?? switchTheme.thumbColor?.resolve(activeStates) ?? _defaultThumbColor.resolve(activeStates); final Color effectiveInactiveThumbColor = widget.thumbColor?.resolve(inactiveStates) ?? _widgetThumbColor.resolve(inactiveStates) ?? switchTheme.thumbColor?.resolve(inactiveStates) ?? _defaultThumbColor.resolve(inactiveStates); final Color effectiveActiveTrackColor = widget.trackColor?.resolve(activeStates) ?? _widgetTrackColor.resolve(activeStates) ?? switchTheme.trackColor?.resolve(activeStates) ?? _defaultTrackColor.resolve(activeStates); final Color effectiveInactiveTrackColor = widget.trackColor?.resolve(inactiveStates) ?? _widgetTrackColor.resolve(inactiveStates) ?? switchTheme.trackColor?.resolve(inactiveStates) ?? _defaultTrackColor.resolve(inactiveStates); final Set<MaterialState> focusedStates = states..add(MaterialState.focused); final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates) ?? widget.focusColor ?? switchTheme.overlayColor?.resolve(focusedStates) ?? theme.focusColor; final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered); final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates) ?? widget.hoverColor ?? switchTheme.overlayColor?.resolve(hoveredStates) ?? theme.hoverColor; final Set<MaterialState> activePressedStates = activeStates..add(MaterialState.pressed); final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates) ?? switchTheme.overlayColor?.resolve(activePressedStates) ?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha); final Set<MaterialState> inactivePressedStates = inactiveStates..add(MaterialState.pressed); final Color effectiveInactivePressedOverlayColor = widget.overlayColor?.resolve(inactivePressedStates) ?? switchTheme.overlayColor?.resolve(inactivePressedStates) ?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha); final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) { return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states) ?? switchTheme.mouseCursor?.resolve(states) ?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, states); }); 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 ..splashRadius = widget.splashRadius ?? switchTheme.splashRadius ?? kRadialReactionRadius ..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) ..surfaceColor = theme.colorScheme.surface, ), ), ); } } class _SwitchPainter extends ToggleablePainter { ImageProvider? get activeThumbImage => _activeThumbImage; ImageProvider? _activeThumbImage; set activeThumbImage(ImageProvider? value) { if (value == _activeThumbImage) return; _activeThumbImage = value; notifyListeners(); } ImageErrorListener? get onActiveThumbImageError => _onActiveThumbImageError; ImageErrorListener? _onActiveThumbImageError; set onActiveThumbImageError(ImageErrorListener? value) { if (value == _onActiveThumbImageError) { return; } _onActiveThumbImageError = value; notifyListeners(); } ImageProvider? get inactiveThumbImage => _inactiveThumbImage; ImageProvider? _inactiveThumbImage; set inactiveThumbImage(ImageProvider? value) { if (value == _inactiveThumbImage) return; _inactiveThumbImage = value; notifyListeners(); } ImageErrorListener? get onInactiveThumbImageError => _onInactiveThumbImageError; ImageErrorListener? _onInactiveThumbImageError; set onInactiveThumbImageError(ImageErrorListener? value) { if (value == _onInactiveThumbImageError) { return; } _onInactiveThumbImageError = value; notifyListeners(); } Color get activeTrackColor => _activeTrackColor!; Color? _activeTrackColor; set activeTrackColor(Color value) { assert(value != null); if (value == _activeTrackColor) return; _activeTrackColor = value; notifyListeners(); } Color get inactiveTrackColor => _inactiveTrackColor!; Color? _inactiveTrackColor; set inactiveTrackColor(Color value) { assert(value != null); if (value == _inactiveTrackColor) return; _inactiveTrackColor = value; notifyListeners(); } ImageConfiguration get configuration => _configuration!; ImageConfiguration? _configuration; set configuration(ImageConfiguration value) { assert(value != null); if (value == _configuration) return; _configuration = value; notifyListeners(); } TextDirection get textDirection => _textDirection!; TextDirection? _textDirection; set textDirection(TextDirection value) { assert(value != null); if (_textDirection == value) return; _textDirection = value; notifyListeners(); } Color get surfaceColor => _surfaceColor!; Color? _surfaceColor; set surfaceColor(Color value) { assert(value != null); if (value == _surfaceColor) return; _surfaceColor = value; notifyListeners(); } bool get isInteractive => _isInteractive!; bool? _isInteractive; set isInteractive(bool value) { if (value == _isInteractive) { return; } _isInteractive = value; notifyListeners(); } double get trackInnerLength => _trackInnerLength!; double? _trackInnerLength; set trackInnerLength(double value) { if (value == _trackInnerLength) { return; } _trackInnerLength = value; notifyListeners(); } Color? _cachedThumbColor; ImageProvider? _cachedThumbImage; ImageErrorListener? _cachedThumbErrorListener; BoxPainter? _cachedThumbPainter; BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider? image, ImageErrorListener? errorListener) { return BoxDecoration( color: color, image: image == null ? null : DecorationImage(image: image, onError: errorListener), shape: BoxShape.circle, boxShadow: kElevationToShadow[1], ); } 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) notifyListeners(); } @override void paint(Canvas canvas, Size size) { final bool isEnabled = isInteractive; final double currentValue = position.value; final double visualPosition; switch (textDirection) { case TextDirection.rtl: visualPosition = 1.0 - currentValue; break; case TextDirection.ltr: visualPosition = currentValue; break; } 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); final ImageProvider? thumbImage = isEnabled ? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage) : inactiveThumbImage; final ImageErrorListener? thumbErrorListener = isEnabled ? (currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError) : onInactiveThumbImageError; final Paint paint = Paint() ..color = trackColor; 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) { final Rect trackRect = Rect.fromLTWH( trackPaintOffset.dx, trackPaintOffset.dy, _kTrackWidth, _kTrackHeight, ); final RRect trackRRect = RRect.fromRectAndRadius( trackRect, const Radius.circular(_kTrackRadius), ); canvas.drawRRect(trackRRect, paint); } void _paintThumbWith( Offset thumbPaintOffset, Canvas canvas, double currentValue, Color thumbColor, ImageProvider? thumbImage, ImageErrorListener? thumbErrorListener, ) { try { _isPainting = true; if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage || thumbErrorListener != _cachedThumbErrorListener) { _cachedThumbColor = thumbColor; _cachedThumbImage = thumbImage; _cachedThumbErrorListener = thumbErrorListener; _cachedThumbPainter?.dispose(); _cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage, thumbErrorListener).createBoxPainter(_handleDecorationChanged); } final BoxPainter thumbPainter = _cachedThumbPainter!; // The thumb contracts slightly during the animation final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0; final double radius = _kThumbRadius - inset; thumbPainter.paint( canvas, thumbPaintOffset + Offset(0, inset), configuration.copyWith(size: Size.fromRadius(radius)), ); } finally { _isPainting = false; } } @override void dispose() { _cachedThumbPainter?.dispose(); _cachedThumbPainter = null; _cachedThumbColor = null; _cachedThumbImage = null; _cachedThumbErrorListener = null; super.dispose(); } }