// Copyright 2015 The Chromium 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/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 'debug.dart'; import 'shadows.dart'; import 'theme.dart'; import 'toggleable.dart'; /// 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. /// /// 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.google.com/components/selection-controls.html#selection-controls-switch> 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 }) : 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 /// new Switch( /// value: _giveVerse, /// onChanged: (bool newValue) { /// setState(() { /// _giveVerse = newValue; /// }); /// }, /// ) /// ``` final ValueChanged<bool> onChanged; /// The color to use when this switch is on. /// /// Defaults to accent color of the current [Theme]. final Color activeColor; /// The color to use on the track when this switch is on. /// /// Defaults to accent color of the current [Theme] with the opacity set at 50%. 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. 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. final Color inactiveTrackColor; /// An image to use on the thumb of this switch when the switch is on. final ImageProvider activeThumbImage; /// An image to use on the thumb of this switch when the switch is off. final ImageProvider inactiveThumbImage; @override _SwitchState createState() => new _SwitchState(); @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description.add(new FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true)); description.add(new ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled')); } } class _SwitchState extends State<Switch> with TickerProviderStateMixin { @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); final ThemeData themeData = Theme.of(context); final bool isDark = themeData.brightness == Brightness.dark; final Color activeThumbColor = widget.activeColor ?? themeData.accentColor; final Color activeTrackColor = widget.activeTrackColor ?? activeThumbColor.withAlpha(0x80); Color inactiveThumbColor; Color inactiveTrackColor; if (widget.onChanged != null) { inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade400 : Colors.grey.shade50); inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white30 : Colors.black26); } else { inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400); inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12); } return new _SwitchRenderObjectWidget( value: widget.value, activeColor: activeThumbColor, inactiveColor: inactiveThumbColor, activeThumbImage: widget.activeThumbImage, inactiveThumbImage: widget.inactiveThumbImage, activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, configuration: createLocalImageConfiguration(context), onChanged: widget.onChanged, vsync: this, ); } } class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { const _SwitchRenderObjectWidget({ Key key, this.value, this.activeColor, this.inactiveColor, this.activeThumbImage, this.inactiveThumbImage, this.activeTrackColor, this.inactiveTrackColor, this.configuration, this.onChanged, this.vsync, }) : super(key: key); final bool value; final Color activeColor; final Color inactiveColor; final ImageProvider activeThumbImage; final ImageProvider inactiveThumbImage; final Color activeTrackColor; final Color inactiveTrackColor; final ImageConfiguration configuration; final ValueChanged<bool> onChanged; final TickerProvider vsync; @override _RenderSwitch createRenderObject(BuildContext context) { return new _RenderSwitch( value: value, activeColor: activeColor, inactiveColor: inactiveColor, activeThumbImage: activeThumbImage, inactiveThumbImage: inactiveThumbImage, activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, configuration: configuration, onChanged: onChanged, textDirection: Directionality.of(context), vsync: vsync, ); } @override void updateRenderObject(BuildContext context, _RenderSwitch renderObject) { renderObject ..value = value ..activeColor = activeColor ..inactiveColor = inactiveColor ..activeThumbImage = activeThumbImage ..inactiveThumbImage = inactiveThumbImage ..activeTrackColor = activeTrackColor ..inactiveTrackColor = inactiveTrackColor ..configuration = configuration ..onChanged = onChanged ..textDirection = Directionality.of(context) ..vsync = vsync; } } 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; class _RenderSwitch extends RenderToggleable { _RenderSwitch({ bool value, Color activeColor, Color inactiveColor, ImageProvider activeThumbImage, ImageProvider inactiveThumbImage, Color activeTrackColor, Color inactiveTrackColor, ImageConfiguration configuration, @required TextDirection textDirection, ValueChanged<bool> onChanged, @required TickerProvider vsync, }) : assert(textDirection != null), _activeThumbImage = activeThumbImage, _inactiveThumbImage = inactiveThumbImage, _activeTrackColor = activeTrackColor, _inactiveTrackColor = inactiveTrackColor, _configuration = configuration, _textDirection = textDirection, super( value: value, activeColor: activeColor, inactiveColor: inactiveColor, onChanged: onChanged, size: const Size(_kSwitchWidth, _kSwitchHeight), vsync: vsync, ) { _drag = new HorizontalDragGestureRecognizer() ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd; } 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(); } @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 new BoxDecoration( color: color, image: image == null ? null : new 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 paint(PaintingContext context, Offset offset) { final Canvas canvas = context.canvas; final bool isActive = 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 = isActive ? Color.lerp(inactiveTrackColor, activeTrackColor, currentValue) : inactiveTrackColor; // Paint the track final Paint paint = new Paint() ..color = trackColor; final double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius; final Rect trackRect = new Rect.fromLTWH( offset.dx + trackHorizontalPadding, offset.dy + (size.height - _kTrackHeight) / 2.0, size.width - 2.0 * trackHorizontalPadding, _kTrackHeight ); final RRect trackRRect = new RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius)); canvas.drawRRect(trackRRect, paint); final Offset thumbPosition = new Offset( kRadialReactionRadius + visualPosition * _trackInnerLength, size.height / 2.0 ); paintRadialReaction(canvas, offset, thumbPosition); try { _isPainting = true; BoxPainter thumbPainter; final Color thumbColor = isActive ? Color.lerp(inactiveColor, activeColor, currentValue) : inactiveColor; final ImageProvider thumbImage = isActive ? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage) : inactiveThumbImage; 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 - new Offset(radius, radius), configuration.copyWith(size: new Size.fromRadius(radius)) ); } finally { _isPainting = false; } } }