blob: 83f3d9de56b3d840685ab9c39362d6b7d7610112 [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.
// Examples can assume:
// bool _giveVerse = false;
// bool _lights = false;
// void setState(VoidCallback fn) { }
/// @docImport 'package:flutter/material.dart';
library;
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'theme.dart';
// Hand coded defaults eyeballed from an iOS simulator running iOS version 17.5.
const double _kDisabledOpacity = 0.5;
const double _kThumbRadius = 14.0;
const double _kTrackHeight = 31.0;
const double _kTrackWidth = 51.0;
const Size _kSwitchSize = Size(59.0, 39.0);
const double _kThumbExtensionFactor = 7.0;
const List<BoxShadow> _kSwitchBoxShadows = <BoxShadow>[
BoxShadow(
color: Color(0x26000000),
offset: Offset(0, 3),
blurRadius: 8.0,
),
BoxShadow(
color: Color(0x0F000000),
offset: Offset(0, 3),
blurRadius: 1.0,
),
];
// Label sizes and padding taken from xcode inspector.
// See https://github.com/flutter/flutter/issues/4830#issuecomment-528495360.
const double _kOnLabelWidth = 1.0;
const double _kOnLabelHeight = 10.0;
const double _kOnLabelPaddingHorizontal = 11.0;
const double _kOffLabelWidth = 1.0;
const double _kOffLabelPaddingHorizontal = 12.0;
const double _kOffLabelRadius = 5.0;
const CupertinoDynamicColor _kOffLabelColor = CupertinoDynamicColor.withBrightnessAndContrast(
debugLabel: 'offSwitchLabel',
// Source: https://github.com/flutter/flutter/pull/39993#discussion_r321946033
color: Color.fromARGB(255, 179, 179, 179),
// Source: https://github.com/flutter/flutter/pull/39993#issuecomment-535196665
darkColor: Color.fromARGB(255, 179, 179, 179),
// Source: https://github.com/flutter/flutter/pull/127776#discussion_r1244208264
highContrastColor: Color.fromARGB(255, 255, 255, 255),
darkHighContrastColor: Color.fromARGB(255, 255, 255, 255),
);
/// An iOS-style switch.
///
/// Used to toggle the on/off state of a single setting.
///
/// The switch itself does not maintain its toggle state. Instead, when the
/// toggle 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.
///
/// {@tool dartpad}
/// This example shows a toggleable [CupertinoSwitch]. When the thumb slides to
/// the other side of the track, the switch is toggled between on/off.
///
/// ** See code in examples/api/lib/cupertino/switch/cupertino_switch.0.dart **
/// {@end-tool}
///
/// {@tool snippet}
///
/// This sample shows how to use a [CupertinoSwitch] in a [CupertinoListTile]. The
/// [MergeSemantics] is used to turn the entire [CupertinoListTile] into a single item
/// for accessibility tools.
///
/// ```dart
/// MergeSemantics(
/// child: CupertinoListTile(
/// title: const Text('Lights'),
/// trailing: CupertinoSwitch(
/// value: _lights,
/// onChanged: (bool value) { setState(() { _lights = value; }); },
/// ),
/// onTap: () { setState(() { _lights = !_lights; }); },
/// ),
/// )
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [Switch], the Material Design equivalent.
/// * <https://developer.apple.com/design/human-interface-guidelines/toggles/>
class CupertinoSwitch extends StatefulWidget {
/// Creates an iOS-style 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.
///
/// The [dragStartBehavior] parameter defaults to [DragStartBehavior.start].
const CupertinoSwitch({
super.key,
required this.value,
required this.onChanged,
@Deprecated(
'Use activeTrackColor instead. '
'This feature was deprecated after v3.24.0-0.2.pre.'
)
Color? activeColor,
@Deprecated(
'Use inactiveTrackColor instead. '
'This feature was deprecated after v3.24.0-0.2.pre.'
)
Color? trackColor,
Color? activeTrackColor,
Color? inactiveTrackColor,
this.thumbColor,
this.inactiveThumbColor,
this.applyTheme,
this.focusColor,
this.onLabelColor,
this.offLabelColor,
this.activeThumbImage,
this.onActiveThumbImageError,
this.inactiveThumbImage,
this.onInactiveThumbImageError,
this.trackOutlineColor,
this.trackOutlineWidth,
this.thumbIcon,
this.mouseCursor,
this.focusNode,
this.onFocusChange,
this.autofocus = false,
this.dragStartBehavior = DragStartBehavior.start,
}) : assert(activeThumbImage != null || onActiveThumbImageError == null),
assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
assert(activeTrackColor == null || activeColor == null),
assert(inactiveTrackColor == null || trackColor == null),
activeTrackColor = activeTrackColor ?? activeColor,
inactiveTrackColor = inactiveTrackColor ?? trackColor;
/// Whether this switch is on or off.
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, which has a reduced opacity.
///
/// 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
/// CupertinoSwitch(
/// value: _giveVerse,
/// onChanged: (bool newValue) {
/// setState(() {
/// _giveVerse = newValue;
/// });
/// },
/// )
/// ```
final ValueChanged<bool>? onChanged;
/// The color to use for the track when the switch is on.
///
/// If null and [applyTheme] is false, defaults to [CupertinoColors.systemGreen]
/// in accordance to native iOS behavior. Otherwise, defaults to
/// [CupertinoThemeData.primaryColor].
///
/// See also:
///
/// * [inactiveTrackColor], the color to use for the track when the switch is off.
@Deprecated(
'Use activeTrackColor instead. '
'This feature was deprecated after v3.24.0-0.2.pre.'
)
Color? get activeColor => activeTrackColor;
/// The color to use for the track when the switch is on.
///
/// If null and [applyTheme] is false, defaults to [CupertinoColors.systemGreen]
/// in accordance to native iOS behavior. Otherwise, defaults to
/// [CupertinoThemeData.primaryColor].
///
/// See also:
///
/// * [inactiveTrackColor], the color to use for the track when the switch is off.
final Color? activeTrackColor;
/// The color to use for the track when the switch is off.
///
/// Defaults to [CupertinoColors.secondarySystemFill] when null.
///
/// See also:
///
/// * [inactiveTrackColor], the color to use for the track when the switch is off.
@Deprecated(
'Use inactiveTrackColor instead. '
'This feature was deprecated after v3.24.0-0.2.pre.'
)
Color? get trackColor => inactiveTrackColor;
/// The color to use for the track when the switch is off.
///
/// Defaults to [CupertinoColors.secondarySystemFill] when null.
///
/// See also:
///
/// * [activeTrackColor], the color to use for the track when the switch is on.
final Color? inactiveTrackColor;
/// The color to use for the thumb when the switch is on.
///
/// If this color is not opaque, it is blended against
/// [CupertinoThemeData.scaffoldBackgroundColor], so as not to see through the
/// thumb to the track underneath.
///
/// Defaults to [CupertinoColors.white] when null.
///
/// See also:
///
/// * [inactiveThumbColor], the color to use for the thumb when the switch is off.
final Color? thumbColor;
/// The color to use on the thumb when the switch is off.
///
/// If this color is not opaque, it is blended against
/// [CupertinoThemeData.scaffoldBackgroundColor], so as not to see through the
/// thumb to the track underneath.
///
/// If null, defaults to [thumbColor]. If that is also null,
/// [CupertinoColors.white] is used.
///
/// See also:
///
/// * [thumbColor], the color to use for the thumb when the switch is on.
final Color? inactiveThumbColor;
/// The color to use for the focus highlight for keyboard interactions.
///
/// Defaults to [activeColor] with an opacity of 0.80, a brightness of 0.69
/// and a saturation of 0.835.
final Color? focusColor;
/// The color to use for the accessibility label when the switch is on.
///
/// Defaults to [CupertinoColors.white] when null.
final Color? onLabelColor;
/// The color to use for the accessibility label when the switch is off.
///
/// Defaults to [Color.fromARGB(255, 179, 179, 179)]
/// (or [Color.fromARGB(255, 255, 255, 255)] in high contrast) when null.
final Color? offLabelColor;
/// {@macro flutter.material.switch.activeThumbImage}
final ImageProvider? activeThumbImage;
/// {@macro flutter.material.switch.onActiveThumbImageError}
final ImageErrorListener? onActiveThumbImageError;
/// {@macro flutter.material.switch.inactiveThumbImage}
final ImageProvider? inactiveThumbImage;
/// {@macro flutter.material.switch.onInactiveThumbImageError}
final ImageErrorListener? onInactiveThumbImageError;
/// The outline color of this [CupertinoSwitch]'s track.
///
/// Resolved in the following states:
/// * [WidgetState.selected].
/// * [WidgetState.hovered].
/// * [WidgetState.focused].
/// * [WidgetState.disabled].
///
/// {@tool snippet}
/// This example resolves the [trackOutlineColor] based on the current
/// [WidgetState] of the [CupertinoSwitch], providing a different [Color] when it is
/// [WidgetState.disabled].
///
/// ```dart
/// CupertinoSwitch(
/// value: true,
/// onChanged: (bool value) { },
/// trackOutlineColor: WidgetStateProperty.resolveWith<Color?>((Set<WidgetState> states) {
/// if (states.contains(WidgetState.disabled)) {
/// return CupertinoColors.activeOrange.withOpacity(.48);
/// }
/// return null; // Use the default color.
/// }),
/// )
/// ```
/// {@end-tool}
///
/// The [CupertinoSwitch] track has no outline by default.
final WidgetStateProperty<Color?>? trackOutlineColor;
/// The outline width of this [CupertinoSwitch]'s track.
///
/// Resolved in the following states:
/// * [WidgetState.selected].
/// * [WidgetState.hovered].
/// * [WidgetState.focused].
/// * [WidgetState.disabled].
///
/// {@tool snippet}
/// This example resolves the [trackOutlineWidth] based on the current
/// [WidgetState] of the [CupertinoSwitch], providing a different outline width when it is
/// [WidgetState.disabled].
///
/// ```dart
/// CupertinoSwitch(
/// value: true,
/// onChanged: (bool value) { },
/// trackOutlineWidth: WidgetStateProperty.resolveWith<double?>((Set<WidgetState> states) {
/// if (states.contains(WidgetState.disabled)) {
/// return 5.0;
/// }
/// return null; // Use the default width.
/// }),
/// )
/// ```
/// {@end-tool}
///
/// Since a [CupertinoSwitch] has no track outline by default, this parameter
/// is set only if [trackOutlineColor] is provided.
///
/// Defaults to 2.0 if a [trackOutlineColor] is provided.
final WidgetStateProperty<double?>? trackOutlineWidth;
/// The icon to use on the thumb of this switch.
///
/// Resolved in the following states:
/// * [WidgetState.selected].
/// * [WidgetState.hovered].
/// * [WidgetState.focused].
/// * [WidgetState.disabled].
///
/// {@tool snippet}
/// This example resolves the [thumbIcon] based on the current
/// [WidgetState] of the [CupertinoSwitch], providing a different [Icon] when it is
/// [WidgetState.disabled].
///
/// ```dart
/// CupertinoSwitch(
/// value: true,
/// onChanged: (bool value) { },
/// thumbIcon: WidgetStateProperty.resolveWith<Icon?>((Set<WidgetState> states) {
/// if (states.contains(WidgetState.disabled)) {
/// return const Icon(Icons.close);
/// }
/// return null; // All other states will use the default thumbIcon.
/// }),
/// )
/// ```
/// {@end-tool}
///
/// If null, then the [CupertinoSwitch] does not have any icons on the thumb.
final WidgetStateProperty<Icon?>? thumbIcon;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// Resolved in the following states:
/// * [WidgetState.selected].
/// * [WidgetState.hovered].
/// * [WidgetState.focused].
/// * [WidgetState.disabled].
///
/// {@tool snippet}
/// This example resolves the [mouseCursor] based on the current
/// [WidgetState] of the [CupertinoSwitch], providing a different [mouseCursor] when it is
/// [WidgetState.disabled].
///
/// ```dart
/// CupertinoSwitch(
/// value: true,
/// onChanged: (bool value) { },
/// mouseCursor: WidgetStateProperty.resolveWith<MouseCursor>((Set<WidgetState> states) {
/// if (states.contains(WidgetState.disabled)) {
/// return SystemMouseCursors.click;
/// }
/// return SystemMouseCursors.basic; // All other states will use the default mouseCursor.
/// }),
/// )
/// ```
/// {@end-tool}
///
/// If null, then [MouseCursor.defer] is used when the switch is disabled.
/// When the switch is enabled, [SystemMouseCursors.click] is used on Web, and
/// [MouseCursor.defer] is used on other platforms.
///
/// See also:
///
/// * [WidgetStateMouseCursor], a [MouseCursor] that implements
/// `WidgetStateProperty` which is used in APIs that need to accept
/// either a [MouseCursor] or a [WidgetStateProperty].
final WidgetStateProperty<MouseCursor>? mouseCursor;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
/// Handler called when the focus changes.
///
/// Called with true if this widget's node gains focus, and false if it loses
/// focus.
final ValueChanged<bool>? onFocusChange;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// {@template flutter.cupertino.CupertinoSwitch.applyTheme}
/// Whether to apply the ambient [CupertinoThemeData].
///
/// If true, the track uses [CupertinoThemeData.primaryColor] for the track
/// when the switch is on.
///
/// Defaults to [CupertinoThemeData.applyThemeToAll].
/// {@endtemplate}
final bool? applyTheme;
/// {@template flutter.cupertino.CupertinoSwitch.dragStartBehavior}
/// Determines the way that drag start behavior is handled.
///
/// If set to [DragStartBehavior.start], the drag behavior used to move the
/// switch from on to off will begin at the position where the drag gesture won
/// the arena. If set to [DragStartBehavior.down] it will begin at the position
/// where a down event was first detected.
///
/// In general, setting this to [DragStartBehavior.start] will make drag
/// animation smoother and setting it to [DragStartBehavior.down] will make
/// drag behavior feel slightly more reactive.
///
/// By default, the drag start behavior is [DragStartBehavior.start].
///
/// See also:
///
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for
/// the different behaviors.
///
/// {@endtemplate}
final DragStartBehavior dragStartBehavior;
@override
State<CupertinoSwitch> createState() => _CupertinoSwitchState();
@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 _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderStateMixin, ToggleableStateMixin {
final _SwitchPainter _painter = _SwitchPainter();
@override
void initState() {
super.initState();
positionController.duration = const Duration(milliseconds: 200);
reactionController.duration = const Duration(milliseconds: 300);
}
@override
void didUpdateWidget(CupertinoSwitch oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != widget.value) {
position
..curve = Curves.ease
..reverseCurve = Curves.ease.flipped;
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;
WidgetStateProperty<Color?> get _widgetThumbColor {
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return widget.thumbColor;
}
return widget.inactiveThumbColor;
});
}
WidgetStateProperty<Color?> get _widgetTrackColor {
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return widget.activeTrackColor;
}
return widget.inactiveTrackColor;
});
}
WidgetStateProperty<MouseCursor> get _defaultMouseCursor =>
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.disabled)) {
return MouseCursor.defer;
}
return kIsWeb ? SystemMouseCursors.click : MouseCursor.defer;
});
Color? _resolveTrackColor(Color? trackColor, Set<WidgetState> states) {
if (trackColor is WidgetStateColor) {
return WidgetStateProperty.resolveAs<Color?>(trackColor, states);
}
return trackColor;
}
Color? _resolveThumbColor(Color? thumbColor, Set<WidgetState> states) {
if (thumbColor is WidgetStateColor) {
return WidgetStateProperty.resolveAs<Color?>(thumbColor, states);
}
return thumbColor;
}
double get _trackInnerLength {
const double trackInnerStart = _kTrackHeight / 2.0;
const double trackInnerEnd = _kTrackWidth - trackInnerStart;
const double trackInnerLength = trackInnerEnd - trackInnerStart;
return trackInnerLength;
}
void _handleDragStart(DragStartDetails details) {
if (isInteractive) {
reactionController.forward();
_emitVibration();
}
}
void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) {
position
..curve = Curves.linear
..reverseCurve = Curves.linear;
final double delta = details.primaryDelta! / _trackInnerLength;
positionController.value += switch (Directionality.of(context)) {
TextDirection.rtl => -delta,
TextDirection.ltr => delta,
};
}
}
bool _needsPositionAnimation = false;
void _handleDragEnd(DragEndDetails details) {
if (position.value >= 0.5 != widget.value) {
widget.onChanged?.call(!widget.value);
// Wait to finish 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?.call(value!);
_emitVibration();
}
void _emitVibration() {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
HapticFeedback.lightImpact();
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
}
@override
Widget build(BuildContext context) {
if (_needsPositionAnimation) {
_needsPositionAnimation = false;
animateToValue();
}
final CupertinoThemeData theme = CupertinoTheme.of(context);
final Color activeColor = CupertinoDynamicColor.resolve(
widget.activeTrackColor
?? ((widget.applyTheme ?? theme.applyThemeToAll) ? theme.primaryColor : null)
?? CupertinoColors.systemGreen,
context,
);
final (Color onLabelColor, Color offLabelColor)? onOffLabelColors =
MediaQuery.onOffSwitchLabelsOf(context)
? (CupertinoDynamicColor.resolve(
widget.onLabelColor ?? CupertinoColors.white,
context,
),
CupertinoDynamicColor.resolve(
widget.offLabelColor ?? _kOffLabelColor,
context,
),
)
: null;
// Colors need to be resolved in selected and non selected states separately
// so that they can be lerped between.
final Set<WidgetState> activeStates = states..add(WidgetState.selected);
final Set<WidgetState> inactiveStates = states..remove(WidgetState.selected);
final Color effectiveActiveThumbColor = _resolveThumbColor(widget.thumbColor, activeStates)
?? _widgetThumbColor.resolve(activeStates)
?? CupertinoColors.white;
final Color effectiveInactiveThumbColor = _resolveThumbColor(widget.inactiveThumbColor, inactiveStates)
?? _widgetThumbColor.resolve(inactiveStates)
?? effectiveActiveThumbColor;
final Color effectiveActiveTrackColor = _widgetTrackColor.resolve(activeStates) ?? activeColor;
final Color? effectiveActiveTrackOutlineColor = widget.trackOutlineColor?.resolve(activeStates);
final double? effectiveActiveTrackOutlineWidth = widget.trackOutlineWidth?.resolve(activeStates);
final Color effectiveInactiveTrackColor = _resolveTrackColor(widget.trackColor, inactiveStates)
?? CupertinoDynamicColor.resolve(CupertinoColors.secondarySystemFill, context);
final Color? effectiveInactiveTrackOutlineColor = widget.trackOutlineColor?.resolve(inactiveStates);
final double? effectiveInactiveTrackOutlineWidth = widget.trackOutlineWidth?.resolve(inactiveStates);
final Icon? effectiveActiveIcon = widget.thumbIcon?.resolve(activeStates);
final Icon? effectiveInactiveIcon = widget.thumbIcon?.resolve(inactiveStates);
final Color effectiveActiveIconColor = effectiveActiveIcon?.color ?? CupertinoColors.black;
final Color effectiveInactiveIconColor = effectiveInactiveIcon?.color ?? CupertinoColors.black;
final Set<WidgetState> activePressedStates = activeStates..add(WidgetState.pressed);
final Color effectiveActivePressedThumbColor = _resolveThumbColor(widget.thumbColor, activePressedStates)
?? _widgetThumbColor.resolve(activePressedStates)
?? CupertinoColors.white;
final Set<WidgetState> inactivePressedStates = inactiveStates..add(WidgetState.pressed);
final Color effectiveInactivePressedThumbColor = _resolveThumbColor(widget.thumbColor, inactivePressedStates)
?? _widgetThumbColor.resolve(inactivePressedStates)
?? CupertinoColors.white;
final WidgetStateProperty<MouseCursor> effectiveMouseCursor = widget.mouseCursor ?? _defaultMouseCursor;
return Semantics(
toggled: widget.value,
child: GestureDetector(
excludeFromSemantics: true,
onHorizontalDragStart: _handleDragStart,
onHorizontalDragUpdate: _handleDragUpdate,
onHorizontalDragEnd: _handleDragEnd,
dragStartBehavior: widget.dragStartBehavior,
child: Opacity(
opacity: onChanged == null ? _kDisabledOpacity : 1,
child: buildToggleable(
mouseCursor: effectiveMouseCursor,
focusNode: widget.focusNode,
onFocusChange: widget.onFocusChange,
autofocus: widget.autofocus,
size: _kSwitchSize,
painter: _painter
..position = position
..reaction = reaction
..reactionFocusFade = reactionFocusFade
..reactionHoverFade = reactionHoverFade
..focusColor = CupertinoDynamicColor.resolve(
widget.focusColor ??
HSLColor
.fromColor(activeColor.withOpacity(kCupertinoFocusColorOpacity))
.withLightness(kCupertinoFocusColorBrightness)
.withSaturation(kCupertinoFocusColorSaturation)
.toColor(),
context)
..downPosition = downPosition
..isFocused = states.contains(WidgetState.focused)
..isHovered = states.contains(WidgetState.hovered)
..activeColor = effectiveActiveThumbColor
..inactiveColor = effectiveInactiveThumbColor
..activePressedColor = effectiveActivePressedThumbColor
..onOffLabelColors = onOffLabelColors
..inactivePressedColor = effectiveInactivePressedThumbColor
..activeThumbImage = widget.activeThumbImage
..onActiveThumbImageError = widget.onActiveThumbImageError
..inactiveThumbImage = widget.inactiveThumbImage
..onInactiveThumbImageError = widget.onInactiveThumbImageError
..activeTrackColor = effectiveActiveTrackColor
..activeTrackOutlineColor = effectiveActiveTrackOutlineColor
..activeTrackOutlineWidth = effectiveActiveTrackOutlineWidth
..inactiveTrackColor = effectiveInactiveTrackColor
..inactiveTrackOutlineColor = effectiveInactiveTrackOutlineColor
..inactiveTrackOutlineWidth = effectiveInactiveTrackOutlineWidth
..configuration = createLocalImageConfiguration(context)
..isInteractive = isInteractive
..trackInnerLength = _trackInnerLength
..textDirection = Directionality.of(context)
..activeIconColor = effectiveActiveIconColor
..inactiveIconColor = effectiveInactiveIconColor
..activeIcon = effectiveActiveIcon
..inactiveIcon = effectiveInactiveIcon
..iconTheme = IconTheme.of(context)
..surfaceColor = theme.scaffoldBackgroundColor
..positionController = positionController
),
),
),
);
}
}
class _SwitchPainter extends ToggleablePainter {
AnimationController get positionController => _positionController!;
AnimationController? _positionController;
set positionController(AnimationController? value) {
assert(value != null);
if (value == _positionController) {
return;
}
_positionController = value;
_colorAnimation?.dispose();
_colorAnimation = CurvedAnimation(
parent: positionController,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn
);
notifyListeners();
}
CurvedAnimation? _colorAnimation;
Icon? get activeIcon => _activeIcon;
Icon? _activeIcon;
set activeIcon(Icon? value) {
if (value == _activeIcon) {
return;
}
_activeIcon = value;
notifyListeners();
}
Icon? get inactiveIcon => _inactiveIcon;
Icon? _inactiveIcon;
set inactiveIcon(Icon? value) {
if (value == _inactiveIcon) {
return;
}
_inactiveIcon = value;
notifyListeners();
}
IconThemeData? get iconTheme => _iconTheme;
IconThemeData? _iconTheme;
set iconTheme(IconThemeData? value) {
if (value == _iconTheme) {
return;
}
_iconTheme = value;
notifyListeners();
}
Color get activeIconColor => _activeIconColor!;
Color? _activeIconColor;
set activeIconColor(Color value) {
if (value == _activeIconColor) {
return;
}
_activeIconColor = value;
notifyListeners();
}
Color get inactiveIconColor => _inactiveIconColor!;
Color? _inactiveIconColor;
set inactiveIconColor(Color value) {
if (value == _inactiveIconColor) {
return;
}
_inactiveIconColor = value;
notifyListeners();
}
Color get activePressedColor => _activePressedColor!;
Color? _activePressedColor;
set activePressedColor(Color? value) {
assert(value != null);
if (value == _activePressedColor) {
return;
}
_activePressedColor = value;
notifyListeners();
}
Color get inactivePressedColor => _inactivePressedColor!;
Color? _inactivePressedColor;
set inactivePressedColor(Color? value) {
assert(value != null);
if (value == _inactivePressedColor) {
return;
}
_inactivePressedColor = value;
notifyListeners();
}
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) {
if (value == _activeTrackColor) {
return;
}
_activeTrackColor = value;
notifyListeners();
}
Color? get activeTrackOutlineColor => _activeTrackOutlineColor;
Color? _activeTrackOutlineColor;
set activeTrackOutlineColor(Color? value) {
if (value == _activeTrackOutlineColor) {
return;
}
_activeTrackOutlineColor = value;
notifyListeners();
}
Color? get inactiveTrackOutlineColor => _inactiveTrackOutlineColor;
Color? _inactiveTrackOutlineColor;
set inactiveTrackOutlineColor(Color? value) {
if (value == _inactiveTrackOutlineColor) {
return;
}
_inactiveTrackOutlineColor = value;
notifyListeners();
}
double? get activeTrackOutlineWidth => _activeTrackOutlineWidth;
double? _activeTrackOutlineWidth;
set activeTrackOutlineWidth(double? value) {
if (value == _activeTrackOutlineWidth) {
return;
}
_activeTrackOutlineWidth = value;
notifyListeners();
}
double? get inactiveTrackOutlineWidth => _inactiveTrackOutlineWidth;
double? _inactiveTrackOutlineWidth;
set inactiveTrackOutlineWidth(double? value) {
if (value == _inactiveTrackOutlineWidth) {
return;
}
_inactiveTrackOutlineWidth = value;
notifyListeners();
}
Color get inactiveTrackColor => _inactiveTrackColor!;
Color? _inactiveTrackColor;
set inactiveTrackColor(Color value) {
if (value == _inactiveTrackColor) {
return;
}
_inactiveTrackColor = value;
notifyListeners();
}
ImageConfiguration get configuration => _configuration!;
ImageConfiguration? _configuration;
set configuration(ImageConfiguration value) {
if (value == _configuration) {
return;
}
_configuration = value;
notifyListeners();
}
TextDirection get textDirection => _textDirection!;
TextDirection? _textDirection;
set textDirection(TextDirection value) {
if (_textDirection == value) {
return;
}
_textDirection = value;
notifyListeners();
}
Color get surfaceColor => _surfaceColor!;
Color? _surfaceColor;
set surfaceColor(Color value) {
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 onLabelColor, Color offLabelColor)? get onOffLabelColors => _onOffLabelColors;
(Color onLabelColor, Color offLabelColor)? _onOffLabelColors;
set onOffLabelColors((Color onLabelColor, Color offLabelColor)? value) {
if (value == _onOffLabelColors) {
return;
}
_onOffLabelColors = value;
notifyListeners();
}
final TextPainter _textPainter = TextPainter();
Color? _cachedThumbColor;
ImageProvider? _cachedThumbImage;
ImageErrorListener? _cachedThumbErrorListener;
BoxPainter? _cachedThumbPainter;
ShapeDecoration _createDefaultThumbDecoration(Color color, ImageProvider? image, ImageErrorListener? errorListener) {
return ShapeDecoration(
color: color,
image: image == null
? null
: DecorationImage(image: image, onError: errorListener),
shape: const StadiumBorder(),
);
}
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();
}
}
bool _stopPressAnimation = false;
late double? _pressedThumbExtension;
@override
void paint(Canvas canvas, Size size) {
final double currentValue = position.value;
final double visualPosition = switch (textDirection) {
TextDirection.rtl => 1.0 - currentValue,
TextDirection.ltr => currentValue,
};
if (reaction.status == AnimationStatus.reverse && !_stopPressAnimation) {
_stopPressAnimation = true;
} else {
_stopPressAnimation = false;
}
_pressedThumbExtension = reaction.value * _kThumbExtensionFactor;
final Size thumbSize = Size(
_kThumbRadius * 2 + _pressedThumbExtension!,
_kThumbRadius * 2,
);
final double colorValue = _colorAnimation!.value;
final Color trackColor = Color.lerp(inactiveTrackColor, activeTrackColor, position.value)!;
final Color? trackOutlineColor = inactiveTrackOutlineColor == null || activeTrackOutlineColor == null
? null
: Color.lerp(inactiveTrackOutlineColor, activeTrackOutlineColor, colorValue);
final double? trackOutlineWidth = lerpDouble(inactiveTrackOutlineWidth, activeTrackOutlineWidth, colorValue);
final Color lerpedThumbColor;
if (!reaction.isDismissed) {
lerpedThumbColor = Color.lerp(inactivePressedColor, activePressedColor, colorValue)!;
} else if (positionController.status == AnimationStatus.forward) {
lerpedThumbColor = Color.lerp(inactivePressedColor, activeColor, colorValue)!;
} else if (positionController.status == AnimationStatus.reverse) {
lerpedThumbColor = Color.lerp(inactiveColor, activePressedColor, colorValue)!;
} else {
lerpedThumbColor = Color.lerp(inactiveColor, activeColor, colorValue)!;
}
// 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 Icon? thumbIcon = currentValue < 0.5 ? inactiveIcon : activeIcon;
final ImageProvider? thumbImage = currentValue < 0.5 ? inactiveThumbImage : activeThumbImage;
final ImageErrorListener? thumbErrorListener = currentValue < 0.5
? onInactiveThumbImageError
: onActiveThumbImageError;
final Paint paint = Paint()..color = trackColor;
final Offset trackPaintOffset = _computeTrackPaintOffset(size);
final Offset thumbPaintOffset = _computeThumbPaintOffset(trackPaintOffset, thumbSize, visualPosition);
final Rect trackRect = Rect.fromLTWH(
trackPaintOffset.dx,
trackPaintOffset.dy,
_kTrackWidth,
_kTrackHeight,
);
_paintTrackWith(canvas, paint, trackPaintOffset, trackOutlineColor, trackOutlineWidth, trackRect);
final double currentReactionValue = reaction.value;
if (_onOffLabelColors != null) {
final (Color onLabelColor, Color offLabelColor) = onOffLabelColors!;
final double leftLabelOpacity = visualPosition * (1.0 - currentReactionValue);
final double rightLabelOpacity = (1.0 - visualPosition) * (1.0 - currentReactionValue);
final (double onLabelOpacity, double offLabelOpacity) = switch (textDirection) {
TextDirection.ltr => (leftLabelOpacity, rightLabelOpacity),
TextDirection.rtl => (rightLabelOpacity, leftLabelOpacity),
};
final (Offset onLabelOffset, Offset offLabelOffset) = switch (textDirection) {
TextDirection.ltr => (
trackRect.centerLeft.translate(_kOnLabelPaddingHorizontal, 0),
trackRect.centerRight.translate(-_kOffLabelPaddingHorizontal, 0),
),
TextDirection.rtl => (
trackRect.centerRight.translate(-_kOnLabelPaddingHorizontal, 0),
trackRect.centerLeft.translate(_kOffLabelPaddingHorizontal, 0),
),
};
// Draws '|' label.
final Rect onLabelRect = Rect.fromCenter(
center: onLabelOffset,
width: _kOnLabelWidth,
height: _kOnLabelHeight,
);
final Paint onLabelPaint = Paint()
..color = onLabelColor.withOpacity(onLabelOpacity)
..style = PaintingStyle.fill;
canvas.drawRect(onLabelRect, onLabelPaint);
// Draws 'O' label.
final Paint offLabelPaint = Paint()
..color = offLabelColor.withOpacity(offLabelOpacity)
..style = PaintingStyle.stroke
..strokeWidth = _kOffLabelWidth;
canvas.drawCircle(
offLabelOffset,
_kOffLabelRadius,
offLabelPaint,
);
}
_paintThumbWith(
thumbPaintOffset,
canvas,
colorValue,
thumbColor,
thumbImage,
thumbErrorListener,
thumbIcon,
thumbSize,
);
}
/// Computes canvas offset for track's upper left corner.
static Offset _computeTrackPaintOffset(Size canvasSize) {
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, Size thumbSize, double visualPosition) {
// How much the thumb radius extends beyond the track.
const double trackRadius = _kTrackHeight / 2;
final double additionalThumbRadius = thumbSize.height / 2 - trackRadius;
final double horizontalProgress = visualPosition * (trackInnerLength - _pressedThumbExtension!);
final double thumbHorizontalOffset = trackPaintOffset.dx + trackRadius + (_pressedThumbExtension! / 2) - thumbSize.width / 2 + horizontalProgress;
final double thumbVerticalOffset = trackPaintOffset.dy - additionalThumbRadius;
return Offset(thumbHorizontalOffset, thumbVerticalOffset);
}
void _paintTrackWith(Canvas canvas, Paint paint, Offset trackPaintOffset, Color? trackOutlineColor, double? trackOutlineWidth, Rect trackRect) {
const double trackRadius = _kTrackHeight / 2;
final RRect trackRRect = RRect.fromRectAndRadius(
trackRect,
const Radius.circular(trackRadius),
);
canvas.drawRRect(trackRRect, paint);
// Paint the track outline.
if (trackOutlineColor != null) {
final Rect outlineTrackRect = Rect.fromLTWH(
trackPaintOffset.dx + 1,
trackPaintOffset.dy + 1,
_kTrackWidth - 2,
_kTrackHeight - 2,
);
final RRect outlineTrackRRect = RRect.fromRectAndRadius(
outlineTrackRect,
const Radius.circular(trackRadius),
);
final Paint outlinePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = trackOutlineWidth ?? 2.0
..color = trackOutlineColor;
canvas.drawRRect(outlineTrackRRect, outlinePaint);
}
if (isFocused) {
final RRect focusedOutline = trackRRect.inflate(1.75);
final Paint focusedPaint = Paint()
..style = PaintingStyle.stroke
..color = focusColor
..strokeWidth = 3.5;
canvas.drawRRect(focusedOutline, focusedPaint);
}
canvas.clipRRect(trackRRect);
}
void _paintThumbWith(
Offset thumbPaintOffset,
Canvas canvas,
double currentValue,
Color thumbColor,
ImageProvider? thumbImage,
ImageErrorListener? thumbErrorListener,
Icon? thumbIcon,
Size thumbSize,
) {
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!;
_paintCupertinoThumbShadowAndBorder(canvas, thumbPaintOffset, thumbSize);
thumbPainter.paint(
canvas,
thumbPaintOffset,
configuration.copyWith(size: thumbSize),
);
if (thumbIcon != null && thumbIcon.icon != null) {
final Color iconColor = Color.lerp(inactiveIconColor, activeIconColor, currentValue)!;
final double iconSize = thumbIcon.size ?? 16.0;
final IconData iconData = thumbIcon.icon!;
final double? iconWeight = thumbIcon.weight ?? iconTheme?.weight;
final double? iconFill = thumbIcon.fill ?? iconTheme?.fill;
final double? iconGrade = thumbIcon.grade ?? iconTheme?.grade;
final double? iconOpticalSize = thumbIcon.opticalSize ?? iconTheme?.opticalSize;
final List<Shadow>? iconShadows = thumbIcon.shadows ?? iconTheme?.shadows;
final TextSpan textSpan = TextSpan(
text: String.fromCharCode(iconData.codePoint),
style: TextStyle(
fontVariations: <FontVariation>[
if (iconFill != null) FontVariation('FILL', iconFill),
if (iconWeight != null) FontVariation('wght', iconWeight),
if (iconGrade != null) FontVariation('GRAD', iconGrade),
if (iconOpticalSize != null) FontVariation('opsz', iconOpticalSize),
],
color: iconColor,
fontSize: iconSize,
inherit: false,
fontFamily: iconData.fontFamily,
package: iconData.fontPackage,
shadows: iconShadows,
),
);
_textPainter
..textDirection = textDirection
..text = textSpan;
_textPainter.layout();
final double additionalHorizontalOffset = (thumbSize.width - iconSize) / 2;
final double additionalVerticalOffset = (thumbSize.height - iconSize) / 2;
final Offset offset = thumbPaintOffset + Offset(additionalHorizontalOffset, additionalVerticalOffset);
_textPainter.paint(canvas, offset);
}
} finally {
_isPainting = false;
}
}
void _paintCupertinoThumbShadowAndBorder(Canvas canvas, Offset thumbPaintOffset, Size thumbSize) {
final RRect thumbBounds = RRect.fromLTRBR(
thumbPaintOffset.dx,
thumbPaintOffset.dy,
thumbPaintOffset.dx + thumbSize.width,
thumbPaintOffset.dy + thumbSize.height,
Radius.circular(thumbSize.height / 2.0),
);
for (final BoxShadow shadow in _kSwitchBoxShadows) {
canvas.drawRRect(thumbBounds.shift(shadow.offset), shadow.toPaint());
}
canvas.drawRRect(
thumbBounds.inflate(0.5),
Paint()..color = const Color(0x0A000000),
);
}
@override
void dispose() {
_textPainter.dispose();
_cachedThumbPainter?.dispose();
_cachedThumbPainter = null;
_cachedThumbColor = null;
_cachedThumbImage = null;
_cachedThumbErrorListener = null;
_colorAnimation?.dispose();
super.dispose();
}
}