blob: b3eb1c84aa9b51553f34d3adb7410b5c622c62a0 [file] [log] [blame]
// 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 '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({
Key? 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),
super(key: key);
/// 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({
Key? 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,
super(key: key);
/// 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].
/// {@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].
/// {@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(ThemeData theme) {
final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize
?? theme.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(Theme.of(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(Theme.of(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({
Key? key,
required this.value,
required this.onChanged,
required this.size,
this.activeColor,
this.activeTrackColor,
this.inactiveThumbColor,
this.inactiveTrackColor,
this.activeThumbImage,
this.onActiveThumbImageError,
this.inactiveThumbImage,
this.onInactiveThumbImageError,
this.thumbColor,
this.trackColor,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
this.focusColor,
this.hoverColor,
this.overlayColor,
this.splashRadius,
this.focusNode,
this.autofocus = false,
}) : assert(dragStartBehavior != null),
assert(activeThumbImage != null || onActiveThumbImageError == null),
assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
super(key: key);
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);
// 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)
?? theme.switchTheme.thumbColor?.resolve(activeStates)
?? _defaultThumbColor.resolve(activeStates);
final Color effectiveInactiveThumbColor = widget.thumbColor?.resolve(inactiveStates)
?? _widgetThumbColor.resolve(inactiveStates)
?? theme.switchTheme.thumbColor?.resolve(inactiveStates)
?? _defaultThumbColor.resolve(inactiveStates);
final Color effectiveActiveTrackColor = widget.trackColor?.resolve(activeStates)
?? _widgetTrackColor.resolve(activeStates)
?? theme.switchTheme.trackColor?.resolve(activeStates)
?? _defaultTrackColor.resolve(activeStates);
final Color effectiveInactiveTrackColor = widget.trackColor?.resolve(inactiveStates)
?? _widgetTrackColor.resolve(inactiveStates)
?? theme.switchTheme.trackColor?.resolve(inactiveStates)
?? _defaultTrackColor.resolve(inactiveStates);
final Set<MaterialState> focusedStates = states..add(MaterialState.focused);
final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor
?? theme.switchTheme.overlayColor?.resolve(focusedStates)
?? theme.focusColor;
final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
?? widget.hoverColor
?? theme.switchTheme.overlayColor?.resolve(hoveredStates)
?? theme.hoverColor;
final Set<MaterialState> activePressedStates = activeStates..add(MaterialState.pressed);
final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates)
?? theme.switchTheme.overlayColor?.resolve(activePressedStates)
?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha);
final Set<MaterialState> inactivePressedStates = inactiveStates..add(MaterialState.pressed);
final Color effectiveInactivePressedOverlayColor = widget.overlayColor?.resolve(inactivePressedStates)
?? theme.switchTheme.overlayColor?.resolve(inactivePressedStates)
?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha);
final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? theme.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 ?? theme.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();
}
}