// 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 'package:flutter/widgets.dart'; import 'colors.dart'; import 'constants.dart'; import 'debug.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 _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReactionRadius; const double _kSwitchHeight = 2 * kRadialReactionRadius + 8.0; const double _kSwitchHeightCollapsed = 2 * kRadialReactionRadius; 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 StatefulWidget { /// 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.inactiveThumbImage, this.materialTapTargetSize, this.dragStartBehavior = DragStartBehavior.start, this.focusColor, this.hoverColor, this.focusNode, this.autofocus = false, }) : _switchType = _SwitchType.material, assert(dragStartBehavior != null), super(key: key); /// Creates a [CupertinoSwitch] if the target platform is iOS, creates a /// material design switch otherwise. /// /// If a [CupertinoSwitch] is created, the following parameters are /// ignored: [activeTrackColor], [inactiveThumbColor], [inactiveTrackColor], /// [activeThumbImage], [inactiveThumbImage], [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.inactiveThumbImage, this.materialTapTargetSize, this.dragStartBehavior = DragStartBehavior.start, this.focusColor, this.hoverColor, this.focusNode, this.autofocus = false, }) : assert(autofocus != 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]. 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]. 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]. 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]. 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 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; /// Configures the minimum size of the tap target. /// /// Defaults to [ThemeData.materialTapTargetSize]. /// /// See also: /// /// * [MaterialTapTargetSize], for a description of how this affects tap targets. final MaterialTapTargetSize materialTapTargetSize; final _SwitchType _switchType; /// {@macro flutter.cupertino.switch.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// The color for the button's [Material] when it has the input focus. final Color focusColor; /// The color for the button's [Material] when a pointer is hovering over it. final Color hoverColor; /// {@macro flutter.widgets.Focus.focusNode} final FocusNode focusNode; /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; @override _SwitchState createState() => _SwitchState(); @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 _SwitchState extends State<Switch> with TickerProviderStateMixin { Map<LocalKey, ActionFactory> _actionMap; @override void initState() { super.initState(); _actionMap = <LocalKey, ActionFactory>{ ActivateAction.key: _createAction, }; } void _actionHandler(FocusNode node, Intent intent){ if (widget.onChanged != null) { widget.onChanged(!widget.value); } final RenderObject renderObject = node.context.findRenderObject(); renderObject.sendSemanticsEvent(const TapSemanticEvent()); } Action _createAction() { return CallbackAction( ActivateAction.key, onInvoke: _actionHandler, ); } bool _focused = false; void _handleFocusHighlightChanged(bool focused) { if (focused != _focused) { setState(() { _focused = focused; }); } } bool _hovering = false; void _handleHoverChanged(bool hovering) { if (hovering != _hovering) { setState(() { _hovering = hovering; }); } } Size getSwitchSize(ThemeData theme) { switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) { case MaterialTapTargetSize.padded: return const Size(_kSwitchWidth, _kSwitchHeight); break; case MaterialTapTargetSize.shrinkWrap: return const Size(_kSwitchWidth, _kSwitchHeightCollapsed); break; } assert(false); return null; } bool get enabled => widget.onChanged != null; Widget buildMaterialSwitch(BuildContext context) { assert(debugCheckHasMaterial(context)); final ThemeData theme = Theme.of(context); final bool isDark = theme.brightness == Brightness.dark; final Color activeThumbColor = widget.activeColor ?? theme.toggleableActiveColor; final Color activeTrackColor = widget.activeTrackColor ?? activeThumbColor.withAlpha(0x80); final Color hoverColor = widget.hoverColor ?? theme.hoverColor; final Color focusColor = widget.focusColor ?? theme.focusColor; Color inactiveThumbColor; Color inactiveTrackColor; if (enabled) { const Color black32 = Color(0x52000000); // Black with 32% opacity inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade400 : Colors.grey.shade50); inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white30 : black32); } else { inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400); inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12); } return FocusableActionDetector( actions: _actionMap, focusNode: widget.focusNode, autofocus: widget.autofocus, enabled: enabled, onShowFocusHighlight: _handleFocusHighlightChanged, onShowHoverHighlight: _handleHoverChanged, child: Builder( builder: (BuildContext context) { return _SwitchRenderObjectWidget( dragStartBehavior: widget.dragStartBehavior, value: widget.value, activeColor: activeThumbColor, inactiveColor: inactiveThumbColor, hoverColor: hoverColor, focusColor: focusColor, activeThumbImage: widget.activeThumbImage, inactiveThumbImage: widget.inactiveThumbImage, activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, configuration: createLocalImageConfiguration(context), onChanged: widget.onChanged, additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)), hasFocus: _focused, hovering: _hovering, vsync: this, ); }, ), ); } Widget buildCupertinoSwitch(BuildContext context) { final Size size = getSwitchSize(Theme.of(context)); return Focus( focusNode: widget.focusNode, autofocus: widget.autofocus, child: Container( width: size.width, // Same size as the Material switch. height: size.height, alignment: Alignment.center, child: CupertinoSwitch( dragStartBehavior: widget.dragStartBehavior, value: widget.value, onChanged: widget.onChanged, activeColor: widget.activeColor, trackColor: widget.inactiveTrackColor ), ), ); } @override Widget build(BuildContext context) { switch (widget._switchType) { case _SwitchType.material: return buildMaterialSwitch(context); case _SwitchType.adaptive: { final ThemeData theme = Theme.of(context); assert(theme.platform != null); switch (theme.platform) { case TargetPlatform.android: case TargetPlatform.fuchsia: return buildMaterialSwitch(context); case TargetPlatform.iOS: case TargetPlatform.macOS: return buildCupertinoSwitch(context); } } } assert(false); return null; } } class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { const _SwitchRenderObjectWidget({ Key key, this.value, this.activeColor, this.inactiveColor, this.hoverColor, this.focusColor, this.activeThumbImage, this.inactiveThumbImage, this.activeTrackColor, this.inactiveTrackColor, this.configuration, this.onChanged, this.vsync, this.additionalConstraints, this.dragStartBehavior, this.hasFocus, this.hovering, }) : super(key: key); final bool value; final Color activeColor; final Color inactiveColor; final Color hoverColor; final Color focusColor; final ImageProvider activeThumbImage; final ImageProvider inactiveThumbImage; final Color activeTrackColor; final Color inactiveTrackColor; final ImageConfiguration configuration; final ValueChanged<bool> onChanged; final TickerProvider vsync; final BoxConstraints additionalConstraints; final DragStartBehavior dragStartBehavior; final bool hasFocus; final bool hovering; @override _RenderSwitch createRenderObject(BuildContext context) { return _RenderSwitch( dragStartBehavior: dragStartBehavior, value: value, activeColor: activeColor, inactiveColor: inactiveColor, hoverColor: hoverColor, focusColor: focusColor, activeThumbImage: activeThumbImage, inactiveThumbImage: inactiveThumbImage, activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, configuration: configuration, onChanged: onChanged, textDirection: Directionality.of(context), additionalConstraints: additionalConstraints, hasFocus: hasFocus, hovering: hovering, vsync: vsync, ); } @override void updateRenderObject(BuildContext context, _RenderSwitch renderObject) { renderObject ..value = value ..activeColor = activeColor ..inactiveColor = inactiveColor ..hoverColor = hoverColor ..focusColor = focusColor ..activeThumbImage = activeThumbImage ..inactiveThumbImage = inactiveThumbImage ..activeTrackColor = activeTrackColor ..inactiveTrackColor = inactiveTrackColor ..configuration = configuration ..onChanged = onChanged ..textDirection = Directionality.of(context) ..additionalConstraints = additionalConstraints ..dragStartBehavior = dragStartBehavior ..hasFocus = hasFocus ..hovering = hovering ..vsync = vsync; } } class _RenderSwitch extends RenderToggleable { _RenderSwitch({ bool value, Color activeColor, Color inactiveColor, Color hoverColor, Color focusColor, ImageProvider activeThumbImage, ImageProvider inactiveThumbImage, Color activeTrackColor, Color inactiveTrackColor, ImageConfiguration configuration, BoxConstraints additionalConstraints, @required TextDirection textDirection, ValueChanged<bool> onChanged, DragStartBehavior dragStartBehavior, bool hasFocus, bool hovering, @required TickerProvider vsync, }) : assert(textDirection != null), _activeThumbImage = activeThumbImage, _inactiveThumbImage = inactiveThumbImage, _activeTrackColor = activeTrackColor, _inactiveTrackColor = inactiveTrackColor, _configuration = configuration, _textDirection = textDirection, super( value: value, tristate: false, activeColor: activeColor, inactiveColor: inactiveColor, hoverColor: hoverColor, focusColor: focusColor, onChanged: onChanged, additionalConstraints: additionalConstraints, hasFocus: hasFocus, hovering: hovering, vsync: vsync, ) { _drag = HorizontalDragGestureRecognizer() ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd ..dragStartBehavior = dragStartBehavior; } ImageProvider get activeThumbImage => _activeThumbImage; ImageProvider _activeThumbImage; set activeThumbImage(ImageProvider value) { if (value == _activeThumbImage) return; _activeThumbImage = value; markNeedsPaint(); } ImageProvider get inactiveThumbImage => _inactiveThumbImage; ImageProvider _inactiveThumbImage; set inactiveThumbImage(ImageProvider value) { if (value == _inactiveThumbImage) return; _inactiveThumbImage = value; markNeedsPaint(); } Color get activeTrackColor => _activeTrackColor; Color _activeTrackColor; set activeTrackColor(Color value) { assert(value != null); if (value == _activeTrackColor) return; _activeTrackColor = value; markNeedsPaint(); } Color get inactiveTrackColor => _inactiveTrackColor; Color _inactiveTrackColor; set inactiveTrackColor(Color value) { assert(value != null); if (value == _inactiveTrackColor) return; _inactiveTrackColor = value; markNeedsPaint(); } ImageConfiguration get configuration => _configuration; ImageConfiguration _configuration; set configuration(ImageConfiguration value) { assert(value != null); if (value == _configuration) return; _configuration = value; markNeedsPaint(); } TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { assert(value != null); if (_textDirection == value) return; _textDirection = value; markNeedsPaint(); } DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior; set dragStartBehavior(DragStartBehavior value) { assert(value != null); if (_drag.dragStartBehavior == value) return; _drag.dragStartBehavior = value; } @override void detach() { _cachedThumbPainter?.dispose(); _cachedThumbPainter = null; super.detach(); } double get _trackInnerLength => size.width - 2.0 * kRadialReactionRadius; HorizontalDragGestureRecognizer _drag; void _handleDragStart(DragStartDetails details) { if (isInteractive) reactionController.forward(); } void _handleDragUpdate(DragUpdateDetails details) { if (isInteractive) { position ..curve = null ..reverseCurve = null; final double delta = details.primaryDelta / _trackInnerLength; switch (textDirection) { case TextDirection.rtl: positionController.value -= delta; break; case TextDirection.ltr: positionController.value += delta; break; } } } void _handleDragEnd(DragEndDetails details) { if (position.value >= 0.5) positionController.forward(); else positionController.reverse(); reactionController.reverse(); } @override void handleEvent(PointerEvent event, BoxHitTestEntry entry) { assert(debugHandleEvent(event, entry)); if (event is PointerDownEvent && onChanged != null) _drag.addPointer(event); super.handleEvent(event, entry); } Color _cachedThumbColor; ImageProvider _cachedThumbImage; BoxPainter _cachedThumbPainter; BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider image) { return BoxDecoration( color: color, image: image == null ? null : DecorationImage(image: image), 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) markNeedsPaint(); } @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); config.isToggled = value == true; } @override void paint(PaintingContext context, Offset offset) { final Canvas canvas = context.canvas; final bool isEnabled = onChanged != null; final double currentValue = position.value; double visualPosition; switch (textDirection) { case TextDirection.rtl: visualPosition = 1.0 - currentValue; break; case TextDirection.ltr: visualPosition = currentValue; break; } final Color trackColor = isEnabled ? Color.lerp(inactiveTrackColor, activeTrackColor, currentValue) : inactiveTrackColor; final Color thumbColor = isEnabled ? Color.lerp(inactiveColor, activeColor, currentValue) : inactiveColor; final ImageProvider thumbImage = isEnabled ? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage) : inactiveThumbImage; // Paint the track final Paint paint = Paint() ..color = trackColor; const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius; final Rect trackRect = Rect.fromLTWH( offset.dx + trackHorizontalPadding, offset.dy + (size.height - _kTrackHeight) / 2.0, size.width - 2.0 * trackHorizontalPadding, _kTrackHeight, ); final RRect trackRRect = RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius)); canvas.drawRRect(trackRRect, paint); final Offset thumbPosition = Offset( kRadialReactionRadius + visualPosition * _trackInnerLength, size.height / 2.0, ); paintRadialReaction(canvas, offset, thumbPosition); try { _isPainting = true; BoxPainter thumbPainter; if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage) { _cachedThumbColor = thumbColor; _cachedThumbImage = thumbImage; _cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage).createBoxPainter(_handleDecorationChanged); } 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, thumbPosition + offset - Offset(radius, radius), configuration.copyWith(size: Size.fromRadius(radius)), ); } finally { _isPainting = false; } } }